Compare commits
254 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 348ecde574 | |||
| b8d9f4ba92 | |||
| 1462c5caa5 | |||
| 58c08c855f | |||
| 9a4763ceee | |||
| f8200a6386 | |||
| e35320f8ee | |||
| 5b52e5e331 | |||
| 6ebd9f1037 | |||
| 99ab16d7be | |||
| e675b35cda | |||
| ddd7acec49 | |||
| e7aa08564b | |||
| 93ccdb8024 | |||
| 501645f918 | |||
| 4fd9d30c98 | |||
| e2b9c16959 | |||
| 4ba85c40cc | |||
| cca95f613b | |||
| 5b50a66c6c | |||
| dd508280f0 | |||
| 2927d0ca15 | |||
| d35e2400b2 | |||
| 47ed599db7 | |||
| f899f4cbe4 | |||
| b3231d2bed | |||
| c24c327151 | |||
| 5e17d7ba80 | |||
| 0261dbfe3d | |||
| 4301f741db | |||
| 7e743c74dc | |||
| ad0b055245 | |||
| 27db3e6f70 | |||
| 4994b7a9ea | |||
| 080f2c0616 | |||
| d469cc04b6 | |||
| e0a25d04ac | |||
| b0f2c79944 | |||
| d2bc40f78d | |||
| 9e54f11960 | |||
| 6eda1f743f | |||
| 6787decef1 | |||
| d56d45a2ab | |||
| fcf16a163a | |||
| dc39c819a3 | |||
| 1048b09487 | |||
| 3806e97404 | |||
| c396f9508a | |||
| b9933f6cb2 | |||
| 0ba4b5eda6 | |||
| 18d2c350de | |||
| f8f9fd11cd | |||
| b422e6f12e | |||
| f8d5082bdd | |||
| ffe677e55a | |||
| 84eefa57bc | |||
| ad77371e38 | |||
| c524acb2df | |||
| 3edfe535c6 | |||
| d3b99e8e39 | |||
| 3c03f21419 | |||
| 5c95c21727 | |||
| a6b578b648 | |||
| 8af824df41 | |||
| 58ab25919e | |||
| 215848f476 | |||
| f9c7766073 | |||
| f976212ec9 | |||
| 8c88e168c1 | |||
| a78c854404 | |||
| 8a5af2f80e | |||
| f7aa26d418 | |||
| b5ff1a9926 | |||
| 729c83ee72 | |||
| 00434f4ac9 | |||
| 2cd9173bfb | |||
| 7ba3ae077f | |||
| a6497b2c6e | |||
| 07956c2299 | |||
| 77d3a1ad4d | |||
| 2e0ff9a582 | |||
| a65d8f1f4c | |||
| 424234c2d1 | |||
| 3bfe0c16d0 | |||
| e5f944c2f8 | |||
| d81a1818d6 | |||
| 921b34235e | |||
| a0cf9cff2a | |||
| 3105609d39 | |||
| 254a292362 | |||
| 95e16f01c1 | |||
| df21b5d1b4 | |||
| c4be60c29f | |||
| ff18181064 | |||
| b167de3096 | |||
| 9dd187e29d | |||
| 413bd0f43b | |||
| 3c3606d516 | |||
| b5dc04e62c | |||
| 234a327a03 | |||
| 90b32c7377 | |||
| 0fc6a63564 | |||
| 5138681c13 | |||
| d6304f4f1d | |||
| 082335df5d | |||
| be506661a5 | |||
| 38385adf22 | |||
| bbcfb47d28 | |||
| 32dd456ac6 | |||
| 4d2765e5f2 | |||
| 000d8bba66 | |||
| 5f909dab76 | |||
| 56e63ff551 | |||
| f31b54582f | |||
| 5b256a92b3 | |||
| cd829bd79a | |||
| 5b97953bfb | |||
| afed762774 | |||
| 5324f29693 | |||
| b96a287a2f | |||
| 0ab62f197a | |||
| 9a2d1b3074 | |||
| e809dbf45e | |||
| 644d6f8ffb | |||
| e37bb73b4e | |||
| 26ec29d8b2 | |||
| c0028c1c44 | |||
| ab38f24198 | |||
| 7d9a1b7eae | |||
| 2107483588 | |||
| c2b05954e9 | |||
| 7756c9a55c | |||
| 3e71542c78 | |||
| 2f2e6e6a00 | |||
| 6f7467e8d8 | |||
| f2a6193735 | |||
| 84eca86e14 | |||
| 86fdcfc437 | |||
| 25eeedbce8 | |||
| 2ba1ff15b7 | |||
| 5aefc47f71 | |||
| 7633c1cdf1 | |||
| 283c2821cd | |||
| fb999c8617 | |||
| 53dc0c3c5e | |||
| 84019238df | |||
| cef865f5ce | |||
| bdf99f6bb4 | |||
| 7424423d9a | |||
| c9b812570d | |||
| e62d157aac | |||
| 99b3337b1c | |||
| dd673bf923 | |||
| 158d1175e3 | |||
| b25da7b9b0 | |||
| 5cd2593f52 | |||
| d8d9e34914 | |||
| e8c0d206ea | |||
| 17818439a0 | |||
| 056be1185d | |||
| b0898ec8bc | |||
| 133b7013a0 | |||
| dabf7e3ec9 | |||
| da3ba50a2c | |||
| 5eababe0b0 | |||
| b2c5ffcaea | |||
| 4ee00091d6 | |||
| c655622bf7 | |||
| 71d331af16 | |||
| 34052d98d6 | |||
| 3543fbbb65 | |||
| aff639e87a | |||
| 5dfe24eeee | |||
| 78ec5aa716 | |||
| 457d5bad9a | |||
| a0f301d700 | |||
| fc311a9fd6 | |||
| a7993d18c6 | |||
| 5595cff6a6 | |||
| 2c3b3c093d | |||
| ab3e783168 | |||
| 7584c66bdb | |||
| d78d20e2d0 | |||
| b85ec52387 | |||
| edcd258cc9 | |||
| d5250bbf51 | |||
| d50e553ebb | |||
| 1eb0f12a61 | |||
| 8129ed91f1 | |||
| c87759affe | |||
| 07b43bffa4 | |||
| 0230c6321f | |||
| 796c5ad42d | |||
| d281fb00e3 | |||
| 76a6c623f3 | |||
| 25c974f0b5 | |||
| ea719f5b5a | |||
| 3105fa4ff8 | |||
| f724480c7b | |||
| 5c5e8c0baf | |||
| bb5278fe9b | |||
| 38b0bd76f5 | |||
| c1aab0bee9 | |||
| ef3283638a | |||
| 68b62a5c00 | |||
| 7a75a4d384 | |||
| 1aae7b222f | |||
| 3535b251ab | |||
| 707f070a3c | |||
| 160a3b6d32 | |||
| d977796f18 | |||
| 3cef76d463 | |||
| e9564a7e5c | |||
| b79ad3ddc3 | |||
| d0f898751f | |||
| 22d9830baa | |||
| c4e23862cd | |||
| e10dc129de | |||
| dd11fc96db | |||
| 7de3c0a09a | |||
| 141e49727c | |||
| 442d61918b | |||
| 58dcc07430 | |||
| 19d0916e34 | |||
| 1265cc7934 | |||
| 3d2688e959 | |||
| 7ab51fad0d | |||
| 3472145df6 | |||
| 5e1ea21f85 | |||
| 2db3977408 | |||
| ca6d5d3bb5 | |||
| 163c2bc533 | |||
| dd286aa12e | |||
| 80babe3ab4 | |||
| ca0cab0a15 | |||
| ad8be226fd | |||
| 9b3e5c38e2 | |||
| 7e1c5f5d1f | |||
| 98bc8d1717 | |||
| 2889a5d5ff | |||
| 0b8c19c620 | |||
| a62b475f46 | |||
| 1691839c6b | |||
| 36265ff349 | |||
| 94427d334b | |||
| b2844569c8 | |||
| ae5e60b1ae | |||
| fffda09f67 | |||
| 5ec7a5c078 | |||
| 190c7270d9 | |||
| f704b28cad | |||
| 5efeec1819 | |||
| 438f950be3 | |||
| 50c1549865 |
@@ -0,0 +1,139 @@
|
|||||||
|
version: 2.0
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build:
|
||||||
|
jobs:
|
||||||
|
- build-1.11
|
||||||
|
- build-1.12
|
||||||
|
- build-1.13
|
||||||
|
- build-latest
|
||||||
|
- test-build-in-docker
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
# build-latest serves as the template
|
||||||
|
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
|
||||||
|
build-latest: &build-latest
|
||||||
|
description: Builds zrepl
|
||||||
|
parameters:
|
||||||
|
image:
|
||||||
|
description: "the docker image that the job should use"
|
||||||
|
type: string
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:latest
|
||||||
|
environment:
|
||||||
|
# required by lazy.sh
|
||||||
|
TERM: xterm
|
||||||
|
working_directory: /go/src/github.com/zrepl/zrepl
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Setup environment variables
|
||||||
|
command: |
|
||||||
|
# used by pip (for docs)
|
||||||
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
|
||||||
|
# we use modules
|
||||||
|
echo 'export GO111MODULE=on' >> $BASH_ENV
|
||||||
|
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- 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 test
|
||||||
|
- run: make lint
|
||||||
|
- run: make release
|
||||||
|
|
||||||
|
|
||||||
|
- store_artifacts:
|
||||||
|
path: ./artifacts/release
|
||||||
|
when: always
|
||||||
|
|
||||||
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
|
when: always
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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}
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
|
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
|
||||||
|
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
|
||||||
|
|
||||||
|
# Push Artifact Link to GitHub
|
||||||
|
REPO="zrepl/zrepl"
|
||||||
|
COMMIT="${CIRCLE_SHA1}"
|
||||||
|
JOB_NAME="${CIRCLE_JOB}"
|
||||||
|
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT?access_token=$GITHUB_COMMIT_STATUS_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
|
||||||
|
|
||||||
|
build-1.11:
|
||||||
|
<<: *build-latest
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.11
|
||||||
|
|
||||||
|
build-1.12:
|
||||||
|
<<: *build-latest
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.12
|
||||||
|
|
||||||
|
build-1.13:
|
||||||
|
<<: *build-latest
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.13
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
working_directory: /go/src/github.com/zrepl/zrepl
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- setup_remote_docker
|
||||||
|
- run:
|
||||||
|
name: (hacky) circleci doesn't allow volume mounts, so copy src to container
|
||||||
|
command: echo "ADD . /src" >> build.Dockerfile
|
||||||
|
- run:
|
||||||
|
name: (hacky) commit modified Dockerfile to avoid failing git clean check in Makefile
|
||||||
|
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:
|
||||||
|
name: build the build image (build deps)
|
||||||
|
command: docker build -t zrepl_build -f build.Dockerfile .
|
||||||
|
- run:
|
||||||
|
name: try compiling
|
||||||
|
command: docker run -it zrepl_build make release
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
patreon: zrepl
|
||||||
|
liberapay: zrepl
|
||||||
|
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||||
|
ko_fi: problame
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- goimports
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
|
||||||
+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"
|
||||||
Generated
-313
@@ -1,313 +0,0 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:c0bec5f9b98d0bc872ff5e834fac186b807b656683bd29cb82fb207a1513fabb"
|
|
||||||
name = "github.com/beorn7/perks"
|
|
||||||
packages = ["quantile"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b"
|
|
||||||
name = "github.com/davecgh/go-spew"
|
|
||||||
packages = ["spew"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:e988ed0ca0d81f4d28772760c02ee95084961311291bdfefc1b04617c178b722"
|
|
||||||
name = "github.com/fatih/color"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
|
||||||
version = "v1.7.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:5d0a2385edf4ba44f3b7b76bc0436ceb8f62bf55aa5d540a9eb9ec6c58d86809"
|
|
||||||
name = "github.com/ftrvxmtrx/fd"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "c6d800382fff6dc1412f34269f71b7f83bd059ad"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:4416ad04caf950d004921268f777e418f8c328080ccf6a91e799f977dc128808"
|
|
||||||
name = "github.com/gdamore/encoding"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:d8088c614a5bffaf38a40c116226e7d230750bafb77942dca25eab779ceeaf1f"
|
|
||||||
name = "github.com/gdamore/tcell"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"termbox",
|
|
||||||
"terminfo",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "de7e78efa4a71b3f36c7154989c529dbdf9ae623"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:6a4a01d58b227c4b6b11111b9f172ec5c17682b82724e58e6daf3f19f4faccd8"
|
|
||||||
name = "github.com/go-logfmt/logfmt"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
|
|
||||||
version = "v0.3.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18"
|
|
||||||
name = "github.com/golang/protobuf"
|
|
||||||
packages = ["proto"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
|
||||||
version = "v1.2.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:cb09475f771b9167fb9333629f5d6a7161572602ea040f1094602b0dc8709878"
|
|
||||||
name = "github.com/jinzhu/copier"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "db4671f3a9b8df855e993f7c94ec5ef1ffb0a23b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:1ed9eeebdf24aadfbca57eb50e6455bd1d2474525e0f0d4454de8c8e9bc7ee9a"
|
|
||||||
name = "github.com/kr/logfmt"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:591a2778aa6e896980757ea87e659b3aa13d8c0e790310614028463a31c0998b"
|
|
||||||
name = "github.com/kr/pretty"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:109e0a9b9e74c5c8adf94a2dc4dd4a9ca2a183d4d87ffecd4e62db69a5ede55a"
|
|
||||||
name = "github.com/kr/text"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "7cafcd837844e784b526369c9bce262804aebc60"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:863ba095ae0954100963826333bd1503c169424bb798d9ce800d9f6651f4c234"
|
|
||||||
name = "github.com/lucasb-eyer/go-colorful"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82"
|
|
||||||
version = "v1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:9ea83adf8e96d6304f394d40436f2eb44c1dc3250d223b74088cc253a6cd0a1c"
|
|
||||||
name = "github.com/mattn/go-colorable"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
|
||||||
version = "v0.0.9"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:78229b46ddb7434f881390029bd1af7661294af31f6802e0e1bedaad4ab0af3c"
|
|
||||||
name = "github.com/mattn/go-isatty"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
|
||||||
version = "v0.0.3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:82b912465c1da0668582a7d1117339c278e786c2536b3c3623029a0c7141c2d0"
|
|
||||||
name = "github.com/mattn/go-runewidth"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
|
|
||||||
version = "v0.0.3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:4c23ced97a470b17d9ffd788310502a077b9c1f60221a85563e49696276b4147"
|
|
||||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
|
||||||
packages = ["pbutil"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
|
||||||
version = "v0.8.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
|
|
||||||
name = "github.com/pmezard/go-difflib"
|
|
||||||
packages = ["difflib"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:1392748e290ca66ac8447ef24961f8ae9e1d846a53af0f58a5a0256982ce0577"
|
|
||||||
name = "github.com/problame/go-netssh"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "c56ad38d2c91397ad3c8dd9443d7448e328a9e9e"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:8c63c44f018bd52b03ebad65c9df26aabbc6793138e421df1c8c84c285a45bc6"
|
|
||||||
name = "github.com/problame/go-rwccmd"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "391d2c78c8404a9683d79f75dd24ab53040f89f7"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:c2ba1c9dc003c15856e4529dac028cacba08ee8924300f058b3467cde9acf7a9"
|
|
||||||
name = "github.com/problame/go-streamrpc"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"internal/pdu",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "de6f6a4041c77f700f02d8fe749e54efa50811f7"
|
|
||||||
version = "v0.4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:ebf8ffdde9bdbf9c83e22121875c68c01d821776523546554b2a3ff6f72773ab"
|
|
||||||
name = "github.com/prometheus/client_golang"
|
|
||||||
packages = [
|
|
||||||
"prometheus",
|
|
||||||
"prometheus/promhttp",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "e11c6ff8170beca9d5fd8b938e71165eeec53ac6"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:60aca47f4eeeb972f1b9da7e7db51dee15ff6c59f7b401c1588b8e6771ba15ef"
|
|
||||||
name = "github.com/prometheus/client_model"
|
|
||||||
packages = ["go"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:af21ee3e0a8212f17bb317cd7237f9920bcb2641a291ac111f30f63b3cab817f"
|
|
||||||
name = "github.com/prometheus/common"
|
|
||||||
packages = [
|
|
||||||
"expfmt",
|
|
||||||
"internal/bitbucket.org/ww/goautoneg",
|
|
||||||
"model",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "d0f7cd64bda49e08b22ae8a730aa57aa0db125d6"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:61df0898746840afc7be5dc2c3eeec83022fab70df11ecee5b16c85e912cf5ed"
|
|
||||||
name = "github.com/prometheus/procfs"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"internal/util",
|
|
||||||
"nfs",
|
|
||||||
"xfs",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:146327ce93be37e68bd3ff8541090d96da8cb3adc9e35d57570e9170a29f6bf6"
|
|
||||||
name = "github.com/spf13/cobra"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "b78744579491c1ceeaaa3b40205e56b0591b93a3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:261bc565833ef4f02121450d74eb88d5ae4bd74bfe5d0e862cddb8550ec35000"
|
|
||||||
name = "github.com/spf13/pflag"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:3926a4ec9a4ff1a072458451aa2d9b98acd059a45b38f7335d31e06c3d6a0159"
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
packages = [
|
|
||||||
"assert",
|
|
||||||
"require",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
|
||||||
version = "v1.1.4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "v2"
|
|
||||||
digest = "1:9d92186f609a73744232323416ddafd56fae67cb552162cc190ab903e36900dd"
|
|
||||||
name = "github.com/zrepl/yaml-config"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "af27d27978ad95808723a62d87557d63c3ff0605"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:9c286cf11d0ca56368185bada5dd6d97b6be4648fc26c354fcba8df7293718f7"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
packages = ["unix"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "bf42f188b9bc6f2cf5b8ee5a912ef1aedd0eba4c"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4"
|
|
||||||
name = "golang.org/x/text"
|
|
||||||
packages = [
|
|
||||||
"encoding",
|
|
||||||
"encoding/internal/identifier",
|
|
||||||
"internal/gen",
|
|
||||||
"transform",
|
|
||||||
"unicode/cldr",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
|
||||||
version = "v0.3.0"
|
|
||||||
|
|
||||||
[solve-meta]
|
|
||||||
analyzer-name = "dep"
|
|
||||||
analyzer-version = 1
|
|
||||||
input-imports = [
|
|
||||||
"github.com/fatih/color",
|
|
||||||
"github.com/gdamore/tcell/termbox",
|
|
||||||
"github.com/go-logfmt/logfmt",
|
|
||||||
"github.com/golang/protobuf/proto",
|
|
||||||
"github.com/jinzhu/copier",
|
|
||||||
"github.com/kr/pretty",
|
|
||||||
"github.com/mattn/go-isatty",
|
|
||||||
"github.com/pkg/errors",
|
|
||||||
"github.com/problame/go-netssh",
|
|
||||||
"github.com/problame/go-rwccmd",
|
|
||||||
"github.com/problame/go-streamrpc",
|
|
||||||
"github.com/prometheus/client_golang/prometheus",
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
|
||||||
"github.com/spf13/cobra",
|
|
||||||
"github.com/spf13/pflag",
|
|
||||||
"github.com/stretchr/testify/assert",
|
|
||||||
"github.com/stretchr/testify/require",
|
|
||||||
"github.com/zrepl/yaml-config",
|
|
||||||
"golang.org/x/sys/unix",
|
|
||||||
]
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
||||||
-75
@@ -1,75 +0,0 @@
|
|||||||
ignored = [ "github.com/inconshreveable/mousetrap" ]
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/ftrvxmtrx/fd"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/jinzhu/copier"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/kr/pretty"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/go-homedir"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/mapstructure"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
version = "0.8.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/spf13/viper"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
version = "1.1.4"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "v2"
|
|
||||||
name = "github.com/zrepl/yaml-config"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/go-logfmt/logfmt"
|
|
||||||
version = "*"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/problame/go-rwccmd"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/problame/go-netssh"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/prometheus/client_golang"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/golang/protobuf"
|
|
||||||
version = "1.2.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/fatih/color"
|
|
||||||
version = "1.7.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/problame/go-streamrpc"
|
|
||||||
version = "0.4.0"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/gdamore/tcell"
|
|
||||||
version = "1.0.0"
|
|
||||||
@@ -1,38 +1,6 @@
|
|||||||
.PHONY: generate build test vet cover release docs docs-clean clean vendordeps
|
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
|
||||||
.DEFAULT_GOAL := build
|
.PHONY: release bins-all release-noarch
|
||||||
|
.DEFAULT_GOAL := zrepl-bin
|
||||||
ROOT := github.com/zrepl/zrepl
|
|
||||||
SUBPKGS += client
|
|
||||||
SUBPKGS += config
|
|
||||||
SUBPKGS += daemon
|
|
||||||
SUBPKGS += daemon/filters
|
|
||||||
SUBPKGS += daemon/job
|
|
||||||
SUBPKGS += daemon/logging
|
|
||||||
SUBPKGS += daemon/nethelpers
|
|
||||||
SUBPKGS += daemon/pruner
|
|
||||||
SUBPKGS += daemon/snapper
|
|
||||||
SUBPKGS += daemon/streamrpcconfig
|
|
||||||
SUBPKGS += daemon/transport
|
|
||||||
SUBPKGS += daemon/transport/connecter
|
|
||||||
SUBPKGS += daemon/transport/serve
|
|
||||||
SUBPKGS += endpoint
|
|
||||||
SUBPKGS += logger
|
|
||||||
SUBPKGS += pruning
|
|
||||||
SUBPKGS += pruning/retentiongrid
|
|
||||||
SUBPKGS += replication
|
|
||||||
SUBPKGS += replication/fsrep
|
|
||||||
SUBPKGS += replication/pdu
|
|
||||||
SUBPKGS += replication/internal/queue
|
|
||||||
SUBPKGS += replication/internal/diff
|
|
||||||
SUBPKGS += tlsconf
|
|
||||||
SUBPKGS += util
|
|
||||||
SUBPKGS += util/socketpair
|
|
||||||
SUBPKGS += util/watchdog
|
|
||||||
SUBPKGS += util/envconst
|
|
||||||
SUBPKGS += version
|
|
||||||
SUBPKGS += zfs
|
|
||||||
|
|
||||||
_TESTPKGS := $(ROOT) $(foreach p,$(SUBPKGS),$(ROOT)/$(p))
|
|
||||||
|
|
||||||
ARTIFACTDIR := artifacts
|
ARTIFACTDIR := artifacts
|
||||||
|
|
||||||
@@ -40,67 +8,174 @@ ifdef ZREPL_VERSION
|
|||||||
_ZREPL_VERSION := $(ZREPL_VERSION)
|
_ZREPL_VERSION := $(ZREPL_VERSION)
|
||||||
endif
|
endif
|
||||||
ifndef _ZREPL_VERSION
|
ifndef _ZREPL_VERSION
|
||||||
_ZREPL_VERSION := $(shell git describe --dirty 2>/dev/null || echo "ZREPL_BUILD_INVALID_VERSION" )
|
_ZREPL_VERSION := $(shell git describe --always --dirty 2>/dev/null || echo "ZREPL_BUILD_INVALID_VERSION" )
|
||||||
ifeq ($(_ZREPL_VERSION),ZREPL_BUILD_INVALID_VERSION) # can't use .SHELLSTATUS because Debian Stretch is still on gmake 4.1
|
ifeq ($(_ZREPL_VERSION),ZREPL_BUILD_INVALID_VERSION) # can't use .SHELLSTATUS because Debian Stretch is still on gmake 4.1
|
||||||
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
GO := go
|
||||||
|
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
||||||
|
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
|
||||||
|
GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"')
|
||||||
|
GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"')
|
||||||
|
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
||||||
|
GO_ENV_VARS := GO111MODULE=on
|
||||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||||
|
GO_MOD_READONLY := -mod=readonly
|
||||||
|
GO_BUILDFLAGS := $(GO_MOD_READONLY)
|
||||||
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
|
GOLANGCI_LINT := golangci-lint
|
||||||
|
ifneq ($(GOARM),)
|
||||||
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||||
|
else
|
||||||
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)
|
||||||
|
endif
|
||||||
|
|
||||||
GO_BUILD := go build -ldflags $(GO_LDFLAGS)
|
.PHONY: printvars
|
||||||
|
printvars:
|
||||||
|
@echo GOOS=$(GOOS)
|
||||||
|
@echo GOARCH=$(GOARCH)
|
||||||
|
@echo GOARM=$(GOARM)
|
||||||
|
|
||||||
RELEASE_BINS := $(ARTIFACTDIR)/zrepl-freebsd-amd64
|
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-amd64
|
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-darwin-amd64
|
|
||||||
|
|
||||||
RELEASE_NOARCH := $(ARTIFACTDIR)/zrepl-noarch.tar
|
##################### PRODUCING A RELEASE #############
|
||||||
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <(go env) && echo "zrepl-$${GOOS}-$${GOARCH}"' )
|
.PHONY: release wrapup-and-checksum check-git-clean sign clean
|
||||||
|
|
||||||
vendordeps:
|
release: clean
|
||||||
dep ensure -v -vendor-only
|
# no cross-platform support for target test
|
||||||
|
$(MAKE) test
|
||||||
|
$(MAKE) bins-all
|
||||||
|
$(MAKE) noarch
|
||||||
|
$(MAKE) wrapup-and-checksum
|
||||||
|
$(MAKE) check-git-clean
|
||||||
|
ifeq (SIGN, 1)
|
||||||
|
$(make) sign
|
||||||
|
endif
|
||||||
|
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
||||||
|
|
||||||
generate: #not part of the build, must do that manually
|
# expects `release` target to have run before
|
||||||
protoc -I=replication/pdu --go_out=replication/pdu replication/pdu/pdu.proto
|
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||||
@for pkg in $(_TESTPKGS); do\
|
wrapup-and-checksum:
|
||||||
go generate "$$pkg" || exit 1; \
|
rm -f $(NOARCH_TARBALL)
|
||||||
done;
|
tar --mtime='1970-01-01' --sort=name \
|
||||||
|
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
||||||
|
--transform 's#dist#zrepl-$(_ZREPL_VERSION)-noarch/dist#' \
|
||||||
|
--transform 's#config/samples#zrepl-$(_ZREPL_VERSION)-noarch/config#' \
|
||||||
|
-acf $(NOARCH_TARBALL) \
|
||||||
|
$(ARTIFACTDIR)/docs/html \
|
||||||
|
$(ARTIFACTDIR)/bash_completion \
|
||||||
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
|
dist \
|
||||||
|
config/samples
|
||||||
|
rm -rf "$(ARTIFACTDIR)/release"
|
||||||
|
mkdir -p "$(ARTIFACTDIR)/release"
|
||||||
|
cp -l $(ARTIFACTDIR)/zrepl-* \
|
||||||
|
$(ARTIFACTDIR)/platformtest-* \
|
||||||
|
"$(ARTIFACTDIR)/release"
|
||||||
|
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
|
||||||
|
|
||||||
build:
|
check-git-clean:
|
||||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl"
|
@if git describe --always --dirty 2>/dev/null | grep dirty >/dev/null; then \
|
||||||
|
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
||||||
test:
|
if [ "$(ZREPL_VERSION)" = "" ]; then \
|
||||||
@for pkg in $(_TESTPKGS); do \
|
echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
|
||||||
echo "Testing $$pkg"; \
|
git status; \
|
||||||
go test "$$pkg" || exit 1; \
|
echo "git diff:"; \
|
||||||
done;
|
git diff | cat; \
|
||||||
|
exit 1; \
|
||||||
vet:
|
|
||||||
@for pkg in $(_TESTPKGS); do \
|
|
||||||
echo "Vetting $$pkg"; \
|
|
||||||
go vet "$$pkg" || exit 1; \
|
|
||||||
done;
|
|
||||||
|
|
||||||
cover: artifacts
|
|
||||||
@for pkg in $(_TESTPKGS); do \
|
|
||||||
profile="$(ARTIFACTDIR)/cover-$$(basename $$pkg).out"; \
|
|
||||||
go test -coverprofile "$$profile" $$pkg || exit 1; \
|
|
||||||
if [ -f "$$profile" ]; then \
|
|
||||||
go tool cover -html="$$profile" -o "$${profile}.html" || exit 2; \
|
|
||||||
fi; \
|
fi; \
|
||||||
done;
|
fi;
|
||||||
|
|
||||||
|
sign:
|
||||||
|
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
||||||
|
--armor \
|
||||||
|
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
|
||||||
|
|
||||||
|
clean: docs-clean
|
||||||
|
rm -rf "$(ARTIFACTDIR)"
|
||||||
|
|
||||||
|
##################### BINARIES #####################
|
||||||
|
.PHONY: bins-all lint test vet zrepl-bin platformtest-bin
|
||||||
|
|
||||||
|
BINS_ALL_TARGETS := zrepl-bin platformtest-bin vet lint
|
||||||
|
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
|
||||||
|
bins-all:
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||||
|
$(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=386
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=darwin GOARCH=amd64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=solaris GOARCH=amd64
|
||||||
|
ifeq ($(GO_SUPPORTS_ILLUMOS), illumos)
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=illumos GOARCH=amd64
|
||||||
|
else ifeq ($(GO_SUPPORTS_ILLUMOS), noillumos)
|
||||||
|
@echo "SKIPPING ILLUMOS BUILD BECAUSE GO VERSION DOESN'T SUPPORT IT"
|
||||||
|
else
|
||||||
|
@echo "CANNOT DETERMINE WHETHER GO VERSION SUPPORTS GOOS=illumos"; exit 1
|
||||||
|
endif
|
||||||
|
|
||||||
|
lint:
|
||||||
|
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
|
||||||
|
test:
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) ./...
|
||||||
|
vet:
|
||||||
|
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||||
|
|
||||||
|
zrepl-bin:
|
||||||
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||||
|
|
||||||
|
platformtest-bin:
|
||||||
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" ./platformtest/harness
|
||||||
|
|
||||||
|
##################### DEV TARGETS #####################
|
||||||
|
# not part of the build, must do that manually
|
||||||
|
.PHONY: generate format platformtest
|
||||||
|
|
||||||
|
generate:
|
||||||
|
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
||||||
|
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
||||||
|
|
||||||
|
format:
|
||||||
|
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')
|
||||||
|
|
||||||
|
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||||
|
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||||
|
ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
||||||
|
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
||||||
|
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
||||||
|
ZREPL_PLATFORMTEST_ARGS :=
|
||||||
|
platformtest: # do not track dependency on platformtest-bin to allow build of platformtest outside of test VM
|
||||||
|
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||||
|
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||||
|
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||||
|
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||||
|
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||||
|
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||||
|
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
|
||||||
|
$(ZREPL_PLATFORMTEST_ARGS)
|
||||||
|
|
||||||
|
##################### NOARCH #####################
|
||||||
|
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||||
|
|
||||||
|
|
||||||
$(ARTIFACTDIR):
|
$(ARTIFACTDIR):
|
||||||
mkdir -p "$@"
|
mkdir -p "$@"
|
||||||
|
|
||||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||||
mkdir -p "$@"
|
mkdir -p "$@"
|
||||||
|
|
||||||
$(ARTIFACTDIR)/bash_completion: $(RELEASE_BINS)
|
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||||
artifacts/$(THIS_PLATFORM_RELEASE_BIN) bashcomp "$@"
|
# pass
|
||||||
|
|
||||||
$(ARTIFACTDIR)/go_version.txt:
|
$(ARTIFACTDIR)/bash_completion:
|
||||||
go version > $@
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) bashcomp "$@"
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/go_env.txt:
|
||||||
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
|
|
||||||
docs: $(ARTIFACTDIR)/docs
|
docs: $(ARTIFACTDIR)/docs
|
||||||
make -C docs \
|
make -C docs \
|
||||||
@@ -111,34 +186,3 @@ docs-clean:
|
|||||||
make -C docs \
|
make -C docs \
|
||||||
clean \
|
clean \
|
||||||
BUILDDIR=../artifacts/docs
|
BUILDDIR=../artifacts/docs
|
||||||
|
|
||||||
|
|
||||||
.PHONY: $(RELEASE_BINS)
|
|
||||||
# TODO: two wildcards possible
|
|
||||||
$(RELEASE_BINS): $(ARTIFACTDIR)/zrepl-%-amd64: generate $(ARTIFACTDIR) vet test
|
|
||||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
|
||||||
GOOS=$* GOARCH=amd64 $(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$*-amd64"
|
|
||||||
|
|
||||||
$(RELEASE_NOARCH): docs $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_version.txt
|
|
||||||
tar --mtime='1970-01-01' --sort=name \
|
|
||||||
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
|
||||||
-acf $@ \
|
|
||||||
$(ARTIFACTDIR)/docs/html \
|
|
||||||
$(ARTIFACTDIR)/bash_completion \
|
|
||||||
$(ARTIFACTDIR)/go_version.txt
|
|
||||||
|
|
||||||
release: $(RELEASE_BINS) $(RELEASE_NOARCH)
|
|
||||||
rm -rf "$(ARTIFACTDIR)/release"
|
|
||||||
mkdir -p "$(ARTIFACTDIR)/release"
|
|
||||||
cp $^ "$(ARTIFACTDIR)/release"
|
|
||||||
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
|
|
||||||
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
|
|
||||||
@if git describe --dirty 2>/dev/null | grep dirty >/dev/null; then \
|
|
||||||
if [ "$(ZREPL_VERSION)" == "" ]; then \
|
|
||||||
echo "[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override"; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
fi;
|
|
||||||
|
|
||||||
clean: docs-clean
|
|
||||||
rm -rf "$(ARTIFACTDIR)"
|
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||||
|
[](https://golang.org/)
|
||||||
|
[](https://zrepl.github.io)
|
||||||
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||||
|
[](https://liberapay.com/zrepl/donate)
|
||||||
|
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||||
|
|
||||||
# zrepl
|
# zrepl
|
||||||
zrepl is a one-stop ZFS backup & replication solution.
|
zrepl is a one-stop ZFS backup & replication solution.
|
||||||
|
|
||||||
@@ -26,13 +33,16 @@ Check out the *Coding Workflow* section below for details.
|
|||||||
* Make sure your distro is compatible with the paths in `docs/installation.rst`.
|
* 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 default config that adheres to your distro's `hier` and logging system.
|
||||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||||
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
|
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
* 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
|
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
||||||
|
* Use `make platformtest` **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.
|
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||||
|
|
||||||
## Developer Documentation
|
## Developer Documentation
|
||||||
|
|
||||||
zrepl is written in [Go](https://golang.org) and uses [`dep`](https://github.com/golang/dep) 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.
|
||||||
|
|
||||||
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
||||||
@@ -45,21 +55,53 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
|||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
├── cmd
|
├── artifacts # build artifcats generate by make
|
||||||
│ ├── endpoint # implementations of endpoints for package replication
|
├── cli # wrapper around CLI package cobra
|
||||||
│ ├── sampleconf # example configuration
|
├── client # all subcommands that are not `daemon`
|
||||||
|
├── config # config data types (=> package yaml-config)
|
||||||
|
│ └── samples
|
||||||
|
├── daemon # the implementation of `zrepl daemon` subcommand
|
||||||
|
│ ├── filters
|
||||||
|
│ ├── job # job implementations
|
||||||
|
│ ├── logging # logging outlets + formatters
|
||||||
|
│ ├── nethelpers
|
||||||
|
│ ├── prometheus
|
||||||
|
│ ├── pruner # pruner implementation
|
||||||
|
│ ├── snapper # snapshotter implementation
|
||||||
├── docs # sphinx-based documentation
|
├── docs # sphinx-based documentation
|
||||||
|
├── dist # supplemental material for users & package maintainers
|
||||||
│ ├── **/*.rst # documentation in reStructuredText
|
│ ├── **/*.rst # documentation in reStructuredText
|
||||||
│ ├── sphinxconf
|
│ ├── sphinxconf
|
||||||
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
||||||
│ ├── requirements.txt # pip3 requirements to build documentation
|
│ ├── requirements.txt # pip3 requirements to build documentation
|
||||||
│ ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo
|
│ ├── 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
|
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
||||||
├── logger # logger package used by zrepl
|
├── endpoint # implementation of replication endpoints (=> package replication)
|
||||||
├── replication # replication functionality
|
├── logger # our own logger package
|
||||||
├── rpc # rpc protocol implementation
|
├── 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 # transports implementation
|
||||||
|
│ ├── fromconfig
|
||||||
|
│ ├── local
|
||||||
|
│ ├── ssh
|
||||||
|
│ ├── tcp
|
||||||
|
│ └── tls
|
||||||
├── util
|
├── util
|
||||||
└── zfs # ZFS wrappers, filesystemm diffing
|
├── vendor # managed by dep
|
||||||
|
├── version # abstraction for versions (filled during build by Makefile)
|
||||||
|
└── zfs # zfs(8) wrappers
|
||||||
```
|
```
|
||||||
|
|
||||||
### Coding Workflow
|
### Coding Workflow
|
||||||
|
|||||||
+16
-8
@@ -2,21 +2,29 @@ FROM golang:latest
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
unzip
|
unzip \
|
||||||
|
gawk
|
||||||
|
|
||||||
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
ADD build.installprotoc.bash ./
|
||||||
RUN echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
RUN bash build.installprotoc.bash
|
||||||
RUN unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
|
||||||
|
|
||||||
ADD lazy.sh /tmp/lazy.sh
|
ADD lazy.sh /tmp/lazy.sh
|
||||||
ADD docs/requirements.txt /tmp/requirements.txt
|
ADD docs/requirements.txt /tmp/requirements.txt
|
||||||
ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
|
ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
|
||||||
RUN /tmp/lazy.sh devsetup
|
RUN /tmp/lazy.sh docdep
|
||||||
|
|
||||||
# prepare volume mount of git checkout to /zrepl
|
# prepare volume mount of git checkout to /zrepl
|
||||||
RUN mkdir -p /go/src/github.com/zrepl/zrepl
|
RUN mkdir -p /src/github.com/zrepl/zrepl
|
||||||
RUN chmod -R 0777 /go
|
|
||||||
RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
||||||
WORKDIR /go/src/github.com/zrepl/zrepl
|
|
||||||
|
|
||||||
|
# $GOPATH is /go
|
||||||
|
# Go 1.12 doesn't use modules within GOPATH, but 1.13 and later do
|
||||||
|
# => store source outside of GOPATH
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
||||||
|
ADD build/ /tmp/build
|
||||||
|
RUN /tmp/lazy.sh godep
|
||||||
|
|
||||||
|
RUN chmod -R 0777 /go
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
MACH=$(uname -m)
|
||||||
|
MACH="${MACH/aarch64/aarch_64}"
|
||||||
|
|
||||||
|
VERSION=3.6.1
|
||||||
|
FILENAME=protoc-"$VERSION"-linux-"$MACH".zip
|
||||||
|
|
||||||
|
if [ -e "$FILENAME" ]; then
|
||||||
|
echo "$FILENAME" already exists 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget https://github.com/protocolbuffers/protobuf/releases/download/v"$VERSION"/"$FILENAME"
|
||||||
|
|
||||||
|
stat "$FILENAME"
|
||||||
|
|
||||||
|
sha256sum -c --ignore-missing <<EOF
|
||||||
|
6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip
|
||||||
|
af8e5aaaf39ddec62ec8dd2be1b8d9602c6da66564883a16393ade5f71170922 protoc-3.6.1-linux-aarch_64.zip
|
||||||
|
EOF
|
||||||
|
|
||||||
|
unzip -d /usr "$FILENAME"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
module github.com/zrepl/zrepl/build
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
|
||||||
|
github.com/alvaroloes/enumer v1.1.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/viper v1.3.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
|
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
|
||||||
|
)
|
||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 h1:pFks+oaqVWDFq0KsLQZFB2pQGB5Us0HfMSBENtieXOs=
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||||
|
github.com/alvaroloes/enumer v1.1.1 h1:v+1scNqEhSFAPb9tLwblQegeTXJhnw8hf9O0amTbOvQ=
|
||||||
|
github.com/alvaroloes/enumer v1.1.1/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc=
|
||||||
|
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
||||||
|
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||||
|
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||||
|
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
|
||||||
|
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||||
|
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||||
|
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
|
||||||
|
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
|
||||||
|
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
||||||
|
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||||
|
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
|
||||||
|
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||||
|
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
|
||||||
|
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
|
||||||
|
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
|
||||||
|
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
||||||
|
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
|
||||||
|
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
|
||||||
|
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
|
||||||
|
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8 h1:vVouagbdmqTVlCIAxpyYsNNTbkKZ3V66VpKOLU/s6W4=
|
||||||
|
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
|
||||||
|
github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg=
|
||||||
|
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
|
||||||
|
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||||
|
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
|
||||||
|
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||||
|
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||||
|
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
|
||||||
|
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||||
|
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||||
|
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||||
|
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w=
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||||
|
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
|
||||||
|
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 h1:MRhC9XbUjE6XDOInSJ8pwHuPagqsyO89QDU9IdVhe3o=
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||||
|
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
|
||||||
|
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0=
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||||
|
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
|
||||||
|
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc=
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
|
||||||
|
github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
|
||||||
|
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI=
|
||||||
|
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4=
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||||
|
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
|
||||||
|
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||||
|
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||||
|
github.com/golangci/misspell v0.3.4 h1:kAD5J0611Zo2VirUn9KFP15RjEqkmfEfnFxyIVxZCYg=
|
||||||
|
github.com/golangci/misspell v0.3.4/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||||
|
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
|
||||||
|
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM=
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||||
|
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
|
||||||
|
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
||||||
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
|
||||||
|
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||||
|
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
|
||||||
|
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
|
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||||
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||||
|
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
|
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
|
||||||
|
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
|
||||||
|
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
||||||
|
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||||
|
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
|
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
|
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||||
|
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
|
||||||
|
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||||
|
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
|
||||||
|
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
|
||||||
|
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||||
|
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||||
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
|
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||||
|
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
|
||||||
|
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||||
|
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM=
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||||
|
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||||
|
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
|
||||||
|
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8 h1:b0PLhFjEMqrIqsD4rS5sp0YxBYgdqKzX0KkkGGKLV8I=
|
||||||
|
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b h1:iEAPfYPbYbxG/2lNN4cMOHkmgKNsCuUwkxlDCK46UlU=
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
|
||||||
|
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
||||||
|
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
|
||||||
|
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
||||||
|
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||||
|
sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o=
|
||||||
|
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// +build tools
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/alvaroloes/enumer"
|
||||||
|
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||||
|
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||||
|
_ "golang.org/x/tools/cmd/goimports"
|
||||||
|
_ "golang.org/x/tools/cmd/stringer"
|
||||||
|
)
|
||||||
+9
-3
@@ -2,10 +2,12 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootArgs struct {
|
var rootArgs struct {
|
||||||
@@ -23,7 +25,10 @@ var bashcompCmd = &cobra.Command{
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||||
cmd.Usage()
|
err := cmd.Usage()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
||||||
@@ -42,6 +47,7 @@ func init() {
|
|||||||
type Subcommand struct {
|
type Subcommand struct {
|
||||||
Use string
|
Use string
|
||||||
Short string
|
Short string
|
||||||
|
Example string
|
||||||
NoRequireConfig bool
|
NoRequireConfig bool
|
||||||
Run func(subcommand *Subcommand, args []string) error
|
Run func(subcommand *Subcommand, args []string) error
|
||||||
SetupFlags func(f *pflag.FlagSet)
|
SetupFlags func(f *pflag.FlagSet)
|
||||||
@@ -94,6 +100,7 @@ func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) {
|
|||||||
cmd := cobra.Command{
|
cmd := cobra.Command{
|
||||||
Use: s.Use,
|
Use: s.Use,
|
||||||
Short: s.Short,
|
Short: s.Short,
|
||||||
|
Example: s.Example,
|
||||||
}
|
}
|
||||||
if s.SetupSubcommands == nil {
|
if s.SetupSubcommands == nil {
|
||||||
cmd.Run = s.run
|
cmd.Run = s.run
|
||||||
@@ -108,7 +115,6 @@ func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) {
|
|||||||
c.AddCommand(&cmd)
|
c.AddCommand(&cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func Run() {
|
func Run() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
+16
-8
@@ -3,16 +3,18 @@ package client
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/zrepl/yaml-config"
|
"github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var configcheckArgs struct {
|
var configcheckArgs struct {
|
||||||
@@ -28,14 +30,22 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
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]")
|
||||||
},
|
},
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||||
formatMap := map[string]func(interface{}) {
|
formatMap := map[string]func(interface{}){
|
||||||
"": func(i interface{}) {},
|
"": func(i interface{}) {},
|
||||||
"pretty": func(i interface{}) { pretty.Println(i) },
|
"pretty": func(i interface{}) {
|
||||||
|
if _, err := pretty.Println(i); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
"json": func(i interface{}) {
|
"json": func(i interface{}) {
|
||||||
json.NewEncoder(os.Stdout).Encode(subcommand.Config())
|
if err := json.NewEncoder(os.Stdout).Encode(subcommand.Config()); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"yaml": func(i interface{}) {
|
"yaml": func(i interface{}) {
|
||||||
yaml.NewEncoder(os.Stdout).Encode(subcommand.Config())
|
if err := yaml.NewEncoder(os.Stdout).Encode(subcommand.Config()); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +81,7 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
whatMap := map[string]func(){
|
||||||
whatMap := map[string]func() {
|
|
||||||
"all": func() {
|
"all": func() {
|
||||||
o := struct {
|
o := struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
@@ -109,4 +118,3 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
HoldsCmd = &cli.Subcommand{
|
||||||
|
Use: "holds",
|
||||||
|
Short: "manage holds & step bookmarks",
|
||||||
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
|
return holdsList
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var holdsList = []*cli.Subcommand{
|
||||||
|
&cli.Subcommand{
|
||||||
|
Use: "list [FSFILTER]",
|
||||||
|
Run: doHoldsList,
|
||||||
|
NoRequireConfig: true,
|
||||||
|
Short: `
|
||||||
|
FSFILTER SYNTAX:
|
||||||
|
representation of a 'filesystems' filter statement on the command line
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fsfilterFromCliArg(arg string) (zfs.DatasetFilter, error) {
|
||||||
|
mappings := strings.Split(arg, ",")
|
||||||
|
f := filters.NewDatasetMapFilter(len(mappings), true)
|
||||||
|
for _, m := range mappings {
|
||||||
|
thisMappingErr := fmt.Errorf("expecting comma-separated list of <dataset-pattern>:<ok|!> pairs, got %q", m)
|
||||||
|
lhsrhs := strings.SplitN(m, ":", 2)
|
||||||
|
if len(lhsrhs) != 2 {
|
||||||
|
return nil, thisMappingErr
|
||||||
|
}
|
||||||
|
err := f.Add(lhsrhs[0], lhsrhs[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %s", thisMappingErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.AsFilter(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doHoldsList(sc *cli.Subcommand, args []string) error {
|
||||||
|
var err error
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
return errors.New("this subcommand takes at most one argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter zfs.DatasetFilter
|
||||||
|
if len(args) == 0 {
|
||||||
|
filter = zfs.NoFilter()
|
||||||
|
} else {
|
||||||
|
filter, err = fsfilterFromCliArg(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot parse filesystem filter args")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listing, err := endpoint.ListZFSHoldsAndBookmarks(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return err // context clear by invocation of command
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent(" ", " ")
|
||||||
|
if err := enc.Encode(listing); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func controlHttpClient(sockpath string) (client http.Client, err error) {
|
func controlHttpClient(sockpath string) (client http.Client, err error) {
|
||||||
@@ -35,7 +36,7 @@ func jsonRequestResponse(c http.Client, endpoint string, req interface{}, res in
|
|||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
var msg bytes.Buffer
|
var msg bytes.Buffer
|
||||||
io.CopyN(&msg, resp.Body, 4096)
|
_, _ = io.CopyN(&msg, resp.Body, 4096) // ignore error, just display what we got
|
||||||
return errors.Errorf("%s", msg.String())
|
return errors.Errorf("%s", msg.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
MigrateCmd = &cli.Subcommand{
|
||||||
|
Use: "migrate",
|
||||||
|
Short: "perform migration of the on-disk / zfs properties",
|
||||||
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
|
return migrations
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var migrations = []*cli.Subcommand{
|
||||||
|
&cli.Subcommand{
|
||||||
|
Use: "0.0.X:0.1:placeholder",
|
||||||
|
Run: doMigratePlaceholder0_1,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cli.Subcommand{
|
||||||
|
Use: "replication-cursor:v1-v2",
|
||||||
|
Run: doMigrateReplicationCursor,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&migrateReplicationCursorArgs.dryRun, "dry-run", false, "dry run")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var migratePlaceholder0_1Args struct {
|
||||||
|
dryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := sc.Config()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot list filesystems")
|
||||||
|
}
|
||||||
|
|
||||||
|
type workItem struct {
|
||||||
|
jobName string
|
||||||
|
rootFS *zfs.DatasetPath
|
||||||
|
fss []*zfs.DatasetPath
|
||||||
|
}
|
||||||
|
var wis []workItem
|
||||||
|
for i, j := range cfg.Jobs {
|
||||||
|
var rfsS string
|
||||||
|
switch job := j.Ret.(type) {
|
||||||
|
case *config.SinkJob:
|
||||||
|
rfsS = job.RootFS
|
||||||
|
case *config.PullJob:
|
||||||
|
rfsS = job.RootFS
|
||||||
|
default:
|
||||||
|
fmt.Printf("ignoring job %q (%d/%d, type %T)\n", j.Name(), i, len(cfg.Jobs), j.Ret)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rfs, err := zfs.NewDatasetPath(rfsS)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "root fs for job %q is not a valid dataset path", j.Name())
|
||||||
|
}
|
||||||
|
var fss []*zfs.DatasetPath
|
||||||
|
for _, fs := range allFSS {
|
||||||
|
if fs.HasPrefix(rfs) {
|
||||||
|
fss = append(fss, fs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wis = append(wis, workItem{j.Name(), rfs, fss})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, wi := range wis {
|
||||||
|
fmt.Printf("job %q => migrate filesystems below root_fs %q\n", wi.jobName, wi.rootFS.ToString())
|
||||||
|
if len(wi.fss) == 0 {
|
||||||
|
fmt.Printf("\tno filesystems\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, fs := range wi.fss {
|
||||||
|
fmt.Printf("\t%q ... ", fs.ToString())
|
||||||
|
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(fs, migratePlaceholder0_1Args.dryRun)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error: %s\n", err)
|
||||||
|
} else if !r.NeedsModification {
|
||||||
|
fmt.Printf("unchanged (placeholder=%v)\n", r.OriginalState.IsPlaceholder)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("migrate (placeholder=%v) (old value = %q)\n",
|
||||||
|
r.OriginalState.IsPlaceholder, r.OriginalState.RawLocalPropertyValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var migrateReplicationCursorArgs struct {
|
||||||
|
dryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var bold = color.New(color.Bold)
|
||||||
|
var succ = color.New(color.FgGreen)
|
||||||
|
var fail = color.New(color.FgRed)
|
||||||
|
|
||||||
|
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
|
||||||
|
|
||||||
|
func doMigrateReplicationCursor(sc *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := sc.Config()
|
||||||
|
jobs, err := job.JobsFromConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
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")
|
||||||
|
return fmt.Errorf("exiting migration after error")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
|
||||||
|
for i, j := range cfg.Jobs {
|
||||||
|
if jobs[i].Name() != j.Name() {
|
||||||
|
panic("implementation error")
|
||||||
|
}
|
||||||
|
switch j.Ret.(type) {
|
||||||
|
case *config.PushJob:
|
||||||
|
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||||
|
case *config.SourceJob:
|
||||||
|
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||||
|
default:
|
||||||
|
fmt.Printf("ignoring job %q (%d/%d, type %T), not supposed to create v1 replication cursors\n", j.Name(), i, len(cfg.Jobs), j.Ret)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan all filesystems for v1 replication cursors
|
||||||
|
|
||||||
|
fss, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "list filesystems")
|
||||||
|
}
|
||||||
|
|
||||||
|
var hadError bool
|
||||||
|
for _, fs := range fss {
|
||||||
|
|
||||||
|
bold.Printf("INSPECT FILESYTEM %q\n", fs.ToString())
|
||||||
|
|
||||||
|
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
||||||
|
if err == migrateReplicationCursorSkipSentinel {
|
||||||
|
bold.Printf("FILESYSTEM SKIPPED\n")
|
||||||
|
} else if err != nil {
|
||||||
|
hadError = true
|
||||||
|
fail.Printf("MIGRATION FAILED: %s\n", err)
|
||||||
|
} else {
|
||||||
|
succ.Printf("FILESYSTEM %q COMPLETE\n", fs.ToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hadError {
|
||||||
|
fail.Printf("\n\none or more filesystems could not be migrated, please inspect output and or re-run migration")
|
||||||
|
return errors.Errorf("")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, fs *zfs.DatasetPath) error {
|
||||||
|
|
||||||
|
var owningJob job.Job = nil
|
||||||
|
for _, job := range v1CursorJobs {
|
||||||
|
conf := job.SenderConfig()
|
||||||
|
if conf == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pass, err := conf.FSF.Filter(fs)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "filesystem filter error in job %q for fs %q", job.Name(), fs.ToString())
|
||||||
|
}
|
||||||
|
if !pass {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if owningJob != nil {
|
||||||
|
return errors.Errorf("jobs %q and %q both match %q\ncannot attribute replication cursor to either one", owningJob.Name(), job.Name(), fs)
|
||||||
|
}
|
||||||
|
owningJob = job
|
||||||
|
}
|
||||||
|
if owningJob == nil {
|
||||||
|
fmt.Printf("no job's Filesystems filter matches\n")
|
||||||
|
return migrateReplicationCursorSkipSentinel
|
||||||
|
}
|
||||||
|
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
||||||
|
|
||||||
|
versions, err := zfs.ZFSListFilesystemVersions(fs, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldCursor *zfs.FilesystemVersion
|
||||||
|
for i, fsv := range versions {
|
||||||
|
if fsv.Type != zfs.Bookmark {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
||||||
|
if err != endpoint.ErrV1ReplicationCursor {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCursor != nil {
|
||||||
|
fmt.Printf("unexpected v1 replication cursor candidate: %q", fsv.ToAbsPath(fs))
|
||||||
|
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldCursor = &versions[i]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCursor == nil {
|
||||||
|
bold.Printf("no v1 replication cursor found for filesystem %q\n", fs.ToString())
|
||||||
|
return migrateReplicationCursorSkipSentinel
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("found v1 replication cursor:\n%s\n", pretty.Sprint(oldCursor))
|
||||||
|
|
||||||
|
mostRecentNew, err := endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs.ToString(), owningJob.SenderConfig().JobID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "get most recent v2 replication cursor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mostRecentNew == nil {
|
||||||
|
return errors.Errorf("no v2 replication cursor found for job %q on filesystem %q", owningJob.SenderConfig().JobID, fs.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("most recent v2 replication cursor:\n%#v", oldCursor)
|
||||||
|
|
||||||
|
if !(mostRecentNew.CreateTXG >= oldCursor.CreateTXG) {
|
||||||
|
return errors.Errorf("v1 replication cursor createtxg is higher than v2 cursor's, skipping this filesystem")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("determined that v2 cursor is bookmark of same or newer version than v1 cursor\n")
|
||||||
|
fmt.Printf("destroying v1 cursor %q\n", oldCursor.ToAbsPath(fs))
|
||||||
|
|
||||||
|
if migrateReplicationCursorArgs.dryRun {
|
||||||
|
succ.Printf("DRY RUN\n")
|
||||||
|
} else {
|
||||||
|
if err := zfs.ZFSDestroyFilesystemVersion(fs, oldCursor); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestMigrationsUnambiguousNames(t *testing.T) {
|
||||||
|
names := make(map[string]bool)
|
||||||
|
for _, mig := range migrations {
|
||||||
|
if _, ok := names[mig.Use]; ok {
|
||||||
|
t.Errorf("duplicate migration name %q", mig.Use)
|
||||||
|
t.FailNow()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
names[mig.Use] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -2,11 +2,12 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon"
|
"github.com/zrepl/zrepl/daemon"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var pprofArgs struct {
|
var pprofArgs struct {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon"
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
|||||||
+443
-179
@@ -2,16 +2,6 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gdamore/tcell/termbox"
|
|
||||||
"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/replication"
|
|
||||||
"github.com/zrepl/zrepl/replication/fsrep"
|
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -20,8 +10,72 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
// tcell is the termbox-compatbile 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 {
|
type tui struct {
|
||||||
x, y int
|
x, y int
|
||||||
indent int
|
indent int
|
||||||
@@ -29,20 +83,23 @@ type tui struct {
|
|||||||
lock sync.Mutex //For report and error
|
lock sync.Mutex //For report and error
|
||||||
report map[string]job.Status
|
report map[string]job.Status
|
||||||
err error
|
err error
|
||||||
|
|
||||||
|
jobFilter string
|
||||||
|
|
||||||
|
replicationProgress map[string]*bytesProgressHistory // by job name
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTui() tui {
|
func newTui() tui {
|
||||||
return tui{}
|
return tui{
|
||||||
|
replicationProgress: make(map[string]*bytesProgressHistory),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tui) moveCursor(x, y int) {
|
const INDENT_MULTIPLIER = 4
|
||||||
t.x += x
|
|
||||||
t.y += y
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) moveLine(dl int, col int) {
|
func (t *tui) moveLine(dl int, col int) {
|
||||||
t.y += dl
|
t.y += dl
|
||||||
t.x = t.indent*4 + col
|
t.x = t.indent*INDENT_MULTIPLIER + col
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tui) write(text string) {
|
func (t *tui) write(text string) {
|
||||||
@@ -60,6 +117,40 @@ func (t *tui) printf(text string, a ...interface{}) {
|
|||||||
t.write(fmt.Sprintf(text, a...))
|
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() {
|
func (t *tui) newline() {
|
||||||
t.moveLine(1, 0)
|
t.moveLine(1, 0)
|
||||||
}
|
}
|
||||||
@@ -76,6 +167,7 @@ func (t *tui) addIndent(indent int) {
|
|||||||
|
|
||||||
var statusFlags struct {
|
var statusFlags struct {
|
||||||
Raw bool
|
Raw bool
|
||||||
|
Job string
|
||||||
}
|
}
|
||||||
|
|
||||||
var StatusCmd = &cli.Subcommand{
|
var StatusCmd = &cli.Subcommand{
|
||||||
@@ -83,6 +175,7 @@ var StatusCmd = &cli.Subcommand{
|
|||||||
Short: "show job activity or dump as JSON for monitoring",
|
Short: "show job activity or dump as JSON for monitoring",
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
||||||
|
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
||||||
},
|
},
|
||||||
Run: runStatus,
|
Run: runStatus,
|
||||||
}
|
}
|
||||||
@@ -94,14 +187,17 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if statusFlags.Raw {
|
if statusFlags.Raw {
|
||||||
resp, err := httpc.Get("http://unix"+daemon.ControlJobEndpointStatus)
|
resp, err := httpc.Get("http://unix" + daemon.ControlJobEndpointStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
||||||
io.CopyN(os.Stderr, resp.Body, 4096)
|
_, err := io.CopyN(os.Stderr, resp.Body, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return errors.Errorf("exit")
|
return errors.Errorf("exit")
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
||||||
@@ -114,6 +210,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
t.lock.Lock()
|
t.lock.Lock()
|
||||||
t.err = errors.New("Got no report yet")
|
t.err = errors.New("Got no report yet")
|
||||||
t.lock.Unlock()
|
t.lock.Unlock()
|
||||||
|
t.jobFilter = statusFlags.Job
|
||||||
|
|
||||||
err = termbox.Init()
|
err = termbox.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -140,7 +237,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
go func() {
|
go func() {
|
||||||
for _ = range ticker.C {
|
for range ticker.C {
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -167,6 +264,15 @@ loop:
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
|
||||||
|
p, ok := t.replicationProgress[jobName]
|
||||||
|
if !ok {
|
||||||
|
p = &bytesProgressHistory{}
|
||||||
|
t.replicationProgress[jobName] = p
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
func (t *tui) draw() {
|
func (t *tui) draw() {
|
||||||
t.lock.Lock()
|
t.lock.Lock()
|
||||||
defer t.lock.Unlock()
|
defer t.lock.Unlock()
|
||||||
@@ -180,19 +286,29 @@ func (t *tui) draw() {
|
|||||||
t.write(t.err.Error())
|
t.write(t.err.Error())
|
||||||
} else {
|
} else {
|
||||||
//Iterate over map in alphabetical order
|
//Iterate over map in alphabetical order
|
||||||
keys := make([]string, len(t.report))
|
keys := make([]string, 0, len(t.report))
|
||||||
i := 0
|
for k := range t.report {
|
||||||
for k, _ := range t.report {
|
|
||||||
keys[i] = k
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
v := t.report[k]
|
|
||||||
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
||||||
continue
|
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.setIndent(0)
|
||||||
|
|
||||||
t.printf("Job: %s", k)
|
t.printf("Job: %s", k)
|
||||||
@@ -202,7 +318,66 @@ func (t *tui) draw() {
|
|||||||
t.setIndent(1)
|
t.setIndent(1)
|
||||||
t.newline()
|
t.newline()
|
||||||
|
|
||||||
if v.Type != job.TypePush && v.Type != job.TypePull {
|
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.getReplicationProgresHistory(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.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||||
t.newline()
|
t.newline()
|
||||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
asYaml, err := yaml.Marshal(v.JobSpecific)
|
||||||
@@ -215,103 +390,107 @@ func (t *tui) draw() {
|
|||||||
t.newline()
|
t.newline()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pushStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
|
||||||
if !ok || pushStatus == nil {
|
|
||||||
t.printf("ActiveSideStatus is null")
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.printf("Replication:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderReplicationReport(pushStatus.Replication)
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
t.printf("Pruning Sender:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderPrunerReport(pushStatus.PruningSender)
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
t.printf("Pruning Receiver:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderPrunerReport(pushStatus.PruningReceiver)
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
termbox.Flush()
|
termbox.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tui) renderReplicationReport(rep *replication.Report) {
|
func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgressHistory) {
|
||||||
if rep == nil {
|
if rep == nil {
|
||||||
t.printf("...\n")
|
t.printf("...\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
all := make([]*fsrep.Report, 0, len(rep.Completed)+len(rep.Pending) + 1)
|
if rep.WaitReconnectError != nil {
|
||||||
all = append(all, rep.Completed...)
|
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
||||||
all = append(all, rep.Pending...)
|
t.newline()
|
||||||
if rep.Active != nil {
|
|
||||||
all = append(all, rep.Active)
|
|
||||||
}
|
}
|
||||||
sort.Slice(all, func(i, j int) bool {
|
if !rep.WaitReconnectSince.IsZero() {
|
||||||
return all[i].Filesystem < all[j].Filesystem
|
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
|
||||||
})
|
})
|
||||||
|
|
||||||
state, err := replication.StateString(rep.Status)
|
t.printf("Status: %s", latest.State)
|
||||||
if err != nil {
|
|
||||||
t.printf("Status: %q (parse error: %q)\n", rep.Status, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.printf("Status: %s", state)
|
|
||||||
t.newline()
|
t.newline()
|
||||||
if rep.Problem != "" {
|
if latest.State == report.AttemptPlanningError {
|
||||||
t.printf("Problem: %s", rep.Problem)
|
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()
|
t.newline()
|
||||||
}
|
}
|
||||||
if rep.SleepUntil.After(time.Now()) && !state.IsTerminal() {
|
|
||||||
t.printf("Sleeping until %s (%s left)\n", rep.SleepUntil, rep.SleepUntil.Sub(time.Now()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if state != replication.Planning && state != replication.PlanningError {
|
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||||
|
// Draw global progress bar
|
||||||
// Progress: [---------------]
|
// Progress: [---------------]
|
||||||
sumUpFSRep := func(rep *fsrep.Report) (transferred, total int64) {
|
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||||
for _, s := range rep.Pending {
|
rate, changeCount := history.Update(replicated)
|
||||||
transferred += s.Bytes
|
eta := time.Duration(0)
|
||||||
total += s.ExpectedBytes
|
if rate > 0 {
|
||||||
}
|
eta = time.Duration((expected-replicated)/rate) * time.Second
|
||||||
for _, s := range rep.Completed {
|
|
||||||
transferred += s.Bytes
|
|
||||||
total += s.ExpectedBytes
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var transferred, total int64
|
|
||||||
for _, fs := range all {
|
|
||||||
fstx, fstotal := sumUpFSRep(fs)
|
|
||||||
transferred += fstx
|
|
||||||
total += fstotal
|
|
||||||
}
|
}
|
||||||
t.write("Progress: ")
|
t.write("Progress: ")
|
||||||
t.drawBar(80, transferred, total)
|
t.drawBar(50, replicated, expected, changeCount)
|
||||||
t.write(fmt.Sprintf(" %s / %s", ByteCountBinary(transferred), ByteCountBinary(total)))
|
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()
|
t.newline()
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxFSLen int
|
var maxFSLen int
|
||||||
for _, fs := range all {
|
for _, fs := range latest.Filesystems {
|
||||||
if len(fs.Filesystem) > maxFSLen {
|
if len(fs.Info.Name) > maxFSLen {
|
||||||
maxFSLen = len(fs.Filesystem)
|
maxFSLen = len(fs.Info.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, fs := range all {
|
for _, fs := range latest.Filesystems {
|
||||||
printFilesystemStatus(fs, t, fs == rep.Active, maxFSLen)
|
t.printFilesystemStatus(fs, false, maxFSLen) // FIXME bring 'active' flag back
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tui) renderPrunerReport(r *pruner.Report) {
|
func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||||
@@ -332,15 +511,12 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|||||||
if r.Error != "" {
|
if r.Error != "" {
|
||||||
t.printf("Error: %s\n", r.Error)
|
t.printf("Error: %s\n", r.Error)
|
||||||
}
|
}
|
||||||
if r.SleepUntil.After(time.Now()) {
|
|
||||||
t.printf("Sleeping until %s (%s left)\n", r.SleepUntil, r.SleepUntil.Sub(time.Now()))
|
|
||||||
}
|
|
||||||
|
|
||||||
type commonFS struct {
|
type commonFS struct {
|
||||||
*pruner.FSReport
|
*pruner.FSReport
|
||||||
completed bool
|
completed bool
|
||||||
}
|
}
|
||||||
all := make([]commonFS, 0, len(r.Pending) + len(r.Completed))
|
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
||||||
for i := range r.Pending {
|
for i := range r.Pending {
|
||||||
all = append(all, commonFS{&r.Pending[i], false})
|
all = append(all, commonFS{&r.Pending[i], false})
|
||||||
}
|
}
|
||||||
@@ -349,9 +525,9 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch state {
|
switch state {
|
||||||
case pruner.Plan: fallthrough
|
case pruner.Plan:
|
||||||
case pruner.PlanWait: fallthrough
|
fallthrough
|
||||||
case pruner.ErrPerm:
|
case pruner.PlanErr:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +554,7 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|||||||
t.write("[")
|
t.write("[")
|
||||||
t.write(times("=", progress))
|
t.write(times("=", progress))
|
||||||
t.write(">")
|
t.write(">")
|
||||||
t.write(times("-", 80 - progress))
|
t.write(times("-", 80-progress))
|
||||||
t.write("]")
|
t.write("]")
|
||||||
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||||
t.newline()
|
t.newline()
|
||||||
@@ -391,8 +567,18 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|||||||
for _, fs := range all {
|
for _, fs := range all {
|
||||||
t.write(rightPad(fs.Filesystem, maxFSname, " "))
|
t.write(rightPad(fs.Filesystem, maxFSname, " "))
|
||||||
t.write(" ")
|
t.write(" ")
|
||||||
if fs.Error != "" {
|
if !fs.SkipReason.NotSkipped() {
|
||||||
t.printf("ERROR: %s\n", fs.Error) // whitespace is padding
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +586,7 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|||||||
len(fs.DestroyList), len(fs.SnapshotList))
|
len(fs.DestroyList), len(fs.SnapshotList))
|
||||||
|
|
||||||
if fs.completed {
|
if fs.completed {
|
||||||
t.printf( "Completed %s\n", pruneRuleActionStr)
|
t.printf("Completed %s\n", pruneRuleActionStr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,23 +601,84 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshotIndent = 1
|
func (t *tui) renderSnapperReport(r *snapper.Report) {
|
||||||
func calculateMaxFSLength(all []*fsrep.Report) (maxFS, maxStatus int) {
|
if r == nil {
|
||||||
for _, e := range all {
|
t.printf("<snapshot type does not have a report>\n")
|
||||||
if len(e.Filesystem) > maxFS {
|
|
||||||
maxFS = len(e.Filesystem)
|
|
||||||
}
|
|
||||||
all2 := make([]*fsrep.StepReport, 0, len(e.Pending) + len(e.Completed))
|
|
||||||
all2 = append(all2, e.Pending...)
|
|
||||||
all2 = append(all2, e.Completed...)
|
|
||||||
for _, e2 := range all2 {
|
|
||||||
elen := len(e2.Problem) + len(e2.From) + len(e2.To) + 60 // random spacing, units, labels, etc
|
|
||||||
if elen > maxStatus {
|
|
||||||
maxStatus = elen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
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) {
|
func times(str string, n int) (out string) {
|
||||||
@@ -445,18 +692,13 @@ func rightPad(str string, length int, pad string) string {
|
|||||||
if len(str) > length {
|
if len(str) > length {
|
||||||
return str[:length]
|
return str[:length]
|
||||||
}
|
}
|
||||||
return str + times(pad, length-len(str))
|
return str + strings.Repeat(pad, length-len(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var arrowPositions = `>\|/`
|
||||||
|
|
||||||
func leftPad(str string, length int, pad string) string {
|
// changeCount = 0 indicates stall / no progresss
|
||||||
if len(str) > length {
|
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||||
return str[len(str)-length:]
|
|
||||||
}
|
|
||||||
return times(pad, length-len(str)) + str
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) drawBar(length int, bytes, totalBytes int64) {
|
|
||||||
var completedLength int
|
var completedLength int
|
||||||
if totalBytes > 0 {
|
if totalBytes > 0 {
|
||||||
completedLength = int(int64(length) * bytes / totalBytes)
|
completedLength = int(int64(length) * bytes / totalBytes)
|
||||||
@@ -469,66 +711,64 @@ func (t *tui) drawBar(length int, bytes, totalBytes int64) {
|
|||||||
|
|
||||||
t.write("[")
|
t.write("[")
|
||||||
t.write(times("=", completedLength))
|
t.write(times("=", completedLength))
|
||||||
t.write(">")
|
t.write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||||
t.write(times("-", length-completedLength))
|
t.write(times("-", length-completedLength))
|
||||||
t.write("]")
|
t.write("]")
|
||||||
}
|
}
|
||||||
|
|
||||||
func StringStepState(s fsrep.StepState) string {
|
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
||||||
switch s {
|
|
||||||
case fsrep.StepReplicationReady: return "Ready"
|
|
||||||
case fsrep.StepReplicationRetry: return "Retry"
|
|
||||||
case fsrep.StepMarkReplicatedReady: return "MarkReady"
|
|
||||||
case fsrep.StepMarkReplicatedRetry: return "MarkRetry"
|
|
||||||
case fsrep.StepPermanentError: return "PermanentError"
|
|
||||||
case fsrep.StepCompleted: return "Completed"
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("UNKNOWN %d", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filesystemStatusString(rep *fsrep.Report, active bool, fsWidth int) (line string, bytes, totalBytes int64) {
|
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||||
bytes = int64(0)
|
sizeEstimationImpreciseNotice := ""
|
||||||
totalBytes = int64(0)
|
if containsInvalidSizeEstimates {
|
||||||
for _, s := range rep.Pending {
|
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||||
bytes += s.Bytes
|
|
||||||
totalBytes += s.ExpectedBytes
|
|
||||||
}
|
}
|
||||||
for _, s := range rep.Completed {
|
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||||
bytes += s.Bytes
|
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||||
totalBytes += s.ExpectedBytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next := ""
|
|
||||||
if rep.Problem != "" {
|
|
||||||
next = " problem: " + rep.Problem
|
|
||||||
} else if len(rep.Pending) > 0 {
|
|
||||||
if rep.Pending[0].From != "" {
|
|
||||||
next = fmt.Sprintf(" next: %s => %s", rep.Pending[0].From, rep.Pending[0].To)
|
|
||||||
} else {
|
|
||||||
next = fmt.Sprintf(" next: %s (full)", rep.Pending[0].To)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||||
rep.Status,
|
strings.ToUpper(string(rep.State)),
|
||||||
len(rep.Completed), len(rep.Pending) + len(rep.Completed),
|
rep.CurrentStep, len(rep.Steps),
|
||||||
ByteCountBinary(bytes), ByteCountBinary(totalBytes),
|
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||||
next,
|
sizeEstimationImpreciseNotice,
|
||||||
)
|
)
|
||||||
|
|
||||||
activeIndicator := " "
|
activeIndicator := " "
|
||||||
if active {
|
if active {
|
||||||
activeIndicator = "*"
|
activeIndicator = "*"
|
||||||
}
|
}
|
||||||
line = fmt.Sprintf("%s %s %s",
|
t.printf("%s %s %s ",
|
||||||
activeIndicator,
|
activeIndicator,
|
||||||
rightPad(rep.Filesystem, fsWidth, " "),
|
rightPad(rep.Info.Name, maxFS, " "),
|
||||||
status)
|
status)
|
||||||
return line, bytes, totalBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func printFilesystemStatus(rep *fsrep.Report, t *tui, active bool, maxFS int) {
|
next := ""
|
||||||
totalStatus, _, _ := filesystemStatusString(rep, active, maxFS)
|
if err := rep.Error(); err != nil {
|
||||||
t.write(totalStatus)
|
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()
|
t.newline()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,3 +784,27 @@ func ByteCountBinary(b int64) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[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,13 +1,15 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/problame/go-netssh"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/problame/go-netssh"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"log"
|
"log"
|
||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
|||||||
+106
-4
@@ -1,18 +1,24 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var TestCmd = &cli.Subcommand {
|
var TestCmd = &cli.Subcommand{
|
||||||
Use: "test",
|
Use: "test",
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
return []*cli.Subcommand{testFilter}
|
return []*cli.Subcommand{testFilter, testPlaceholder, testDecodeResumeToken}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +56,10 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
switch j := job.Ret.(type) {
|
switch j := job.Ret.(type) {
|
||||||
case *config.SourceJob: confFilter = j.Filesystems
|
case *config.SourceJob:
|
||||||
case *config.PushJob: confFilter = j.Filesystems
|
confFilter = j.Filesystems
|
||||||
|
case *config.PushJob:
|
||||||
|
confFilter = j.Filesystems
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
||||||
}
|
}
|
||||||
@@ -106,3 +114,97 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var testPlaceholderArgs struct {
|
||||||
|
ds string
|
||||||
|
all bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var testPlaceholder = &cli.Subcommand{
|
||||||
|
Use: "placeholder [--all | --dataset DATASET]",
|
||||||
|
Short: fmt.Sprintf("list received placeholder filesystems (zfs property %q)", zfs.PlaceholderPropertyName),
|
||||||
|
Example: `
|
||||||
|
placeholder --all
|
||||||
|
placeholder --dataset path/to/sink/clientident/fs`,
|
||||||
|
NoRequireConfig: true,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.StringVar(&testPlaceholderArgs.ds, "dataset", "", "dataset path (not required to exist)")
|
||||||
|
f.BoolVar(&testPlaceholderArgs.all, "all", false, "list tab-separated placeholder status of all filesystems")
|
||||||
|
},
|
||||||
|
Run: runTestPlaceholder,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
|
var checkDPs []*zfs.DatasetPath
|
||||||
|
|
||||||
|
// all actions first
|
||||||
|
if testPlaceholderArgs.all {
|
||||||
|
out, err := zfs.ZFSList([]string{"name"})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not list ZFS filesystems")
|
||||||
|
}
|
||||||
|
for _, row := range out {
|
||||||
|
dp, err := zfs.NewDatasetPath(row[0])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
checkDPs = append(checkDPs, dp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if dp.Empty() {
|
||||||
|
return fmt.Errorf("must specify --dataset DATASET or --all")
|
||||||
|
}
|
||||||
|
checkDPs = append(checkDPs, dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
|
||||||
|
for _, dp := range checkDPs {
|
||||||
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(dp)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot get placeholder state")
|
||||||
|
}
|
||||||
|
if !ph.FSExists {
|
||||||
|
panic("placeholder state inconsistent: filesystem " + ph.FS + " must exist in this context")
|
||||||
|
}
|
||||||
|
is := "yes"
|
||||||
|
if !ph.IsPlaceholder {
|
||||||
|
is = "no"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\t%s\t%s\n", is, dp.ToString(), ph.RawLocalPropertyValue)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var testDecodeResumeTokenArgs struct {
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
var testDecodeResumeToken = &cli.Subcommand{
|
||||||
|
Use: "decoderesumetoken --token TOKEN",
|
||||||
|
Short: "decode resume token",
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.StringVar(&testDecodeResumeTokenArgs.token, "token", "", "the resume token obtained from the receive_resume_token property")
|
||||||
|
},
|
||||||
|
Run: runTestDecodeResumeTokenCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestDecodeResumeTokenCmd(subcommand *cli.Subcommand, args []string) error {
|
||||||
|
if testDecodeResumeTokenArgs.token == "" {
|
||||||
|
return fmt.Errorf("token argument must be specified")
|
||||||
|
}
|
||||||
|
token, err := zfs.ParseResumeToken(context.Background(), testDecodeResumeTokenArgs.token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err := enc.Encode(&token); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+3
-1
@@ -2,12 +2,14 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon"
|
"github.com/zrepl/zrepl/daemon"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var versionArgs struct {
|
var versionArgs struct {
|
||||||
|
|||||||
+206
-32
@@ -2,14 +2,16 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log/syslog"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zrepl/yaml-config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -33,12 +35,18 @@ type JobEnum struct {
|
|||||||
func (j JobEnum) Name() string {
|
func (j JobEnum) Name() string {
|
||||||
var name string
|
var name string
|
||||||
switch v := j.Ret.(type) {
|
switch v := j.Ret.(type) {
|
||||||
case *PushJob: name = v.Name
|
case *SnapJob:
|
||||||
case *SinkJob: name = v.Name
|
name = v.Name
|
||||||
case *PullJob: name = v.Name
|
case *PushJob:
|
||||||
case *SourceJob: name = v.Name
|
name = v.Name
|
||||||
|
case *SinkJob:
|
||||||
|
name = v.Name
|
||||||
|
case *PullJob:
|
||||||
|
name = v.Name
|
||||||
|
case *SourceJob:
|
||||||
|
name = v.Name
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknownn job type %T", v))
|
panic(fmt.Sprintf("unknown job type %T", v))
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
@@ -58,27 +66,95 @@ type PassiveJob struct {
|
|||||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SnapJob struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Pruning PruningLocal `yaml:"pruning"`
|
||||||
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendOptions struct {
|
||||||
|
Encrypted bool `yaml:"encrypted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Defaulter = (*SendOptions)(nil)
|
||||||
|
|
||||||
|
func (l *SendOptions) SetDefault() {
|
||||||
|
*l = SendOptions{Encrypted: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecvOptions struct {
|
||||||
|
// Note: we cannot enforce encrypted recv as the ZFS cli doesn't provide a mechanism for it
|
||||||
|
// Encrypted bool `yaml:"may_encrypted"`
|
||||||
|
|
||||||
|
// Future:
|
||||||
|
// Reencrypt bool `yaml:"reencrypt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Defaulter = (*RecvOptions)(nil)
|
||||||
|
|
||||||
|
func (l *RecvOptions) SetDefault() {
|
||||||
|
*l = RecvOptions{}
|
||||||
|
}
|
||||||
|
|
||||||
type PushJob struct {
|
type PushJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PullJob struct {
|
type PullJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
RootFS string `yaml:"root_fs"`
|
RootFS string `yaml:"root_fs"`
|
||||||
Interval time.Duration `yaml:"interval,positive"`
|
Interval PositiveDurationOrManual `yaml:"interval"`
|
||||||
|
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PositiveDurationOrManual struct {
|
||||||
|
Interval time.Duration
|
||||||
|
Manual bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Unmarshaler = (*PositiveDurationOrManual)(nil)
|
||||||
|
|
||||||
|
func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||||
|
var s string
|
||||||
|
if err := u(&s, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch s {
|
||||||
|
case "manual":
|
||||||
|
i.Manual = true
|
||||||
|
i.Interval = 0
|
||||||
|
case "":
|
||||||
|
return fmt.Errorf("value must not be empty")
|
||||||
|
default:
|
||||||
|
i.Manual = false
|
||||||
|
i.Interval, err = time.ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if i.Interval <= 0 {
|
||||||
|
return fmt.Errorf("value must be a positive duration, got %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SinkJob struct {
|
type SinkJob struct {
|
||||||
PassiveJob `yaml:",inline"`
|
PassiveJob `yaml:",inline"`
|
||||||
RootFS string `yaml:"root_fs"`
|
RootFS string `yaml:"root_fs"`
|
||||||
|
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SourceJob struct {
|
type SourceJob struct {
|
||||||
PassiveJob `yaml:",inline"`
|
PassiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilesystemsFilter map[string]bool
|
type FilesystemsFilter map[string]bool
|
||||||
@@ -91,6 +167,7 @@ type SnapshottingPeriodic struct {
|
|||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Prefix string `yaml:"prefix"`
|
Prefix string `yaml:"prefix"`
|
||||||
Interval time.Duration `yaml:"interval,positive"`
|
Interval time.Duration `yaml:"interval,positive"`
|
||||||
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshottingManual struct {
|
type SnapshottingManual struct {
|
||||||
@@ -130,7 +207,6 @@ type Global struct {
|
|||||||
Monitoring []MonitoringEnum `yaml:"monitoring,optional"`
|
Monitoring []MonitoringEnum `yaml:"monitoring,optional"`
|
||||||
Control *GlobalControl `yaml:"control,optional,fromdefaults"`
|
Control *GlobalControl `yaml:"control,optional,fromdefaults"`
|
||||||
Serve *GlobalServe `yaml:"serve,optional,fromdefaults"`
|
Serve *GlobalServe `yaml:"serve,optional,fromdefaults"`
|
||||||
RPC *RPCConfig `yaml:"rpc,optional,fromdefaults"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Default(i interface{}) {
|
func Default(i interface{}) {
|
||||||
@@ -145,39 +221,28 @@ func Default(i interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RPCConfig struct {
|
|
||||||
Timeout time.Duration `yaml:"timeout,optional,positive,default=10s"`
|
|
||||||
TxChunkSize uint32 `yaml:"tx_chunk_size,optional,default=32768"`
|
|
||||||
RxStructuredMaxLen uint32 `yaml:"rx_structured_max,optional,default=16777216"`
|
|
||||||
RxStreamChunkMaxLen uint32 `yaml:"rx_stream_chunk_max,optional,default=16777216"`
|
|
||||||
RxHeaderMaxLen uint32 `yaml:"rx_header_max,optional,default=40960"`
|
|
||||||
SendHeartbeatInterval time.Duration `yaml:"send_heartbeat_interval,optional,positive,default=5s"`
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectEnum struct {
|
type ConnectEnum struct {
|
||||||
Ret interface{}
|
Ret interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectCommon struct {
|
type ConnectCommon struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
RPC *RPCConfig `yaml:"rpc,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TCPConnect struct {
|
type TCPConnect struct {
|
||||||
ConnectCommon `yaml:",inline"`
|
ConnectCommon `yaml:",inline"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address,hostport"`
|
||||||
DialTimeout time.Duration `yaml:"dial_timeout,positive,default=10s"`
|
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLSConnect struct {
|
type TLSConnect struct {
|
||||||
ConnectCommon `yaml:",inline"`
|
ConnectCommon `yaml:",inline"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address,hostport"`
|
||||||
Ca string `yaml:"ca"`
|
Ca string `yaml:"ca"`
|
||||||
Cert string `yaml:"cert"`
|
Cert string `yaml:"cert"`
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
ServerCN string `yaml:"server_cn"`
|
ServerCN string `yaml:"server_cn"`
|
||||||
DialTimeout time.Duration `yaml:"dial_timeout,positive,default=10s"`
|
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SSHStdinserverConnect struct {
|
type SSHStdinserverConnect struct {
|
||||||
@@ -189,13 +254,14 @@ type SSHStdinserverConnect struct {
|
|||||||
TransportOpenCommand []string `yaml:"transport_open_command,optional"` //TODO unused
|
TransportOpenCommand []string `yaml:"transport_open_command,optional"` //TODO unused
|
||||||
SSHCommand string `yaml:"ssh_command,optional"` //TODO unused
|
SSHCommand string `yaml:"ssh_command,optional"` //TODO unused
|
||||||
Options []string `yaml:"options,optional"`
|
Options []string `yaml:"options,optional"`
|
||||||
DialTimeout time.Duration `yaml:"dial_timeout,positive,default=10s"`
|
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalConnect struct {
|
type LocalConnect struct {
|
||||||
ConnectCommon `yaml:",inline"`
|
ConnectCommon `yaml:",inline"`
|
||||||
ListenerName string `yaml:"listener_name"`
|
ListenerName string `yaml:"listener_name"`
|
||||||
ClientIdentity string `yaml:"client_identity"`
|
ClientIdentity string `yaml:"client_identity"`
|
||||||
|
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=2s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServeEnum struct {
|
type ServeEnum struct {
|
||||||
@@ -204,23 +270,24 @@ type ServeEnum struct {
|
|||||||
|
|
||||||
type ServeCommon struct {
|
type ServeCommon struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
RPC *RPCConfig `yaml:"rpc,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TCPServe struct {
|
type TCPServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
Clients map[string]string `yaml:"clients"`
|
Clients map[string]string `yaml:"clients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLSServe struct {
|
type TLSServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
Ca string `yaml:"ca"`
|
Ca string `yaml:"ca"`
|
||||||
Cert string `yaml:"cert"`
|
Cert string `yaml:"cert"`
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
ClientCNs []string `yaml:"client_cns"`
|
ClientCNs []string `yaml:"client_cns"`
|
||||||
HandshakeTimeout time.Duration `yaml:"handshake_timeout,positive,default=10s"`
|
HandshakeTimeout time.Duration `yaml:"handshake_timeout,zeropositive,default=10s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StdinserverServer struct {
|
type StdinserverServer struct {
|
||||||
@@ -250,6 +317,7 @@ type PruneKeepLastN struct {
|
|||||||
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Regex string `yaml:"regex"`
|
Regex string `yaml:"regex"`
|
||||||
|
Negate bool `yaml:"negate,optional,default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoggingOutletEnum struct {
|
type LoggingOutletEnum struct {
|
||||||
@@ -270,12 +338,13 @@ type StdoutLoggingOutlet struct {
|
|||||||
|
|
||||||
type SyslogLoggingOutlet struct {
|
type SyslogLoggingOutlet struct {
|
||||||
LoggingOutletCommon `yaml:",inline"`
|
LoggingOutletCommon `yaml:",inline"`
|
||||||
|
Facility *SyslogFacility `yaml:"facility,optional,fromdefaults"`
|
||||||
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TCPLoggingOutlet struct {
|
type TCPLoggingOutlet struct {
|
||||||
LoggingOutletCommon `yaml:",inline"`
|
LoggingOutletCommon `yaml:",inline"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address,hostport"`
|
||||||
Net string `yaml:"net,default=tcp"`
|
Net string `yaml:"net,default=tcp"`
|
||||||
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
||||||
TLS *TCPLoggingOutletTLS `yaml:"tls,optional"`
|
TLS *TCPLoggingOutletTLS `yaml:"tls,optional"`
|
||||||
@@ -293,9 +362,18 @@ type MonitoringEnum struct {
|
|||||||
|
|
||||||
type PrometheusMonitoring struct {
|
type PrometheusMonitoring struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SyslogFacility syslog.Priority
|
||||||
|
|
||||||
|
func (f *SyslogFacility) SetDefault() {
|
||||||
|
*f = SyslogFacility(syslog.LOG_LOCAL0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Defaulter = (*SyslogFacility)(nil)
|
||||||
|
|
||||||
type GlobalControl struct {
|
type GlobalControl struct {
|
||||||
SockPath string `yaml:"sockpath,default=/var/run/zrepl/control"`
|
SockPath string `yaml:"sockpath,default=/var/run/zrepl/control"`
|
||||||
}
|
}
|
||||||
@@ -316,6 +394,38 @@ type JobDebugSettings struct {
|
|||||||
RPCLog bool `yaml:"rpc_log,optional,default=false"`
|
RPCLog bool `yaml:"rpc_log,optional,default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HookList []HookEnum
|
||||||
|
|
||||||
|
type HookEnum struct {
|
||||||
|
Ret interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookCommand struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||||
|
Filesystems FilesystemsFilter `yaml:"filesystems,optional,default={'<': true}"`
|
||||||
|
HookSettingsCommon `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookPostgresCheckpoint struct {
|
||||||
|
HookSettingsCommon `yaml:",inline"`
|
||||||
|
DSN string `yaml:"dsn"`
|
||||||
|
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||||
|
Filesystems FilesystemsFilter `yaml:"filesystems"` // required, user should not CHECKPOINT for every FS
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookMySQLLockTables struct {
|
||||||
|
HookSettingsCommon `yaml:",inline"`
|
||||||
|
DSN string `yaml:"dsn"`
|
||||||
|
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||||
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookSettingsCommon struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
ErrIsFatal bool `yaml:"err_is_fatal,optional,default=false"`
|
||||||
|
}
|
||||||
|
|
||||||
func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}) (interface{}, error) {
|
func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}) (interface{}, error) {
|
||||||
var in struct {
|
var in struct {
|
||||||
Type string
|
Type string
|
||||||
@@ -339,6 +449,7 @@ func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}
|
|||||||
|
|
||||||
func (t *JobEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
func (t *JobEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||||
|
"snap": &SnapJob{},
|
||||||
"push": &PushJob{},
|
"push": &PushJob{},
|
||||||
"sink": &SinkJob{},
|
"sink": &SinkJob{},
|
||||||
"pull": &PullJob{},
|
"pull": &PullJob{},
|
||||||
@@ -362,7 +473,7 @@ func (t *ServeEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|||||||
"tcp": &TCPServe{},
|
"tcp": &TCPServe{},
|
||||||
"tls": &TLSServe{},
|
"tls": &TLSServe{},
|
||||||
"stdinserver": &StdinserverServer{},
|
"stdinserver": &StdinserverServer{},
|
||||||
"local" : &LocalServe{},
|
"local": &LocalServe{},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -401,6 +512,69 @@ func (t *MonitoringEnum) UnmarshalYAML(u func(interface{}, bool) error) (err err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *SyslogFacility) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||||
|
var s string
|
||||||
|
if err := u(&s, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var level syslog.Priority
|
||||||
|
switch s {
|
||||||
|
case "kern":
|
||||||
|
level = syslog.LOG_KERN
|
||||||
|
case "user":
|
||||||
|
level = syslog.LOG_USER
|
||||||
|
case "mail":
|
||||||
|
level = syslog.LOG_MAIL
|
||||||
|
case "daemon":
|
||||||
|
level = syslog.LOG_DAEMON
|
||||||
|
case "auth":
|
||||||
|
level = syslog.LOG_AUTH
|
||||||
|
case "syslog":
|
||||||
|
level = syslog.LOG_SYSLOG
|
||||||
|
case "lpr":
|
||||||
|
level = syslog.LOG_LPR
|
||||||
|
case "news":
|
||||||
|
level = syslog.LOG_NEWS
|
||||||
|
case "uucp":
|
||||||
|
level = syslog.LOG_UUCP
|
||||||
|
case "cron":
|
||||||
|
level = syslog.LOG_CRON
|
||||||
|
case "authpriv":
|
||||||
|
level = syslog.LOG_AUTHPRIV
|
||||||
|
case "ftp":
|
||||||
|
level = syslog.LOG_FTP
|
||||||
|
case "local0":
|
||||||
|
level = syslog.LOG_LOCAL0
|
||||||
|
case "local1":
|
||||||
|
level = syslog.LOG_LOCAL1
|
||||||
|
case "local2":
|
||||||
|
level = syslog.LOG_LOCAL2
|
||||||
|
case "local3":
|
||||||
|
level = syslog.LOG_LOCAL3
|
||||||
|
case "local4":
|
||||||
|
level = syslog.LOG_LOCAL4
|
||||||
|
case "local5":
|
||||||
|
level = syslog.LOG_LOCAL5
|
||||||
|
case "local6":
|
||||||
|
level = syslog.LOG_LOCAL6
|
||||||
|
case "local7":
|
||||||
|
level = syslog.LOG_LOCAL7
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid syslog level: %q", s)
|
||||||
|
}
|
||||||
|
*t = SyslogFacility(level)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HookEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||||
|
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||||
|
"command": &HookCommand{},
|
||||||
|
"postgres-checkpoint": &HookPostgresCheckpoint{},
|
||||||
|
"mysql-lock-tables": &HookMySQLLockTables{},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var ConfigFileDefaultLocations = []string{
|
var ConfigFileDefaultLocations = []string{
|
||||||
"/etc/zrepl/zrepl.yml",
|
"/etc/zrepl/zrepl.yml",
|
||||||
"/usr/local/etc/zrepl/zrepl.yml",
|
"/usr/local/etc/zrepl/zrepl.yml",
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/syslog"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"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"
|
"github.com/zrepl/yaml-config"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testValidGlobalSection(t *testing.T, s string) *Config {
|
func testValidGlobalSection(t *testing.T, s string) *Config {
|
||||||
@@ -22,7 +25,7 @@ jobs:
|
|||||||
`
|
`
|
||||||
_, err := ParseConfigBytes([]byte(jobdef))
|
_, err := ParseConfigBytes([]byte(jobdef))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return testValidConfig(t, s + jobdef)
|
return testValidConfig(t, s+jobdef)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOutletTypes(t *testing.T) {
|
func TestOutletTypes(t *testing.T) {
|
||||||
@@ -72,6 +75,36 @@ global:
|
|||||||
assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
||||||
|
type SyslogFacilityPriority struct {
|
||||||
|
Facility string
|
||||||
|
Priority syslog.Priority
|
||||||
|
}
|
||||||
|
syslogFacilitiesPriorities := []SyslogFacilityPriority{
|
||||||
|
{"", syslog.LOG_LOCAL0}, // default
|
||||||
|
{"kern", syslog.LOG_KERN}, {"daemon", syslog.LOG_DAEMON}, {"auth", syslog.LOG_AUTH},
|
||||||
|
{"syslog", syslog.LOG_SYSLOG}, {"lpr", syslog.LOG_LPR}, {"news", syslog.LOG_NEWS},
|
||||||
|
{"uucp", syslog.LOG_UUCP}, {"cron", syslog.LOG_CRON}, {"authpriv", syslog.LOG_AUTHPRIV},
|
||||||
|
{"ftp", syslog.LOG_FTP}, {"local0", syslog.LOG_LOCAL0}, {"local1", syslog.LOG_LOCAL1},
|
||||||
|
{"local2", syslog.LOG_LOCAL2}, {"local3", syslog.LOG_LOCAL3}, {"local4", syslog.LOG_LOCAL4},
|
||||||
|
{"local5", syslog.LOG_LOCAL5}, {"local6", syslog.LOG_LOCAL6}, {"local7", syslog.LOG_LOCAL7},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sFP := range syslogFacilitiesPriorities {
|
||||||
|
logcfg := fmt.Sprintf(`
|
||||||
|
global:
|
||||||
|
logging:
|
||||||
|
- type: syslog
|
||||||
|
level: info
|
||||||
|
format: human
|
||||||
|
facility: %s
|
||||||
|
`, sFP.Facility)
|
||||||
|
conf := testValidGlobalSection(t, logcfg)
|
||||||
|
assert.Equal(t, 1, len(*conf.Global.Logging))
|
||||||
|
assert.True(t, SyslogFacility(sFP.Priority) == *(*conf.Global.Logging)[0].Ret.(*SyslogLoggingOutlet).Facility)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoggingOutletEnumList_SetDefaults(t *testing.T) {
|
func TestLoggingOutletEnumList_SetDefaults(t *testing.T) {
|
||||||
e := &LoggingOutletEnumList{}
|
e := &LoggingOutletEnumList{}
|
||||||
var i yaml.Defaulter = e
|
var i yaml.Defaulter = e
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package config
|
||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/zrepl/yaml-config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPositiveDurationOrManual(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Comment, Input string
|
||||||
|
Result *PositiveDurationOrManual
|
||||||
|
}{
|
||||||
|
{"empty is error", "", nil},
|
||||||
|
{"negative is error", "-1s", nil},
|
||||||
|
{"zero seconds is error", "0s", nil},
|
||||||
|
{"zero is error", "0", nil},
|
||||||
|
{"non-manual is error", "something", nil},
|
||||||
|
{"positive seconds works", "1s", &PositiveDurationOrManual{Manual: false, Interval: 1 * time.Second}},
|
||||||
|
{"manual works", "manual", &PositiveDurationOrManual{Manual: true, Interval: 0}},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.Comment, func(t *testing.T) {
|
||||||
|
var out struct {
|
||||||
|
FieldName PositiveDurationOrManual `yaml:"fieldname"`
|
||||||
|
}
|
||||||
|
input := fmt.Sprintf("\nfieldname: %s\n", tc.Input)
|
||||||
|
err := yaml.UnmarshalStrict([]byte(input), &out)
|
||||||
|
if tc.Result == nil {
|
||||||
|
assert.Error(t, err)
|
||||||
|
t.Logf("%#v", out)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, *tc.Result, out.FieldName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRPC(t *testing.T) {
|
|
||||||
conf := testValidConfig(t, `
|
|
||||||
jobs:
|
|
||||||
- name: pull_servers
|
|
||||||
type: pull
|
|
||||||
connect:
|
|
||||||
type: tcp
|
|
||||||
address: "server1.foo.bar:8888"
|
|
||||||
rpc:
|
|
||||||
timeout: 20s # different form default, should merge
|
|
||||||
root_fs: "pool2/backup_servers"
|
|
||||||
interval: 10m
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: not_replicated
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 100
|
|
||||||
|
|
||||||
- name: pull_servers2
|
|
||||||
type: pull
|
|
||||||
connect:
|
|
||||||
type: tcp
|
|
||||||
address: "server1.foo.bar:8888"
|
|
||||||
rpc:
|
|
||||||
tx_chunk_size: 0xabcd # different from default, should merge
|
|
||||||
root_fs: "pool2/backup_servers"
|
|
||||||
interval: 10m
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: not_replicated
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 100
|
|
||||||
|
|
||||||
- type: sink
|
|
||||||
name: "laptop_sink"
|
|
||||||
root_fs: "pool2/backup_laptops"
|
|
||||||
serve:
|
|
||||||
type: tcp
|
|
||||||
listen: "192.168.122.189:8888"
|
|
||||||
clients: {
|
|
||||||
"10.23.42.23":"client1"
|
|
||||||
}
|
|
||||||
rpc:
|
|
||||||
rx_structured_max: 0x2342
|
|
||||||
|
|
||||||
- type: sink
|
|
||||||
name: "other_sink"
|
|
||||||
root_fs: "pool2/backup_laptops"
|
|
||||||
serve:
|
|
||||||
type: tcp
|
|
||||||
listen: "192.168.122.189:8888"
|
|
||||||
clients: {
|
|
||||||
"10.23.42.23":"client1"
|
|
||||||
}
|
|
||||||
rpc:
|
|
||||||
send_heartbeat_interval: 10s
|
|
||||||
|
|
||||||
`)
|
|
||||||
|
|
||||||
assert.Equal(t, 20*time.Second, conf.Jobs[0].Ret.(*PullJob).Connect.Ret.(*TCPConnect).RPC.Timeout)
|
|
||||||
assert.Equal(t, uint32(0xabcd), conf.Jobs[1].Ret.(*PullJob).Connect.Ret.(*TCPConnect).RPC.TxChunkSize)
|
|
||||||
assert.Equal(t, uint32(0x2342), conf.Jobs[2].Ret.(*SinkJob).Serve.Ret.(*TCPServe).RPC.RxStructuredMaxLen)
|
|
||||||
assert.Equal(t, 10*time.Second, conf.Jobs[3].Ret.(*SinkJob).Serve.Ret.(*TCPServe).RPC.SendHeartbeatInterval)
|
|
||||||
defConf := RPCConfig{}
|
|
||||||
Default(&defConf)
|
|
||||||
assert.Equal(t, defConf.Timeout, conf.Global.RPC.Timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGlobal_DefaultRPCConfig(t *testing.T) {
|
|
||||||
assert.NotPanics(t, func() {
|
|
||||||
var c RPCConfig
|
|
||||||
Default(&c)
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
assert.Equal(t, c.TxChunkSize, uint32(1)<<15)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSendOptions(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
|
||||||
|
`
|
||||||
|
encrypted_false := `
|
||||||
|
send:
|
||||||
|
encrypted: false
|
||||||
|
`
|
||||||
|
|
||||||
|
encrypted_true := `
|
||||||
|
send:
|
||||||
|
encrypted: true
|
||||||
|
`
|
||||||
|
|
||||||
|
encrypted_unspecified := `
|
||||||
|
send: {}
|
||||||
|
`
|
||||||
|
|
||||||
|
send_not_specified := `
|
||||||
|
`
|
||||||
|
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
var c *Config
|
||||||
|
|
||||||
|
t.Run("encrypted_false", func(t *testing.T) {
|
||||||
|
c = testValidConfig(t, fill(encrypted_false))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, false, encrypted)
|
||||||
|
})
|
||||||
|
t.Run("encrypted_true", func(t *testing.T) {
|
||||||
|
c = testValidConfig(t, fill(encrypted_true))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, true, encrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("encrypted_unspecified", func(t *testing.T) {
|
||||||
|
c, err := testConfig(t, fill(encrypted_unspecified))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send_not_specified", func(t *testing.T) {
|
||||||
|
c, err := testConfig(t, fill(send_not_specified))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSnapshotting(t *testing.T) {
|
func TestSnapshotting(t *testing.T) {
|
||||||
@@ -37,7 +38,30 @@ jobs:
|
|||||||
interval: 10m
|
interval: 10m
|
||||||
`
|
`
|
||||||
|
|
||||||
fillSnapshotting := func(s string) string {return fmt.Sprintf(tmpl, s)}
|
hooks := `
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
prefix: zrepl_
|
||||||
|
interval: 10m
|
||||||
|
hooks:
|
||||||
|
- type: command
|
||||||
|
path: /tmp/path/to/command
|
||||||
|
- type: command
|
||||||
|
path: /tmp/path/to/command
|
||||||
|
filesystems: { "zroot<": true, "<": false }
|
||||||
|
- type: postgres-checkpoint
|
||||||
|
dsn: "host=localhost port=5432 user=postgres sslmode=disable"
|
||||||
|
filesystems: {
|
||||||
|
"tank/postgres/data11": true
|
||||||
|
}
|
||||||
|
- type: mysql-lock-tables
|
||||||
|
dsn: "root@tcp(localhost)/"
|
||||||
|
filesystems: {
|
||||||
|
"tank/mysql": true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
var c *Config
|
var c *Config
|
||||||
|
|
||||||
t.Run("manual", func(t *testing.T) {
|
t.Run("manual", func(t *testing.T) {
|
||||||
@@ -51,7 +75,16 @@ jobs:
|
|||||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
||||||
assert.Equal(t, "periodic", snp.Type)
|
assert.Equal(t, "periodic", snp.Type)
|
||||||
assert.Equal(t, 10*time.Minute, snp.Interval)
|
assert.Equal(t, 10*time.Minute, snp.Interval)
|
||||||
assert.Equal(t, "zrepl_" , snp.Prefix)
|
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("hooks", func(t *testing.T) {
|
||||||
|
c = testValidConfig(t, fillSnapshotting(hooks))
|
||||||
|
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
|
||||||
|
assert.Equal(t, hs[0].Ret.(*HookCommand).Filesystems["<"], true)
|
||||||
|
assert.Equal(t, hs[1].Ret.(*HookCommand).Filesystems["zroot<"], true)
|
||||||
|
assert.Equal(t, hs[2].Ret.(*HookPostgresCheckpoint).Filesystems["tank/postgres/data11"], true)
|
||||||
|
assert.Equal(t, hs[3].Ret.(*HookMySQLLockTables).Filesystems["tank/mysql"], true)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-3
@@ -1,11 +1,18 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/kr/pretty"
|
"bufio"
|
||||||
"github.com/stretchr/testify/require"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
||||||
@@ -35,8 +42,22 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// template must be a template/text template with a single '{{ . }}' as placehodler for val
|
||||||
|
//nolint[:deadcode,unused]
|
||||||
|
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
||||||
|
tmp, err := template.New("master").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = tmp.Execute(&buf, val)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return testValidConfig(t, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
func testValidConfig(t *testing.T, input string) (*Config) {
|
func testValidConfig(t *testing.T, input string) *Config {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
conf, err := testConfig(t, input)
|
conf, err := testConfig(t, input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -48,3 +69,20 @@ func testConfig(t *testing.T, input string) (*Config, error) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
return ParseConfigBytes([]byte(input))
|
return ParseConfigBytes([]byte(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trimSpaceEachLineAndPad(s, pad string) string {
|
||||||
|
var out strings.Builder
|
||||||
|
scan := bufio.NewScanner(strings.NewReader(s))
|
||||||
|
for scan.Scan() {
|
||||||
|
fmt.Fprintf(&out, "%s%s\n", pad, bytes.TrimSpace(scan.Bytes()))
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimSpaceEachLineAndPad(t *testing.T) {
|
||||||
|
foo := `
|
||||||
|
foo
|
||||||
|
bar baz
|
||||||
|
`
|
||||||
|
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransportConnect(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: foo
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
%s
|
||||||
|
filesystems: {"<": true}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`
|
||||||
|
|
||||||
|
mconf := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
Name string
|
||||||
|
ExpectError bool
|
||||||
|
Connect string
|
||||||
|
}
|
||||||
|
|
||||||
|
testTable := []test{
|
||||||
|
{
|
||||||
|
Name: "tcp_with_address_and_port",
|
||||||
|
ExpectError: false,
|
||||||
|
Connect: `
|
||||||
|
type: tcp
|
||||||
|
address: 10.0.0.23:42
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tls_with_host_and_port",
|
||||||
|
ExpectError: false,
|
||||||
|
Connect: `
|
||||||
|
type: tls
|
||||||
|
address: "server1.foo.bar:8888"
|
||||||
|
ca: /etc/zrepl/ca.crt
|
||||||
|
cert: /etc/zrepl/backupserver.fullchain
|
||||||
|
key: /etc/zrepl/backupserver.key
|
||||||
|
server_cn: "server1"
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tcp_without_port",
|
||||||
|
ExpectError: true,
|
||||||
|
Connect: `
|
||||||
|
type: tcp
|
||||||
|
address: 10.0.0.23
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tls_without_port",
|
||||||
|
ExpectError: true,
|
||||||
|
Connect: `
|
||||||
|
type: tls
|
||||||
|
address: 10.0.0.23
|
||||||
|
ca: /etc/zrepl/ca.crt
|
||||||
|
cert: /etc/zrepl/backupserver.fullchain
|
||||||
|
key: /etc/zrepl/backupserver.key
|
||||||
|
server_cn: "server1"
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testTable {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
require.NotEmpty(t, tc.Connect)
|
||||||
|
connect := trimSpaceEachLineAndPad(tc.Connect, " ")
|
||||||
|
conf := mconf(connect)
|
||||||
|
config, err := testConfig(t, conf)
|
||||||
|
if tc.ExpectError && err == nil {
|
||||||
|
t.Errorf("expected test failure, but got valid config %v", config)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tc.ExpectError && err != nil {
|
||||||
|
t.Errorf("not expecint test failure but got error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("error=%v", err)
|
||||||
|
t.Logf("config=%v", config)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -31,10 +31,6 @@ func (i *RetentionInterval) KeepCount() int {
|
|||||||
|
|
||||||
const RetentionGridKeepCountAll int = -1
|
const RetentionGridKeepCountAll int = -1
|
||||||
|
|
||||||
type RetentionGrid struct {
|
|
||||||
intervals []RetentionInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||||
var in string
|
var in string
|
||||||
if err := u(&in, true); err != nil {
|
if err := u(&in, true); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "$ZREPL_DRYRUN" = "true" ] && DRYRUN="echo DRYRUN: "
|
||||||
|
|
||||||
|
pre_snapshot() {
|
||||||
|
$DRYRUN date
|
||||||
|
}
|
||||||
|
|
||||||
|
post_snapshot() {
|
||||||
|
$DRYRUN date
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ZREPL_HOOKTYPE" in
|
||||||
|
pre_snapshot|post_snapshot)
|
||||||
|
"$ZREPL_HOOKTYPE"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'Unrecognized hook type: %s\n' "$ZREPL_HOOKTYPE"
|
||||||
|
exit 255
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
port: 22
|
port: 22
|
||||||
identity_file: /etc/zrepl/ssh/identity
|
identity_file: /etc/zrepl/ssh/identity
|
||||||
options: # optional, default [], `-o` arguments passed to ssh
|
options: # optional, default [], `-o` arguments passed to ssh
|
||||||
- "Compression=on"
|
- "Compression=yes"
|
||||||
root_fs: "pool2/backup_servers"
|
root_fs: "pool2/backup_servers"
|
||||||
interval: 10m
|
interval: 10m
|
||||||
pruning:
|
pruning:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ jobs:
|
|||||||
address: "backup-server.foo.bar:8888"
|
address: "backup-server.foo.bar:8888"
|
||||||
snapshotting:
|
snapshotting:
|
||||||
type: manual
|
type: manual
|
||||||
|
send:
|
||||||
|
encrypted: false
|
||||||
pruning:
|
pruning:
|
||||||
keep_sender:
|
keep_sender:
|
||||||
- type: not_replicated
|
- type: not_replicated
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
jobs:
|
||||||
|
- name: snapjob
|
||||||
|
type: snap
|
||||||
|
filesystems: {
|
||||||
|
"tank/frequently_changed<": true,
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
interval: 2m
|
||||||
|
prefix: zrepl_snapjob_
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 60
|
||||||
+64
-22
@@ -5,16 +5,21 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
|
||||||
"github.com/zrepl/zrepl/daemon/nethelpers"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"github.com/zrepl/zrepl/version"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/daemon/nethelpers"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
"github.com/zrepl/zrepl/version"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type controlJob struct {
|
type controlJob struct {
|
||||||
@@ -38,6 +43,10 @@ func (j *controlJob) Name() string { return jobNameControl }
|
|||||||
|
|
||||||
func (j *controlJob) Status() *job.Status { return &job.Status{Type: job.TypeInternal} }
|
func (j *controlJob) Status() *job.Status { return &job.Status{Type: job.TypeInternal} }
|
||||||
|
|
||||||
|
func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||||
|
|
||||||
|
func (j *controlJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
var promControl struct {
|
var promControl struct {
|
||||||
requestBegin *prometheus.CounterVec
|
requestBegin *prometheus.CounterVec
|
||||||
requestFinished *prometheus.HistogramVec
|
requestFinished *prometheus.HistogramVec
|
||||||
@@ -56,7 +65,7 @@ func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
Subsystem: "control",
|
Subsystem: "control",
|
||||||
Name: "request_finished",
|
Name: "request_finished",
|
||||||
Help: "time it took a request to finih",
|
Help: "time it took a request to finih",
|
||||||
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3,10e-3, 100e-3, 200e-3,400e-3,800e-3, 1, 10, 20},
|
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3, 10e-3, 100e-3, 200e-3, 400e-3, 800e-3, 1, 10, 20},
|
||||||
}, []string{"endpoint"})
|
}, []string{"endpoint"})
|
||||||
registerer.MustRegister(promControl.requestBegin)
|
registerer.MustRegister(promControl.requestBegin)
|
||||||
registerer.MustRegister(promControl.requestFinished)
|
registerer.MustRegister(promControl.requestFinished)
|
||||||
@@ -81,10 +90,16 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pprofServer := NewPProfServer(ctx)
|
pprofServer := NewPProfServer(ctx)
|
||||||
|
if listen := envconst.String("ZREPL_DAEMON_AUTOSTART_PPROF_SERVER", ""); listen != "" {
|
||||||
|
pprofServer.Control(PprofServerControlMsg{
|
||||||
|
Run: true,
|
||||||
|
HttpListenAddress: listen,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle(ControlJobEndpointPProf,
|
mux.Handle(ControlJobEndpointPProf,
|
||||||
requestLogger{log: log, handler: jsonRequestResponder{func(decoder jsonDecoder) (interface{}, error) {
|
requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (interface{}, error) {
|
||||||
var msg PprofServerControlMsg
|
var msg PprofServerControlMsg
|
||||||
err := decoder(&msg)
|
err := decoder(&msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,18 +110,19 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
}}})
|
}}})
|
||||||
|
|
||||||
mux.Handle(ControlJobEndpointVersion,
|
mux.Handle(ControlJobEndpointVersion,
|
||||||
requestLogger{log: log, handler: jsonResponder{func() (interface{}, error) {
|
requestLogger{log: log, handler: jsonResponder{log, func() (interface{}, error) {
|
||||||
return version.NewZreplVersionInformation(), nil
|
return version.NewZreplVersionInformation(), nil
|
||||||
}}})
|
}}})
|
||||||
|
|
||||||
mux.Handle(ControlJobEndpointStatus,
|
mux.Handle(ControlJobEndpointStatus,
|
||||||
requestLogger{log: log, handler: jsonResponder{func() (interface{}, error) {
|
// don't log requests to status endpoint, too spammy
|
||||||
|
jsonResponder{log, func() (interface{}, error) {
|
||||||
s := j.jobs.status()
|
s := j.jobs.status()
|
||||||
return s, nil
|
return s, nil
|
||||||
}}})
|
}})
|
||||||
|
|
||||||
mux.Handle(ControlJobEndpointSignal,
|
mux.Handle(ControlJobEndpointSignal,
|
||||||
requestLogger{log: log, handler: jsonRequestResponder{func(decoder jsonDecoder) (interface{}, error) {
|
requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (interface{}, error) {
|
||||||
type reqT struct {
|
type reqT struct {
|
||||||
Name string
|
Name string
|
||||||
Op string
|
Op string
|
||||||
@@ -131,8 +147,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: 1*time.Second,
|
WriteTimeout: 1 * time.Second,
|
||||||
ReadTimeout: 1*time.Second,
|
ReadTimeout: 1 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
@@ -147,7 +163,10 @@ outer:
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.WithError(ctx.Err()).Info("context done")
|
log.WithError(ctx.Err()).Info("context done")
|
||||||
server.Shutdown(context.Background())
|
err := server.Shutdown(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("cannot shutdown server")
|
||||||
|
}
|
||||||
break outer
|
break outer
|
||||||
case err = <-served:
|
case err = <-served:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -161,33 +180,50 @@ outer:
|
|||||||
}
|
}
|
||||||
|
|
||||||
type jsonResponder struct {
|
type jsonResponder struct {
|
||||||
|
log Logger
|
||||||
producer func() (interface{}, error)
|
producer func() (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j jsonResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (j jsonResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logIoErr := func(err error) {
|
||||||
|
if err != nil {
|
||||||
|
j.log.WithError(err).Error("control handler io error")
|
||||||
|
}
|
||||||
|
}
|
||||||
res, err := j.producer()
|
res, err := j.producer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
j.log.WithError(err).Error("control handler error")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
io.WriteString(w, err.Error())
|
_, err = io.WriteString(w, err.Error())
|
||||||
|
logIoErr(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = json.NewEncoder(&buf).Encode(res)
|
err = json.NewEncoder(&buf).Encode(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
j.log.WithError(err).Error("control handler json marshal error")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
io.WriteString(w, err.Error())
|
_, err = io.WriteString(w, err.Error())
|
||||||
} else {
|
} else {
|
||||||
io.Copy(w, &buf)
|
_, err = io.Copy(w, &buf)
|
||||||
}
|
}
|
||||||
|
logIoErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonDecoder = func(interface{}) error
|
type jsonDecoder = func(interface{}) error
|
||||||
|
|
||||||
type jsonRequestResponder struct {
|
type jsonRequestResponder struct {
|
||||||
|
log Logger
|
||||||
producer func(decoder jsonDecoder) (interface{}, error)
|
producer func(decoder jsonDecoder) (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logIoErr := func(err error) {
|
||||||
|
if err != nil {
|
||||||
|
j.log.WithError(err).Error("control handler io error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var decodeError error
|
var decodeError error
|
||||||
decoder := func(i interface{}) error {
|
decoder := func(i interface{}) error {
|
||||||
err := json.NewDecoder(r.Body).Decode(&i)
|
err := json.NewDecoder(r.Body).Decode(&i)
|
||||||
@@ -199,22 +235,28 @@ func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
|||||||
//If we had a decode error ignore output of producer and return error
|
//If we had a decode error ignore output of producer and return error
|
||||||
if decodeError != nil {
|
if decodeError != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
io.WriteString(w, decodeError.Error())
|
_, err := io.WriteString(w, decodeError.Error())
|
||||||
|
logIoErr(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if producerErr != nil {
|
if producerErr != nil {
|
||||||
|
j.log.WithError(producerErr).Error("control handler error")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
io.WriteString(w, producerErr.Error())
|
_, err := io.WriteString(w, producerErr.Error())
|
||||||
|
logIoErr(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
encodeErr := json.NewEncoder(&buf).Encode(res)
|
encodeErr := json.NewEncoder(&buf).Encode(res)
|
||||||
if encodeErr != nil {
|
if encodeErr != nil {
|
||||||
|
j.log.WithError(producerErr).Error("control handler json marhsal error")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
io.WriteString(w, encodeErr.Error())
|
_, err := io.WriteString(w, encodeErr.Error())
|
||||||
|
logIoErr(err)
|
||||||
} else {
|
} else {
|
||||||
io.Copy(w, &buf)
|
_, err := io.Copy(w, &buf)
|
||||||
|
logIoErr(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-10
@@ -3,8 +3,16 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||||
@@ -12,12 +20,6 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(conf *config.Config) error {
|
func Run(conf *config.Config) error {
|
||||||
@@ -74,12 +76,11 @@ func Run(conf *config.Config) error {
|
|||||||
return errors.Errorf("unknown monitoring job #%d (type %T)", i, v)
|
return errors.Errorf("unknown monitoring job #%d (type %T)", i, v)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err,"cannot build monitorin gjob #%d", i)
|
return errors.Wrapf(err, "cannot build monitoring job #%d", i)
|
||||||
}
|
}
|
||||||
jobs.start(ctx, job, true)
|
jobs.start(ctx, job, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
log.Info("starting daemon")
|
log.Info("starting daemon")
|
||||||
|
|
||||||
// start regular jobs
|
// start regular jobs
|
||||||
@@ -117,8 +118,6 @@ func newJobs() *jobs {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
logJobField string = "job"
|
logJobField string = "job"
|
||||||
logTaskField string = "task"
|
|
||||||
logSubsysField string = "subsystem"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *jobs) wait() <-chan struct{} {
|
func (s *jobs) wait() <-chan struct{} {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package filters
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DatasetMapFilter struct {
|
type DatasetMapFilter struct {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDatasetMapFilter(t *testing.T) {
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
filter map[string]string
|
||||||
|
checkPass map[string]bool // each entry is checked to match the filter's `pass` return value
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []testCase{
|
||||||
|
{
|
||||||
|
"default_no_match",
|
||||||
|
map[string]string{},
|
||||||
|
map[string]bool{
|
||||||
|
"": false,
|
||||||
|
"foo": false,
|
||||||
|
"zroot": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"more_specific_path_has_precedence",
|
||||||
|
map[string]string{
|
||||||
|
"tank<": "ok",
|
||||||
|
"tank/tmp<": "!",
|
||||||
|
"tank/home/x<": "!",
|
||||||
|
"tank/home/x/1": "ok",
|
||||||
|
},
|
||||||
|
map[string]bool{
|
||||||
|
"zroot": false,
|
||||||
|
"tank": true,
|
||||||
|
"tank/tmp": false,
|
||||||
|
"tank/tmp/foo": false,
|
||||||
|
"tank/home/x": false,
|
||||||
|
"tank/home/y": true,
|
||||||
|
"tank/home/x/1": true,
|
||||||
|
"tank/home/x/2": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"precedence_of_specific_over_subtree_wildcard_on_same_path",
|
||||||
|
map[string]string{
|
||||||
|
"tank/home/bob": "ok",
|
||||||
|
"tank/home/bob<": "!",
|
||||||
|
},
|
||||||
|
map[string]bool{
|
||||||
|
"tank/home/bob": true,
|
||||||
|
"tank/home/bob/downloads": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tc := range tcs {
|
||||||
|
t.Run(tcs[tc].name, func(t *testing.T) {
|
||||||
|
c := tcs[tc]
|
||||||
|
f := NewDatasetMapFilter(len(c.filter), true)
|
||||||
|
for p, a := range c.filter {
|
||||||
|
err := f.Add(p, a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("incorrect filter spec: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for p, checkPass := range c.checkPass {
|
||||||
|
zp, err := zfs.NewDatasetPath(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("incorrect path spec: %s", err)
|
||||||
|
}
|
||||||
|
pass, err := f.Filter(zp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected filter error: %s", err)
|
||||||
|
}
|
||||||
|
ok := pass == checkPass
|
||||||
|
failstr := "OK"
|
||||||
|
if !ok {
|
||||||
|
failstr = "FAIL"
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
t.Logf("%-40q %5v (exp=%v act=%v)", p, failstr, checkPass, pass)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package filters
|
package filters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AnyFSVFilter struct{}
|
type AnyFSVFilter struct{}
|
||||||
@@ -17,7 +18,6 @@ func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err err
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type PrefixFilter struct {
|
type PrefixFilter struct {
|
||||||
prefix string
|
prefix string
|
||||||
fstype zfs.VersionType
|
fstype zfs.VersionType
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Code generated by "stringer -type=Edge"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package hooks
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[Pre-1]
|
||||||
|
_ = x[Callback-2]
|
||||||
|
_ = x[Post-4]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_Edge_name_0 = "PreCallback"
|
||||||
|
_Edge_name_1 = "Post"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_Edge_index_0 = [...]uint8{0, 3, 11}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i Edge) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 2:
|
||||||
|
i -= 1
|
||||||
|
return _Edge_name_0[_Edge_index_0[i]:_Edge_index_0[i+1]]
|
||||||
|
case i == 4:
|
||||||
|
return _Edge_name_1
|
||||||
|
default:
|
||||||
|
return "Edge(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type List []Hook
|
||||||
|
|
||||||
|
func HookFromConfig(in config.HookEnum) (Hook, error) {
|
||||||
|
switch v := in.Ret.(type) {
|
||||||
|
case *config.HookCommand:
|
||||||
|
return NewCommandHook(v)
|
||||||
|
case *config.HookPostgresCheckpoint:
|
||||||
|
return PgChkptHookFromConfig(v)
|
||||||
|
case *config.HookMySQLLockTables:
|
||||||
|
return MyLockTablesFromConfig(v)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown hook type %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListFromConfig(in *config.HookList) (r *List, err error) {
|
||||||
|
hl := make(List, len(*in))
|
||||||
|
|
||||||
|
for i, h := range *in {
|
||||||
|
hl[i], err = HookFromConfig(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create hook #%d: %s", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &hl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l List) CopyFilteredForFilesystem(fs *zfs.DatasetPath) (ret List, err error) {
|
||||||
|
ret = make(List, 0, len(l))
|
||||||
|
|
||||||
|
for _, h := range l {
|
||||||
|
var passFilesystem bool
|
||||||
|
if passFilesystem, err = h.Filesystems().Filter(fs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if passFilesystem {
|
||||||
|
ret = append(ret, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Package hooks implements pre- and post snapshot hooks.
|
||||||
|
//
|
||||||
|
// Plan is a generic executor for ExpectStepReports before and after an activity specified in a callback.
|
||||||
|
// It provides a reporting facility that can be polled while the plan is executing to gather progress information.
|
||||||
|
//
|
||||||
|
// This package also provides all supported hook type implementations and abstractions around them.
|
||||||
|
//
|
||||||
|
// 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:
|
||||||
|
//
|
||||||
|
// The Hook interface requires a hook to provide a Filesystems() filter, which doesn't make sense for
|
||||||
|
// all kinds of activities.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// How This Package Is Used By Package Snapper
|
||||||
|
//
|
||||||
|
// Deserialize a config.List using ListFromConfig().
|
||||||
|
// Then it MUST filter the list to only contain hooks for a particular filesystem using
|
||||||
|
// hooksList.CopyFilteredForFilesystem(fs).
|
||||||
|
//
|
||||||
|
// Then create a CallbackHook using NewCallbackHookForFilesystem().
|
||||||
|
//
|
||||||
|
// Pass all of the above to NewPlan() which provides a Report() and Run() method:
|
||||||
|
//
|
||||||
|
// Plan.Run(ctx context.Context,dryRun bool) executes the plan and take a context as argument that should contain a logger added using hooks.WithLogger()).
|
||||||
|
// The value of dryRun is passed through to the hooks' Run() method.
|
||||||
|
// 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").
|
||||||
|
//
|
||||||
|
package hooks
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Re-export type here so that
|
||||||
|
// every file in package hooks doesn't
|
||||||
|
// have to import github.com/zrepl/zrepl/zfs
|
||||||
|
type Filter zfs.DatasetFilter
|
||||||
|
|
||||||
|
type Hook interface {
|
||||||
|
Filesystems() Filter
|
||||||
|
|
||||||
|
// If true and the Pre edge invocation of Run fails, Post edge will not run and other Pre edges will not run.
|
||||||
|
ErrIsFatal() bool
|
||||||
|
|
||||||
|
// Run is invoked by HookPlan for a Pre edge.
|
||||||
|
// If HookReport.HadError() == false, the Post edge will be invoked, too.
|
||||||
|
Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport
|
||||||
|
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Phase string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PhaseSnapshot = Phase("snapshot")
|
||||||
|
PhaseTesting = Phase("testing")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Phase) String() string {
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate stringer -type=Edge
|
||||||
|
type Edge uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
Pre = Edge(1 << iota)
|
||||||
|
Callback
|
||||||
|
Post
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e Edge) StringForPhase(phase Phase) string {
|
||||||
|
return fmt.Sprintf("%s_%s", e.String(), phase.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate enumer -type=StepStatus -trimprefix=Step
|
||||||
|
type StepStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StepPending StepStatus = 1 << iota
|
||||||
|
StepExec
|
||||||
|
StepOk
|
||||||
|
StepErr
|
||||||
|
StepSkippedDueToFatalErr
|
||||||
|
StepSkippedDueToPreErr
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookReport interface {
|
||||||
|
String() string
|
||||||
|
HadError() bool
|
||||||
|
Error() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Step struct {
|
||||||
|
Hook Hook
|
||||||
|
Edge Edge
|
||||||
|
Status StepStatus
|
||||||
|
Begin, End time.Time
|
||||||
|
// Report may be nil
|
||||||
|
// FIXME cannot serialize this for client status, but contains interesting info (like what error happened)
|
||||||
|
Report HookReport
|
||||||
|
state map[interface{}]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Step) String() (out string) {
|
||||||
|
fatal := "~"
|
||||||
|
if s.Hook.ErrIsFatal() && s.Edge == Pre {
|
||||||
|
fatal = "!"
|
||||||
|
}
|
||||||
|
runTime := "..."
|
||||||
|
if s.Status != StepPending {
|
||||||
|
t := s.End.Sub(s.Begin)
|
||||||
|
runTime = t.Round(time.Millisecond).String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s] [%5s] %s [%s] %s", s.Status, runTime, fatal, s.Edge, s.Hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plan struct {
|
||||||
|
mtx sync.RWMutex
|
||||||
|
|
||||||
|
steps []*Step
|
||||||
|
pre []*Step // protected by mtx
|
||||||
|
cb *Step
|
||||||
|
post []*Step // not reversed, i.e. entry at index i corresponds to pre-edge in pre[i]
|
||||||
|
|
||||||
|
phase Phase
|
||||||
|
env Env
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlan(hooks *List, phase Phase, cb *CallbackHook, extra Env) (*Plan, error) {
|
||||||
|
|
||||||
|
var pre, post []*Step
|
||||||
|
// TODO sanity check unique name of hook?
|
||||||
|
for _, hook := range *hooks {
|
||||||
|
state := make(map[interface{}]interface{})
|
||||||
|
preE := &Step{
|
||||||
|
Hook: hook,
|
||||||
|
Edge: Pre,
|
||||||
|
Status: StepPending,
|
||||||
|
state: state,
|
||||||
|
}
|
||||||
|
pre = append(pre, preE)
|
||||||
|
postE := &Step{
|
||||||
|
Hook: hook,
|
||||||
|
Edge: Post,
|
||||||
|
Status: StepPending,
|
||||||
|
state: state,
|
||||||
|
}
|
||||||
|
post = append(post, postE)
|
||||||
|
}
|
||||||
|
|
||||||
|
cbE := &Step{
|
||||||
|
Hook: cb,
|
||||||
|
Edge: Callback,
|
||||||
|
Status: StepPending,
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := make([]*Step, 0, len(pre)+len(post)+1)
|
||||||
|
steps = append(steps, pre...)
|
||||||
|
steps = append(steps, cbE)
|
||||||
|
for i := len(post) - 1; i >= 0; i-- {
|
||||||
|
steps = append(steps, post[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := &Plan{
|
||||||
|
phase: phase,
|
||||||
|
env: extra,
|
||||||
|
steps: steps,
|
||||||
|
pre: pre,
|
||||||
|
post: post,
|
||||||
|
cb: cbE,
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanReport []Step
|
||||||
|
|
||||||
|
func (p *Plan) Report() PlanReport {
|
||||||
|
p.mtx.RLock()
|
||||||
|
defer p.mtx.RUnlock()
|
||||||
|
rep := make([]Step, len(p.steps))
|
||||||
|
for i := range rep {
|
||||||
|
rep[i] = *p.steps[i]
|
||||||
|
}
|
||||||
|
return rep
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PlanReport) HadError() bool {
|
||||||
|
for _, e := range r {
|
||||||
|
if e.Status == StepErr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PlanReport) HadFatalError() bool {
|
||||||
|
for _, e := range r {
|
||||||
|
if e.Status == StepSkippedDueToFatalErr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PlanReport) String() string {
|
||||||
|
stepStrings := make([]string, len(r))
|
||||||
|
for i, e := range r {
|
||||||
|
stepStrings[i] = fmt.Sprintf("%02d %s", i+1, e)
|
||||||
|
}
|
||||||
|
return strings.Join(stepStrings, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plan) Run(ctx context.Context, dryRun bool) {
|
||||||
|
p.mtx.RLock()
|
||||||
|
defer p.mtx.RUnlock()
|
||||||
|
w := func(f func()) {
|
||||||
|
p.mtx.RUnlock()
|
||||||
|
defer p.mtx.RLock()
|
||||||
|
p.mtx.Lock()
|
||||||
|
defer p.mtx.Unlock()
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
runHook := func(s *Step, ctx context.Context, edge Edge) HookReport {
|
||||||
|
w(func() { s.Status = StepExec })
|
||||||
|
begin := time.Now()
|
||||||
|
r := s.Hook.Run(ctx, edge, p.phase, dryRun, p.env, s.state)
|
||||||
|
end := time.Now()
|
||||||
|
w(func() {
|
||||||
|
s.Report = r
|
||||||
|
s.Status = StepOk
|
||||||
|
if r.HadError() {
|
||||||
|
s.Status = StepErr
|
||||||
|
}
|
||||||
|
s.Begin, s.End = begin, end
|
||||||
|
})
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
l := getLogger(ctx)
|
||||||
|
|
||||||
|
// it's a stack, execute until we reach the end of the list (last item in)
|
||||||
|
// or fail inbetween
|
||||||
|
l.Info("run pre-edges in configuration order")
|
||||||
|
next := 0
|
||||||
|
for ; next < len(p.pre); next++ {
|
||||||
|
e := p.pre[next]
|
||||||
|
l := l.WithField("hook", e.Hook)
|
||||||
|
r := runHook(e, ctx, Pre)
|
||||||
|
if r.HadError() {
|
||||||
|
l.WithError(r).Error("hook invocation failed for pre-edge")
|
||||||
|
if e.Hook.ErrIsFatal() {
|
||||||
|
l.Error("the hook run was aborted due to a fatal error in this hook")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hadFatalErr := next != len(p.pre)
|
||||||
|
if hadFatalErr {
|
||||||
|
l.Error("fatal error in a pre-snapshot hook invocation")
|
||||||
|
l.Error("no snapshot will be taken")
|
||||||
|
l.Error("only running post-edges for successful pre-edges")
|
||||||
|
w(func() {
|
||||||
|
p.post[next].Status = StepSkippedDueToFatalErr
|
||||||
|
for i := next + 1; i < len(p.pre); i++ {
|
||||||
|
p.pre[i].Status = StepSkippedDueToFatalErr
|
||||||
|
p.post[i].Status = StepSkippedDueToFatalErr
|
||||||
|
}
|
||||||
|
p.cb.Status = StepSkippedDueToFatalErr
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info("running callback")
|
||||||
|
cbR := runHook(p.cb, ctx, Callback)
|
||||||
|
if cbR.HadError() {
|
||||||
|
l.WithError(cbR).Error("callback failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info("run post-edges for successful pre-edges in reverse configuration order")
|
||||||
|
|
||||||
|
// the constructor produces pre and post entries
|
||||||
|
// post is NOT reversed
|
||||||
|
next-- // now at index of last executed pre-edge
|
||||||
|
for ; next >= 0; next-- {
|
||||||
|
e := p.post[next]
|
||||||
|
l := l.WithField("hook", e.Hook)
|
||||||
|
|
||||||
|
if p.pre[next].Status != StepOk {
|
||||||
|
if p.pre[next].Status != StepErr {
|
||||||
|
panic(fmt.Sprintf("expecting a pre-edge hook report to be either Ok or Err, got %s", p.pre[next].Status))
|
||||||
|
}
|
||||||
|
l.Info("skip post-edge because pre-edge failed")
|
||||||
|
w(func() {
|
||||||
|
e.Status = StepSkippedDueToPreErr
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
report := runHook(e, ctx, Post)
|
||||||
|
|
||||||
|
if report.HadError() {
|
||||||
|
l.WithError(report).Error("hook invocation failed for post-edge")
|
||||||
|
l.Error("subsequent post-edges run regardless of this post-edge failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrIsFatal is only relevant for Pre
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeyLog contextKey = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger = logger.Logger
|
||||||
|
|
||||||
|
func WithLogger(ctx context.Context, log Logger) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyLog, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogger(ctx context.Context) Logger { return getLogger(ctx) }
|
||||||
|
|
||||||
|
func getLogger(ctx context.Context) Logger {
|
||||||
|
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
return logger.NewNullLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
||||||
|
|
||||||
|
type logWriter struct {
|
||||||
|
/*
|
||||||
|
Mutex prevents:
|
||||||
|
concurrent writes to buf, scanner in Write([]byte)
|
||||||
|
data race on scanner vs Write([]byte)
|
||||||
|
and concurrent write to buf (call to buf.Reset())
|
||||||
|
in Close()
|
||||||
|
|
||||||
|
(Also, Close() should generally block until any Write() call completes.)
|
||||||
|
*/
|
||||||
|
mtx *sync.Mutex
|
||||||
|
buf bytes.Buffer
|
||||||
|
scanner *bufio.Scanner
|
||||||
|
logger Logger
|
||||||
|
level logger.Level
|
||||||
|
field string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogWriter(mtx *sync.Mutex, logger Logger, level logger.Level, field string) *logWriter {
|
||||||
|
w := new(logWriter)
|
||||||
|
w.mtx = mtx
|
||||||
|
w.scanner = bufio.NewScanner(&w.buf)
|
||||||
|
w.logger = logger
|
||||||
|
w.level = level
|
||||||
|
w.field = field
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) log(line string) {
|
||||||
|
w.logger.WithField(w.field, line).Log(w.level, "hook output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) logUnreadBytes() error {
|
||||||
|
for w.scanner.Scan() {
|
||||||
|
w.log(w.scanner.Text())
|
||||||
|
}
|
||||||
|
if w.buf.Cap() > envconst.Int("ZREPL_MAX_HOOK_LOG_SIZE", MAX_HOOK_LOG_SIZE_DEFAULT) {
|
||||||
|
w.buf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) Write(in []byte) (int, error) {
|
||||||
|
w.mtx.Lock()
|
||||||
|
defer w.mtx.Unlock()
|
||||||
|
|
||||||
|
n, bufErr := w.buf.Write(in)
|
||||||
|
if bufErr != nil {
|
||||||
|
return n, bufErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.logUnreadBytes()
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
// Always reset the scanner for the next Write
|
||||||
|
w.scanner = bufio.NewScanner(&w.buf)
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) Close() (err error) {
|
||||||
|
w.mtx.Lock()
|
||||||
|
defer w.mtx.Unlock()
|
||||||
|
|
||||||
|
return w.logUnreadBytes()
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookJobCallback func(ctx context.Context) error
|
||||||
|
|
||||||
|
type CallbackHook struct {
|
||||||
|
cb HookJobCallback
|
||||||
|
filter Filter
|
||||||
|
displayString string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCallbackHookForFilesystem(displayString string, fs *zfs.DatasetPath, cb HookJobCallback) *CallbackHook {
|
||||||
|
filter, _ := filters.DatasetMapFilterFromConfig(map[string]bool{fs.ToString(): true})
|
||||||
|
return NewCallbackHook(displayString, cb, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCallbackHook(displayString string, cb HookJobCallback, filter Filter) *CallbackHook {
|
||||||
|
return &CallbackHook{
|
||||||
|
cb: cb,
|
||||||
|
filter: filter,
|
||||||
|
displayString: displayString,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackHook) Filesystems() Filter {
|
||||||
|
return h.filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackHook) ErrIsFatal() bool {
|
||||||
|
return false // callback is by definition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackHook) String() string {
|
||||||
|
return h.displayString
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallbackHookReport struct {
|
||||||
|
Name string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CallbackHookReport) String() string {
|
||||||
|
if r.HadError() {
|
||||||
|
return r.Error()
|
||||||
|
}
|
||||||
|
return r.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CallbackHookReport) HadError() bool { return r.Err != nil }
|
||||||
|
|
||||||
|
func (r *CallbackHookReport) Error() string {
|
||||||
|
return fmt.Sprintf("%s error: %s", r.Name, r.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||||
|
err := h.cb(ctx)
|
||||||
|
return &CallbackHookReport{h.displayString, err}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/circlog"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookEnvVar string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvType HookEnvVar = "ZREPL_HOOKTYPE"
|
||||||
|
EnvDryRun HookEnvVar = "ZREPL_DRYRUN"
|
||||||
|
EnvFS HookEnvVar = "ZREPL_FS"
|
||||||
|
EnvSnapshot HookEnvVar = "ZREPL_SNAPNAME"
|
||||||
|
EnvTimeout HookEnvVar = "ZREPL_TIMEOUT"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Env map[HookEnvVar]string
|
||||||
|
|
||||||
|
func NewHookEnv(edge Edge, phase Phase, dryRun bool, timeout time.Duration, extra Env) Env {
|
||||||
|
r := Env{
|
||||||
|
EnvTimeout: fmt.Sprintf("%.f", math.Floor(timeout.Seconds())),
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeString := edge.StringForPhase(phase)
|
||||||
|
r[EnvType] = strings.ToLower(edgeString)
|
||||||
|
|
||||||
|
var dryRunString string
|
||||||
|
if dryRun {
|
||||||
|
dryRunString = "true"
|
||||||
|
} else {
|
||||||
|
dryRunString = ""
|
||||||
|
}
|
||||||
|
r[EnvDryRun] = dryRunString
|
||||||
|
|
||||||
|
for k, v := range extra {
|
||||||
|
r[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandHook struct {
|
||||||
|
edge Edge
|
||||||
|
filter Filter
|
||||||
|
errIsFatal bool
|
||||||
|
command string
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandHookReport struct {
|
||||||
|
Command string
|
||||||
|
Args []string // currently always empty
|
||||||
|
Env Env
|
||||||
|
Err error
|
||||||
|
CapturedStdoutStderrCombined []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CommandHookReport) String() string {
|
||||||
|
// Reproduces a POSIX shell-compatible command line
|
||||||
|
var cmdLine strings.Builder
|
||||||
|
sep := ""
|
||||||
|
|
||||||
|
// Make sure environment variables are always
|
||||||
|
// printed in the same order
|
||||||
|
var hookEnvKeys []HookEnvVar
|
||||||
|
for k := range r.Env {
|
||||||
|
hookEnvKeys = append(hookEnvKeys, k)
|
||||||
|
}
|
||||||
|
sort.Slice(hookEnvKeys, func(i, j int) bool { return string(hookEnvKeys[i]) < string(hookEnvKeys[j]) })
|
||||||
|
|
||||||
|
for _, k := range hookEnvKeys {
|
||||||
|
cmdLine.WriteString(fmt.Sprintf("%s%s='%s'", sep, k, r.Env[k]))
|
||||||
|
sep = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdLine.WriteString(fmt.Sprintf("%s%s", sep, r.Command))
|
||||||
|
for _, a := range r.Args {
|
||||||
|
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
|
||||||
|
}
|
||||||
|
func (r *CommandHookReport) Error() string {
|
||||||
|
if r.Err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s FAILED with error: %s", r.String(), r.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CommandHookReport) HadError() bool {
|
||||||
|
return r.Err != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommandHook(in *config.HookCommand) (r *CommandHook, err error) {
|
||||||
|
r = &CommandHook{
|
||||||
|
errIsFatal: in.ErrIsFatal,
|
||||||
|
command: in.Path,
|
||||||
|
timeout: in.Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.filter, err = filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse filesystem filter: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.edge = Pre | Post
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHook) Filesystems() Filter {
|
||||||
|
return h.filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHook) ErrIsFatal() bool {
|
||||||
|
return h.errIsFatal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHook) String() string {
|
||||||
|
return h.command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||||
|
l := getLogger(ctx).WithField("command", h.command)
|
||||||
|
|
||||||
|
cmdCtx, cancel := context.WithTimeout(ctx, h.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmdExec := exec.CommandContext(cmdCtx, h.command)
|
||||||
|
|
||||||
|
hookEnv := NewHookEnv(edge, phase, dryRun, h.timeout, extra)
|
||||||
|
cmdEnv := os.Environ()
|
||||||
|
for k, v := range hookEnv {
|
||||||
|
cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
cmdExec.Env = cmdEnv
|
||||||
|
|
||||||
|
var scanMutex sync.Mutex
|
||||||
|
combinedOutput, err := circlog.NewCircularLog(envconst.Int("ZREPL_MAX_HOOK_LOG_SIZE", MAX_HOOK_LOG_SIZE_DEFAULT))
|
||||||
|
if err != nil {
|
||||||
|
return &CommandHookReport{Err: err}
|
||||||
|
}
|
||||||
|
logErrWriter := NewLogWriter(&scanMutex, l, logger.Warn, "stderr")
|
||||||
|
logOutWriter := NewLogWriter(&scanMutex, l, logger.Info, "stdout")
|
||||||
|
defer logErrWriter.Close()
|
||||||
|
defer logOutWriter.Close()
|
||||||
|
|
||||||
|
cmdExec.Stderr = io.MultiWriter(logErrWriter, combinedOutput)
|
||||||
|
cmdExec.Stdout = io.MultiWriter(logOutWriter, combinedOutput)
|
||||||
|
|
||||||
|
report := &CommandHookReport{
|
||||||
|
Command: h.command,
|
||||||
|
Env: hookEnv,
|
||||||
|
// no report.Args
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdExec.Start()
|
||||||
|
if err != nil {
|
||||||
|
report.Err = err
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmdExec.Wait()
|
||||||
|
combinedOutputBytes := combinedOutput.Bytes()
|
||||||
|
report.CapturedStdoutStderrCombined = make([]byte, len(combinedOutputBytes))
|
||||||
|
copy(report.CapturedStdoutStderrCombined, combinedOutputBytes)
|
||||||
|
if err != nil {
|
||||||
|
if cmdCtx.Err() == context.DeadlineExceeded {
|
||||||
|
report.Err = fmt.Errorf("timed out after %s: %s", h.timeout, err)
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
report.Err = err
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
return report
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
sqldriver "database/sql/driver"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
|
||||||
|
//
|
||||||
|
// Making Backups Using a File System Snapshot:
|
||||||
|
//
|
||||||
|
// 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 another shell, execute mount vxfs snapshot.
|
||||||
|
// From the first client, execute UNLOCK TABLES.
|
||||||
|
// Copy files from the snapshot.
|
||||||
|
// Unmount the snapshot.
|
||||||
|
//
|
||||||
|
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
||||||
|
type MySQLLockTables struct {
|
||||||
|
errIsFatal bool
|
||||||
|
connector sqldriver.Connector
|
||||||
|
filesystems Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
type myLockTablesStateKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
myLockTablesConnection myLockTablesStateKey = 1 + iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func MyLockTablesFromConfig(in *config.HookMySQLLockTables) (*MySQLLockTables, error) {
|
||||||
|
conf, err := mysql.ParseDSN(in.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`dsn` invalid")
|
||||||
|
}
|
||||||
|
cn, err := mysql.NewConnector(conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`connect` invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
filesystems, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`filesystems` invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MySQLLockTables{
|
||||||
|
in.ErrIsFatal,
|
||||||
|
cn,
|
||||||
|
filesystems,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MySQLLockTables) ErrIsFatal() bool { return h.errIsFatal }
|
||||||
|
func (h *MySQLLockTables) Filesystems() Filter { return h.filesystems }
|
||||||
|
func (h *MySQLLockTables) String() string { return "MySQL FLUSH TABLES WITH READ LOCK" }
|
||||||
|
|
||||||
|
type MyLockTablesReport struct {
|
||||||
|
What string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MyLockTablesReport) HadError() bool { return r.Err != nil }
|
||||||
|
func (r *MyLockTablesReport) Error() string { return r.String() }
|
||||||
|
func (r *MyLockTablesReport) String() string {
|
||||||
|
var s strings.Builder
|
||||||
|
s.WriteString(r.What)
|
||||||
|
if r.Err != nil {
|
||||||
|
fmt.Fprintf(&s, ": %s", r.Err)
|
||||||
|
}
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MySQLLockTables) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||||
|
fs, ok := extra[EnvFS]
|
||||||
|
if !ok {
|
||||||
|
panic(extra)
|
||||||
|
}
|
||||||
|
dp, err := zfs.NewDatasetPath(fs)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if pass, err := h.filesystems.Filter(dp); err != nil {
|
||||||
|
return &MyLockTablesReport{What: "filesystem filter", Err: err}
|
||||||
|
} else if !pass {
|
||||||
|
getLogger(ctx).Debug("filesystem does not match filter, skipping")
|
||||||
|
return &MyLockTablesReport{What: "filesystem filter skipped this filesystem", Err: nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch edge {
|
||||||
|
case Pre:
|
||||||
|
err := h.doRunPre(ctx, dp, dryRun, state)
|
||||||
|
return &MyLockTablesReport{"FLUSH TABLES WITH READ LOCK", err}
|
||||||
|
case Post:
|
||||||
|
err := h.doRunPost(ctx, dp, dryRun, state)
|
||||||
|
return &MyLockTablesReport{"UNLOCK TABLES", err}
|
||||||
|
}
|
||||||
|
return &MyLockTablesReport{What: "skipped this edge", Err: nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MySQLLockTables) doRunPre(ctx context.Context, fs *zfs.DatasetPath, dry bool, state map[interface{}]interface{}) (err error) {
|
||||||
|
db := sql.OpenDB(h.connector)
|
||||||
|
defer func(err *error) {
|
||||||
|
if *err != nil {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
}(&err)
|
||||||
|
|
||||||
|
getLogger(ctx).Debug("do FLUSH TABLES WITH READ LOCK")
|
||||||
|
_, err = db.ExecContext(ctx, "FLUSH TABLES WITH READ LOCK")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state[myLockTablesConnection] = db
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MySQLLockTables) doRunPost(ctx context.Context, fs *zfs.DatasetPath, dry bool, state map[interface{}]interface{}) error {
|
||||||
|
|
||||||
|
db := state[myLockTablesConnection].(*sql.DB)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
getLogger(ctx).Debug("do UNLOCK TABLES")
|
||||||
|
_, err := db.ExecContext(ctx, "UNLOCK TABLES")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PgChkptHook struct {
|
||||||
|
errIsFatal bool
|
||||||
|
connector *pq.Connector
|
||||||
|
filesystems Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func PgChkptHookFromConfig(in *config.HookPostgresCheckpoint) (*PgChkptHook, error) {
|
||||||
|
filesystems, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`filesystems` invalid")
|
||||||
|
}
|
||||||
|
cn, err := pq.NewConnector(in.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "`dsn` invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PgChkptHook{
|
||||||
|
in.ErrIsFatal,
|
||||||
|
cn,
|
||||||
|
filesystems,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PgChkptHook) ErrIsFatal() bool { return h.errIsFatal }
|
||||||
|
func (h *PgChkptHook) Filesystems() Filter { return h.filesystems }
|
||||||
|
func (h *PgChkptHook) String() string { return "postgres checkpoint" }
|
||||||
|
|
||||||
|
type PgChkptHookReport struct{ Err error }
|
||||||
|
|
||||||
|
func (r *PgChkptHookReport) HadError() bool { return r.Err != nil }
|
||||||
|
func (r *PgChkptHookReport) Error() string { return r.Err.Error() }
|
||||||
|
func (r *PgChkptHookReport) String() string {
|
||||||
|
if r.Err != nil {
|
||||||
|
return fmt.Sprintf("postgres CHECKPOINT failed: %s", r.Err)
|
||||||
|
} else {
|
||||||
|
return "postgres CHECKPOINT completed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PgChkptHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||||
|
if edge != Pre {
|
||||||
|
return &PgChkptHookReport{nil}
|
||||||
|
}
|
||||||
|
fs, ok := extra[EnvFS]
|
||||||
|
if !ok {
|
||||||
|
panic(extra)
|
||||||
|
}
|
||||||
|
dp, err := zfs.NewDatasetPath(fs)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = h.doRunPre(ctx, dp, dryRun)
|
||||||
|
return &PgChkptHookReport{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PgChkptHook) doRunPre(ctx context.Context, fs *zfs.DatasetPath, dry bool) error {
|
||||||
|
|
||||||
|
if pass, err := h.filesystems.Filter(fs); err != nil || !pass {
|
||||||
|
getLogger(ctx).Debug("filesystem does not match filter, skipping")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := sql.OpenDB(h.connector)
|
||||||
|
defer db.Close()
|
||||||
|
dl, ok := ctx.Deadline()
|
||||||
|
if ok {
|
||||||
|
timeout := uint64(math.Floor(time.Until(dl).Seconds() * 1000)) // TODO go1.13 milliseconds
|
||||||
|
getLogger(ctx).WithField("statement_timeout", timeout).Debug("setting statement timeout for CHECKPOINT")
|
||||||
|
_, err := db.ExecContext(ctx, "SET statement_timeout TO ?", timeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dry {
|
||||||
|
getLogger(ctx).Debug("dry-run - use ping instead of CHECKPOINT")
|
||||||
|
return db.PingContext(ctx)
|
||||||
|
}
|
||||||
|
getLogger(ctx).Info("execute CHECKPOINT command")
|
||||||
|
_, err := db.ExecContext(ctx, "CHECKPOINT")
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
package hooks_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type comparisonAssertionFunc func(require.TestingT, interface{}, interface{}, ...interface{})
|
||||||
|
type valueAssertionFunc func(require.TestingT, interface{}, ...interface{})
|
||||||
|
|
||||||
|
type expectStep struct {
|
||||||
|
ExpectedEdge hooks.Edge
|
||||||
|
ExpectStatus hooks.StepStatus
|
||||||
|
OutputTest valueAssertionFunc
|
||||||
|
ErrorTest valueAssertionFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
Name string
|
||||||
|
Config []string
|
||||||
|
IsSlow bool
|
||||||
|
SuppressOutput bool
|
||||||
|
|
||||||
|
ExpectCallbackSkipped bool
|
||||||
|
ExpectHadFatalErr bool
|
||||||
|
ExpectHadError bool
|
||||||
|
ExpectStepReports []expectStep
|
||||||
|
}
|
||||||
|
|
||||||
|
func curryLeft(f comparisonAssertionFunc, expected interface{}) valueAssertionFunc {
|
||||||
|
return curry(f, expected, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func curryRight(f comparisonAssertionFunc, expected interface{}) valueAssertionFunc {
|
||||||
|
return curry(f, expected, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func curry(f comparisonAssertionFunc, expected interface{}, right bool) (ret valueAssertionFunc) {
|
||||||
|
ret = func(t require.TestingT, s interface{}, v ...interface{}) {
|
||||||
|
var x interface{}
|
||||||
|
var y interface{}
|
||||||
|
if right {
|
||||||
|
x = s
|
||||||
|
y = expected
|
||||||
|
} else {
|
||||||
|
x = expected
|
||||||
|
y = s
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v) > 0 {
|
||||||
|
f(t, x, y, v)
|
||||||
|
} else {
|
||||||
|
f(t, x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHooks(t *testing.T) {
|
||||||
|
testFSName := "testpool/testdataset"
|
||||||
|
testSnapshotName := "testsnap"
|
||||||
|
|
||||||
|
tmpl, err := template.New("TestHooks").Parse(`
|
||||||
|
jobs:
|
||||||
|
- name: TestHooks
|
||||||
|
type: snap
|
||||||
|
filesystems: {"<": true}
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
interval: 1m
|
||||||
|
prefix: zrepl_snapjob_
|
||||||
|
hooks:
|
||||||
|
{{- template "List" . }}
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
regexpTest := func(s string) valueAssertionFunc {
|
||||||
|
return curryLeft(require.Regexp, regexp.MustCompile(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
containsTest := func(s string) valueAssertionFunc {
|
||||||
|
return curryRight(require.Contains, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
testTable := []testCase{
|
||||||
|
testCase{
|
||||||
|
Name: "no_hooks",
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
Name: "timeout",
|
||||||
|
IsSlow: true,
|
||||||
|
ExpectHadError: true,
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-timeout.sh, timeout: 2s}`},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s ZREPL_TIMEOUT=2", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest(`timed out after 2(.\d+)?s`),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
Name: "check_env",
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`},
|
||||||
|
ExpectHadError: false,
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "nonfatal_pre_error_continues",
|
||||||
|
ExpectCallbackSkipped: false,
|
||||||
|
ExpectHadError: true,
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-error.sh}`},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToPreErr, // post-edge is not executed for failing pre-edge
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "pre_error_fatal_skips_subsequent_pre_edges_and_callback_and_its_post_edge_and_post_edges",
|
||||||
|
ExpectCallbackSkipped: true,
|
||||||
|
ExpectHadFatalErr: true,
|
||||||
|
ExpectHadError: true,
|
||||||
|
Config: []string{
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-error.sh, err_is_fatal: true}`,
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||||
|
},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepSkippedDueToFatalErr},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "post_error_fails_are_ignored_even_if_fatal",
|
||||||
|
ExpectHadFatalErr: false, // only occurs during Post, so it's not a fatal error
|
||||||
|
ExpectHadError: true,
|
||||||
|
Config: []string{
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-post-error.sh, err_is_fatal: true}`,
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||||
|
},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
// No-action run of test-post-error.sh
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: require.Empty,
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
// Pre run of test-report-env.sh
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "cleanup_check_env",
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "pre_error_cancels_post",
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-pre-error-post-ok.sh}`},
|
||||||
|
ExpectHadError: true,
|
||||||
|
ExpectHadFatalErr: false,
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "pre_error_does_not_cancel_other_posts_but_itself",
|
||||||
|
Config: []string{
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-pre-error-post-ok.sh}`,
|
||||||
|
},
|
||||||
|
ExpectHadError: true,
|
||||||
|
ExpectHadFatalErr: false,
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepErr,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||||
|
},
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Name: "exceed_buffer_limit",
|
||||||
|
SuppressOutput: true,
|
||||||
|
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-large-stdout.sh}`},
|
||||||
|
ExpectHadError: false,
|
||||||
|
ExpectHadFatalErr: false,
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Pre,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: func(t require.TestingT, s interface{}, v ...interface{}) {
|
||||||
|
require.Len(t, s, 1<<20)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
|
expectStep{
|
||||||
|
// No-action run of above hook
|
||||||
|
ExpectedEdge: hooks.Post,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
OutputTest: require.Empty,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
Following not intended to test functionality of
|
||||||
|
filter package. Only to demonstrate that hook
|
||||||
|
filters are being applied. The following should
|
||||||
|
result in NO hooks running. If it does run, a
|
||||||
|
fatal hooks.RunReport will be returned.
|
||||||
|
*/
|
||||||
|
testCase{
|
||||||
|
Name: "exclude_all_filesystems",
|
||||||
|
Config: []string{
|
||||||
|
`{type: command, path: {{.WorkDir}}/test/test-error.sh, err_is_fatal: true, filesystems: {"<": false}}`,
|
||||||
|
},
|
||||||
|
ExpectStepReports: []expectStep{
|
||||||
|
expectStep{
|
||||||
|
ExpectedEdge: hooks.Callback,
|
||||||
|
ExpectStatus: hooks.StepOk,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
parseHookConfig := func(t *testing.T, in string) *config.Config {
|
||||||
|
t.Helper()
|
||||||
|
conf, err := config.ParseConfigBytes([]byte(in))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, conf)
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
fillHooks := func(tt *testCase) string {
|
||||||
|
// make hook path absolute
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
panic("os.Getwd() failed")
|
||||||
|
}
|
||||||
|
var hooksTmpl string = "\n"
|
||||||
|
for _, l := range tt.Config {
|
||||||
|
hooksTmpl += fmt.Sprintf(" - %s\n", l)
|
||||||
|
}
|
||||||
|
tmpl.New("List").Parse(hooksTmpl)
|
||||||
|
|
||||||
|
var outBytes bytes.Buffer
|
||||||
|
data := struct {
|
||||||
|
WorkDir string
|
||||||
|
}{
|
||||||
|
WorkDir: cwd,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(&outBytes, data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outBytes.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var c *config.Config
|
||||||
|
fs, err := zfs.NewDatasetPath(testFSName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
log := logger.NewTestLogger(t)
|
||||||
|
|
||||||
|
var cbReached bool
|
||||||
|
cb := hooks.NewCallbackHookForFilesystem("testcallback", fs, func(_ context.Context) error {
|
||||||
|
cbReached = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
hookEnvExtra := hooks.Env{
|
||||||
|
hooks.EnvFS: fs.ToString(),
|
||||||
|
hooks.EnvSnapshot: testSnapshotName,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testTable {
|
||||||
|
if testing.Short() && tt.IsSlow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
c = parseHookConfig(t, fillHooks(&tt))
|
||||||
|
snp := c.Jobs[0].Ret.(*config.SnapJob).Snapshotting.Ret.(*config.SnapshottingPeriodic)
|
||||||
|
hookList, err := hooks.ListFromConfig(&snp.Hooks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
filteredHooks, err := hookList.CopyFilteredForFilesystem(fs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
plan, err := hooks.NewPlan(&filteredHooks, hooks.PhaseTesting, cb, hookEnvExtra)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("REPORT PRE EXECUTION:\n%s", plan.Report())
|
||||||
|
|
||||||
|
cbReached = false
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if testing.Verbose() && !tt.SuppressOutput {
|
||||||
|
ctx = hooks.WithLogger(ctx, log)
|
||||||
|
}
|
||||||
|
plan.Run(ctx, false)
|
||||||
|
report := plan.Report()
|
||||||
|
|
||||||
|
t.Logf("REPORT POST EXECUTION:\n%s", report)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TEST ASSERTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
t.Logf("len(runReports)=%v", len(report))
|
||||||
|
t.Logf("len(tt.ExpectStepReports)=%v", len(tt.ExpectStepReports))
|
||||||
|
require.Equal(t, len(tt.ExpectStepReports), len(report), "ExpectStepReports must be same length as expected number of hook runs, excluding possible Callback")
|
||||||
|
|
||||||
|
// Check if callback ran, when required
|
||||||
|
if tt.ExpectCallbackSkipped {
|
||||||
|
require.False(t, cbReached, "callback ran but should not have run")
|
||||||
|
} else {
|
||||||
|
require.True(t, cbReached, "callback should have run but did not")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a fatal run error occurred and was expected
|
||||||
|
require.Equal(t, tt.ExpectHadFatalErr, report.HadFatalError(), "non-matching HadFatalError")
|
||||||
|
require.Equal(t, tt.ExpectHadError, report.HadError(), "non-matching HadError")
|
||||||
|
|
||||||
|
if tt.ExpectHadFatalErr {
|
||||||
|
require.True(t, tt.ExpectHadError, "ExpectHadFatalErr implies ExpectHadError")
|
||||||
|
}
|
||||||
|
if !tt.ExpectHadError {
|
||||||
|
require.False(t, tt.ExpectHadFatalErr, "!ExpectHadError implies !ExpectHadFatalErr")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through each expected hook run
|
||||||
|
for i, hook := range tt.ExpectStepReports {
|
||||||
|
t.Logf("expecting report conforming to %v", hook)
|
||||||
|
|
||||||
|
exp, act := hook.ExpectStatus, report[i].Status
|
||||||
|
require.Equal(t, exp, act, "%s != %s", exp, act)
|
||||||
|
|
||||||
|
// Check for required ExpectedEdge
|
||||||
|
require.NotZero(t, hook.ExpectedEdge, "each hook must have an ExpectedEdge")
|
||||||
|
require.Equal(t, hook.ExpectedEdge, report[i].Edge,
|
||||||
|
"incorrect edge: expected %q, actual %q", hook.ExpectedEdge.String(), report[i].Edge.String(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check for expected output
|
||||||
|
if hook.OutputTest != nil {
|
||||||
|
require.IsType(t, (*hooks.CommandHookReport)(nil), report[i].Report)
|
||||||
|
chr := report[i].Report.(*hooks.CommandHookReport)
|
||||||
|
hook.OutputTest(t, string(chr.CapturedStdoutStderrCombined))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for expected errors
|
||||||
|
if hook.ErrorTest != nil {
|
||||||
|
hook.ErrorTest(t, string(report[i].Report.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Code generated by "enumer -type=StepStatus -trimprefix=Step"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_StepStatusName_0 = "PendingExec"
|
||||||
|
_StepStatusName_1 = "Ok"
|
||||||
|
_StepStatusName_2 = "Err"
|
||||||
|
_StepStatusName_3 = "SkippedDueToFatalErr"
|
||||||
|
_StepStatusName_4 = "SkippedDueToPreErr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_StepStatusIndex_0 = [...]uint8{0, 7, 11}
|
||||||
|
_StepStatusIndex_1 = [...]uint8{0, 2}
|
||||||
|
_StepStatusIndex_2 = [...]uint8{0, 3}
|
||||||
|
_StepStatusIndex_3 = [...]uint8{0, 20}
|
||||||
|
_StepStatusIndex_4 = [...]uint8{0, 18}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i StepStatus) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 2:
|
||||||
|
i -= 1
|
||||||
|
return _StepStatusName_0[_StepStatusIndex_0[i]:_StepStatusIndex_0[i+1]]
|
||||||
|
case i == 4:
|
||||||
|
return _StepStatusName_1
|
||||||
|
case i == 8:
|
||||||
|
return _StepStatusName_2
|
||||||
|
case i == 16:
|
||||||
|
return _StepStatusName_3
|
||||||
|
case i == 32:
|
||||||
|
return _StepStatusName_4
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("StepStatus(%d)", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _StepStatusValues = []StepStatus{1, 2, 4, 8, 16, 32}
|
||||||
|
|
||||||
|
var _StepStatusNameToValueMap = map[string]StepStatus{
|
||||||
|
_StepStatusName_0[0:7]: 1,
|
||||||
|
_StepStatusName_0[7:11]: 2,
|
||||||
|
_StepStatusName_1[0:2]: 4,
|
||||||
|
_StepStatusName_2[0:3]: 8,
|
||||||
|
_StepStatusName_3[0:20]: 16,
|
||||||
|
_StepStatusName_4[0:18]: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepStatusString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func StepStatusString(s string) (StepStatus, error) {
|
||||||
|
if val, ok := _StepStatusNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to StepStatus values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepStatusValues returns all values of the enum
|
||||||
|
func StepStatusValues() []StepStatus {
|
||||||
|
return _StepStatusValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAStepStatus returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i StepStatus) IsAStepStatus() bool {
|
||||||
|
for _, v := range _StepStatusValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||||
|
|
||||||
|
exit 1
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
# Exceed default hook log size of 1<<20 bytes by double
|
||||||
|
# The Go test should fail if buffer size exceeds 1MB
|
||||||
|
|
||||||
|
pre_testing() {
|
||||||
|
ln=0
|
||||||
|
while :; do printf '%06d: 012345678901234567890123456789012345678901234567890123456789012345678901\n' "$ln"; ln="$(($ln + 1))"; done \
|
||||||
|
| head -c$(( 2 << 20 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ZREPL_HOOKTYPE" in
|
||||||
|
pre_testing)
|
||||||
|
"$ZREPL_HOOKTYPE";;
|
||||||
|
*)
|
||||||
|
# Not handled by this script
|
||||||
|
exit 0;;
|
||||||
|
esac
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
post_testing() {
|
||||||
|
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ZREPL_HOOKTYPE" in
|
||||||
|
post_testing)
|
||||||
|
"$ZREPL_HOOKTYPE";;
|
||||||
|
*)
|
||||||
|
# Not handled by this script
|
||||||
|
exit 0;;
|
||||||
|
esac
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
if [ "$ZREPL_HOOKTYPE" = "pre_testing" ]; then
|
||||||
|
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||||
|
exit 1
|
||||||
|
elif [ "$ZREPL_HOOKTYPE" = "post_testing" ]; then
|
||||||
|
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||||
|
else
|
||||||
|
printf "Unknown hook type: %s" "$ZREPL_HOOKTYPE"
|
||||||
|
exit 255
|
||||||
|
fi
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME ${ZREPL_DRYRUN:+(dry run)}"
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME ZREPL_TIMEOUT=$ZREPL_TIMEOUT"
|
||||||
|
|
||||||
|
exec sleep $(($ZREPL_TIMEOUT + 1))
|
||||||
+222
-109
@@ -2,34 +2,38 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/problame/go-streamrpc"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"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/transport/connecter"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/replication"
|
"github.com/zrepl/zrepl/replication"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/replication/driver"
|
||||||
"github.com/zrepl/zrepl/util/watchdog"
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
|
"github.com/zrepl/zrepl/replication/report"
|
||||||
|
"github.com/zrepl/zrepl/rpc"
|
||||||
|
"github.com/zrepl/zrepl/transport"
|
||||||
|
"github.com/zrepl/zrepl/transport/fromconfig"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
"sync"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActiveSide struct {
|
type ActiveSide struct {
|
||||||
mode activeMode
|
mode activeMode
|
||||||
name string
|
name endpoint.JobID
|
||||||
clientFactory *connecter.ClientFactory
|
connecter transport.Connecter
|
||||||
|
|
||||||
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
|
||||||
@@ -38,18 +42,34 @@ type ActiveSide struct {
|
|||||||
tasks activeSideTasks
|
tasks activeSideTasks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:generate enumer -type=ActiveSideState
|
||||||
|
type ActiveSideState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActiveSideReplicating ActiveSideState = 1 << iota
|
||||||
|
ActiveSidePruneSender
|
||||||
|
ActiveSidePruneReceiver
|
||||||
|
ActiveSideDone // also errors
|
||||||
|
)
|
||||||
|
|
||||||
type activeSideTasks struct {
|
type activeSideTasks struct {
|
||||||
replication *replication.Replication
|
state ActiveSideState
|
||||||
|
|
||||||
|
// valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
||||||
|
replicationReport driver.ReportFunc
|
||||||
replicationCancel context.CancelFunc
|
replicationCancel context.CancelFunc
|
||||||
|
|
||||||
|
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
||||||
prunerSender, prunerReceiver *pruner.Pruner
|
prunerSender, prunerReceiver *pruner.Pruner
|
||||||
|
|
||||||
|
// valid for state ActiveSidePruneReceiver, ActiveSideDone
|
||||||
prunerSenderCancel, prunerReceiverCancel context.CancelFunc
|
prunerSenderCancel, prunerReceiverCancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
||||||
a.tasksMtx.Lock()
|
a.tasksMtx.Lock()
|
||||||
defer a.tasksMtx.Unlock()
|
defer a.tasksMtx.Unlock()
|
||||||
var copy activeSideTasks
|
copy := a.tasks
|
||||||
copy = a.tasks
|
|
||||||
if u == nil {
|
if u == nil {
|
||||||
return copy
|
return copy
|
||||||
}
|
}
|
||||||
@@ -59,36 +79,85 @@ func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type activeMode interface {
|
type activeMode interface {
|
||||||
SenderReceiver(client *streamrpc.Client) (replication.Sender, replication.Receiver, error)
|
ConnectEndpoints(rpcLoggers rpc.Loggers, connecter transport.Connecter)
|
||||||
|
DisconnectEndpoints()
|
||||||
|
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||||
Type() Type
|
Type() Type
|
||||||
|
PlannerPolicy() logic.PlannerPolicy
|
||||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
||||||
|
SnapperReport() *snapper.Report
|
||||||
|
ResetConnectBackoff()
|
||||||
}
|
}
|
||||||
|
|
||||||
type modePush struct {
|
type modePush struct {
|
||||||
fsfilter endpoint.FSFilter
|
setupMtx sync.Mutex
|
||||||
|
sender *endpoint.Sender
|
||||||
|
receiver *rpc.Client
|
||||||
|
senderConfig *endpoint.SenderConfig
|
||||||
|
plannerPolicy *logic.PlannerPolicy
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) SenderReceiver(client *streamrpc.Client) (replication.Sender, replication.Receiver, error) {
|
func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
||||||
sender := endpoint.NewSender(m.fsfilter)
|
m.setupMtx.Lock()
|
||||||
receiver := endpoint.NewRemote(client)
|
defer m.setupMtx.Unlock()
|
||||||
return sender, receiver, nil
|
if m.receiver != nil || m.sender != nil {
|
||||||
|
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
||||||
|
}
|
||||||
|
m.sender = endpoint.NewSender(*m.senderConfig)
|
||||||
|
m.receiver = rpc.NewClient(connecter, loggers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *modePush) DisconnectEndpoints() {
|
||||||
|
m.setupMtx.Lock()
|
||||||
|
defer m.setupMtx.Unlock()
|
||||||
|
m.receiver.Close()
|
||||||
|
m.sender = nil
|
||||||
|
m.receiver = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *modePush) SenderReceiver() (logic.Sender, logic.Receiver) {
|
||||||
|
m.setupMtx.Lock()
|
||||||
|
defer m.setupMtx.Unlock()
|
||||||
|
return m.sender, m.receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) Type() Type { return TypePush }
|
func (m *modePush) Type() Type { return TypePush }
|
||||||
|
|
||||||
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan <- struct{}) {
|
func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||||
|
|
||||||
|
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
m.snapper.Run(ctx, wakeUpCommon)
|
m.snapper.Run(ctx, wakeUpCommon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *modePush) SnapperReport() *snapper.Report {
|
||||||
|
return m.snapper.Report()
|
||||||
|
}
|
||||||
|
|
||||||
func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error) {
|
func (m *modePush) ResetConnectBackoff() {
|
||||||
|
m.setupMtx.Lock()
|
||||||
|
defer m.setupMtx.Unlock()
|
||||||
|
if m.receiver != nil {
|
||||||
|
m.receiver.ResetConnectBackoff()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||||
m := &modePush{}
|
m := &modePush{}
|
||||||
|
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
||||||
}
|
}
|
||||||
m.fsfilter = fsf
|
|
||||||
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
|
FSF: fsf,
|
||||||
|
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||||
|
JobID: jobID,
|
||||||
|
}
|
||||||
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
|
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||||
|
}
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, 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")
|
||||||
@@ -98,20 +167,50 @@ func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modePull struct {
|
type modePull struct {
|
||||||
|
setupMtx sync.Mutex
|
||||||
|
receiver *endpoint.Receiver
|
||||||
|
receiverConfig endpoint.ReceiverConfig
|
||||||
|
sender *rpc.Client
|
||||||
rootFS *zfs.DatasetPath
|
rootFS *zfs.DatasetPath
|
||||||
interval time.Duration
|
plannerPolicy *logic.PlannerPolicy
|
||||||
|
interval config.PositiveDurationOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePull) SenderReceiver(client *streamrpc.Client) (replication.Sender, replication.Receiver, error) {
|
func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
||||||
sender := endpoint.NewRemote(client)
|
m.setupMtx.Lock()
|
||||||
receiver, err := endpoint.NewReceiver(m.rootFS)
|
defer m.setupMtx.Unlock()
|
||||||
return sender, receiver, err
|
if m.receiver != nil || m.sender != nil {
|
||||||
|
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
||||||
|
}
|
||||||
|
m.receiver = endpoint.NewReceiver(m.receiverConfig)
|
||||||
|
m.sender = rpc.NewClient(connecter, loggers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *modePull) DisconnectEndpoints() {
|
||||||
|
m.setupMtx.Lock()
|
||||||
|
defer m.setupMtx.Unlock()
|
||||||
|
m.sender.Close()
|
||||||
|
m.sender = nil
|
||||||
|
m.receiver = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *modePull) SenderReceiver() (logic.Sender, logic.Receiver) {
|
||||||
|
m.setupMtx.Lock()
|
||||||
|
defer m.setupMtx.Unlock()
|
||||||
|
return m.sender, m.receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*modePull) Type() Type { return TypePull }
|
func (*modePull) Type() Type { return TypePull }
|
||||||
|
|
||||||
|
func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||||
|
|
||||||
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
t := time.NewTicker(m.interval)
|
if m.interval.Manual {
|
||||||
|
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
||||||
|
// "waiting for wakeups" is printed in common ActiveSide.do
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := time.NewTicker(m.interval.Interval)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -130,11 +229,20 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err error) {
|
func (m *modePull) SnapperReport() *snapper.Report {
|
||||||
m = &modePull{}
|
return nil
|
||||||
if in.Interval <= 0 {
|
}
|
||||||
return nil, errors.New("interval must be positive")
|
|
||||||
|
func (m *modePull) ResetConnectBackoff() {
|
||||||
|
m.setupMtx.Lock()
|
||||||
|
defer m.setupMtx.Unlock()
|
||||||
|
if m.sender != nil {
|
||||||
|
m.sender.ResetConnectBackoff()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.JobID) (m *modePull, err error) {
|
||||||
|
m = &modePull{}
|
||||||
m.interval = in.Interval
|
m.interval = in.Interval
|
||||||
|
|
||||||
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
||||||
@@ -145,29 +253,59 @@ func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err
|
|||||||
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
|
EncryptedSend: logic.DontCare,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.receiverConfig = endpoint.ReceiverConfig{
|
||||||
|
JobID: jobID,
|
||||||
|
RootWithoutClientComponent: m.rootFS,
|
||||||
|
AppendClientIdentity: false, // !
|
||||||
|
UpdateLastReceivedHold: true,
|
||||||
|
}
|
||||||
|
if err := m.receiverConfig.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *ActiveSide, err error) {
|
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
||||||
|
|
||||||
|
j = &ActiveSide{}
|
||||||
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := configJob.(type) {
|
||||||
|
case *config.PushJob:
|
||||||
|
j.mode, err = modePushFromConfig(g, v, j.name) // shadow
|
||||||
|
case *config.PullJob:
|
||||||
|
j.mode, err = modePullFromConfig(g, v, j.name) // shadow
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("implementation error: unknown job type %T", v))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err // no wrapping required
|
||||||
|
}
|
||||||
|
|
||||||
j = &ActiveSide{mode: mode}
|
|
||||||
j.name = in.Name
|
|
||||||
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
Name: "state_time",
|
Name: "state_time",
|
||||||
Help: "seconds spent during replication",
|
Help: "seconds spent during replication",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job":j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"state"})
|
}, []string{"state"})
|
||||||
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
|
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
Name: "bytes_replicated",
|
Name: "bytes_replicated",
|
||||||
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},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"filesystem"})
|
}, []string{"filesystem"})
|
||||||
|
|
||||||
j.clientFactory, err = connecter.FromConfig(g, in.Connect)
|
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")
|
||||||
}
|
}
|
||||||
@@ -177,7 +315,7 @@ func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *Act
|
|||||||
Subsystem: "pruning",
|
Subsystem: "pruning",
|
||||||
Name: "time",
|
Name: "time",
|
||||||
Help: "seconds spent in pruner",
|
Help: "seconds spent in pruner",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job":j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"prune_side"})
|
}, []string{"prune_side"})
|
||||||
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
|
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,11 +331,12 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
registerer.MustRegister(j.promBytesReplicated)
|
registerer.MustRegister(j.promBytesReplicated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Name() string { return j.name }
|
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||||
|
|
||||||
type ActiveSideStatus struct {
|
type ActiveSideStatus struct {
|
||||||
Replication *replication.Report
|
Replication *report.Report
|
||||||
PruningSender, PruningReceiver *pruner.Report
|
PruningSender, PruningReceiver *pruner.Report
|
||||||
|
Snapshotting *snapper.Report
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Status() *Status {
|
func (j *ActiveSide) Status() *Status {
|
||||||
@@ -205,8 +344,8 @@ func (j *ActiveSide) Status() *Status {
|
|||||||
|
|
||||||
s := &ActiveSideStatus{}
|
s := &ActiveSideStatus{}
|
||||||
t := j.mode.Type()
|
t := j.mode.Type()
|
||||||
if tasks.replication != nil {
|
if tasks.replicationReport != nil {
|
||||||
s.Replication = tasks.replication.Report()
|
s.Replication = tasks.replicationReport()
|
||||||
}
|
}
|
||||||
if tasks.prunerSender != nil {
|
if tasks.prunerSender != nil {
|
||||||
s.PruningSender = tasks.prunerSender.Report()
|
s.PruningSender = tasks.prunerSender.Report()
|
||||||
@@ -214,9 +353,28 @@ func (j *ActiveSide) Status() *Status {
|
|||||||
if tasks.prunerReceiver != nil {
|
if tasks.prunerReceiver != nil {
|
||||||
s.PruningReceiver = tasks.prunerReceiver.Report()
|
s.PruningReceiver = tasks.prunerReceiver.Report()
|
||||||
}
|
}
|
||||||
|
s.Snapshotting = j.mode.SnapperReport()
|
||||||
return &Status{Type: t, JobSpecific: s}
|
return &Status{Type: t, JobSpecific: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||||
|
pull, ok := j.mode.(*modePull)
|
||||||
|
if !ok {
|
||||||
|
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return pull.rootFS.Copy(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||||
|
push, ok := j.mode.(*modePush)
|
||||||
|
if !ok {
|
||||||
|
_ = j.mode.(*modePull) // make sure we didn't introduce a new job type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return push.senderConfig
|
||||||
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Run(ctx context.Context) {
|
func (j *ActiveSide) Run(ctx context.Context) {
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
@@ -238,6 +396,7 @@ outer:
|
|||||||
break outer
|
break outer
|
||||||
|
|
||||||
case <-wakeup.Wait(ctx):
|
case <-wakeup.Wait(ctx):
|
||||||
|
j.mode.ResetConnectBackoff()
|
||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
@@ -250,15 +409,15 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
|
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
|
loggers := rpc.GetLoggersOrPanic(ctx) // filled by WithSubsystemLoggers
|
||||||
|
j.mode.ConnectEndpoints(loggers, j.connecter)
|
||||||
|
defer j.mode.DisconnectEndpoints()
|
||||||
|
|
||||||
// allow cancellation of an invocation (this function)
|
// allow cancellation of an invocation (this function)
|
||||||
ctx, cancelThisRun := context.WithCancel(ctx)
|
ctx, cancelThisRun := context.WithCancel(ctx)
|
||||||
defer cancelThisRun()
|
defer cancelThisRun()
|
||||||
runDone := make(chan struct{})
|
|
||||||
defer close(runDone)
|
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case <-runDone:
|
|
||||||
case <-reset.Wait(ctx):
|
case <-reset.Wait(ctx):
|
||||||
log.Info("reset received, cancelling current invocation")
|
log.Info("reset received, cancelling current invocation")
|
||||||
cancelThisRun()
|
cancelThisRun()
|
||||||
@@ -266,64 +425,7 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// watchdog
|
sender, receiver := j.mode.SenderReceiver()
|
||||||
go func() {
|
|
||||||
// if no progress after 1 minute, kill the task
|
|
||||||
wdto := envconst.Duration("ZREPL_JOB_WATCHDOG_TIMEOUT", 1*time.Minute)
|
|
||||||
log.WithField("watchdog_timeout", wdto.String()).Debug("starting watchdog")
|
|
||||||
|
|
||||||
t := time.NewTicker(wdto)
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
var (
|
|
||||||
rep, prunerSender, prunerReceiver watchdog.Progress
|
|
||||||
)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-runDone:
|
|
||||||
return
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-t.C: // fall
|
|
||||||
}
|
|
||||||
|
|
||||||
log := log.WithField("watchdog_timeout", wdto.String()) // shadowing!
|
|
||||||
|
|
||||||
j.updateTasks(func(tasks *activeSideTasks) {
|
|
||||||
if tasks.replication != nil &&
|
|
||||||
!tasks.replication.Progress.ExpectProgress(&rep) &&
|
|
||||||
!tasks.replication.State().IsTerminal() {
|
|
||||||
log.Error("replication did not make progress, cancelling")
|
|
||||||
tasks.replicationCancel()
|
|
||||||
}
|
|
||||||
if tasks.prunerSender != nil &&
|
|
||||||
!tasks.prunerSender.Progress.ExpectProgress(&prunerSender) &&
|
|
||||||
!tasks.prunerSender.State().IsTerminal() {
|
|
||||||
log.Error("pruner:sender did not make progress, cancelling")
|
|
||||||
tasks.prunerSenderCancel()
|
|
||||||
}
|
|
||||||
if tasks.prunerReceiver != nil &&
|
|
||||||
!tasks.prunerReceiver.Progress.ExpectProgress(&prunerReceiver) &&
|
|
||||||
!tasks.prunerReceiver.State().IsTerminal() {
|
|
||||||
log.Error("pruner:receiver did not make progress, cancelling")
|
|
||||||
tasks.prunerReceiverCancel()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
log.WithField("replication_progress", rep.String()).
|
|
||||||
WithField("pruner_sender_progress", prunerSender.String()).
|
|
||||||
WithField("pruner_receiver_progress", prunerReceiver.String()).
|
|
||||||
Debug("watchdog did run")
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client, err := j.clientFactory.NewClient()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("factory cannot instantiate streamrpc client")
|
|
||||||
}
|
|
||||||
defer client.Close(ctx)
|
|
||||||
|
|
||||||
sender, receiver, err := j.mode.SenderReceiver(client)
|
|
||||||
|
|
||||||
{
|
{
|
||||||
select {
|
select {
|
||||||
@@ -332,14 +434,18 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
ctx, repCancel := context.WithCancel(ctx)
|
ctx, repCancel := context.WithCancel(ctx)
|
||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
var repWait driver.WaitFunc
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
// reset it
|
// reset it
|
||||||
*tasks = activeSideTasks{}
|
*tasks = activeSideTasks{}
|
||||||
tasks.replicationCancel = repCancel
|
tasks.replicationCancel = repCancel
|
||||||
tasks.replication = replication.NewReplication(j.promRepStateSecs, j.promBytesReplicated)
|
tasks.replicationReport, repWait = replication.Do(
|
||||||
|
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||||
|
)
|
||||||
|
tasks.state = ActiveSideReplicating
|
||||||
})
|
})
|
||||||
log.Info("start replication")
|
log.Info("start replication")
|
||||||
tasks.replication.Drive(ctx, sender, receiver)
|
repWait(true) // wait blocking
|
||||||
repCancel() // always cancel to free up context resources
|
repCancel() // always cancel to free up context resources
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +459,7 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||||
tasks.prunerSenderCancel = senderCancel
|
tasks.prunerSenderCancel = senderCancel
|
||||||
|
tasks.state = ActiveSidePruneSender
|
||||||
})
|
})
|
||||||
log.Info("start pruning sender")
|
log.Info("start pruning sender")
|
||||||
tasks.prunerSender.Prune()
|
tasks.prunerSender.Prune()
|
||||||
@@ -369,10 +476,16 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
||||||
tasks.prunerReceiverCancel = receiverCancel
|
tasks.prunerReceiverCancel = receiverCancel
|
||||||
|
tasks.state = ActiveSidePruneReceiver
|
||||||
})
|
})
|
||||||
log.Info("start pruning receiver")
|
log.Info("start pruning receiver")
|
||||||
tasks.prunerReceiver.Prune()
|
tasks.prunerReceiver.Prune()
|
||||||
log.Info("finished pruning receiver")
|
log.Info("finished pruning receiver")
|
||||||
receiverCancel()
|
receiverCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.state = ActiveSideDone
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Code generated by "enumer -type=ActiveSideState"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ActiveSideStateName_0 = "ActiveSideReplicatingActiveSidePruneSender"
|
||||||
|
_ActiveSideStateName_1 = "ActiveSidePruneReceiver"
|
||||||
|
_ActiveSideStateName_2 = "ActiveSideDone"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ActiveSideStateIndex_0 = [...]uint8{0, 21, 42}
|
||||||
|
_ActiveSideStateIndex_1 = [...]uint8{0, 23}
|
||||||
|
_ActiveSideStateIndex_2 = [...]uint8{0, 14}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i ActiveSideState) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 2:
|
||||||
|
i -= 1
|
||||||
|
return _ActiveSideStateName_0[_ActiveSideStateIndex_0[i]:_ActiveSideStateIndex_0[i+1]]
|
||||||
|
case i == 4:
|
||||||
|
return _ActiveSideStateName_1
|
||||||
|
case i == 8:
|
||||||
|
return _ActiveSideStateName_2
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("ActiveSideState(%d)", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ActiveSideStateValues = []ActiveSideState{1, 2, 4, 8}
|
||||||
|
|
||||||
|
var _ActiveSideStateNameToValueMap = map[string]ActiveSideState{
|
||||||
|
_ActiveSideStateName_0[0:21]: 1,
|
||||||
|
_ActiveSideStateName_0[21:42]: 2,
|
||||||
|
_ActiveSideStateName_1[0:23]: 4,
|
||||||
|
_ActiveSideStateName_2[0:14]: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveSideStateString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func ActiveSideStateString(s string) (ActiveSideState, error) {
|
||||||
|
if val, ok := _ActiveSideStateNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to ActiveSideState values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveSideStateValues returns all values of the enum
|
||||||
|
func ActiveSideStateValues() []ActiveSideState {
|
||||||
|
return _ActiveSideStateValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAActiveSideState returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i ActiveSideState) IsAActiveSideState() bool {
|
||||||
|
for _, v := range _ActiveSideStateValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+55
-17
@@ -2,7 +2,11 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +22,22 @@ func JobsFromConfig(c *config.Config) ([]Job, error) {
|
|||||||
}
|
}
|
||||||
js[i] = j
|
js[i] = j
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// receiving-side root filesystems must not overlap
|
||||||
|
{
|
||||||
|
rfss := make([]string, 0, len(js))
|
||||||
|
for _, j := range js {
|
||||||
|
jrfs, ok := j.OwnedDatasetSubtreeRoot()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rfss = append(rfss, jrfs.ToString())
|
||||||
|
}
|
||||||
|
if err := validateReceivingSidesDoNotOverlap(rfss); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return js, nil
|
return js, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,38 +48,27 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
|||||||
// FIXME prettify this
|
// FIXME prettify this
|
||||||
switch v := in.Ret.(type) {
|
switch v := in.Ret.(type) {
|
||||||
case *config.SinkJob:
|
case *config.SinkJob:
|
||||||
m, err := modeSinkFromConfig(c, v)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.SourceJob:
|
case *config.SourceJob:
|
||||||
m, err := modeSourceFromConfig(c, v)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
|
case *config.SnapJob:
|
||||||
|
j, err = snapJobFromConfig(c, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
m, err := modePushFromConfig(c, v)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = activeSide(c, &v.ActiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PullJob:
|
case *config.PullJob:
|
||||||
m, err := modePullFromConfig(c, v)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = activeSide(c, &v.ActiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -69,3 +78,32 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
|||||||
return j, nil
|
return j, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
||||||
|
if len(receivingRootFSs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rfss := make([]string, len(receivingRootFSs))
|
||||||
|
copy(rfss, receivingRootFSs)
|
||||||
|
sort.Slice(rfss, func(i, j int) bool {
|
||||||
|
return strings.Compare(rfss[i], rfss[j]) == -1
|
||||||
|
})
|
||||||
|
// add tailing slash because of hierarchy-simulation
|
||||||
|
// rootfs/ is not root of rootfs2/
|
||||||
|
for i := 0; i < len(rfss); i++ {
|
||||||
|
rfss[i] += "/"
|
||||||
|
}
|
||||||
|
// idea:
|
||||||
|
// no path in rfss must be prefix of another
|
||||||
|
//
|
||||||
|
// rfss is now lexicographically sorted, which means that
|
||||||
|
// if i is prefix of j, i < j (in lexicographical order)
|
||||||
|
// thus,
|
||||||
|
// if any i is prefix of i+n (n >= 1), there is overlap
|
||||||
|
for i := 0; i < len(rfss)-1; i++ {
|
||||||
|
if strings.HasPrefix(rfss[i+1], rfss[i]) {
|
||||||
|
return fmt.Errorf("receiving jobs with overlapping root filesystems are forbidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
err bool
|
||||||
|
input []string
|
||||||
|
}
|
||||||
|
tcs := []testCase{
|
||||||
|
{false, nil},
|
||||||
|
{false, []string{}},
|
||||||
|
{false, []string{""}}, // not our job to determine valid paths
|
||||||
|
{false, []string{"a"}},
|
||||||
|
{false, []string{"some/path"}},
|
||||||
|
{false, []string{"zroot/sink1", "zroot/sink2", "zroot/sink3"}},
|
||||||
|
{false, []string{"zroot/foo", "zroot/foobar"}},
|
||||||
|
{true, []string{"zroot/b", "zroot/b"}},
|
||||||
|
{true, []string{"zroot/foo", "zroot/foo/bar", "zroot/baz"}},
|
||||||
|
{false, []string{"a/x", "b/x"}},
|
||||||
|
{false, []string{"a", "b"}},
|
||||||
|
{true, []string{"a", "a"}},
|
||||||
|
{true, []string{"a/x/y", "a/x"}},
|
||||||
|
{true, []string{"a/x", "a/x/y"}},
|
||||||
|
{true, []string{"a/x", "b/x", "a/x/y"}},
|
||||||
|
{true, []string{"a", "a/b", "a/c", "a/b"}},
|
||||||
|
{true, []string{"a/b", "a/c", "a/b", "a/d", "a/c"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Logf("input: %v", tc.input)
|
||||||
|
err := validateReceivingSidesDoNotOverlap(tc.input)
|
||||||
|
if tc.err {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobIDErrorHandling(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: %s
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: foo
|
||||||
|
client_identity: bar
|
||||||
|
filesystems: {"<": true}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
|
||||||
|
type Case struct {
|
||||||
|
jobName string
|
||||||
|
valid bool
|
||||||
|
}
|
||||||
|
cases := []Case{
|
||||||
|
{"validjobname", true},
|
||||||
|
{"valid with spaces", true},
|
||||||
|
{"invalid\twith\ttabs", false},
|
||||||
|
{"invalid#withdelimiter", false},
|
||||||
|
{"invalid@withdelimiter", false},
|
||||||
|
{"withnewline\\nmiddle", false},
|
||||||
|
{"withnewline\\n", false},
|
||||||
|
{"withslash/", false},
|
||||||
|
{"withslash/inthemiddle", false},
|
||||||
|
{"/", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cases {
|
||||||
|
t.Run(cases[i].jobName, func(t *testing.T) {
|
||||||
|
c := cases[i]
|
||||||
|
|
||||||
|
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
||||||
|
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
||||||
|
require.NotNil(t, conf)
|
||||||
|
jobs, err := JobsFromConfig(conf)
|
||||||
|
|
||||||
|
if c.valid {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.Len(t, jobs, 1)
|
||||||
|
assert.Equal(t, c.jobName, jobs[0].Name())
|
||||||
|
} else {
|
||||||
|
t.Logf("error: %s", err)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, jobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+21
-4
@@ -4,8 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Logger = logger.Logger
|
type Logger = logger.Logger
|
||||||
@@ -27,18 +31,22 @@ func WithLogger(ctx context.Context, l Logger) context.Context {
|
|||||||
return context.WithValue(ctx, contextKeyLog, l)
|
return context.WithValue(ctx, contextKeyLog, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Job interface {
|
type Job interface {
|
||||||
Name() string
|
Name() string
|
||||||
Run(ctx context.Context)
|
Run(ctx context.Context)
|
||||||
Status() *Status
|
Status() *Status
|
||||||
RegisterMetrics(registerer prometheus.Registerer)
|
RegisterMetrics(registerer prometheus.Registerer)
|
||||||
|
// Jobs that return a subtree of the dataset hierarchy
|
||||||
|
// must return the root of that subtree as rfs and ok = true
|
||||||
|
OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
||||||
|
SenderConfig() *endpoint.SenderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type Type string
|
type Type string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeInternal Type = "internal"
|
TypeInternal Type = "internal"
|
||||||
|
TypeSnap Type = "snap"
|
||||||
TypePush Type = "push"
|
TypePush Type = "push"
|
||||||
TypeSink Type = "sink"
|
TypeSink Type = "sink"
|
||||||
TypePull Type = "pull"
|
TypePull Type = "pull"
|
||||||
@@ -59,7 +67,7 @@ func (s *Status) MarshalJSON() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
m := map[string]json.RawMessage {
|
m := map[string]json.RawMessage{
|
||||||
"type": typeJson,
|
"type": typeJson,
|
||||||
string(s.Type): jobJSON,
|
string(s.Type): jobJSON,
|
||||||
}
|
}
|
||||||
@@ -84,16 +92,25 @@ func (s *Status) UnmarshalJSON(in []byte) (err error) {
|
|||||||
return fmt.Errorf("field '%s', not found", key)
|
return fmt.Errorf("field '%s', not found", key)
|
||||||
}
|
}
|
||||||
switch s.Type {
|
switch s.Type {
|
||||||
case TypePull: fallthrough
|
case TypeSnap:
|
||||||
|
var st SnapJobStatus
|
||||||
|
err = json.Unmarshal(jobJSON, &st)
|
||||||
|
s.JobSpecific = &st
|
||||||
|
|
||||||
|
case TypePull:
|
||||||
|
fallthrough
|
||||||
case TypePush:
|
case TypePush:
|
||||||
var st ActiveSideStatus
|
var st ActiveSideStatus
|
||||||
err = json.Unmarshal(jobJSON, &st)
|
err = json.Unmarshal(jobJSON, &st)
|
||||||
s.JobSpecific = &st
|
s.JobSpecific = &st
|
||||||
case TypeSource: fallthrough
|
|
||||||
|
case TypeSource:
|
||||||
|
fallthrough
|
||||||
case TypeSink:
|
case TypeSink:
|
||||||
var st PassiveStatus
|
var st PassiveStatus
|
||||||
err = json.Unmarshal(jobJSON, &st)
|
err = json.Unmarshal(jobJSON, &st)
|
||||||
s.JobSpecific = &st
|
s.JobSpecific = &st
|
||||||
|
|
||||||
case TypeInternal:
|
case TypeInternal:
|
||||||
// internal jobs do not report specifics
|
// internal jobs do not report specifics
|
||||||
default:
|
default:
|
||||||
|
|||||||
+103
-103
@@ -2,87 +2,86 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/problame/go-streamrpc"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/daemon/transport/serve"
|
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/rpc"
|
||||||
|
"github.com/zrepl/zrepl/transport"
|
||||||
|
"github.com/zrepl/zrepl/transport/fromconfig"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
"path"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PassiveSide struct {
|
type PassiveSide struct {
|
||||||
mode passiveMode
|
mode passiveMode
|
||||||
name string
|
name endpoint.JobID
|
||||||
l serve.ListenerFactory
|
listen transport.AuthenticatedListenerFactory
|
||||||
rpcConf *streamrpc.ConnConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type passiveMode interface {
|
type passiveMode interface {
|
||||||
ConnHandleFunc(ctx context.Context, conn serve.AuthenticatedConn) streamrpc.HandlerFunc
|
Handler() rpc.Handler
|
||||||
RunPeriodic(ctx context.Context)
|
RunPeriodic(ctx context.Context)
|
||||||
|
SnapperReport() *snapper.Report // may be nil
|
||||||
Type() Type
|
Type() Type
|
||||||
}
|
}
|
||||||
|
|
||||||
type modeSink struct {
|
type modeSink struct {
|
||||||
rootDataset *zfs.DatasetPath
|
receiverConfig endpoint.ReceiverConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSink) Type() Type { return TypeSink }
|
func (m *modeSink) Type() Type { return TypeSink }
|
||||||
|
|
||||||
func (m *modeSink) ConnHandleFunc(ctx context.Context, conn serve.AuthenticatedConn) streamrpc.HandlerFunc {
|
func (m *modeSink) Handler() rpc.Handler {
|
||||||
log := GetLogger(ctx)
|
return endpoint.NewReceiver(m.receiverConfig)
|
||||||
|
|
||||||
clientRootStr := path.Join(m.rootDataset.ToString(), conn.ClientIdentity())
|
|
||||||
clientRoot, err := zfs.NewDatasetPath(clientRootStr)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).
|
|
||||||
WithField("client_identity", conn.ClientIdentity()).
|
|
||||||
Error("cannot build client filesystem map (client identity must be a valid ZFS FS name")
|
|
||||||
}
|
|
||||||
log.WithField("client_root", clientRoot).Debug("client root")
|
|
||||||
|
|
||||||
local, err := endpoint.NewReceiver(clientRoot)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("unexpected error: cannot convert mapping to filter")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
h := endpoint.NewHandler(local)
|
|
||||||
return h.Handle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
||||||
|
func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
||||||
|
|
||||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob) (m *modeSink, err error) {
|
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||||
m = &modeSink{}
|
m = &modeSink{}
|
||||||
m.rootDataset, err = zfs.NewDatasetPath(in.RootFS)
|
|
||||||
|
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
||||||
}
|
}
|
||||||
if m.rootDataset.Length() <= 0 {
|
|
||||||
return nil, errors.New("root dataset must not be empty") // duplicates error check of receiver
|
m.receiverConfig = endpoint.ReceiverConfig{
|
||||||
|
JobID: jobID,
|
||||||
|
RootWithoutClientComponent: rootDataset,
|
||||||
|
AppendClientIdentity: true, // !
|
||||||
|
UpdateLastReceivedHold: true,
|
||||||
}
|
}
|
||||||
|
if err := m.receiverConfig.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type modeSource struct {
|
type modeSource struct {
|
||||||
fsfilter zfs.DatasetFilter
|
senderConfig *endpoint.SenderConfig
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (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)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
||||||
}
|
}
|
||||||
m.fsfilter = fsf
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
|
FSF: fsf,
|
||||||
|
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||||
|
JobID: jobID,
|
||||||
|
}
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, 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")
|
||||||
@@ -93,32 +92,73 @@ func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource
|
|||||||
|
|
||||||
func (m *modeSource) Type() Type { return TypeSource }
|
func (m *modeSource) Type() Type { return TypeSource }
|
||||||
|
|
||||||
func (m *modeSource) ConnHandleFunc(ctx context.Context, conn serve.AuthenticatedConn) streamrpc.HandlerFunc {
|
func (m *modeSource) Handler() rpc.Handler {
|
||||||
sender := endpoint.NewSender(m.fsfilter)
|
return endpoint.NewSender(*m.senderConfig)
|
||||||
h := endpoint.NewHandler(sender)
|
|
||||||
return h.Handle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSource) RunPeriodic(ctx context.Context) {
|
func (m *modeSource) RunPeriodic(ctx context.Context) {
|
||||||
m.snapper.Run(ctx, nil)
|
m.snapper.Run(ctx, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) {
|
func (m *modeSource) SnapperReport() *snapper.Report {
|
||||||
|
return m.snapper.Report()
|
||||||
|
}
|
||||||
|
|
||||||
s = &PassiveSide{mode: mode, name: in.Name}
|
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}) (s *PassiveSide, err error) {
|
||||||
if s.l, s.rpcConf, err = serve.FromConfig(g, in.Serve); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build server")
|
s = &PassiveSide{}
|
||||||
|
|
||||||
|
s.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := configJob.(type) {
|
||||||
|
case *config.SinkJob:
|
||||||
|
s.mode, err = modeSinkFromConfig(g, v, s.name) // shadow
|
||||||
|
case *config.SourceJob:
|
||||||
|
s.mode, err = modeSourceFromConfig(g, v, s.name) // shadow
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err // no wrapping necessary
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build listener factory")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *PassiveSide) Name() string { return j.name }
|
func (j *PassiveSide) Name() string { return j.name.String() }
|
||||||
|
|
||||||
type PassiveStatus struct {}
|
type PassiveStatus struct {
|
||||||
|
Snapper *snapper.Report
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PassiveSide) Status() *Status {
|
func (s *PassiveSide) Status() *Status {
|
||||||
return &Status{Type: s.mode.Type()} // FIXME PassiveStatus
|
st := &PassiveStatus{
|
||||||
|
Snapper: s.mode.SnapperReport(),
|
||||||
|
}
|
||||||
|
return &Status{Type: s.mode.Type(), JobSpecific: st}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||||
|
sink, ok := j.mode.(*modeSink)
|
||||||
|
if !ok {
|
||||||
|
_ = j.mode.(*modeSource) // make sure we didn't introduce a new job type
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return sink.receiverConfig.RootWithoutClientComponent.Copy(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PassiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||||
|
source, ok := j.mode.(*modeSource)
|
||||||
|
if !ok {
|
||||||
|
_ = j.mode.(*modeSink) // make sure we didn't introduce a new job type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return source.senderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||||
@@ -127,70 +167,30 @@ func (j *PassiveSide) Run(ctx context.Context) {
|
|||||||
|
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
l, err := j.l.Listen()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("cannot listen")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer l.Close()
|
|
||||||
|
|
||||||
{
|
{
|
||||||
ctx, cancel := context.WithCancel(logging.WithSubsystemLoggers(ctx, log)) // shadowing
|
ctx, cancel := context.WithCancel(ctx) // shadowing
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go j.mode.RunPeriodic(ctx)
|
go j.mode.RunPeriodic(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("addr", l.Addr()).Debug("accepting connections")
|
handler := j.mode.Handler()
|
||||||
var connId int
|
if handler == nil {
|
||||||
outer:
|
panic(fmt.Sprintf("implementation error: j.mode.Handler() returned nil: %#v", j))
|
||||||
for {
|
|
||||||
|
|
||||||
select {
|
|
||||||
case res := <-accept(ctx, l):
|
|
||||||
if res.err != nil {
|
|
||||||
log.WithError(res.err).Info("accept error")
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
conn := res.conn
|
|
||||||
connId++
|
ctxInterceptor := func(handlerCtx context.Context) context.Context {
|
||||||
connLog := log.
|
return logging.WithSubsystemLoggers(handlerCtx, log)
|
||||||
WithField("connID", connId)
|
}
|
||||||
connLog.
|
|
||||||
WithField("addr", conn.RemoteAddr()).
|
rpcLoggers := rpc.GetLoggersOrPanic(ctx) // WithSubsystemLoggers above
|
||||||
WithField("client_identity", conn.ClientIdentity()).
|
server := rpc.NewServer(handler, rpcLoggers, ctxInterceptor)
|
||||||
Info("handling connection")
|
|
||||||
go func() {
|
listener, err := j.listen()
|
||||||
defer connLog.Info("finished handling connection")
|
if err != nil {
|
||||||
defer conn.Close()
|
log.WithError(err).Error("cannot listen")
|
||||||
ctx := logging.WithSubsystemLoggers(ctx, connLog)
|
|
||||||
handleFunc := j.mode.ConnHandleFunc(ctx, conn)
|
|
||||||
if handleFunc == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := streamrpc.ServeConn(ctx, conn, j.rpcConf, handleFunc); err != nil {
|
|
||||||
log.WithError(err).Error("error serving client")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
break outer
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
server.Serve(ctx, listener)
|
||||||
|
|
||||||
type acceptResult struct {
|
|
||||||
conn serve.AuthenticatedConn
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func accept(ctx context.Context, listener serve.AuthenticatedListener) <-chan acceptResult {
|
|
||||||
c := make(chan acceptResult, 1)
|
|
||||||
go func() {
|
|
||||||
conn, err := listener.Accept(ctx)
|
|
||||||
c <- acceptResult{conn, err}
|
|
||||||
}()
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnapJob struct {
|
||||||
|
name endpoint.JobID
|
||||||
|
fsfilter zfs.DatasetFilter
|
||||||
|
snapper *snapper.PeriodicOrManual
|
||||||
|
|
||||||
|
prunerFactory *pruner.LocalPrunerFactory
|
||||||
|
|
||||||
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
|
|
||||||
|
pruner *pruner.Pruner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *SnapJob) Name() string { return j.name.String() }
|
||||||
|
|
||||||
|
func (j *SnapJob) Type() Type { return TypeSnap }
|
||||||
|
|
||||||
|
func snapJobFromConfig(g *config.Global, in *config.SnapJob) (j *SnapJob, err error) {
|
||||||
|
j = &SnapJob{}
|
||||||
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
|
}
|
||||||
|
j.fsfilter = fsf
|
||||||
|
|
||||||
|
if j.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
|
}
|
||||||
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
|
j.promPruneSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Namespace: "zrepl",
|
||||||
|
Subsystem: "pruning",
|
||||||
|
Name: "time",
|
||||||
|
Help: "seconds spent in pruner",
|
||||||
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
|
}, []string{"prune_side"})
|
||||||
|
j.prunerFactory, err = pruner.NewLocalPrunerFactory(in.Pruning, j.promPruneSecs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build snapjob pruning rules")
|
||||||
|
}
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *SnapJob) RegisterMetrics(registerer prometheus.Registerer) {
|
||||||
|
registerer.MustRegister(j.promPruneSecs)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapJobStatus struct {
|
||||||
|
Pruning *pruner.Report
|
||||||
|
Snapshotting *snapper.Report // may be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *SnapJob) Status() *Status {
|
||||||
|
s := &SnapJobStatus{}
|
||||||
|
t := j.Type()
|
||||||
|
if j.pruner != nil {
|
||||||
|
s.Pruning = j.pruner.Report()
|
||||||
|
}
|
||||||
|
s.Snapshotting = j.snapper.Report()
|
||||||
|
return &Status{Type: t, JobSpecific: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *SnapJob) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *SnapJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
|
func (j *SnapJob) Run(ctx context.Context) {
|
||||||
|
log := GetLogger(ctx)
|
||||||
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
|
|
||||||
|
defer log.Info("job exiting")
|
||||||
|
|
||||||
|
periodicDone := make(chan struct{})
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
go j.snapper.Run(ctx, periodicDone)
|
||||||
|
|
||||||
|
invocationCount := 0
|
||||||
|
outer:
|
||||||
|
for {
|
||||||
|
log.Info("wait for wakeups")
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.WithError(ctx.Err()).Info("context")
|
||||||
|
break outer
|
||||||
|
|
||||||
|
case <-wakeup.Wait(ctx):
|
||||||
|
case <-periodicDone:
|
||||||
|
}
|
||||||
|
invocationCount++
|
||||||
|
invLog := log.WithField("invocation", invocationCount)
|
||||||
|
j.doPrune(WithLogger(ctx, invLog))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adaptor that implements pruner.History around a pruner.Target.
|
||||||
|
// The ReplicationCursor method is Get-op only and always returns
|
||||||
|
// the filesystem's most recent version's GUID.
|
||||||
|
//
|
||||||
|
// TODO:
|
||||||
|
// This is a work-around for the current package daemon/pruner
|
||||||
|
// and package pruning.Snapshot limitation: they require the
|
||||||
|
// `Replicated` getter method be present, but obviously,
|
||||||
|
// a local job like SnapJob can't deliver on that.
|
||||||
|
// But the pruner.Pruner gives up on an FS if no replication
|
||||||
|
// cursor is present, which is why this pruner returns the
|
||||||
|
// most recent filesystem version.
|
||||||
|
type alwaysUpToDateReplicationCursorHistory struct {
|
||||||
|
// the Target passed as Target to BuildLocalPruner
|
||||||
|
target pruner.Target
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
||||||
|
|
||||||
|
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||||
|
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||||
|
Filesystem: req.GetFilesystem(),
|
||||||
|
}
|
||||||
|
res, err := h.target.ListFilesystemVersions(ctx, fsvReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fsvs := res.GetVersions()
|
||||||
|
if len(fsvs) <= 0 {
|
||||||
|
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Notexist{Notexist: true}}, nil
|
||||||
|
}
|
||||||
|
// always return must recent version
|
||||||
|
sort.Slice(fsvs, func(i, j int) bool {
|
||||||
|
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
||||||
|
})
|
||||||
|
mostRecent := fsvs[len(fsvs)-1]
|
||||||
|
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: mostRecent.GetGuid()}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h alwaysUpToDateReplicationCursorHistory) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
||||||
|
return h.target.ListFilesystems(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *SnapJob) doPrune(ctx context.Context) {
|
||||||
|
log := GetLogger(ctx)
|
||||||
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
|
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||||
|
JobID: j.name,
|
||||||
|
FSF: j.fsfilter,
|
||||||
|
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||||
|
Encrypt: &zfs.NilBool{B: true},
|
||||||
|
})
|
||||||
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
|
log.Info("start pruning")
|
||||||
|
j.pruner.Prune()
|
||||||
|
log.Info("finished pruning")
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/problame/go-streamrpc"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type streamrpcLogAdaptor = twoClassLogAdaptor
|
|
||||||
|
|
||||||
type twoClassLogAdaptor struct {
|
|
||||||
logger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ streamrpc.Logger = twoClassLogAdaptor{}
|
|
||||||
|
|
||||||
func (a twoClassLogAdaptor) Errorf(fmtStr string, args ...interface{}) {
|
|
||||||
const errorSuffix = ": %s"
|
|
||||||
if len(args) == 1 {
|
|
||||||
if err, ok := args[0].(error); ok && strings.HasSuffix(fmtStr, errorSuffix) {
|
|
||||||
msg := strings.TrimSuffix(fmtStr, errorSuffix)
|
|
||||||
a.WithError(err).Error(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a.Logger.Error(fmt.Sprintf(fmtStr, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a twoClassLogAdaptor) Infof(fmtStr string, args ...interface{}) {
|
|
||||||
a.Logger.Debug(fmt.Sprintf(fmtStr, args...))
|
|
||||||
}
|
|
||||||
@@ -4,18 +4,24 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"log/syslog"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/problame/go-streamrpc"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/replication"
|
"github.com/zrepl/zrepl/replication/driver"
|
||||||
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
|
"github.com/zrepl/zrepl/rpc"
|
||||||
|
"github.com/zrepl/zrepl/rpc/transportmux"
|
||||||
"github.com/zrepl/zrepl/tlsconf"
|
"github.com/zrepl/zrepl/tlsconf"
|
||||||
"os"
|
"github.com/zrepl/zrepl/transport"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
|
||||||
"github.com/zrepl/zrepl/daemon/transport/serve"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
||||||
@@ -32,7 +38,7 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
|
|||||||
var syslogOutlets, stdoutOutlets int
|
var syslogOutlets, stdoutOutlets int
|
||||||
for lei, le := range in {
|
for lei, le := range in {
|
||||||
|
|
||||||
outlet, minLevel, err := parseOutlet(le)
|
outlet, minLevel, err := ParseOutlet(le)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
||||||
}
|
}
|
||||||
@@ -60,22 +66,44 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Subsystem string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SubsysReplication = "repl"
|
SubsysReplication Subsystem = "repl"
|
||||||
SubsysStreamrpc = "rpc"
|
SubsyEndpoint Subsystem = "endpoint"
|
||||||
SubsyEndpoint = "endpoint"
|
SubsysPruning Subsystem = "pruning"
|
||||||
|
SubsysSnapshot Subsystem = "snapshot"
|
||||||
|
SubsysHooks Subsystem = "hook"
|
||||||
|
SubsysTransport Subsystem = "transport"
|
||||||
|
SubsysTransportMux Subsystem = "transportmux"
|
||||||
|
SubsysRPC Subsystem = "rpc"
|
||||||
|
SubsysRPCControl Subsystem = "rpc.ctrl"
|
||||||
|
SubsysRPCData Subsystem = "rpc.data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
|
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
|
||||||
ctx = replication.WithLogger(ctx, log.WithField(SubsysField, "repl"))
|
ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
||||||
ctx = streamrpc.ContextWithLogger(ctx, streamrpcLogAdaptor{log.WithField(SubsysField, "rpc")})
|
ctx = driver.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
||||||
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, "endpoint"))
|
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsyEndpoint))
|
||||||
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, "pruning"))
|
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
|
||||||
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, "snapshot"))
|
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
||||||
ctx = serve.WithLogger(ctx, log.WithField(SubsysField, "serve"))
|
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
|
||||||
|
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
||||||
|
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
||||||
|
ctx = rpc.WithLoggers(ctx,
|
||||||
|
rpc.Loggers{
|
||||||
|
General: log.WithField(SubsysField, SubsysRPC),
|
||||||
|
Control: log.WithField(SubsysField, SubsysRPCControl),
|
||||||
|
Data: log.WithField(SubsysField, SubsysRPCData),
|
||||||
|
},
|
||||||
|
)
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LogSubsystem(log logger.Logger, subsys Subsystem) logger.Logger {
|
||||||
|
return log.ReplaceField(SubsysField, subsys)
|
||||||
|
}
|
||||||
|
|
||||||
func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
||||||
var is string
|
var is string
|
||||||
switch j := i.(type) {
|
switch j := i.(type) {
|
||||||
@@ -98,7 +126,7 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
|
func ParseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
|
||||||
|
|
||||||
parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) {
|
parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) {
|
||||||
if common.Level == "" || common.Format == "" {
|
if common.Level == "" || common.Format == "" {
|
||||||
@@ -200,6 +228,7 @@ func parseSyslogOutlet(in *config.SyslogLoggingOutlet, formatter EntryFormatter)
|
|||||||
out = &SyslogOutlet{}
|
out = &SyslogOutlet{}
|
||||||
out.Formatter = formatter
|
out.Formatter = formatter
|
||||||
out.Formatter.SetMetadataFlags(MetadataNone)
|
out.Formatter.SetMetadataFlags(MetadataNone)
|
||||||
|
out.Facility = syslog.Priority(*in.Facility)
|
||||||
out.RetryInterval = in.RetryInterval
|
out.RetryInterval = in.RetryInterval
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/go-logfmt/logfmt"
|
"github.com/go-logfmt/logfmt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -86,7 +88,7 @@ func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
|
|||||||
prefixFields := []string{JobField, SubsysField}
|
prefixFields := []string{JobField, SubsysField}
|
||||||
prefixed := make(map[string]bool, len(prefixFields)+2)
|
prefixed := make(map[string]bool, len(prefixFields)+2)
|
||||||
for _, field := range prefixFields {
|
for _, field := range prefixFields {
|
||||||
val, ok := e.Fields[field].(string)
|
val, ok := e.Fields[field]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -159,10 +161,16 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
|
|||||||
enc := logfmt.NewEncoder(&buf)
|
enc := logfmt.NewEncoder(&buf)
|
||||||
|
|
||||||
if f.metadataFlags&MetadataTime != 0 {
|
if f.metadataFlags&MetadataTime != 0 {
|
||||||
enc.EncodeKeyval(FieldTime, e.Time)
|
err := enc.EncodeKeyval(FieldTime, e.Time)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "logfmt: encode time")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if f.metadataFlags&MetadataLevel != 0 {
|
if f.metadataFlags&MetadataLevel != 0 {
|
||||||
enc.EncodeKeyval(FieldLevel, e.Level)
|
err := enc.EncodeKeyval(FieldLevel, e.Level)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "logfmt: encode level")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// at least try and put job and task in front
|
// at least try and put job and task in front
|
||||||
@@ -179,8 +187,10 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
|
|||||||
prefixed[pf] = true
|
prefixed[pf] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
enc.EncodeKeyval(FieldMessage, e.Message)
|
err := enc.EncodeKeyval(FieldMessage, e.Message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "logfmt: encode message")
|
||||||
|
}
|
||||||
for k, v := range e.Fields {
|
for k, v := range e.Fields {
|
||||||
if !prefixed[k] {
|
if !prefixed[k] {
|
||||||
if err := logfmtTryEncodeKeyval(enc, k, v); err != nil {
|
if err := logfmtTryEncodeKeyval(enc, k, v); err != nil {
|
||||||
@@ -199,7 +209,10 @@ func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error
|
|||||||
case nil: // ok
|
case nil: // ok
|
||||||
return nil
|
return nil
|
||||||
case logfmt.ErrUnsupportedValueType:
|
case logfmt.ErrUnsupportedValueType:
|
||||||
enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot encode unsuuported value type Go type")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return errors.Wrapf(err, "cannot encode field '%s'", field)
|
return errors.Wrapf(err, "cannot encode field '%s'", field)
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"io"
|
"io"
|
||||||
"log/syslog"
|
"log/syslog"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EntryFormatter interface {
|
type EntryFormatter interface {
|
||||||
@@ -28,7 +30,10 @@ func (h WriterOutlet) WriteEntry(entry logger.Entry) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = h.writer.Write(bytes)
|
_, err = h.writer.Write(bytes)
|
||||||
h.writer.Write([]byte("\n"))
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = h.writer.Write([]byte("\n"))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +97,10 @@ func (h *TCPOutlet) outLoop(retryInterval time.Duration) {
|
|||||||
conn = nil
|
conn = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conn.SetWriteDeadline(time.Now().Add(retryInterval))
|
err = conn.SetWriteDeadline(time.Now().Add(retryInterval))
|
||||||
|
if err == nil {
|
||||||
_, err = io.Copy(conn, msg)
|
_, err = io.Copy(conn, msg)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
retry = time.Now().Add(retryInterval)
|
retry = time.Now().Add(retryInterval)
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@@ -124,6 +131,7 @@ func (h *TCPOutlet) WriteEntry(e logger.Entry) error {
|
|||||||
type SyslogOutlet struct {
|
type SyslogOutlet struct {
|
||||||
Formatter EntryFormatter
|
Formatter EntryFormatter
|
||||||
RetryInterval time.Duration
|
RetryInterval time.Duration
|
||||||
|
Facility syslog.Priority
|
||||||
writer *syslog.Writer
|
writer *syslog.Writer
|
||||||
lastConnectAttempt time.Time
|
lastConnectAttempt time.Time
|
||||||
}
|
}
|
||||||
@@ -142,7 +150,7 @@ func (o *SyslogOutlet) WriteEntry(entry logger.Entry) error {
|
|||||||
if now.Sub(o.lastConnectAttempt) < o.RetryInterval {
|
if now.Sub(o.lastConnectAttempt) < o.RetryInterval {
|
||||||
return nil // not an error toward logger
|
return nil // not an error toward logger
|
||||||
}
|
}
|
||||||
o.writer, err = syslog.New(syslog.LOG_LOCAL0, "zrepl")
|
o.writer, err = syslog.New(o.Facility, "zrepl")
|
||||||
o.lastConnectAttempt = time.Now()
|
o.lastConnectAttempt = time.Now()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.writer = nil
|
o.writer = nil
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
type Logger = logger.Logger
|
type Logger = logger.Logger
|
||||||
|
|
||||||
var DaemonCmd = &cli.Subcommand {
|
var DaemonCmd = &cli.Subcommand{
|
||||||
Use: "daemon",
|
Use: "daemon",
|
||||||
Short: "run the zrepl daemon",
|
Short: "run the zrepl daemon",
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package nethelpers
|
package nethelpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PreparePrivateSockpath(sockpath string) error {
|
func PreparePrivateSockpath(sockpath string) error {
|
||||||
|
|||||||
+10
-2
@@ -7,11 +7,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pprofServer struct {
|
type pprofServer struct {
|
||||||
cc chan PprofServerControlMsg
|
cc chan PprofServerControlMsg
|
||||||
state PprofServerControlMsg
|
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +64,14 @@ outer:
|
|||||||
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
|
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
|
||||||
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
|
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
|
||||||
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
|
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
|
||||||
go http.Serve(s.listener, mux)
|
go func() {
|
||||||
|
err := http.Serve(s.listener, mux)
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
job.GetLogger(ctx).WithError(err).Error("pprof server serve error")
|
||||||
|
}
|
||||||
|
}()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-13
@@ -2,25 +2,31 @@ package daemon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
||||||
|
"github.com/zrepl/zrepl/util/tcpsock"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type prometheusJob struct {
|
type prometheusJob struct {
|
||||||
listen string
|
listen string
|
||||||
|
freeBind bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
||||||
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &prometheusJob{in.Listen}, nil
|
return &prometheusJob{in.Listen, in.ListenFreeBind}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var prom struct {
|
var prom struct {
|
||||||
@@ -41,6 +47,10 @@ func (j *prometheusJob) Name() string { return jobNamePrometheus }
|
|||||||
|
|
||||||
func (j *prometheusJob) Status() *job.Status { return &job.Status{Type: job.TypeInternal} }
|
func (j *prometheusJob) Status() *job.Status { return &job.Status{Type: job.TypeInternal} }
|
||||||
|
|
||||||
|
func (j *prometheusJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||||
|
|
||||||
|
func (j *prometheusJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
|
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||||
|
|
||||||
func (j *prometheusJob) Run(ctx context.Context) {
|
func (j *prometheusJob) Run(ctx context.Context) {
|
||||||
@@ -49,24 +59,27 @@ func (j *prometheusJob) Run(ctx context.Context) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := frameconn.PrometheusRegister(prometheus.DefaultRegisterer); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
log := job.GetLogger(ctx)
|
log := job.GetLogger(ctx)
|
||||||
|
|
||||||
l, err := net.Listen("tcp", j.listen)
|
l, err := tcpsock.Listen(j.listen, j.freeBind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("cannot listen")
|
log.WithError(err).Error("cannot listen")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
<-ctx.Done()
|
||||||
case <-ctx.Done():
|
|
||||||
l.Close()
|
l.Close()
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/metrics", promhttp.Handler())
|
mux.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
err = http.Serve(l, mux)
|
err = http.Serve(l, mux)
|
||||||
if err != nil {
|
if err != nil && ctx.Err() == nil {
|
||||||
log.WithError(err).Error("error while serving")
|
log.WithError(err).Error("error while serving")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,4 +99,3 @@ func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
|
|||||||
prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc()
|
prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+242
-212
@@ -3,28 +3,31 @@ package pruner
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/pruning"
|
"github.com/zrepl/zrepl/pruning"
|
||||||
"github.com/zrepl/zrepl/replication/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/util/watchdog"
|
|
||||||
"net"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Try to keep it compatible with gitub.com/zrepl/zrepl/replication.Endpoint
|
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type History 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type Target interface {
|
type Target interface {
|
||||||
ListFilesystems(ctx context.Context) ([]*pdu.Filesystem, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
ListFilesystemVersions(ctx context.Context, fs string) ([]*pdu.FilesystemVersion, error) // fix depS
|
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||||
DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error)
|
DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,19 +61,15 @@ type args struct {
|
|||||||
type Pruner struct {
|
type Pruner struct {
|
||||||
args args
|
args args
|
||||||
|
|
||||||
Progress watchdog.KeepAlive
|
|
||||||
|
|
||||||
mtx sync.RWMutex
|
mtx sync.RWMutex
|
||||||
|
|
||||||
state State
|
state State
|
||||||
|
|
||||||
// State ErrWait|ErrPerm
|
// State PlanErr
|
||||||
sleepUntil time.Time
|
|
||||||
err error
|
err error
|
||||||
|
|
||||||
// State Exec
|
// State Exec
|
||||||
prunePending []*fs
|
execQueue *execQueue
|
||||||
pruneCompleted []*fs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PrunerFactory struct {
|
type PrunerFactory struct {
|
||||||
@@ -81,17 +80,30 @@ type PrunerFactory struct {
|
|||||||
promPruneSecs *prometheus.HistogramVec
|
promPruneSecs *prometheus.HistogramVec
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkContainsKeep1(rules []pruning.KeepRule) error {
|
type LocalPrunerFactory struct {
|
||||||
if len(rules) == 0 {
|
keepRules []pruning.KeepRule
|
||||||
return nil //No keep rules means keep all - ok
|
retryWait time.Duration
|
||||||
|
promPruneSecs *prometheus.HistogramVec
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocalPrunerFactory(in config.PruningLocal, promPruneSecs *prometheus.HistogramVec) (*LocalPrunerFactory, error) {
|
||||||
|
rules, err := pruning.RulesFromConfig(in.Keep)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build pruning rules")
|
||||||
}
|
}
|
||||||
for _, e := range rules {
|
for _, r := range in.Keep {
|
||||||
switch e.(type) {
|
if _, ok := r.Ret.(*config.PruneKeepNotReplicated); ok {
|
||||||
case *pruning.KeepLastN:
|
// rule NotReplicated for a local pruner doesn't make sense
|
||||||
return nil
|
// because no replication happens with that job type
|
||||||
|
return nil, fmt.Errorf("single-site pruner cannot support `not_replicated` keep rule")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errors.New("sender keep rules must contain last_n or be empty so that the last snapshot is definitely kept")
|
f := &LocalPrunerFactory{
|
||||||
|
keepRules: rules,
|
||||||
|
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
|
||||||
|
promPruneSecs: promPruneSecs,
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus.HistogramVec) (*PrunerFactory, error) {
|
func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus.HistogramVec) (*PrunerFactory, error) {
|
||||||
@@ -116,7 +128,7 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
|||||||
f := &PrunerFactory{
|
f := &PrunerFactory{
|
||||||
senderRules: keepRulesSender,
|
senderRules: keepRulesSender,
|
||||||
receiverRules: keepRulesReceiver,
|
receiverRules: keepRulesReceiver,
|
||||||
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 4 * time.Second),
|
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
|
||||||
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
|
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
|
||||||
promPruneSecs: promPruneSecs,
|
promPruneSecs: promPruneSecs,
|
||||||
}
|
}
|
||||||
@@ -155,67 +167,55 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||||
|
p := &Pruner{
|
||||||
|
args: args{
|
||||||
|
ctx,
|
||||||
|
target,
|
||||||
|
receiver,
|
||||||
|
f.keepRules,
|
||||||
|
f.retryWait,
|
||||||
|
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
||||||
|
f.promPruneSecs.WithLabelValues("local"),
|
||||||
|
},
|
||||||
|
state: Plan,
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
//go:generate enumer -type=State
|
//go:generate enumer -type=State
|
||||||
type State int
|
type State int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Plan State = 1 << iota
|
Plan State = 1 << iota
|
||||||
PlanWait
|
PlanErr
|
||||||
Exec
|
Exec
|
||||||
ExecWait
|
ExecErr
|
||||||
ErrPerm
|
|
||||||
Done
|
Done
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s State) statefunc() state {
|
type updater func(func(*Pruner))
|
||||||
var statemap = map[State]state{
|
|
||||||
Plan: statePlan,
|
|
||||||
PlanWait: statePlanWait,
|
|
||||||
Exec: stateExec,
|
|
||||||
ExecWait: stateExecWait,
|
|
||||||
ErrPerm: nil,
|
|
||||||
Done: nil,
|
|
||||||
}
|
|
||||||
return statemap[s]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s State) IsTerminal() bool {
|
|
||||||
return s.statefunc() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type updater func(func(*Pruner)) State
|
|
||||||
type state func(args *args, u updater) state
|
|
||||||
|
|
||||||
func (p *Pruner) Prune() {
|
func (p *Pruner) Prune() {
|
||||||
p.prune(p.args)
|
p.prune(p.args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pruner) prune(args args) {
|
func (p *Pruner) prune(args args) {
|
||||||
s := p.state.statefunc()
|
u := func(f func(*Pruner)) {
|
||||||
for s != nil {
|
|
||||||
pre := p.state
|
|
||||||
s = s(&args, func(f func(*Pruner)) State {
|
|
||||||
p.mtx.Lock()
|
p.mtx.Lock()
|
||||||
defer p.mtx.Unlock()
|
defer p.mtx.Unlock()
|
||||||
f(p)
|
f(p)
|
||||||
return p.state
|
|
||||||
})
|
|
||||||
post := p.state
|
|
||||||
GetLogger(args.ctx).
|
|
||||||
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
|
||||||
Debug("state transition")
|
|
||||||
if err := p.Error(); err != nil {
|
|
||||||
GetLogger(args.ctx).
|
|
||||||
WithError(p.err).
|
|
||||||
WithField("state", post.String()).
|
|
||||||
Error("entering error state after error")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// TODO support automatic retries
|
||||||
|
// It is advisable to merge this code with package replication/driver before
|
||||||
|
// That will likely require re-modelling struct fs like replication/driver.attempt,
|
||||||
|
// including figuring out how to resume a plan after being interrupted by network errors
|
||||||
|
// The non-retrying code in this package should move straight to replication/logic.
|
||||||
|
doOneAttempt(&args, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Report struct {
|
type Report struct {
|
||||||
State string
|
State string
|
||||||
SleepUntil time.Time
|
|
||||||
Error string
|
Error string
|
||||||
Pending, Completed []FSReport
|
Pending, Completed []FSReport
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,8 @@ type Report struct {
|
|||||||
type FSReport struct {
|
type FSReport struct {
|
||||||
Filesystem string
|
Filesystem string
|
||||||
SnapshotList, DestroyList []SnapshotReport
|
SnapshotList, DestroyList []SnapshotReport
|
||||||
Error string
|
SkipReason FSSkipReason
|
||||||
|
LastError string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshotReport struct {
|
type SnapshotReport struct {
|
||||||
@@ -238,26 +239,12 @@ func (p *Pruner) Report() *Report {
|
|||||||
|
|
||||||
r := Report{State: p.state.String()}
|
r := Report{State: p.state.String()}
|
||||||
|
|
||||||
if p.state & PlanWait|ExecWait != 0 {
|
|
||||||
r.SleepUntil = p.sleepUntil
|
|
||||||
}
|
|
||||||
if p.state & PlanWait|ExecWait|ErrPerm != 0 {
|
|
||||||
if p.err != nil {
|
if p.err != nil {
|
||||||
r.Error = p.err.Error()
|
r.Error = p.err.Error()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if p.state & Plan|PlanWait == 0 {
|
if p.execQueue != nil {
|
||||||
return &r
|
r.Pending, r.Completed = p.execQueue.Report()
|
||||||
}
|
|
||||||
|
|
||||||
r.Pending = make([]FSReport, len(p.prunePending))
|
|
||||||
for i, fs := range p.prunePending{
|
|
||||||
r.Pending[i] = fs.Report()
|
|
||||||
}
|
|
||||||
r.Completed = make([]FSReport, len(p.pruneCompleted))
|
|
||||||
for i, fs := range p.pruneCompleted{
|
|
||||||
r.Completed[i] = fs.Report()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &r
|
return &r
|
||||||
@@ -269,18 +256,17 @@ func (p *Pruner) State() State {
|
|||||||
return p.state
|
return p.state
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pruner) Error() error {
|
|
||||||
p.mtx.Lock()
|
|
||||||
defer p.mtx.Unlock()
|
|
||||||
if p.state & (PlanWait|ExecWait|ErrPerm) != 0 {
|
|
||||||
return p.err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type fs struct {
|
type fs struct {
|
||||||
path string
|
path string
|
||||||
|
|
||||||
|
// permanent error during planning
|
||||||
|
planErr error
|
||||||
|
planErrContext string
|
||||||
|
|
||||||
|
// if != "", the fs was skipped for planning and the field
|
||||||
|
// contains the reason
|
||||||
|
skipReason FSSkipReason
|
||||||
|
|
||||||
// snapshots presented by target
|
// snapshots presented by target
|
||||||
// (type snapshot)
|
// (type snapshot)
|
||||||
snaps []pruning.Snapshot
|
snaps []pruning.Snapshot
|
||||||
@@ -289,14 +275,21 @@ type fs struct {
|
|||||||
destroyList []pruning.Snapshot
|
destroyList []pruning.Snapshot
|
||||||
|
|
||||||
mtx sync.RWMutex
|
mtx sync.RWMutex
|
||||||
// for Plan
|
|
||||||
err error
|
// only during Exec state, also used by execQueue
|
||||||
|
execErrLast error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fs) Update(err error) {
|
type FSSkipReason string
|
||||||
f.mtx.Lock()
|
|
||||||
defer f.mtx.Unlock()
|
const (
|
||||||
f.err = err
|
NotSkipped = ""
|
||||||
|
SkipPlaceholder = "filesystem is placeholder"
|
||||||
|
SkipNoCorrespondenceOnSender = "filesystem has no correspondence on sender"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r FSSkipReason) NotSkipped() bool {
|
||||||
|
return r == NotSkipped
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fs) Report() FSReport {
|
func (f *fs) Report() FSReport {
|
||||||
@@ -305,8 +298,15 @@ func (f *fs) Report() FSReport {
|
|||||||
|
|
||||||
r := FSReport{}
|
r := FSReport{}
|
||||||
r.Filesystem = f.path
|
r.Filesystem = f.path
|
||||||
if f.err != nil {
|
r.SkipReason = f.skipReason
|
||||||
r.Error = f.err.Error()
|
if !r.SkipReason.NotSkipped() {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.planErr != nil {
|
||||||
|
r.LastError = f.planErr.Error()
|
||||||
|
} else if f.execErrLast != nil {
|
||||||
|
r.LastError = f.execErrLast.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
r.SnapshotList = make([]SnapshotReport, len(f.snaps))
|
r.SnapshotList = make([]SnapshotReport, len(f.snaps))
|
||||||
@@ -315,7 +315,7 @@ func (f *fs) Report() FSReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.DestroyList = make([]SnapshotReport, len(f.destroyList))
|
r.DestroyList = make([]SnapshotReport, len(f.destroyList))
|
||||||
for i, snap := range f.destroyList{
|
for i, snap := range f.destroyList {
|
||||||
r.DestroyList[i] = snap.(snapshot).Report()
|
r.DestroyList[i] = snap.(snapshot).Report()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,85 +344,85 @@ func (s snapshot) Replicated() bool { return s.replicated }
|
|||||||
|
|
||||||
func (s snapshot) Date() time.Time { return s.date }
|
func (s snapshot) Date() time.Time { return s.date }
|
||||||
|
|
||||||
func shouldRetry(e error) bool {
|
func doOneAttempt(a *args, u updater) {
|
||||||
if neterr, ok := e.(net.Error); ok {
|
|
||||||
return neterr.Temporary()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func onErr(u updater, e error) state {
|
|
||||||
return u(func(p *Pruner) {
|
|
||||||
p.err = e
|
|
||||||
if !shouldRetry(e) {
|
|
||||||
p.state = ErrPerm
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch p.state {
|
|
||||||
case Plan:
|
|
||||||
p.state = PlanWait
|
|
||||||
case Exec:
|
|
||||||
p.state = ExecWait
|
|
||||||
default:
|
|
||||||
panic(p.state)
|
|
||||||
}
|
|
||||||
}).statefunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
func statePlan(a *args, u updater) state {
|
|
||||||
|
|
||||||
ctx, target, receiver := a.ctx, a.target, a.receiver
|
ctx, target, receiver := a.ctx, a.target, a.receiver
|
||||||
|
|
||||||
tfss, err := target.ListFilesystems(ctx)
|
sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return onErr(u, err)
|
u(func(p *Pruner) {
|
||||||
|
p.state = PlanErr
|
||||||
|
p.err = err
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sfss := make(map[string]*pdu.Filesystem)
|
||||||
|
for _, sfs := range sfssres.GetFilesystems() {
|
||||||
|
sfss[sfs.GetPath()] = sfs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tfssres, err := target.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
|
if err != nil {
|
||||||
|
u(func(p *Pruner) {
|
||||||
|
p.state = PlanErr
|
||||||
|
p.err = err
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tfss := tfssres.GetFilesystems()
|
||||||
|
|
||||||
pfss := make([]*fs, len(tfss))
|
pfss := make([]*fs, len(tfss))
|
||||||
fsloop:
|
tfss_loop:
|
||||||
for i, tfs := range tfss {
|
for i, tfs := range tfss {
|
||||||
|
|
||||||
l := GetLogger(ctx).WithField("fs", tfs.Path)
|
l := GetLogger(ctx).WithField("fs", tfs.Path)
|
||||||
l.Debug("plan filesystem")
|
l.Debug("plan filesystem")
|
||||||
|
|
||||||
|
|
||||||
pfs := &fs{
|
pfs := &fs{
|
||||||
path: tfs.Path,
|
path: tfs.Path,
|
||||||
}
|
}
|
||||||
pfss[i] = pfs
|
pfss[i] = pfs
|
||||||
|
|
||||||
tfsvs, err := target.ListFilesystemVersions(ctx, tfs.Path)
|
if tfs.GetIsPlaceholder() {
|
||||||
|
pfs.skipReason = SkipPlaceholder
|
||||||
|
l.WithField("skip_reason", pfs.skipReason).Debug("skipping filesystem")
|
||||||
|
continue
|
||||||
|
} else if sfs := sfss[tfs.GetPath()]; sfs == nil {
|
||||||
|
pfs.skipReason = SkipNoCorrespondenceOnSender
|
||||||
|
l.WithField("skip_reason", pfs.skipReason).WithField("sfs", sfs.GetPath()).Debug("skipping filesystem")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pfsPlanErrAndLog := func(err error, message string) {
|
||||||
|
t := fmt.Sprintf("%T", err)
|
||||||
|
pfs.planErr = err
|
||||||
|
pfs.planErrContext = message
|
||||||
|
l.WithField("orig_err_type", t).WithError(err).Error(fmt.Sprintf("%s: plan error, skipping filesystem", message))
|
||||||
|
}
|
||||||
|
|
||||||
|
tfsvsres, err := target.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: tfs.Path})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot list filesystem versions")
|
pfsPlanErrAndLog(err, "cannot list filesystem versions")
|
||||||
if shouldRetry(err) {
|
continue tfss_loop
|
||||||
return onErr(u, err)
|
|
||||||
}
|
|
||||||
pfs.err = err
|
|
||||||
continue fsloop
|
|
||||||
}
|
}
|
||||||
|
tfsvs := tfsvsres.GetVersions()
|
||||||
|
// no progress here since we could run in a live-lock (must have used target AND receiver before progress)
|
||||||
|
|
||||||
pfs.snaps = make([]pruning.Snapshot, 0, len(tfsvs))
|
pfs.snaps = make([]pruning.Snapshot, 0, len(tfsvs))
|
||||||
|
|
||||||
rcReq := &pdu.ReplicationCursorReq{
|
rcReq := &pdu.ReplicationCursorReq{
|
||||||
Filesystem: tfs.Path,
|
Filesystem: tfs.Path,
|
||||||
Op: &pdu.ReplicationCursorReq_Get{
|
|
||||||
Get: &pdu.ReplicationCursorReq_GetOp{},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot get replication cursor")
|
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||||
if shouldRetry(err) {
|
continue tfss_loop
|
||||||
return onErr(u, err)
|
|
||||||
}
|
}
|
||||||
pfs.err = err
|
if rc.GetNotexist() {
|
||||||
continue fsloop
|
err := errors.New("replication cursor bookmark does not exist (one successful replication is required before pruning works)")
|
||||||
|
pfsPlanErrAndLog(err, "")
|
||||||
|
continue tfss_loop
|
||||||
}
|
}
|
||||||
if rc.GetError() != "" {
|
|
||||||
l.WithField("reqErr", rc.GetError()).Error("cannot get replication cursor")
|
|
||||||
pfs.err = fmt.Errorf("%s", rc.GetError())
|
|
||||||
continue fsloop
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// scan from older to newer, all snapshots older than cursor are interpreted as replicated
|
// scan from older to newer, all snapshots older than cursor are interpreted as replicated
|
||||||
sort.Slice(tfsvs, func(i, j int) bool {
|
sort.Slice(tfsvs, func(i, j int) bool {
|
||||||
@@ -445,9 +445,9 @@ fsloop:
|
|||||||
}
|
}
|
||||||
creation, err := tfsv.CreationAsTime()
|
creation, err := tfsv.CreationAsTime()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pfs.err = fmt.Errorf("%s%s has invalid creation date: %s", tfs, tfsv.RelName(), err)
|
err := fmt.Errorf("%s: %s", tfsv.RelName(), err)
|
||||||
l.WithError(pfs.err).Error("")
|
pfsPlanErrAndLog(err, "fs version with invalid creation date")
|
||||||
continue fsloop
|
continue tfss_loop
|
||||||
}
|
}
|
||||||
// note that we cannot use CreateTXG because target and receiver could be on different pools
|
// note that we cannot use CreateTXG because target and receiver could be on different pools
|
||||||
atCursor := tfsv.Guid == rc.GetGuid()
|
atCursor := tfsv.Guid == rc.GetGuid()
|
||||||
@@ -459,47 +459,61 @@ fsloop:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if preCursor {
|
if preCursor {
|
||||||
pfs.err = fmt.Errorf("replication cursor not found in prune target filesystem versions")
|
pfsPlanErrAndLog(fmt.Errorf("replication cursor not found in prune target filesystem versions"), "")
|
||||||
l.WithError(pfs.err).Error("")
|
continue tfss_loop
|
||||||
continue fsloop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply prune rules
|
// Apply prune rules
|
||||||
pfs.destroyList = pruning.PruneSnapshots(pfs.snaps, a.rules)
|
pfs.destroyList = pruning.PruneSnapshots(pfs.snaps, a.rules)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return u(func(pruner *Pruner) {
|
u(func(pruner *Pruner) {
|
||||||
|
pruner.execQueue = newExecQueue(len(pfss))
|
||||||
for _, pfs := range pfss {
|
for _, pfs := range pfss {
|
||||||
if pfs.err != nil {
|
pruner.execQueue.Put(pfs, nil, false)
|
||||||
pruner.pruneCompleted = append(pruner.pruneCompleted, pfs)
|
|
||||||
} else {
|
|
||||||
pruner.prunePending = append(pruner.prunePending, pfs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pruner.state = Exec
|
pruner.state = Exec
|
||||||
}).statefunc()
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
var pfs *fs
|
||||||
|
u(func(pruner *Pruner) {
|
||||||
|
pfs = pruner.execQueue.Pop()
|
||||||
|
})
|
||||||
|
if pfs == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
doOneAttemptExec(a, u, pfs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rep *Report
|
||||||
|
{
|
||||||
|
// must not hold lock for report
|
||||||
|
var pruner *Pruner
|
||||||
|
u(func(p *Pruner) {
|
||||||
|
pruner = p
|
||||||
|
})
|
||||||
|
rep = pruner.Report()
|
||||||
|
}
|
||||||
|
u(func(p *Pruner) {
|
||||||
|
if len(rep.Pending) > 0 {
|
||||||
|
panic("queue should not have pending items at this point")
|
||||||
|
}
|
||||||
|
hadErr := false
|
||||||
|
for _, fsr := range rep.Completed {
|
||||||
|
hadErr = hadErr || fsr.SkipReason.NotSkipped() && fsr.LastError != ""
|
||||||
|
}
|
||||||
|
if hadErr {
|
||||||
|
p.state = ExecErr
|
||||||
|
} else {
|
||||||
|
p.state = Done
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stateExec(a *args, u updater) state {
|
// attempts to exec pfs, puts it back into the queue with the result
|
||||||
|
func doOneAttemptExec(a *args, u updater, pfs *fs) {
|
||||||
var pfs *fs
|
|
||||||
state := u(func(pruner *Pruner) {
|
|
||||||
if len(pruner.prunePending) == 0 {
|
|
||||||
nextState := Done
|
|
||||||
for _, pfs := range pruner.pruneCompleted {
|
|
||||||
if pfs.err != nil {
|
|
||||||
nextState = ErrPerm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pruner.state = nextState
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pfs = pruner.prunePending[0]
|
|
||||||
})
|
|
||||||
if state != Exec {
|
|
||||||
return state.statefunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyList := make([]*pdu.FilesystemVersion, len(pfs.destroyList))
|
destroyList := make([]*pdu.FilesystemVersion, len(pfs.destroyList))
|
||||||
for i := range destroyList {
|
for i := range destroyList {
|
||||||
@@ -509,41 +523,57 @@ func stateExec(a *args, u updater) state {
|
|||||||
WithField("destroy_snap", destroyList[i].Name).
|
WithField("destroy_snap", destroyList[i].Name).
|
||||||
Debug("policy destroys snapshot")
|
Debug("policy destroys snapshot")
|
||||||
}
|
}
|
||||||
pfs.Update(nil)
|
|
||||||
req := pdu.DestroySnapshotsReq{
|
req := pdu.DestroySnapshotsReq{
|
||||||
Filesystem: pfs.path,
|
Filesystem: pfs.path,
|
||||||
Snapshots: destroyList,
|
Snapshots: destroyList,
|
||||||
}
|
}
|
||||||
_, err := a.target.DestroySnapshots(a.ctx, &req)
|
GetLogger(a.ctx).WithField("fs", pfs.path).Debug("destroying snapshots")
|
||||||
pfs.Update(err)
|
res, err := a.target.DestroySnapshots(a.ctx, &req)
|
||||||
if err != nil && shouldRetry(err) {
|
if err != nil {
|
||||||
return onErr(u, err)
|
u(func(pruner *Pruner) {
|
||||||
|
pruner.execQueue.Put(pfs, err, false)
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// if it's not retryable, treat is like as being done
|
// check if all snapshots were destroyed
|
||||||
|
destroyResults := make(map[string]*pdu.DestroySnapshotRes)
|
||||||
return u(func(pruner *Pruner) {
|
for _, fsres := range res.Results {
|
||||||
pruner.pruneCompleted = append(pruner.pruneCompleted, pfs)
|
destroyResults[fsres.Snapshot.Name] = fsres
|
||||||
pruner.prunePending = pruner.prunePending[1:]
|
}
|
||||||
}).statefunc()
|
err = nil
|
||||||
}
|
destroyFails := make([]*pdu.DestroySnapshotRes, 0)
|
||||||
|
for _, reqDestroy := range destroyList {
|
||||||
func stateExecWait(a *args, u updater) state {
|
res, ok := destroyResults[reqDestroy.Name]
|
||||||
return doWait(Exec, a, u)
|
if !ok {
|
||||||
}
|
err = fmt.Errorf("missing destroy-result for %s", reqDestroy.RelName())
|
||||||
|
break
|
||||||
func statePlanWait(a *args, u updater) state {
|
} else if res.Error != "" {
|
||||||
return doWait(Plan, a, u)
|
destroyFails = append(destroyFails, res)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
func doWait(goback State, a *args, u updater) state {
|
if err == nil && len(destroyFails) > 0 {
|
||||||
timer := time.NewTimer(a.retryWait)
|
names := make([]string, len(destroyFails))
|
||||||
defer timer.Stop()
|
pairs := make([]string, len(destroyFails))
|
||||||
select {
|
allSame := true
|
||||||
case <-timer.C:
|
lastMsg := destroyFails[0].Error
|
||||||
return u(func(pruner *Pruner) {
|
for i := 0; i < len(destroyFails); i++ {
|
||||||
pruner.state = goback
|
allSame = allSame && destroyFails[i].Error == lastMsg
|
||||||
}).statefunc()
|
relname := destroyFails[i].Snapshot.RelName()
|
||||||
case <-a.ctx.Done():
|
names[i] = relname
|
||||||
return onErr(u, a.ctx.Err())
|
pairs[i] = fmt.Sprintf("(%s: %s)", relname, destroyFails[i].Error)
|
||||||
|
}
|
||||||
|
if allSame {
|
||||||
|
err = fmt.Errorf("destroys failed %s: %s",
|
||||||
|
strings.Join(names, ", "), lastMsg)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("destroys failed: %s", strings.Join(pairs, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u(func(pruner *Pruner) {
|
||||||
|
pruner.execQueue.Put(pfs, err, err == nil)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
GetLogger(a.ctx).WithError(err).Error("target could not destroy snapshots")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package pruner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type execQueue struct {
|
||||||
|
mtx sync.Mutex
|
||||||
|
pending, completed []*fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecQueue(cap int) *execQueue {
|
||||||
|
q := execQueue{
|
||||||
|
pending: make([]*fs, 0, cap),
|
||||||
|
completed: make([]*fs, 0, cap),
|
||||||
|
}
|
||||||
|
return &q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *execQueue) Report() (pending, completed []FSReport) {
|
||||||
|
q.mtx.Lock()
|
||||||
|
defer q.mtx.Unlock()
|
||||||
|
|
||||||
|
pending = make([]FSReport, len(q.pending))
|
||||||
|
for i, fs := range q.pending {
|
||||||
|
pending[i] = fs.Report()
|
||||||
|
}
|
||||||
|
completed = make([]FSReport, len(q.completed))
|
||||||
|
for i, fs := range q.completed {
|
||||||
|
completed[i] = fs.Report()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending, completed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *execQueue) HasCompletedFSWithErrors() bool {
|
||||||
|
q.mtx.Lock()
|
||||||
|
defer q.mtx.Unlock()
|
||||||
|
for _, fs := range q.completed {
|
||||||
|
if fs.execErrLast != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *execQueue) Pop() *fs {
|
||||||
|
if len(q.pending) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fs := q.pending[0]
|
||||||
|
q.pending = q.pending[1:]
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *execQueue) Put(fs *fs, err error, done bool) {
|
||||||
|
fs.mtx.Lock()
|
||||||
|
fs.execErrLast = err
|
||||||
|
if done || err != nil {
|
||||||
|
fs.mtx.Unlock()
|
||||||
|
q.mtx.Lock()
|
||||||
|
q.completed = append(q.completed, fs)
|
||||||
|
q.mtx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fs.mtx.Unlock()
|
||||||
|
|
||||||
|
q.mtx.Lock()
|
||||||
|
// inefficient priority q
|
||||||
|
q.pending = append(q.pending, fs)
|
||||||
|
sort.SliceStable(q.pending, func(i, j int) bool {
|
||||||
|
q.pending[i].mtx.Lock()
|
||||||
|
defer q.pending[i].mtx.Unlock()
|
||||||
|
q.pending[j].mtx.Lock()
|
||||||
|
defer q.pending[j].mtx.Unlock()
|
||||||
|
return strings.Compare(q.pending[i].path, q.pending[j].path) == -1
|
||||||
|
})
|
||||||
|
q.mtx.Unlock()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package pruner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"github.com/zrepl/zrepl/pruning"
|
|
||||||
"github.com/zrepl/zrepl/replication/pdu"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockFS struct {
|
|
||||||
path string
|
|
||||||
snaps []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFS) Filesystem() *pdu.Filesystem {
|
|
||||||
return &pdu.Filesystem{
|
|
||||||
Path: m.path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockFS) FilesystemVersions() []*pdu.FilesystemVersion {
|
|
||||||
versions := make([]*pdu.FilesystemVersion, len(m.snaps))
|
|
||||||
for i, v := range m.snaps {
|
|
||||||
versions[i] = &pdu.FilesystemVersion{
|
|
||||||
Type: pdu.FilesystemVersion_Snapshot,
|
|
||||||
Name: v,
|
|
||||||
Creation: pdu.FilesystemVersionCreation(time.Unix(0, 0)),
|
|
||||||
Guid: uint64(i),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return versions
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockTarget struct {
|
|
||||||
fss []mockFS
|
|
||||||
destroyed map[string][]string
|
|
||||||
listVersionsErrs map[string][]error
|
|
||||||
listFilesystemsErr []error
|
|
||||||
destroyErrs map[string][]error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mockTarget) ListFilesystems(ctx context.Context) ([]*pdu.Filesystem, error) {
|
|
||||||
if len(t.listFilesystemsErr) > 0 {
|
|
||||||
e := t.listFilesystemsErr[0]
|
|
||||||
t.listFilesystemsErr = t.listFilesystemsErr[1:]
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
fss := make([]*pdu.Filesystem, len(t.fss))
|
|
||||||
for i := range fss {
|
|
||||||
fss[i] = t.fss[i].Filesystem()
|
|
||||||
}
|
|
||||||
return fss, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mockTarget) ListFilesystemVersions(ctx context.Context, fs string) ([]*pdu.FilesystemVersion, error) {
|
|
||||||
if len(t.listVersionsErrs[fs]) != 0 {
|
|
||||||
e := t.listVersionsErrs[fs][0]
|
|
||||||
t.listVersionsErrs[fs] = t.listVersionsErrs[fs][1:]
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mfs := range t.fss {
|
|
||||||
if mfs.path != fs {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return mfs.FilesystemVersions(), nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("filesystem %s does not exist", fs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mockTarget) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
|
||||||
fs, snaps := req.Filesystem, req.Snapshots
|
|
||||||
if len(t.destroyErrs[fs]) != 0 {
|
|
||||||
e := t.destroyErrs[fs][0]
|
|
||||||
t.destroyErrs[fs] = t.destroyErrs[fs][1:]
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
destroyed := t.destroyed[fs]
|
|
||||||
res := make([]*pdu.DestroySnapshotRes, len(snaps))
|
|
||||||
for i, s := range snaps {
|
|
||||||
destroyed = append(destroyed, s.Name)
|
|
||||||
res[i] = &pdu.DestroySnapshotRes{Error: "", Snapshot: s}
|
|
||||||
}
|
|
||||||
t.destroyed[fs] = destroyed
|
|
||||||
return &pdu.DestroySnapshotsRes{Results: res}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockCursor struct {
|
|
||||||
snapname string
|
|
||||||
guid uint64
|
|
||||||
}
|
|
||||||
type mockHistory struct {
|
|
||||||
errs map[string][]error
|
|
||||||
cursors map[string]*mockCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *mockHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
|
||||||
fs := req.Filesystem
|
|
||||||
if len(r.errs[fs]) > 0 {
|
|
||||||
e := r.errs[fs][0]
|
|
||||||
r.errs[fs] = r.errs[fs][1:]
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: 0}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type stubNetErr struct {
|
|
||||||
msg string
|
|
||||||
temporary, timeout bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ net.Error = stubNetErr{}
|
|
||||||
|
|
||||||
func (e stubNetErr) Error() string {
|
|
||||||
return e.msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e stubNetErr) Temporary() bool { return e.temporary }
|
|
||||||
|
|
||||||
func (e stubNetErr) Timeout() bool { return e.timeout }
|
|
||||||
|
|
||||||
func TestPruner_Prune(t *testing.T) {
|
|
||||||
|
|
||||||
var _ net.Error = &net.OpError{} // we use it below
|
|
||||||
target := &mockTarget{
|
|
||||||
listFilesystemsErr: []error{
|
|
||||||
stubNetErr{msg: "fakerror0", temporary: true},
|
|
||||||
},
|
|
||||||
listVersionsErrs: map[string][]error{
|
|
||||||
"zroot/foo": {
|
|
||||||
stubNetErr{msg: "fakeerror1", temporary: true}, // should be classified as temporaty
|
|
||||||
stubNetErr{msg: "fakeerror2", temporary: true,},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destroyErrs: map[string][]error{
|
|
||||||
"zroot/foo": {
|
|
||||||
fmt.Errorf("permanent error"),
|
|
||||||
},
|
|
||||||
"zroot/bar": {
|
|
||||||
stubNetErr{msg: "fakeerror3", temporary: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destroyed: make(map[string][]string),
|
|
||||||
fss: []mockFS{
|
|
||||||
{
|
|
||||||
path: "zroot/foo",
|
|
||||||
snaps: []string{
|
|
||||||
"keep_a",
|
|
||||||
"keep_b",
|
|
||||||
"drop_c",
|
|
||||||
"keep_d",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "zroot/bar",
|
|
||||||
snaps: []string{
|
|
||||||
"keep_e",
|
|
||||||
"keep_f",
|
|
||||||
"drop_g",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "zroot/baz",
|
|
||||||
snaps: []string{
|
|
||||||
"keep_h",
|
|
||||||
"drop_i",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
history := &mockHistory{
|
|
||||||
errs: map[string][]error{
|
|
||||||
"zroot/foo": {
|
|
||||||
stubNetErr{msg: "fakeerror4", temporary: true},
|
|
||||||
},
|
|
||||||
"zroot/baz": {
|
|
||||||
fmt.Errorf("permanent error2"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
keepRules := []pruning.KeepRule{pruning.MustKeepRegex("^keep")}
|
|
||||||
|
|
||||||
p := Pruner{
|
|
||||||
args: args{
|
|
||||||
ctx: WithLogger(context.Background(), logger.NewTestLogger(t)),
|
|
||||||
target: target,
|
|
||||||
receiver: history,
|
|
||||||
rules: keepRules,
|
|
||||||
retryWait: 10*time.Millisecond,
|
|
||||||
},
|
|
||||||
state: Plan,
|
|
||||||
}
|
|
||||||
p.Prune()
|
|
||||||
|
|
||||||
exp := map[string][]string{
|
|
||||||
"zroot/bar": {"drop_g"},
|
|
||||||
// drop_c is prohibited by failing destroy
|
|
||||||
// drop_i is prohibiteed by failing ReplicationCursor call
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, exp, target.destroyed)
|
|
||||||
|
|
||||||
//assert.Equal(t, map[string][]error{}, target.listVersionsErrs, "retried")
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Code generated by "enumer -type=State"; DO NOT EDIT.
|
// Code generated by "enumer -type=State"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
package pruner
|
package pruner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,19 +8,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
_StateName_0 = "PlanPlanWait"
|
_StateName_0 = "PlanPlanErr"
|
||||||
_StateName_1 = "Exec"
|
_StateName_1 = "Exec"
|
||||||
_StateName_2 = "ExecWait"
|
_StateName_2 = "ExecErr"
|
||||||
_StateName_3 = "ErrPerm"
|
_StateName_3 = "Done"
|
||||||
_StateName_4 = "Done"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_StateIndex_0 = [...]uint8{0, 4, 12}
|
_StateIndex_0 = [...]uint8{0, 4, 11}
|
||||||
_StateIndex_1 = [...]uint8{0, 4}
|
_StateIndex_1 = [...]uint8{0, 4}
|
||||||
_StateIndex_2 = [...]uint8{0, 8}
|
_StateIndex_2 = [...]uint8{0, 7}
|
||||||
_StateIndex_3 = [...]uint8{0, 7}
|
_StateIndex_3 = [...]uint8{0, 4}
|
||||||
_StateIndex_4 = [...]uint8{0, 4}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (i State) String() string {
|
func (i State) String() string {
|
||||||
@@ -33,22 +32,19 @@ func (i State) String() string {
|
|||||||
return _StateName_2
|
return _StateName_2
|
||||||
case i == 16:
|
case i == 16:
|
||||||
return _StateName_3
|
return _StateName_3
|
||||||
case i == 32:
|
|
||||||
return _StateName_4
|
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("State(%d)", i)
|
return fmt.Sprintf("State(%d)", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _StateValues = []State{1, 2, 4, 8, 16, 32}
|
var _StateValues = []State{1, 2, 4, 8, 16}
|
||||||
|
|
||||||
var _StateNameToValueMap = map[string]State{
|
var _StateNameToValueMap = map[string]State{
|
||||||
_StateName_0[0:4]: 1,
|
_StateName_0[0:4]: 1,
|
||||||
_StateName_0[4:12]: 2,
|
_StateName_0[4:11]: 2,
|
||||||
_StateName_1[0:4]: 4,
|
_StateName_1[0:4]: 4,
|
||||||
_StateName_2[0:8]: 8,
|
_StateName_2[0:7]: 8,
|
||||||
_StateName_3[0:7]: 16,
|
_StateName_3[0:4]: 16,
|
||||||
_StateName_4[0:4]: 32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateString retrieves an enum value from the enum constants string name.
|
// StateString retrieves an enum value from the enum constants string name.
|
||||||
|
|||||||
+204
-63
@@ -1,18 +1,21 @@
|
|||||||
package snapper
|
package snapper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"time"
|
|
||||||
"context"
|
"context"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"sort"
|
"sort"
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"sync"
|
"sync"
|
||||||
)
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
//go:generate stringer -type=SnapState
|
//go:generate stringer -type=SnapState
|
||||||
type SnapState uint
|
type SnapState uint
|
||||||
@@ -24,18 +27,20 @@ const (
|
|||||||
SnapError
|
SnapError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// All fields protected by Snapper.mtx
|
||||||
type snapProgress struct {
|
type snapProgress struct {
|
||||||
state SnapState
|
state SnapState
|
||||||
|
|
||||||
// SnapStarted, SnapDone, SnapError
|
// SnapStarted, SnapDone, SnapError
|
||||||
name string
|
name string
|
||||||
startAt time.Time
|
startAt time.Time
|
||||||
|
hookPlan *hooks.Plan
|
||||||
|
|
||||||
// SnapDone
|
// SnapDone
|
||||||
doneAt time.Time
|
doneAt time.Time
|
||||||
|
|
||||||
// SnapErr
|
// SnapErr TODO disambiguate state
|
||||||
err error
|
runResults hooks.PlanReport
|
||||||
}
|
}
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
@@ -44,7 +49,9 @@ type args struct {
|
|||||||
prefix string
|
prefix string
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
fsf *filters.DatasetMapFilter
|
fsf *filters.DatasetMapFilter
|
||||||
snapshotsTaken chan<-struct{}
|
snapshotsTaken chan<- struct{}
|
||||||
|
hooks *hooks.List
|
||||||
|
dryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Snapper struct {
|
type Snapper struct {
|
||||||
@@ -57,7 +64,7 @@ type Snapper struct {
|
|||||||
lastInvocation time.Time
|
lastInvocation time.Time
|
||||||
|
|
||||||
// valid for state Snapshotting
|
// valid for state Snapshotting
|
||||||
plan map[*zfs.DatasetPath]snapProgress
|
plan map[*zfs.DatasetPath]*snapProgress
|
||||||
|
|
||||||
// valid for state SyncUp and Waiting
|
// valid for state SyncUp and Waiting
|
||||||
sleepUntil time.Time
|
sleepUntil time.Time
|
||||||
@@ -70,7 +77,8 @@ type Snapper struct {
|
|||||||
type State uint
|
type State uint
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SyncUp State = 1<<iota
|
SyncUp State = 1 << iota
|
||||||
|
SyncUpErrWait
|
||||||
Planning
|
Planning
|
||||||
Snapshotting
|
Snapshotting
|
||||||
Waiting
|
Waiting
|
||||||
@@ -81,6 +89,7 @@ const (
|
|||||||
func (s State) sf() state {
|
func (s State) sf() state {
|
||||||
m := map[State]state{
|
m := map[State]state{
|
||||||
SyncUp: syncUp,
|
SyncUp: syncUp,
|
||||||
|
SyncUpErrWait: wait,
|
||||||
Planning: plan,
|
Planning: plan,
|
||||||
Snapshotting: snapshot,
|
Snapshotting: snapshot,
|
||||||
Waiting: wait,
|
Waiting: wait,
|
||||||
@@ -120,10 +129,16 @@ func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *con
|
|||||||
return nil, errors.New("interval must be positive")
|
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{
|
args := args{
|
||||||
prefix: in.Prefix,
|
prefix: in.Prefix,
|
||||||
interval: in.Interval,
|
interval: in.Interval,
|
||||||
fsf: fsf,
|
fsf: fsf,
|
||||||
|
hooks: hookList,
|
||||||
// ctx and log is set in Run()
|
// ctx and log is set in Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +153,7 @@ func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|||||||
s.args.snapshotsTaken = snapshotsTaken
|
s.args.snapshotsTaken = snapshotsTaken
|
||||||
s.args.ctx = ctx
|
s.args.ctx = ctx
|
||||||
s.args.log = getLogger(ctx)
|
s.args.log = getLogger(ctx)
|
||||||
|
s.args.dryRun = false // for future expansion
|
||||||
|
|
||||||
u := func(u func(*Snapper)) State {
|
u := func(u func(*Snapper)) State {
|
||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
@@ -165,7 +181,16 @@ func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|||||||
func onErr(err error, u updater) state {
|
func onErr(err error, u updater) state {
|
||||||
return u(func(s *Snapper) {
|
return u(func(s *Snapper) {
|
||||||
s.err = err
|
s.err = err
|
||||||
|
preState := s.state
|
||||||
|
switch s.state {
|
||||||
|
case SyncUp:
|
||||||
|
s.state = SyncUpErrWait
|
||||||
|
case Planning:
|
||||||
|
fallthrough
|
||||||
|
case Snapshotting:
|
||||||
s.state = ErrorWait
|
s.state = ErrorWait
|
||||||
|
}
|
||||||
|
s.args.log.WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
||||||
}).sf()
|
}).sf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +202,10 @@ func onMainCtxDone(ctx context.Context, u updater) state {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncUp(a args, u updater) state {
|
func syncUp(a args, u updater) state {
|
||||||
fss, err := listFSes(a.fsf)
|
u(func(snapper *Snapper) {
|
||||||
|
snapper.lastInvocation = time.Now()
|
||||||
|
})
|
||||||
|
fss, err := listFSes(a.ctx, a.fsf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return onErr(err, u)
|
return onErr(err, u)
|
||||||
}
|
}
|
||||||
@@ -185,10 +213,10 @@ func syncUp(a args, u updater) state {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return onErr(err, u)
|
return onErr(err, u)
|
||||||
}
|
}
|
||||||
u(func(s *Snapper){
|
u(func(s *Snapper) {
|
||||||
s.sleepUntil = syncPoint
|
s.sleepUntil = syncPoint
|
||||||
})
|
})
|
||||||
t := time.NewTimer(syncPoint.Sub(time.Now()))
|
t := time.NewTimer(time.Until(syncPoint))
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
select {
|
select {
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
@@ -204,29 +232,35 @@ func plan(a args, u updater) state {
|
|||||||
u(func(snapper *Snapper) {
|
u(func(snapper *Snapper) {
|
||||||
snapper.lastInvocation = time.Now()
|
snapper.lastInvocation = time.Now()
|
||||||
})
|
})
|
||||||
fss, err := listFSes(a.fsf)
|
fss, err := listFSes(a.ctx, a.fsf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return onErr(err, u)
|
return onErr(err, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
plan := make(map[*zfs.DatasetPath]snapProgress, len(fss))
|
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
||||||
for _, fs := range fss {
|
for _, fs := range fss {
|
||||||
plan[fs] = snapProgress{state: SnapPending}
|
plan[fs] = &snapProgress{state: SnapPending}
|
||||||
}
|
}
|
||||||
return u(func(s *Snapper) {
|
return u(func(s *Snapper) {
|
||||||
s.state = Snapshotting
|
s.state = Snapshotting
|
||||||
s.plan = plan
|
s.plan = plan
|
||||||
|
s.err = nil
|
||||||
}).sf()
|
}).sf()
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshot(a args, u updater) state {
|
func snapshot(a args, u updater) state {
|
||||||
|
|
||||||
var plan map[*zfs.DatasetPath]snapProgress
|
var plan map[*zfs.DatasetPath]*snapProgress
|
||||||
u(func(snapper *Snapper) {
|
u(func(snapper *Snapper) {
|
||||||
plan = snapper.plan
|
plan = snapper.plan
|
||||||
})
|
})
|
||||||
|
|
||||||
hadErr := false
|
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?
|
// TODO channel programs -> allow a little jitter?
|
||||||
for fs, progress := range plan {
|
for fs, progress := range plan {
|
||||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||||
@@ -236,27 +270,71 @@ func snapshot(a args, u updater) state {
|
|||||||
WithField("fs", fs.ToString()).
|
WithField("fs", fs.ToString()).
|
||||||
WithField("snap", snapname)
|
WithField("snap", snapname)
|
||||||
|
|
||||||
|
hookEnvExtra := hooks.Env{
|
||||||
|
hooks.EnvFS: fs.ToString(),
|
||||||
|
hooks.EnvSnapshot: snapname,
|
||||||
|
}
|
||||||
|
|
||||||
|
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
|
||||||
|
l.Debug("create snapshot")
|
||||||
|
err = zfs.ZFSSnapshot(fs, snapname, false) // TODO propagagte 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 {
|
||||||
|
l.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
|
||||||
|
l.WithError(planErr).Error("cannot create job hook plan")
|
||||||
|
goto updateFSState
|
||||||
|
}
|
||||||
|
}
|
||||||
u(func(snapper *Snapper) {
|
u(func(snapper *Snapper) {
|
||||||
progress.name = snapname
|
progress.name = snapname
|
||||||
progress.startAt = time.Now()
|
progress.startAt = time.Now()
|
||||||
|
progress.hookPlan = plan
|
||||||
progress.state = SnapStarted
|
progress.state = SnapStarted
|
||||||
})
|
})
|
||||||
|
{
|
||||||
l.Debug("create snapshot")
|
l := hooks.GetLogger(a.ctx).WithField("fs", fs.ToString()).WithField("snap", snapname)
|
||||||
err := zfs.ZFSSnapshot(fs, snapname, false)
|
l.WithField("report", plan.Report().String()).Debug("begin run job plan")
|
||||||
if err != nil {
|
plan.Run(hooks.WithLogger(a.ctx, l), a.dryRun)
|
||||||
hadErr = true
|
planReport = plan.Report()
|
||||||
l.WithError(err).Error("cannot create snapshot")
|
fsHadErr = planReport.HadError() // not just fatal errors
|
||||||
|
if fsHadErr {
|
||||||
|
l.WithField("report", planReport.String()).Error("end run job plan with error")
|
||||||
|
} else {
|
||||||
|
l.WithField("report", planReport.String()).Info("end run job plan successful")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
doneAt := time.Now()
|
|
||||||
|
|
||||||
|
updateFSState:
|
||||||
|
anyFsHadErr = anyFsHadErr || fsHadErr
|
||||||
u(func(snapper *Snapper) {
|
u(func(snapper *Snapper) {
|
||||||
progress.doneAt = doneAt
|
progress.doneAt = time.Now()
|
||||||
progress.state = SnapDone
|
progress.state = SnapDone
|
||||||
if err != nil {
|
if fsHadErr {
|
||||||
progress.state = SnapError
|
progress.state = SnapError
|
||||||
progress.err = err
|
|
||||||
}
|
}
|
||||||
|
progress.runResults = planReport
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,12 +346,26 @@ func snapshot(a args, u updater) state {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for h, mc := range hookMatchCount {
|
||||||
|
if mc == 0 {
|
||||||
|
hookIdx := -1
|
||||||
|
for idx, ah := range *a.hooks {
|
||||||
|
if ah == h {
|
||||||
|
hookIdx = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.log.WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return u(func(snapper *Snapper) {
|
return u(func(snapper *Snapper) {
|
||||||
if hadErr {
|
if anyFsHadErr {
|
||||||
snapper.state = ErrorWait
|
snapper.state = ErrorWait
|
||||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
||||||
} else {
|
} else {
|
||||||
snapper.state = Waiting
|
snapper.state = Waiting
|
||||||
|
snapper.err = nil
|
||||||
}
|
}
|
||||||
}).sf()
|
}).sf()
|
||||||
}
|
}
|
||||||
@@ -284,9 +376,15 @@ func wait(a args, u updater) state {
|
|||||||
lastTick := snapper.lastInvocation
|
lastTick := snapper.lastInvocation
|
||||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
snapper.sleepUntil = lastTick.Add(a.interval)
|
||||||
sleepUntil = snapper.sleepUntil
|
sleepUntil = snapper.sleepUntil
|
||||||
|
log := a.log.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(sleepUntil.Sub(time.Now()))
|
t := time.NewTimer(time.Until(sleepUntil))
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -299,13 +397,23 @@ func wait(a args, u updater) state {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listFSes(mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) {
|
func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) {
|
||||||
return zfs.ZFSListMapping(mf)
|
return zfs.ZFSListMapping(ctx, mf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
||||||
|
|
||||||
|
// see docs/snapshotting.rst
|
||||||
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
||||||
|
|
||||||
|
const (
|
||||||
|
prioHasVersions int = iota
|
||||||
|
prioNoVersions
|
||||||
|
)
|
||||||
|
|
||||||
type snapTime struct {
|
type snapTime struct {
|
||||||
ds *zfs.DatasetPath
|
ds *zfs.DatasetPath
|
||||||
|
prio int // lower is higher
|
||||||
time time.Time
|
time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,22 +422,75 @@ func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval t
|
|||||||
}
|
}
|
||||||
|
|
||||||
snaptimes := make([]snapTime, 0, len(fss))
|
snaptimes := make([]snapTime, 0, len(fss))
|
||||||
|
hardErrs := 0
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
log.Debug("examine filesystem state")
|
log.Debug("examine filesystem state to find sync point")
|
||||||
for _, d := range fss {
|
for _, d := range fss {
|
||||||
|
|
||||||
l := log.WithField("fs", d.ToString())
|
l := log.WithField("fs", d.ToString())
|
||||||
|
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(l, now, interval, prefix, d)
|
||||||
|
if err == findSyncPointFSNoFilesystemVersionsErr {
|
||||||
|
snaptimes = append(snaptimes, snapTime{
|
||||||
|
ds: d,
|
||||||
|
prio: prioNoVersions,
|
||||||
|
time: now,
|
||||||
|
})
|
||||||
|
} else if err != nil {
|
||||||
|
hardErrs++
|
||||||
|
l.WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
||||||
|
} else {
|
||||||
|
l.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 := log.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(l Logger, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot list filesystem versions")
|
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if len(fsvs) <= 0 {
|
if len(fsvs) <= 0 {
|
||||||
l.WithField("prefix", prefix).Debug("no filesystem versions with prefix")
|
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort versions by creation
|
// Sort versions by creation
|
||||||
@@ -338,32 +499,12 @@ func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval t
|
|||||||
})
|
})
|
||||||
|
|
||||||
latest := fsvs[len(fsvs)-1]
|
latest := fsvs[len(fsvs)-1]
|
||||||
l.WithField("creation", latest.Creation).
|
l.WithField("creation", latest.Creation).Debug("found latest snapshot")
|
||||||
Debug("found latest snapshot")
|
|
||||||
|
|
||||||
since := now.Sub(latest.Creation)
|
since := now.Sub(latest.Creation)
|
||||||
if since < 0 {
|
if since < 0 {
|
||||||
l.WithField("snapshot", latest.Name).
|
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
||||||
WithField("creation", latest.Creation).
|
|
||||||
Error("snapshot is from the future")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
next := now
|
|
||||||
if since < interval {
|
|
||||||
next = latest.Creation.Add(interval)
|
|
||||||
}
|
|
||||||
snaptimes = append(snaptimes, snapTime{d, next})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(snaptimes) == 0 {
|
return latest.Creation.Add(interval), nil
|
||||||
snaptimes = append(snaptimes, snapTime{nil, now})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(snaptimes, func(i, j int) bool {
|
|
||||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
|
||||||
})
|
|
||||||
|
|
||||||
return snaptimes[0].time, nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package snapper
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
)
|
)
|
||||||
@@ -17,12 +18,20 @@ type PeriodicOrManual struct {
|
|||||||
s *Snapper
|
s *Snapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan <- struct{}) {
|
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
if s.s != nil {
|
if s.s != nil {
|
||||||
s.s.Run(ctx, wakeUpCommon)
|
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) {
|
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||||
switch v := in.Ret.(type) {
|
switch v := in.Ret.(type) {
|
||||||
case *config.SnapshottingPeriodic:
|
case *config.SnapshottingPeriodic:
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -4,6 +4,16 @@ package snapper
|
|||||||
|
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[SnapPending-1]
|
||||||
|
_ = x[SnapStarted-2]
|
||||||
|
_ = x[SnapDone-4]
|
||||||
|
_ = x[SnapError-8]
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
_SnapState_name_0 = "SnapPendingSnapStarted"
|
_SnapState_name_0 = "SnapPendingSnapStarted"
|
||||||
_SnapState_name_1 = "SnapDone"
|
_SnapState_name_1 = "SnapDone"
|
||||||
|
|||||||
@@ -4,16 +4,30 @@ package snapper
|
|||||||
|
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[SyncUp-1]
|
||||||
|
_ = x[SyncUpErrWait-2]
|
||||||
|
_ = x[Planning-4]
|
||||||
|
_ = x[Snapshotting-8]
|
||||||
|
_ = x[Waiting-16]
|
||||||
|
_ = x[ErrorWait-32]
|
||||||
|
_ = x[Stopped-64]
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
_State_name_0 = "SyncUpPlanning"
|
_State_name_0 = "SyncUpSyncUpErrWait"
|
||||||
_State_name_1 = "Snapshotting"
|
_State_name_1 = "Planning"
|
||||||
_State_name_2 = "Waiting"
|
_State_name_2 = "Snapshotting"
|
||||||
_State_name_3 = "ErrorWait"
|
_State_name_3 = "Waiting"
|
||||||
_State_name_4 = "Stopped"
|
_State_name_4 = "ErrorWait"
|
||||||
|
_State_name_5 = "Stopped"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_State_index_0 = [...]uint8{0, 6, 14}
|
_State_index_0 = [...]uint8{0, 6, 19}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (i State) String() string {
|
func (i State) String() string {
|
||||||
@@ -29,6 +43,8 @@ func (i State) String() string {
|
|||||||
return _State_name_3
|
return _State_name_3
|
||||||
case i == 32:
|
case i == 32:
|
||||||
return _State_name_4
|
return _State_name_4
|
||||||
|
case i == 64:
|
||||||
|
return _State_name_5
|
||||||
default:
|
default:
|
||||||
return "State(" + strconv.FormatInt(int64(i), 10) + ")"
|
return "State(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package streamrpcconfig
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/problame/go-streamrpc"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FromDaemonConfig(g *config.Global, in *config.RPCConfig) (*streamrpc.ConnConfig, error) {
|
|
||||||
conf := in
|
|
||||||
if conf == nil {
|
|
||||||
conf = g.RPC
|
|
||||||
}
|
|
||||||
srpcConf := &streamrpc.ConnConfig{
|
|
||||||
RxHeaderMaxLen: conf.RxHeaderMaxLen,
|
|
||||||
RxStructuredMaxLen: conf.RxStructuredMaxLen,
|
|
||||||
RxStreamMaxChunkSize: conf.RxStreamChunkMaxLen,
|
|
||||||
TxChunkSize: conf.TxChunkSize,
|
|
||||||
Timeout: conf.Timeout,
|
|
||||||
SendHeartbeatInterval: conf.SendHeartbeatInterval,
|
|
||||||
}
|
|
||||||
if err := srpcConf.Validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return srpcConf, nil
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package connecter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/transport/serve"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LocalConnecter struct {
|
|
||||||
listenerName string
|
|
||||||
clientIdentity string
|
|
||||||
}
|
|
||||||
|
|
||||||
func LocalConnecterFromConfig(in *config.LocalConnect) (*LocalConnecter, error) {
|
|
||||||
if in.ClientIdentity == "" {
|
|
||||||
return nil, fmt.Errorf("ClientIdentity must not be empty")
|
|
||||||
}
|
|
||||||
if in.ListenerName == "" {
|
|
||||||
return nil, fmt.Errorf("ListenerName must not be empty")
|
|
||||||
}
|
|
||||||
return &LocalConnecter{listenerName: in.ListenerName, clientIdentity: in.ClientIdentity}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *LocalConnecter) Connect(dialCtx context.Context) (conn net.Conn, err error) {
|
|
||||||
l := serve.GetLocalListener(c.listenerName)
|
|
||||||
return l.Connect(dialCtx, c.clientIdentity)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package connecter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/problame/go-streamrpc"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/streamrpcconfig"
|
|
||||||
"github.com/zrepl/zrepl/daemon/transport"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
type HandshakeConnecter struct {
|
|
||||||
connecter streamrpc.Connecter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c HandshakeConnecter) Connect(ctx context.Context) (net.Conn, error) {
|
|
||||||
conn, err := c.connecter.Connect(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dl, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
dl = time.Now().Add(10 * time.Second) // FIXME constant
|
|
||||||
}
|
|
||||||
if err := transport.DoHandshakeCurrentVersion(conn, dl); err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func FromConfig(g *config.Global, in config.ConnectEnum) (*ClientFactory, error) {
|
|
||||||
var (
|
|
||||||
connecter streamrpc.Connecter
|
|
||||||
errConnecter, errRPC error
|
|
||||||
connConf *streamrpc.ConnConfig
|
|
||||||
)
|
|
||||||
switch v := in.Ret.(type) {
|
|
||||||
case *config.SSHStdinserverConnect:
|
|
||||||
connecter, errConnecter = SSHStdinserverConnecterFromConfig(v)
|
|
||||||
connConf, errRPC = streamrpcconfig.FromDaemonConfig(g, v.RPC)
|
|
||||||
case *config.TCPConnect:
|
|
||||||
connecter, errConnecter = TCPConnecterFromConfig(v)
|
|
||||||
connConf, errRPC = streamrpcconfig.FromDaemonConfig(g, v.RPC)
|
|
||||||
case *config.TLSConnect:
|
|
||||||
connecter, errConnecter = TLSConnecterFromConfig(v)
|
|
||||||
connConf, errRPC = streamrpcconfig.FromDaemonConfig(g, v.RPC)
|
|
||||||
case *config.LocalConnect:
|
|
||||||
connecter, errConnecter = LocalConnecterFromConfig(v)
|
|
||||||
connConf, errRPC = streamrpcconfig.FromDaemonConfig(g, v.RPC)
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("implementation error: unknown connecter type %T", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
if errConnecter != nil {
|
|
||||||
return nil, errConnecter
|
|
||||||
}
|
|
||||||
if errRPC != nil {
|
|
||||||
return nil, errRPC
|
|
||||||
}
|
|
||||||
|
|
||||||
config := streamrpc.ClientConfig{ConnConfig: connConf}
|
|
||||||
if err := config.Validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
connecter = HandshakeConnecter{connecter}
|
|
||||||
|
|
||||||
return &ClientFactory{connecter: connecter, config: &config}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientFactory struct {
|
|
||||||
connecter streamrpc.Connecter
|
|
||||||
config *streamrpc.ClientConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f ClientFactory) NewClient() (*streamrpc.Client, error) {
|
|
||||||
return streamrpc.NewClient(f.connecter, f.config)
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HandshakeMessage struct {
|
|
||||||
ProtocolVersion int
|
|
||||||
Extensions []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *HandshakeMessage) Encode() ([]byte, error) {
|
|
||||||
if m.ProtocolVersion <= 0 || m.ProtocolVersion > 9999 {
|
|
||||||
return nil, fmt.Errorf("protocol version must be in [1, 9999]")
|
|
||||||
}
|
|
||||||
if len(m.Extensions) >= 9999 {
|
|
||||||
return nil, fmt.Errorf("protocol only supports [0, 9999] extensions")
|
|
||||||
}
|
|
||||||
// EXTENSIONS is a count of subsequent \n separated lines that contain protocol extensions
|
|
||||||
var extensions strings.Builder
|
|
||||||
for i, ext := range m.Extensions {
|
|
||||||
if strings.ContainsAny(ext, "\n") {
|
|
||||||
return nil, fmt.Errorf("Extension #%d contains forbidden newline character", i)
|
|
||||||
}
|
|
||||||
if !utf8.ValidString(ext) {
|
|
||||||
return nil, fmt.Errorf("Extension #%d is not valid UTF-8", i)
|
|
||||||
}
|
|
||||||
extensions.WriteString(ext)
|
|
||||||
extensions.WriteString("\n")
|
|
||||||
}
|
|
||||||
withoutLen := fmt.Sprintf("ZREPL_ZFS_REPLICATION PROTOVERSION=%04d EXTENSIONS=%04d\n%s",
|
|
||||||
m.ProtocolVersion, len(m.Extensions), extensions.String())
|
|
||||||
withLen := fmt.Sprintf("%010d %s", len(withoutLen), withoutLen)
|
|
||||||
return []byte(withLen), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
|
|
||||||
var lenAndSpace [11]byte
|
|
||||||
if _, err := io.ReadFull(r, lenAndSpace[:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !utf8.Valid(lenAndSpace[:]) {
|
|
||||||
return fmt.Errorf("invalid start of handshake message: not valid UTF-8")
|
|
||||||
}
|
|
||||||
var followLen int
|
|
||||||
n, err := fmt.Sscanf(string(lenAndSpace[:]), "%010d ", &followLen)
|
|
||||||
if n != 1 || err != nil {
|
|
||||||
return fmt.Errorf("could not parse handshake message length")
|
|
||||||
}
|
|
||||||
if followLen > maxLen {
|
|
||||||
return fmt.Errorf("handshake message length exceeds max length (%d vs %d)",
|
|
||||||
followLen, maxLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_, err = io.Copy(&buf, io.LimitReader(r, int64(followLen)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
protoVersion, extensionCount int
|
|
||||||
)
|
|
||||||
n, err = fmt.Fscanf(&buf, "ZREPL_ZFS_REPLICATION PROTOVERSION=%04d EXTENSIONS=%4d\n",
|
|
||||||
&protoVersion, &extensionCount)
|
|
||||||
if n != 2 || err != nil {
|
|
||||||
return fmt.Errorf("could not parse handshake message: %s", err)
|
|
||||||
}
|
|
||||||
if protoVersion < 1 {
|
|
||||||
return fmt.Errorf("invalid protocol version %q", protoVersion)
|
|
||||||
}
|
|
||||||
m.ProtocolVersion = protoVersion
|
|
||||||
|
|
||||||
if extensionCount < 0 {
|
|
||||||
return fmt.Errorf("invalid extension count %q", extensionCount)
|
|
||||||
}
|
|
||||||
if extensionCount == 0 {
|
|
||||||
if buf.Len() != 0 {
|
|
||||||
return fmt.Errorf("unexpected data trailing after header")
|
|
||||||
}
|
|
||||||
m.Extensions = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s := buf.String()
|
|
||||||
if strings.Count(s, "\n") != extensionCount {
|
|
||||||
return fmt.Errorf("inconsistent extension count: found %d, header says %d", len(m.Extensions), extensionCount)
|
|
||||||
}
|
|
||||||
exts := strings.Split(s, "\n")
|
|
||||||
if exts[len(exts)-1] != "" {
|
|
||||||
return fmt.Errorf("unexpected data trailing after last extension newline")
|
|
||||||
}
|
|
||||||
m.Extensions = exts[0:len(exts)-1]
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) error {
|
|
||||||
// current protocol version is hardcoded here
|
|
||||||
return DoHandshakeVersion(conn, deadline, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DoHandshakeVersion(conn net.Conn, deadline time.Time, version int) error {
|
|
||||||
ours := HandshakeMessage{
|
|
||||||
ProtocolVersion: version,
|
|
||||||
Extensions: nil,
|
|
||||||
}
|
|
||||||
hsb, err := ours.Encode()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not encode protocol banner: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.SetDeadline(deadline)
|
|
||||||
_, err = io.Copy(conn, bytes.NewBuffer(hsb))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not send protocol banner: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
theirs := HandshakeMessage{}
|
|
||||||
if err := theirs.DecodeReader(conn, 16 * 4096); err != nil { // FIXME constant
|
|
||||||
return fmt.Errorf("could not decode protocol banner: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if theirs.ProtocolVersion != ours.ProtocolVersion {
|
|
||||||
return fmt.Errorf("protocol versions do not match: ours is %d, theirs is %d",
|
|
||||||
ours.ProtocolVersion, theirs.ProtocolVersion)
|
|
||||||
}
|
|
||||||
// ignore extensions, we don't use them
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package serve
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/transport"
|
|
||||||
"net"
|
|
||||||
"github.com/zrepl/zrepl/daemon/streamrpcconfig"
|
|
||||||
"github.com/problame/go-streamrpc"
|
|
||||||
"context"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const contextKeyLog contextKey = 0
|
|
||||||
|
|
||||||
type Logger = logger.Logger
|
|
||||||
|
|
||||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKeyLog, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLogger(ctx context.Context) Logger {
|
|
||||||
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
|
||||||
return log
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthenticatedConn interface {
|
|
||||||
net.Conn
|
|
||||||
// ClientIdentity must be a string that satisfies ValidateClientIdentity
|
|
||||||
ClientIdentity() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// A client identity must be a single component in a ZFS filesystem path
|
|
||||||
func ValidateClientIdentity(in string) (err error) {
|
|
||||||
path, err := zfs.NewDatasetPath(in)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if path.Length() != 1 {
|
|
||||||
return errors.New("client identity must be a single path comonent (not empty, no '/')")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type authConn struct {
|
|
||||||
net.Conn
|
|
||||||
clientIdentity string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ AuthenticatedConn = authConn{}
|
|
||||||
|
|
||||||
func (c authConn) ClientIdentity() string {
|
|
||||||
if err := ValidateClientIdentity(c.clientIdentity); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return c.clientIdentity
|
|
||||||
}
|
|
||||||
|
|
||||||
// like net.Listener, but with an AuthenticatedConn instead of net.Conn
|
|
||||||
type AuthenticatedListener interface {
|
|
||||||
Addr() (net.Addr)
|
|
||||||
Accept(ctx context.Context) (AuthenticatedConn, error)
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListenerFactory interface {
|
|
||||||
Listen() (AuthenticatedListener, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HandshakeListenerFactory struct {
|
|
||||||
lf ListenerFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lf HandshakeListenerFactory) Listen() (AuthenticatedListener, error) {
|
|
||||||
l, err := lf.lf.Listen()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return HandshakeListener{l}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type HandshakeListener struct {
|
|
||||||
l AuthenticatedListener
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l HandshakeListener) Addr() (net.Addr) { return l.l.Addr() }
|
|
||||||
|
|
||||||
func (l HandshakeListener) Close() error { return l.l.Close() }
|
|
||||||
|
|
||||||
func (l HandshakeListener) Accept(ctx context.Context) (AuthenticatedConn, error) {
|
|
||||||
conn, err := l.l.Accept(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dl, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
dl = time.Now().Add(10*time.Second) // FIXME constant
|
|
||||||
}
|
|
||||||
if err := transport.DoHandshakeCurrentVersion(conn, dl); err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromConfig(g *config.Global, in config.ServeEnum) (lf ListenerFactory, conf *streamrpc.ConnConfig, _ error) {
|
|
||||||
|
|
||||||
var (
|
|
||||||
lfError, rpcErr error
|
|
||||||
)
|
|
||||||
switch v := in.Ret.(type) {
|
|
||||||
case *config.TCPServe:
|
|
||||||
lf, lfError = TCPListenerFactoryFromConfig(g, v)
|
|
||||||
conf, rpcErr = streamrpcconfig.FromDaemonConfig(g, v.RPC)
|
|
||||||
case *config.TLSServe:
|
|
||||||
lf, lfError = TLSListenerFactoryFromConfig(g, v)
|
|
||||||
conf, rpcErr = streamrpcconfig.FromDaemonConfig(g, v.RPC)
|
|
||||||
case *config.StdinserverServer:
|
|
||||||
lf, lfError = MultiStdinserverListenerFactoryFromConfig(g, v)
|
|
||||||
conf, rpcErr = streamrpcconfig.FromDaemonConfig(g, v.RPC)
|
|
||||||
case *config.LocalServe:
|
|
||||||
lf, lfError = LocalListenerFactoryFromConfig(g, v)
|
|
||||||
conf, rpcErr = streamrpcconfig.FromDaemonConfig(g, v.RPC)
|
|
||||||
default:
|
|
||||||
return nil, nil, errors.Errorf("internal error: unknown serve type %T", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if lfError != nil {
|
|
||||||
return nil, nil, lfError
|
|
||||||
}
|
|
||||||
if rpcErr != nil {
|
|
||||||
return nil, nil, rpcErr
|
|
||||||
}
|
|
||||||
|
|
||||||
lf = HandshakeListenerFactory{lf}
|
|
||||||
|
|
||||||
return lf, conf, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package serve
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/tlsconf"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TLSListenerFactory struct {
|
|
||||||
address string
|
|
||||||
clientCA *x509.CertPool
|
|
||||||
serverCert tls.Certificate
|
|
||||||
handshakeTimeout time.Duration
|
|
||||||
clientCNs map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (lf *TLSListenerFactory, err error) {
|
|
||||||
lf = &TLSListenerFactory{
|
|
||||||
address: in.Listen,
|
|
||||||
handshakeTimeout: in.HandshakeTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
if in.Ca == "" || in.Cert == "" || in.Key == "" {
|
|
||||||
return nil, errors.New("fields 'ca', 'cert' and 'key'must be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
lf.clientCA, err = tlsconf.ParseCAFile(in.Ca)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse ca file")
|
|
||||||
}
|
|
||||||
|
|
||||||
lf.serverCert, err = tls.LoadX509KeyPair(in.Cert, in.Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse cer/key pair")
|
|
||||||
}
|
|
||||||
|
|
||||||
lf.clientCNs = make(map[string]struct{}, len(in.ClientCNs))
|
|
||||||
for i, cn := range in.ClientCNs {
|
|
||||||
if err := ValidateClientIdentity(cn); err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "unsuitable client_cn #%d %q", i, cn)
|
|
||||||
}
|
|
||||||
// dupes are ok fr now
|
|
||||||
lf.clientCNs[cn] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TLSListenerFactory) Listen() (AuthenticatedListener, error) {
|
|
||||||
l, err := net.Listen("tcp", f.address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tl := tlsconf.NewClientAuthListener(l, f.clientCA, f.serverCert, f.handshakeTimeout)
|
|
||||||
return tlsAuthListener{tl, f.clientCNs}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type tlsAuthListener struct {
|
|
||||||
*tlsconf.ClientAuthListener
|
|
||||||
clientCNs map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l tlsAuthListener) Accept(ctx context.Context) (AuthenticatedConn, error) {
|
|
||||||
c, cn, err := l.ClientAuthListener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, ok := l.clientCNs[cn]; !ok {
|
|
||||||
if err := c.Close(); err != nil {
|
|
||||||
getLogger(ctx).WithError(err).Error("error closing connection with unauthorized common name")
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unauthorized client common name %q from %s", cn, c.RemoteAddr())
|
|
||||||
}
|
|
||||||
return authConn{c, cn}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
+595
@@ -0,0 +1,595 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"$$hashKey": "object:3351",
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 1,
|
||||||
|
"id": 7,
|
||||||
|
"iteration": 1552746418826,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"content": "# zrepl Prometheus Metrics\n\nzrepl exposes Prometheus metrics and ships with this Grafana dashboard.\nThe exported metrics are suitable for health checks:\n\n* The log should generally be warning & error-free\n * The `Log Messages that require attention` graph visualizes log message counts indicating problems.\n* The number of goroutines should not grow unboundedly over time.\n * During replication, the number of goroutines can be way higher than during idle time.\n * If the goroutine count grows with each replication, there is clearly a goroutine leak. Please open a bug report.\n* The sys memory consumption should not grow unboundedly over time.\n * Note that the Go runtime pre-allocates some of its heap from the OS.\n * zrepl actually uses much less memory than allocated from the OS.\n * Since Go 1.11, Go pre-allocates more aggressively.\n* Monitor that some data is replicated, although that metric does not guarantee that replication was successful.\n\n**In general, note that the exported metrics are not stable unless declared otherwise.**",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 35,
|
||||||
|
"mode": "markdown",
|
||||||
|
"title": "Panel Title",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 9
|
||||||
|
},
|
||||||
|
"id": 15,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": true,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"$$hashKey": "object:3436",
|
||||||
|
"expr": "up{job='$prom_job_name'}",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "zrepl Instances Up",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": "5",
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 9
|
||||||
|
},
|
||||||
|
"id": 22,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "increase(zrepl_daemon_log_entries{job='$prom_job_name',level=~'warn|error'}[$__interval])",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Log Messages that require attention",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 18
|
||||||
|
},
|
||||||
|
"id": 33,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [
|
||||||
|
{
|
||||||
|
"alias": "/replicated bytes in last.*/",
|
||||||
|
"yaxis": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(zrepl_replication_bytes_replicated{job='$prom_job_name'}[$__interval])) by (zrepl_job)",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"interval": "",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "replication data rate zrepl_job={{zrepl_job}}",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "sum(increase(zrepl_replication_bytes_replicated{job='$prom_job_name'}[10m])) by (zrepl_job)",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"interval": "",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "replicated bytes in last 10min zrepl_job={{zrepl_job}}",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Replication Data Rate and Volume(integrates last 10min)",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "Bps",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "bytes",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 18
|
||||||
|
},
|
||||||
|
"id": 23,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(increase(zrepl_daemon_log_entries{job='$prom_job_name',zrepl_job=~\"^[^_].*\"}[$__interval])) by (instance,zrepl_job)",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Log Activity (without internal jobs)",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 27
|
||||||
|
},
|
||||||
|
"id": 17,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"$$hashKey": "object:3535",
|
||||||
|
"expr": "go_memstats_sys_bytes{job='$prom_job_name'}",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Memory Allocated by the Go runtime from the OS (should not grow unboundedly)",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "bytes",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 27
|
||||||
|
},
|
||||||
|
"id": 19,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "go_goroutines{job='$prom_job_name'}",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "number of goroutines (should not grow unboundedly)",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "1m",
|
||||||
|
"schemaVersion": 16,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"allValue": null,
|
||||||
|
"current": {
|
||||||
|
"text": "zrepl",
|
||||||
|
"value": "zrepl"
|
||||||
|
},
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Prometheus Job Name",
|
||||||
|
"multi": false,
|
||||||
|
"name": "prom_job_name",
|
||||||
|
"options": [],
|
||||||
|
"query": "label_values(up, job)",
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"sort": 1,
|
||||||
|
"tagValuesQuery": "",
|
||||||
|
"tags": [],
|
||||||
|
"tagsQuery": "",
|
||||||
|
"type": "query",
|
||||||
|
"useTags": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-2d",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "zrepl 0.1",
|
||||||
|
"uid": "xTljn4qmk",
|
||||||
|
"version": 6
|
||||||
|
}
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=zrepl daemon
|
||||||
|
Documentation=https://zrepl.github.io
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStartPre=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml configcheck
|
||||||
|
ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
|
||||||
|
RuntimeDirectory=zrepl zrepl/stdinserver
|
||||||
|
RuntimeDirectoryMode=0700
|
||||||
|
|
||||||
|
ProtectSystem=strict
|
||||||
|
#PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this?
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
#PrivateUsers=yes # TODO Does not work, why?
|
||||||
|
ProtectKernelModules=true
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=yes
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
|
||||||
|
ProtectHome=read-only
|
||||||
|
# ProtectHome=tmpfs totally possible, not by default though because of Debian stretch
|
||||||
|
|
||||||
|
# SystemCallFilter
|
||||||
|
# ~@privileged doesn't work with Ubuntu 18.04 ssh
|
||||||
|
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources
|
||||||
|
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
+130
-15
@@ -1,8 +1,11 @@
|
|||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
.. |break_config| replace:: **[CONFIG]**
|
.. |break_config| replace:: **[CONFIG]**
|
||||||
.. |break| replace:: **[BREAK]**
|
.. |break| replace:: **[BREAK]**
|
||||||
.. |bugfix| replace:: [BUG]
|
.. |bugfix| replace:: [BUG]
|
||||||
.. |docs| replace:: [DOCS]
|
.. |docs| replace:: [DOCS]
|
||||||
.. |feature| replace:: [FEATURE]
|
.. |feature| replace:: [FEATURE]
|
||||||
|
.. |mig| replace:: **[MIGRATION]**
|
||||||
|
|
||||||
.. _changelog:
|
.. _changelog:
|
||||||
|
|
||||||
@@ -19,10 +22,100 @@ We use the following annotations for classifying changes:
|
|||||||
* |break| Change that breaks interoperability or persistent state representation with previous releases.
|
* |break| Change that breaks interoperability or persistent state representation with previous releases.
|
||||||
As a package maintainer, make sure to warn your users about config breakage somehow.
|
As a package maintainer, make sure to warn your users about config breakage somehow.
|
||||||
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.
|
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.
|
||||||
* |feature| Change that introduces new functionality.
|
* |feature| Change that introduces new functionality.
|
||||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||||
* |docs| Change to the documentation.
|
* |docs| Change to the documentation.
|
||||||
|
|
||||||
|
0.3
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a big one! Headlining features:
|
||||||
|
|
||||||
|
* **Resumable Send & Recv Support**
|
||||||
|
No knobs required, automatically used where supported.
|
||||||
|
* **Hold-Protected Send & Recv**
|
||||||
|
Automatic ZFS holds to ensure that we can always resume a replication step.
|
||||||
|
* **Encrypted Send & Recv Support** for OpenZFS native encryption.
|
||||||
|
:ref:`Configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||||
|
* **Receive-side hold on last received dataset**
|
||||||
|
The counterpart to the replication cursor bookmark on the send-side.
|
||||||
|
Ensures that incremental replication will always be possible between a sender and receiver.
|
||||||
|
|
||||||
|
Actual changelog:
|
||||||
|
|
||||||
|
* |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.
|
||||||
|
|
||||||
|
* |break| |mig| replication cursor representation changed
|
||||||
|
|
||||||
|
* zrepl now manages the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||||
|
In the future, this will permit multiple sending jobs to send from the same filesystems.
|
||||||
|
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
||||||
|
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
||||||
|
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
|
||||||
|
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
||||||
|
|
||||||
|
* |bugfix| missing logger context vars in control connection handlers
|
||||||
|
* |bugfix| improved error messages on ``zfs send`` errors
|
||||||
|
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||||
|
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
||||||
|
that will not be snapshotted until the sync-up phase is over
|
||||||
|
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
|
||||||
|
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
||||||
|
|
||||||
|
0.2.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* |feature| Illumos (and Solaris) compatibility and binary builds (thanks, `MNX.io <https://mnx.io>`_ )
|
||||||
|
* |feature| 32bit binaries for Linux and FreeBSD (untested, though)
|
||||||
|
* |bugfix| better error messages in ``ssh+stdinserver`` transport
|
||||||
|
* |bugfix| systemd + ``ssh+stdinserver``: automatically create ``/var/run/zrepl/stdinserver``
|
||||||
|
* |bugfix| crash if Prometheus listening socket cannot be opened
|
||||||
|
|
||||||
|
* [MAINTAINER NOTICE] ``Makefile`` refactoring, see :commit:`080f2c0`
|
||||||
|
|
||||||
|
0.2
|
||||||
|
---
|
||||||
|
|
||||||
|
* |feature| :ref:`Pre- and Post-Snapshot Hooks <job-snapshotting-hooks>`
|
||||||
|
with built-in support for MySQL and Postgres checkpointing
|
||||||
|
as well as custom scripts (thanks, `@overhacked <https://github.com/overhacked>`_!)
|
||||||
|
* |feature| Use ``zfs destroy pool/fs@snap1,snap2,...`` CLI feature if available
|
||||||
|
* |feature| Linux ARM64 Docker build support & binary builds
|
||||||
|
* |feature| ``zrepl status`` now displays snapshotting reports
|
||||||
|
* |feature| ``zrepl status --job <JOBNAME>`` filter flag
|
||||||
|
* |bugfix| i386 build
|
||||||
|
* |bugfix| early validation of host:port tuples in config
|
||||||
|
* |bugfix| ``zrepl status`` now supports ``TERM=screen`` (tmux on FreeBSD / FreeNAS)
|
||||||
|
* |bugfix| ignore *connection reset by peer* errors when shutting down connections
|
||||||
|
* |bugfix| correct error messages when receive-side pool or ``root_fs`` dataset is not imported
|
||||||
|
* |bugfix| fail fast for misconfigured local transport
|
||||||
|
* |bugfix| race condition in replication report generation would crash the daemon when running ``zrepl status``
|
||||||
|
* |bugfix| rpc goroutine leak in ``push`` mode if zfs recv fails on the ``sink`` side
|
||||||
|
* [MAINTAINER NOTICE] Go modules for dependency management both inside and outside of GOPATH
|
||||||
|
(``lazy.sh`` and ``Makefile`` force ``GO111MODULE=on``)
|
||||||
|
* [MAINTAINER NOTICE] ``make platformtest`` target to check zrepl's ZFS abstractions (screen scraping, etc.).
|
||||||
|
These tests only work on a system with ZFS installed, and must be run as root because they create a file-backed pool for each test case.
|
||||||
|
The pool name ``zreplplatformtest`` is reserved for this use case.
|
||||||
|
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||||
|
| You can support maintenance and feature development through one of the following services:
|
||||||
|
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||||
|
| Note that PayPal processing fees are relatively high for small donations.
|
||||||
|
| For SEPA wire transfer and **commerical support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
|
|
||||||
|
0.1.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* |bugfix| :issue:`162` :commit:`d6304f4` : fix I/O timeout errors on variable receive rate
|
||||||
|
|
||||||
|
* A significant reduction or sudden stall of the receive rate (e.g. recv pool has other I/O to do)
|
||||||
|
would cause a ``writev I/O timeout`` error after approximately ten seconds.
|
||||||
|
|
||||||
0.1
|
0.1
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -40,38 +133,59 @@ It breaks both configuration and transport format, and thus requires manual inte
|
|||||||
Notes to Package Maintainers
|
Notes to Package Maintainers
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Notify users about config changes and migrations (see changes attributed with |break| and |mig| below)
|
||||||
* If the daemon crashes, the stack trace produced by the Go runtime and possibly diagnostic output of zrepl will be written to stderr.
|
* If the daemon crashes, the stack trace produced by the Go runtime and possibly diagnostic output of zrepl will be written to stderr.
|
||||||
This behavior is independent from the ``stdout`` outlet type.
|
This behavior is independent from the ``stdout`` outlet type.
|
||||||
Please make sure the stderr output of the daemon is captured to a file.
|
Please make sure the stderr output of the daemon is captured somewhere.
|
||||||
Rotation should not be necessary because stderr is not written to under normal circumstances.
|
|
||||||
To conserve precious stack traces, make sure that multiple service restarts do not directly discard previous stderr output.
|
To conserve precious stack traces, make sure that multiple service restarts do not directly discard previous stderr output.
|
||||||
* Make it obvious for users how to set the ``GOTRACEBACK`` environment variable to ``GOTRACEBACK=crash``.
|
* Make it obvious for users how to set the ``GOTRACEBACK`` environment variable to ``GOTRACEBACK=crash``.
|
||||||
This functionality will cause SIGABRT on panics and can be used to capture a coredump of the panicking process.
|
This functionality will cause SIGABRT on panics and can be used to capture a coredump of the panicking process.
|
||||||
To that extend, make sure that your package build system, your OS's coredump collection and the Go delve debugger work together.
|
To that extend, make sure that your package build system, your OS's coredump collection and the Go delve debugger work together.
|
||||||
Use your build system to package the Go program in `this tutorial on Go coredumps and the delve debugger <https://rakyll.org/coredumps/>`_ , and make sure the symbol resolution etc. work on coredumps captured from the binary produced by your build system. (Special focus on symbol stripping, etc.)
|
Use your build system to package the Go program in `this tutorial on Go coredumps and the delve debugger <https://rakyll.org/coredumps/>`_ , and make sure the symbol resolution etc. work on coredumps captured from the binary produced by your build system. (Special focus on symbol stripping, etc.)
|
||||||
|
* Consider using the ``zrepl configcheck`` subcommand in startup scripts to abort a restart that would fail due to an invalid config.
|
||||||
|
|
||||||
Changes
|
Changes
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
* |break| |mig| Placeholder property representation changed
|
||||||
|
|
||||||
|
* The :ref:`placeholder property <replication-placeholder-property>` now uses ``on|off`` as values
|
||||||
|
instead of hashes of the dataset path. This permits renames of the sink filesystem without
|
||||||
|
updating all placeholder properties.
|
||||||
|
* Relevant for 0.0.X-0.1-rc* to 0.1 migrations
|
||||||
|
* Make sure your config is valid with ``zrepl configcheck``
|
||||||
|
* Run ``zrepl migrate 0.0.X:0.1:placeholder``
|
||||||
|
|
||||||
* |feature| :issue:`55` : Push replication (see :ref:`push job <job-push>` and :ref:`sink job <job-sink>`)
|
* |feature| :issue:`55` : Push replication (see :ref:`push job <job-push>` and :ref:`sink job <job-sink>`)
|
||||||
* |feature| :ref:`TCP Transport <transport-tcp>`
|
* |feature| :ref:`TCP Transport <transport-tcp>`
|
||||||
* |feature| :ref:`TCP + TLS client authentication transport <transport-tcp+tlsclientauth>`
|
* |feature| :ref:`TCP + TLS client authentication transport <transport-tcp+tlsclientauth>`
|
||||||
* |feature| :issue:`78` TODO MERGE COMMIT Replication protocol rewrite
|
* |feature| :issue:`111`: RPC protocol rewrite
|
||||||
|
|
||||||
* Uses ``github.com/problame/go-streamrpc`` for RPC layer
|
* |break| Protocol breakage; Update and restart of all zrepl daemons is required.
|
||||||
* |break| zrepl 0.1 and restart on both sides of a replication setup is required
|
* Use `gRPC <https://grpc.io/>`_ for control RPCs and a custom protocol for bulk data transfer.
|
||||||
* |feature| :issue:`83`: Improved error handling of network-level errors (zrepl retries instead of failing the entire job)
|
* Automatic retries for network-temporary errors
|
||||||
* |bugfix| :issue:`75` :issue:`81`: use connection timeouts and protocol-level heartbeats
|
|
||||||
* |break| |break_config|: mappings are no longer supported
|
* Limited to errors during replication for this release.
|
||||||
|
Addresses the common problem of ISP-forced reconnection at night, but will become
|
||||||
|
way more useful with resumable send & recv support.
|
||||||
|
Pruning errors are handled per FS, i.e., a prune RPC is attempted at least once per FS.
|
||||||
|
|
||||||
|
* |feature| Proper timeout handling for the :ref:`SSH transport <transport-ssh+stdinserver>`
|
||||||
|
|
||||||
|
* |break| Requires Go 1.11 or later.
|
||||||
|
|
||||||
|
* |break| |break_config|: mappings are no longer supported
|
||||||
|
|
||||||
* Receiving sides (``pull`` and ``sink`` job) specify a single ``root_fs``.
|
* Receiving sides (``pull`` and ``sink`` job) specify a single ``root_fs``.
|
||||||
Received filesystems are then stored *per client* in ``${root_fs}/${client_identity}``.
|
Received filesystems are then stored *per client* in ``${root_fs}/${client_identity}``.
|
||||||
|
See :ref:`job-overview` for details.
|
||||||
|
|
||||||
* |feature| |break| |break_config| Manual snapshotting + triggering of replication
|
* |feature| |break| |break_config| Manual snapshotting + triggering of replication
|
||||||
|
|
||||||
* |feature| :issue:`69`: include manually created snapshots in replication
|
* |feature| :issue:`69`: include manually created snapshots in replication
|
||||||
* |break_config| ``manual`` and ``periodic`` :ref:`snapshotting types <job-snapshotting-spec>`
|
* |break_config| ``manual`` and ``periodic`` :ref:`snapshotting types <job-snapshotting-spec>`
|
||||||
* |feature| ``zrepl wakeup JOB`` subcommand to trigger *just* replication
|
* |feature| ``zrepl signal wakeup JOB`` subcommand to trigger replication + pruning
|
||||||
|
* |feature| ``zrepl signal reset JOB`` subcommand to abort current replication + pruning
|
||||||
|
|
||||||
* |feature| |break| |break_config| New pruning system
|
* |feature| |break| |break_config| New pruning system
|
||||||
|
|
||||||
@@ -85,16 +199,22 @@ Changes
|
|||||||
* |feature| |break| Bookmark pruning is no longer necessary
|
* |feature| |break| Bookmark pruning is no longer necessary
|
||||||
|
|
||||||
* Per filesystem, zrepl creates a single bookmark (``#zrepl_replication_cursor``) and moves it forward with the most recent successfully replicated snapshot on the receiving side.
|
* Per filesystem, zrepl creates a single bookmark (``#zrepl_replication_cursor``) and moves it forward with the most recent successfully replicated snapshot on the receiving side.
|
||||||
* Old bookmarks created prior to zrepl 0.1 (named like their corresponding snapshot) must be deleted manually.
|
* Old bookmarks created by prior versions of zrepl (named like their corresponding snapshot) must be deleted manually.
|
||||||
* |break_config| ``keep_bookmarks`` parameter of the ``grid`` keep rule has been removed
|
* |break_config| ``keep_bookmarks`` parameter of the ``grid`` keep rule has been removed
|
||||||
|
|
||||||
* |feature| ``zrepl status`` for live-updating replication progress (it's really cool!)
|
* |feature| ``zrepl status`` for live-updating replication progress (it's really cool!)
|
||||||
|
* |feature| :ref:`Snapshot- & pruning-only job type <job-snap>` (for local snapshot management)
|
||||||
* |feature| :issue:`67`: Expose `Prometheus <https://prometheus.io>`_ metrics via HTTP (:ref:`config docs <monitoring-prometheus>`)
|
* |feature| :issue:`67`: Expose `Prometheus <https://prometheus.io>`_ metrics via HTTP (:ref:`config docs <monitoring-prometheus>`)
|
||||||
|
|
||||||
|
* Compatible Grafana dashboard shipping in ``dist/grafana``
|
||||||
|
|
||||||
* |break_config| Logging outlet types must be specified using the ``type`` instead of ``outlet`` key
|
* |break_config| Logging outlet types must be specified using the ``type`` instead of ``outlet`` key
|
||||||
* |break| :issue:`53`: CLI: ``zrepl control *`` subcommands have been made direct subcommands of ``zrepl *``
|
* |break| :issue:`53`: CLI: ``zrepl control *`` subcommands have been made direct subcommands of ``zrepl *``
|
||||||
* |bugfix| Goroutine leak on ssh transport connection timeouts
|
* |bugfix| Goroutine leak on ssh transport connection timeouts
|
||||||
* |bugfix| :issue:`81` :issue:`77` : handle failed accepts correctly (``source`` job)
|
* |bugfix| :issue:`81` :issue:`77` : handle failed accepts correctly (``source`` job)
|
||||||
|
* |bugfix| :issue:`100`: fix incompatibility with ZoL 0.8
|
||||||
|
* |feature| :issue:`115`: logging: configurable syslog facility
|
||||||
|
* |feature| Systemd unit file in ``dist/systemd``
|
||||||
|
|
||||||
.. |lastrelease| replace:: 0.0.3
|
.. |lastrelease| replace:: 0.0.3
|
||||||
|
|
||||||
@@ -104,8 +224,3 @@ Previous Releases
|
|||||||
.. NOTE::
|
.. NOTE::
|
||||||
Due to limitations in our documentation system, we only show the changelog since the last release and the time this documentation is built.
|
Due to limitations in our documentation system, we only show the changelog since the last release and the time this documentation is built.
|
||||||
For the changelog of previous releases, use the version selection in the hosted version of these docs at `zrepl.github.io <https://zrepl.github.io>`_.
|
For the changelog of previous releases, use the version selection in the hosted version of these docs at `zrepl.github.io <https://zrepl.github.io>`_.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
W
|
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ Configuration
|
|||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
||||||
configuration/preface
|
configuration/overview
|
||||||
configuration/jobs
|
configuration/jobs
|
||||||
configuration/transports
|
configuration/transports
|
||||||
configuration/filter_syntax
|
configuration/filter_syntax
|
||||||
|
configuration/sendrecvoptions
|
||||||
|
configuration/snapshotting
|
||||||
configuration/prune
|
configuration/prune
|
||||||
configuration/logging
|
configuration/logging
|
||||||
configuration/monitoring
|
configuration/monitoring
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Filter Syntax
|
Filter Syntax
|
||||||
=============
|
=============
|
||||||
|
|
||||||
For :ref:`source jobs <job-source>` and :ref:`push jobs <job-push>`, a filesystem filter must be defined (field ``filesystems``).
|
For :ref:`source<job-source>`, :ref:`push<job-push>` and :ref:`snap<job-snap>` jobs, a filesystem filter must be defined (field ``filesystems``).
|
||||||
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
||||||
|
|
||||||
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
||||||
|
|||||||
+38
-157
@@ -1,159 +1,9 @@
|
|||||||
.. include:: ../global.rst.inc
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
|
||||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
|
||||||
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
|
||||||
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
|
||||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
|
||||||
|
|
||||||
.. _job:
|
.. _job:
|
||||||
|
|
||||||
Job Types & Replication
|
Job Types in Detail
|
||||||
=======================
|
===================
|
||||||
|
|
||||||
Overview & Terminology
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
A *job* is the unit of activity tracked by the zrepl daemon and configured in the |mainconfig|.
|
|
||||||
Every job has a unique ``name``, a ``type`` and type-dependent fields which are documented on this page.
|
|
||||||
|
|
||||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
|
||||||
The active side executes the replication logic whereas the passive side responds to requests after checking the active side's permissions.
|
|
||||||
For communication, the active side connects to the passive side using a :ref:`transport <transport>` and starts issuing remote procedure calls (RPCs).
|
|
||||||
|
|
||||||
The following table shows how different job types can be combined to achieve both push and pull mode setups:
|
|
||||||
|
|
||||||
+-----------------------+--------------+----------------------------------+-----------------------------------------------+
|
|
||||||
| Setup name | active side | passive side | use case |
|
|
||||||
+=======================+==============+==================================+===============================================+
|
|
||||||
| Push mode | ``push`` | ``sink`` | * Laptop backup |
|
|
||||||
| | | | * NAS behind NAT to offsite |
|
|
||||||
+-----------------------+--------------+----------------------------------+-----------------------------------------------+
|
|
||||||
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
|
||||||
| | | | * Remote server to NAS behind NAT |
|
|
||||||
+-----------------------+--------------+----------------------------------+-----------------------------------------------+
|
|
||||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool |
|
|
||||||
| | | with :ref:`local transport <transport-local>` | |
|
|
||||||
+-----------------------+--------------+----------------------------------+-----------------------------------------------+
|
|
||||||
|
|
||||||
How the Active Side Works
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
|
|
||||||
|
|
||||||
* Wakeup because of finished snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
|
||||||
* Connect to the corresponding passive side using a :ref:`transport <transport>` and instantiate an RPC client.
|
|
||||||
* Replicate data from the sending to the receiving side.
|
|
||||||
* Prune on sender & receiver.
|
|
||||||
|
|
||||||
.. TIP::
|
|
||||||
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
|
||||||
|
|
||||||
How the Passive Side Works
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
|
||||||
using the transport listener type specified in the ``serve`` field of the job configuration.
|
|
||||||
Each transport listener provides a client's identity to the passive side job.
|
|
||||||
It uses the client identity for access control:
|
|
||||||
|
|
||||||
* The ``sink`` job only allows pushes to those ZFS filesystems to the active side that are located below ``root_fs/${client_identity}``.
|
|
||||||
* The ``source`` job has a whitelist of client identities that are allowed pull access.
|
|
||||||
|
|
||||||
.. TIP::
|
|
||||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
|
||||||
|
|
||||||
How Replication Works
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
One of the major design goals of the replication module is to avoid any duplication of the nontrivial logic.
|
|
||||||
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
|
||||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
|
||||||
|
|
||||||
The following steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
|
||||||
|
|
||||||
* Plan the replication:
|
|
||||||
|
|
||||||
* Compare sender and receiver filesystem snapshots
|
|
||||||
* Build the **replication plan**
|
|
||||||
|
|
||||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
|
||||||
* Build a list of replication steps
|
|
||||||
|
|
||||||
* If possible, use incremental sends (``zfs send -i``)
|
|
||||||
* Otherwise, use full send of most recent snapshot on sender
|
|
||||||
* Give up on filesystems that cannot be replicated without data loss
|
|
||||||
|
|
||||||
* Retry on errors that are likely temporary (i.e. network failures).
|
|
||||||
* Give up on filesystems where a permanent error was received over RPC.
|
|
||||||
|
|
||||||
* Execute the plan
|
|
||||||
|
|
||||||
* Perform replication steps in the following order:
|
|
||||||
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
|
||||||
* After a successful replication step, update the replication cursor bookmark (see below)
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
.. _replication-cursor-bookmark:
|
|
||||||
|
|
||||||
The **replication cursor bookmark** ``#zrepl_replication_cursor`` is kept per filesystem on the sending side of a replication setup:
|
|
||||||
It is a bookmark of the most recent successfully replicated snapshot to the receiving side.
|
|
||||||
It is is used by the :ref:`not_replicated <prune-keep-not-replicated>` keep rule to identify all snapshots that have not yet been replicated to the receiving side.
|
|
||||||
Regardless of whether that keep rule is used, the bookmark ensures that replication can always continue incrementally.
|
|
||||||
|
|
||||||
.. ATTENTION::
|
|
||||||
|
|
||||||
Currently, zrepl does not replicate filesystem properties.
|
|
||||||
Whe receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
|
||||||
This is temporary and being worked on :issue:`24`.
|
|
||||||
|
|
||||||
|
|
||||||
.. _job-snapshotting-spec:
|
|
||||||
|
|
||||||
Taking Snaphots
|
|
||||||
---------------
|
|
||||||
|
|
||||||
The ``push`` and ``source`` 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.
|
|
||||||
|
|
||||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- type: push
|
|
||||||
filesystems: {
|
|
||||||
"<": true,
|
|
||||||
"tmp": false
|
|
||||||
}
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
...
|
|
||||||
|
|
||||||
There is also a ``manual`` snapshotting type, which covers the following use cases:
|
|
||||||
|
|
||||||
* Existing infrastructure for automatic snapshots: you only want to use zrepl for replication.
|
|
||||||
* Run scripts before and after taking snapshots (like locking database tables).
|
|
||||||
We are working on better integration for this use case: see :issue:`74`.
|
|
||||||
|
|
||||||
Note that you will have to trigger replication manually using the ``zrepl wakeup JOB`` subcommand in that case.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- type: push
|
|
||||||
filesystems: {
|
|
||||||
"<": true,
|
|
||||||
"tmp": false
|
|
||||||
}
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
.. _job-push:
|
.. _job-push:
|
||||||
|
|
||||||
@@ -174,6 +24,8 @@ Job Type ``push``
|
|||||||
- |connect-transport|
|
- |connect-transport|
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
@@ -199,8 +51,8 @@ Job Type ``sink``
|
|||||||
* - ``serve``
|
* - ``serve``
|
||||||
- |serve-transport|
|
- |serve-transport|
|
||||||
* - ``root_fs``
|
* - ``root_fs``
|
||||||
- ZFS dataset path are received to
|
- ZFS filesystems are received to
|
||||||
``$root_fs/$client_identity``
|
``$root_fs/$client_identity/$source_path``
|
||||||
|
|
||||||
Example config: :sampleconf:`/sink.yml`
|
Example config: :sampleconf:`/sink.yml`
|
||||||
|
|
||||||
@@ -222,10 +74,11 @@ Job Type ``pull``
|
|||||||
* - ``connect``
|
* - ``connect``
|
||||||
- |connect-transport|
|
- |connect-transport|
|
||||||
* - ``root_fs``
|
* - ``root_fs``
|
||||||
- ZFS dataset path are received to
|
- ZFS filesystems are received to
|
||||||
``$root_fs/$client_identity``
|
``$root_fs/$source_path``
|
||||||
* - ``interval``
|
* - ``interval``
|
||||||
- Interval at which to pull from the source job
|
- | Interval at which to pull from the source job (e.g. ``10m``).
|
||||||
|
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
- |pruning-spec|
|
- |pruning-spec|
|
||||||
|
|
||||||
@@ -250,11 +103,14 @@ Job Type ``source``
|
|||||||
- |serve-transport|
|
- |serve-transport|
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
|
|
||||||
Example config: :sampleconf:`/source.yml`
|
Example config: :sampleconf:`/source.yml`
|
||||||
|
|
||||||
|
|
||||||
.. _replication-local:
|
.. _replication-local:
|
||||||
|
|
||||||
Local replication
|
Local replication
|
||||||
@@ -265,3 +121,28 @@ If you have the need for local replication (most likely between two local storag
|
|||||||
Example config: :sampleconf:`/local.yml`.
|
Example config: :sampleconf:`/local.yml`.
|
||||||
|
|
||||||
|
|
||||||
|
.. _job-snap:
|
||||||
|
|
||||||
|
Job Type ``snap`` (snapshot & prune only)
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
Job type that only takes snapshots and performs pruning on the local machine.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``snap``
|
||||||
|
* - ``name``
|
||||||
|
- unique name of the job
|
||||||
|
* - ``filesystems``
|
||||||
|
- |filter-spec| for filesystems to be snapshotted
|
||||||
|
* - ``snapshotting``
|
||||||
|
- |snapshotting-spec|
|
||||||
|
* - ``pruning``
|
||||||
|
- |pruning-spec|
|
||||||
|
|
||||||
|
Example config: :sampleconf:`/snap.yml`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Logging
|
|||||||
|
|
||||||
zrepl uses structured logging to provide users with easily processable log messages.
|
zrepl uses structured logging to provide users with easily processable log messages.
|
||||||
|
|
||||||
Logging outlets are configured in the ``global`` section of the |mainconfig|.
|
Logging outlets are configured in the ``global`` section of the config file.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -147,6 +147,8 @@ Can only be specified once.
|
|||||||
- minimum :ref:`log level <logging-levels>`
|
- minimum :ref:`log level <logging-levels>`
|
||||||
* - ``format``
|
* - ``format``
|
||||||
- output :ref:`format <logging-formats>`
|
- output :ref:`format <logging-formats>`
|
||||||
|
* - ``facility``
|
||||||
|
- Which syslog facility to use (default = ``local0``)
|
||||||
* - ``retry_interval``
|
* - ``retry_interval``
|
||||||
- Interval between reconnection attempts to syslog (default = 0)
|
- Interval between reconnection attempts to syslog (default = 0)
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ Miscellaneous
|
|||||||
Runtime Directories & UNIX Sockets
|
Runtime Directories & UNIX Sockets
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
zrepl daemon creates various UNIX sockets to allow communicating with it:
|
The zrepl daemon needs to open various UNIX sockets in a runtime directory:
|
||||||
|
|
||||||
* the :ref:`transport-ssh+stdinserver` transport connects to a socket named after ``client_identity`` parameter
|
* a ``control`` socket that the CLI commands use to interact with the daemon
|
||||||
* the ``control`` CLI subcommand connects to a defined control socket
|
* the :ref:`transport-ssh+stdinserver` listener opens one socket per configured client, named after ``client_identity`` parameter
|
||||||
|
|
||||||
There is no further authentication on these sockets.
|
There is no authentication on these sockets except the UNIX permissions.
|
||||||
Therefore we have to make sure they can only be created and accessed by ``zrepl daemon``.
|
The zrepl daemon will refuse to bind any of the above sockets in a directory that is world-accessible.
|
||||||
In fact, ``zrepl daemon`` will not bind a socket to a path in a directory that is world-accessible.
|
|
||||||
|
|
||||||
The directories can be configured in the main configuration file, the defaults are provided below:
|
The following sections of the ``global`` config shows the default paths.
|
||||||
|
The shell script below shows how the default runtime directory can be created.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -27,6 +27,12 @@ The directories can be configured in the main configuration file, the defaults a
|
|||||||
sockdir: /var/run/zrepl/stdinserver
|
sockdir: /var/run/zrepl/stdinserver
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p /var/run/zrepl/stdinserver
|
||||||
|
chmod -R 0700 /var/run/zrepl
|
||||||
|
|
||||||
|
|
||||||
Durations & Intervals
|
Durations & Intervals
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user