Compare commits
58 Commits
v0.1.0-rc4
...
v0.2.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+40
-9
@@ -5,6 +5,7 @@ workflows:
|
||||
jobs:
|
||||
- build-1.11
|
||||
- build-1.12
|
||||
- build-1.13
|
||||
- build-latest
|
||||
jobs:
|
||||
# build-latest serves as the template
|
||||
@@ -27,11 +28,12 @@ jobs:
|
||||
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
|
||||
- vendor
|
||||
- protobuf
|
||||
|
||||
- checkout
|
||||
@@ -50,30 +52,59 @@ jobs:
|
||||
paths:
|
||||
- "/usr/include/google/protobuf"
|
||||
|
||||
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
|
||||
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev
|
||||
- run: ./lazy.sh devsetup
|
||||
|
||||
- run: make vendordeps
|
||||
- save_cache:
|
||||
key: vendor
|
||||
paths:
|
||||
- "./vendor"
|
||||
|
||||
- run: make
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,6 +29,7 @@ matrix:
|
||||
- make
|
||||
- make vet
|
||||
- make test
|
||||
- make lint
|
||||
- make artifacts/zrepl-freebsd-amd64
|
||||
- make artifacts/zrepl-linux-amd64
|
||||
- make artifacts/zrepl-darwin-amd64
|
||||
|
||||
Generated
-477
@@ -1,477 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8cf2cf1ab10480b5e0df950dac1517aaabde05d055d9d955652997ae4b9ecbbf"
|
||||
name = "github.com/alvaroloes/enumer"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "6bcfe2edaac32ad71b88ce4cf92d34cd643e4ecb"
|
||||
|
||||
[[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",
|
||||
"protoc-gen-go",
|
||||
"protoc-gen-go/descriptor",
|
||||
"protoc-gen-go/generator",
|
||||
"protoc-gen-go/generator/internal/remap",
|
||||
"protoc-gen-go/grpc",
|
||||
"protoc-gen-go/plugin",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ad92aa49f34cbc3546063c7eb2cabb55ee2278b72842eda80e2a20a8a06a8d73"
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[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:4ff67dde814694496d7aa31be44b900f9717a10c8bc9136b13f49c8ef97f439a"
|
||||
name = "github.com/montanaflynn/stats"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "63fbb2597b7a13043b453a4b819945badb8f8926"
|
||||
version = "v0.5.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f60ff065b58bd53e641112b38bbda9d2684deb828393c7ffb89c69a1ee301d17"
|
||||
name = "github.com/pascaldekloe/name"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0fd16699aae1833640fca52a937944c6f3b1d58c"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1cbc6b98173422a756ae79e485952cb37a0a460c710541c75d3e9961c5a60782"
|
||||
name = "github.com/pkg/profile"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "5b67d428864e92711fcbd2f8629456121a56d91f"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = ""
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:fa72f780ae3b4820ed12cef7a034291ab10d83e2da4ab5ba81afa44d5bf3a529"
|
||||
name = "github.com/problame/go-netssh"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "09d6bc45d284784cb3e5aaa1998513f37eb19cc6"
|
||||
|
||||
[[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]]
|
||||
digest = "1:3962f553b77bf6c03fc07cd687a22dd3b00fe11aa14d31194f5505f5bb65cdc8"
|
||||
name = "github.com/sergi/go-diff"
|
||||
packages = ["diffmatchpatch"]
|
||||
pruneopts = ""
|
||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[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]]
|
||||
digest = "1:f80053a92d9ac874ad97d665d3005c1118ed66e9e88401361dc32defe6bef21c"
|
||||
name = "github.com/theckman/goconstraint"
|
||||
packages = ["go1.11/gte"]
|
||||
pruneopts = ""
|
||||
revision = "93babf24513d0e8277635da8169fcc5a46ae3f6a"
|
||||
version = "v1.11.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:529ed3f98838f69e13761788d0cc71b44e130058fab13bae2ce09f7a176bced4"
|
||||
name = "github.com/yudai/gojsondiff"
|
||||
packages = [
|
||||
".",
|
||||
"formatter",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
|
||||
version = "1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9857bb2293f372b2181004d8b62179bbdb4ab0982ec6f762abe6cf2bfedaff85"
|
||||
name = "github.com/yudai/golcs"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
digest = "1:6b8a6afafde7ed31cd0c577ba40d88ce39e8f1c5eb76d7836be7d5b74f1c534a"
|
||||
name = "github.com/zrepl/yaml-config"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "08227ad854131f7dfcdfb12579fb73dd8a38a03a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ea539c13b066dac72a940b62f37600a20ab8e88057397c78f3197c1a48475425"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/timeseries",
|
||||
"trace",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "351d144fa1fc0bd934e2408202be0c29f25e35a0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f358024b019f87eecaadcb098113a40852c94fe58ea670ef3c3e2d2c7bd93db1"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
pruneopts = ""
|
||||
revision = "4ed8d59d0b35e1e29334a206d1b3f38b1e5dfb31"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"encoding",
|
||||
"encoding/internal/identifier",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4cd780b2ee42c8eac9c02bfb6e6b52dcbaef770774458c8938f5cbfb73a7b6d3"
|
||||
name = "golang.org/x/tools"
|
||||
packages = ["cmd/stringer"]
|
||||
pruneopts = ""
|
||||
revision = "d0ca3933b724e6be513276cc2edb34e10d667438"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5fc6c317675b746d0c641b29aa0aab5fcb403c0d07afdbf0de86b0d447a0502a"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
pruneopts = ""
|
||||
revision = "bd91e49a0898e27abb88c339b432fa53d7497ac0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d141efe4aaad714e3059c340901aab3147b6253e58c85dafbcca3dd8b0e88ad6"
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"binarylog/grpc_binarylog_v1",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"credentials/internal",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"internal/backoff",
|
||||
"internal/binarylog",
|
||||
"internal/channelz",
|
||||
"internal/envconfig",
|
||||
"internal/grpcrand",
|
||||
"internal/grpcsync",
|
||||
"internal/syscall",
|
||||
"internal/transport",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "df014850f6dee74ba2fc94874043a9f3f75fbfd8"
|
||||
version = "v1.17.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/alvaroloes/enumer",
|
||||
"github.com/fatih/color",
|
||||
"github.com/gdamore/tcell/termbox",
|
||||
"github.com/go-logfmt/logfmt",
|
||||
"github.com/golang/protobuf/proto",
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
"github.com/google/uuid",
|
||||
"github.com/jinzhu/copier",
|
||||
"github.com/kr/pretty",
|
||||
"github.com/mattn/go-isatty",
|
||||
"github.com/montanaflynn/stats",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/pkg/profile",
|
||||
"github.com/problame/go-netssh",
|
||||
"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/yudai/gojsondiff",
|
||||
"github.com/yudai/gojsondiff/formatter",
|
||||
"github.com/zrepl/yaml-config",
|
||||
"golang.org/x/net/context",
|
||||
"golang.org/x/sys/unix",
|
||||
"golang.org/x/tools/cmd/stringer",
|
||||
"google.golang.org/grpc",
|
||||
"google.golang.org/grpc/codes",
|
||||
"google.golang.org/grpc/credentials",
|
||||
"google.golang.org/grpc/keepalive",
|
||||
"google.golang.org/grpc/peer",
|
||||
"google.golang.org/grpc/status",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
ignored = [
|
||||
"github.com/inconshreveable/mousetrap",
|
||||
]
|
||||
|
||||
required = [
|
||||
"golang.org/x/tools/cmd/stringer",
|
||||
"github.com/alvaroloes/enumer",
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/copier"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/pretty"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/cobra"
|
||||
|
||||
[[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-netssh"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/color"
|
||||
version = "1.7.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gdamore/tcell"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "google.golang.org/grpc"
|
||||
version = "1"
|
||||
|
||||
[[constraint]]
|
||||
version = "1.1.0"
|
||||
name = "github.com/google/uuid"
|
||||
@@ -1,4 +1,4 @@
|
||||
.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
|
||||
|
||||
ARTIFACTDIR := artifacts
|
||||
@@ -12,36 +12,54 @@ ifndef _ZREPL_VERSION
|
||||
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
||||
endif
|
||||
endif
|
||||
GO := go
|
||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||
GO_MOD_READONLY := -mod=readonly
|
||||
GO_BUILDFLAGS := $(GO_MOD_READONLY)
|
||||
GO_BUILD := GO111MODULE=on $(GO) build $(GO_BUILDFLAGS) -v -ldflags $(GO_LDFLAGS)
|
||||
|
||||
GO_BUILD := go build -ldflags $(GO_LDFLAGS)
|
||||
|
||||
# keep in sync with vet target
|
||||
RELEASE_BINS := $(ARTIFACTDIR)/zrepl-freebsd-amd64
|
||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-amd64
|
||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-arm64
|
||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-darwin-amd64
|
||||
|
||||
RELEASE_NOARCH := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <(go env) && echo "zrepl-$${GOOS}-$${GOARCH}"' )
|
||||
|
||||
vendordeps:
|
||||
dep ensure -v -vendor-only
|
||||
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <($(GO) env) && echo "zrepl-$${GOOS}-$${GOARCH}"' )
|
||||
|
||||
generate: #not part of the build, must do that manually
|
||||
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
||||
go generate -x ./...
|
||||
$(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')
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
build:
|
||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl"
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
$(GO) test $(GO_BUILDFLAGS) ./...
|
||||
# TODO compile the tests for each supported platform
|
||||
# but `go test -c ./...` is not supported
|
||||
|
||||
vet:
|
||||
$(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
# for each supported platform to cover conditional compilation
|
||||
GOOS=linux go vet ./...
|
||||
GOOS=darwin go vet ./...
|
||||
GOOS=freebsd go vet ./...
|
||||
# (keep in sync with RELEASE_BINS)
|
||||
GOOS=freebsd GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
GOOS=linux GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
GOOS=linux GOARCH=arm64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
GOOS=darwin GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
|
||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||
$(ARTIFACTDIR)/zrepl_platformtest:
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl_platformtest" ./platformtest/harness
|
||||
platformtest: $(ARTIFACTDIR)/zrepl_platformtest
|
||||
"$(ARTIFACTDIR)/zrepl_platformtest" -poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" -imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)"
|
||||
|
||||
$(ARTIFACTDIR):
|
||||
mkdir -p "$@"
|
||||
@@ -52,8 +70,9 @@ $(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||
$(ARTIFACTDIR)/bash_completion: $(RELEASE_BINS)
|
||||
artifacts/$(THIS_PLATFORM_RELEASE_BIN) bashcomp "$@"
|
||||
|
||||
.PHONY: $(ARTIFACTDIR)/go_version.txt
|
||||
$(ARTIFACTDIR)/go_version.txt:
|
||||
go version > $@
|
||||
$(GO) version > $@
|
||||
|
||||
docs: $(ARTIFACTDIR)/docs
|
||||
make -C docs \
|
||||
@@ -65,12 +84,11 @@ docs-clean:
|
||||
clean \
|
||||
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_BINS): $(ARTIFACTDIR)/zrepl-%: generate $(ARTIFACTDIR) vet test lint
|
||||
STEM=$*; GOOS="$${STEM%%-*}"; GOARCH="$${STEM##*-}"; export GOOS GOARCH; \
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$$GOOS-$$GOARCH"
|
||||
|
||||
$(RELEASE_NOARCH): docs $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_version.txt
|
||||
tar --mtime='1970-01-01' --sort=name \
|
||||
@@ -90,6 +108,9 @@ release: $(RELEASE_BINS) $(RELEASE_NOARCH)
|
||||
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
||||
if [ "$(ZREPL_VERSION)" = "" ]; then \
|
||||
echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
|
||||
git status; \
|
||||
echo "git diff:"; \
|
||||
git diff | cat; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[](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://liberapay.com/zrepl/donate)
|
||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||
|
||||
# zrepl
|
||||
@@ -37,11 +37,12 @@ Check out the *Coding Workflow* section below for details.
|
||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
||||
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
||||
* Use `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.
|
||||
|
||||
## 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.
|
||||
|
||||
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
||||
|
||||
+10
-12
@@ -4,9 +4,8 @@ RUN apt-get update && apt-get install -y \
|
||||
python3-pip \
|
||||
unzip
|
||||
|
||||
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 unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
||||
ADD build.installprotoc.bash ./
|
||||
RUN bash build.installprotoc.bash
|
||||
|
||||
ADD lazy.sh /tmp/lazy.sh
|
||||
ADD docs/requirements.txt /tmp/requirements.txt
|
||||
@@ -14,18 +13,17 @@ ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
|
||||
RUN /tmp/lazy.sh docdep
|
||||
|
||||
# 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 mkdir -p /.cache && chmod -R 0777 /.cache
|
||||
WORKDIR /go/src/github.com/zrepl/zrepl
|
||||
|
||||
ADD Gopkg.toml Gopkg.lock ./
|
||||
# $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
|
||||
|
||||
# godep will install the Go dependencies to vendor in order to then build and install
|
||||
# build dependencies like stringer to $GOPATH/bin.
|
||||
# However, since users volume-mount their Git checkout into /go/src/github.com/zrepl/zrepl
|
||||
# the vendor directory will be empty at build time, allowing them to experiment with
|
||||
# new checkouts, etc.
|
||||
# Thus, we only use the vendored deps for building dependencies.
|
||||
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
||||
ADD go.mod go.sum ./
|
||||
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"
|
||||
+2
-1
@@ -11,7 +11,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "fmt"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/alvaroloes/enumer"
|
||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||
_ "golang.org/x/tools/cmd/stringer"
|
||||
|
||||
+17
-13
@@ -2,10 +2,12 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"os"
|
||||
)
|
||||
|
||||
var rootArgs struct {
|
||||
@@ -23,7 +25,10 @@ var bashcompCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||
cmd.Usage()
|
||||
err := cmd.Usage()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
||||
@@ -40,15 +45,15 @@ func init() {
|
||||
}
|
||||
|
||||
type Subcommand struct {
|
||||
Use string
|
||||
Short string
|
||||
Example string
|
||||
NoRequireConfig bool
|
||||
Run func(subcommand *Subcommand, args []string) error
|
||||
SetupFlags func(f *pflag.FlagSet)
|
||||
SetupSubcommands func() []*Subcommand
|
||||
Use string
|
||||
Short string
|
||||
Example string
|
||||
NoRequireConfig bool
|
||||
Run func(subcommand *Subcommand, args []string) error
|
||||
SetupFlags func(f *pflag.FlagSet)
|
||||
SetupSubcommands func() []*Subcommand
|
||||
|
||||
config *config.Config
|
||||
config *config.Config
|
||||
configErr error
|
||||
}
|
||||
|
||||
@@ -93,8 +98,8 @@ func AddSubcommand(s *Subcommand) {
|
||||
|
||||
func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) {
|
||||
cmd := cobra.Command{
|
||||
Use: s.Use,
|
||||
Short: s.Short,
|
||||
Use: s.Use,
|
||||
Short: s.Short,
|
||||
Example: s.Example,
|
||||
}
|
||||
if s.SetupSubcommands == nil {
|
||||
@@ -110,7 +115,6 @@ func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) {
|
||||
c.AddCommand(&cmd)
|
||||
}
|
||||
|
||||
|
||||
func Run() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
|
||||
+20
-12
@@ -3,39 +3,49 @@ package client
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/zrepl/yaml-config"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"os"
|
||||
)
|
||||
|
||||
var configcheckArgs struct {
|
||||
format string
|
||||
what string
|
||||
what string
|
||||
}
|
||||
|
||||
var ConfigcheckCmd = &cli.Subcommand{
|
||||
Use: "configcheck",
|
||||
Use: "configcheck",
|
||||
Short: "check if config can be parsed without errors",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
||||
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
||||
},
|
||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||
formatMap := map[string]func(interface{}) {
|
||||
formatMap := map[string]func(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.NewEncoder(os.Stdout).Encode(subcommand.Config())
|
||||
if err := json.NewEncoder(os.Stdout).Encode(subcommand.Config()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
"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,12 +81,11 @@ var ConfigcheckCmd = &cli.Subcommand{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
whatMap := map[string]func() {
|
||||
whatMap := map[string]func(){
|
||||
"all": func() {
|
||||
o := struct {
|
||||
config *config.Config
|
||||
jobs []job.Job
|
||||
config *config.Config
|
||||
jobs []job.Job
|
||||
logging *logger.Outlets
|
||||
}{
|
||||
subcommand.Config(),
|
||||
@@ -109,4 +118,3 @@ var ConfigcheckCmd = &cli.Subcommand{
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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 {
|
||||
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())
|
||||
}
|
||||
|
||||
|
||||
+1
-5
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
@@ -22,11 +23,6 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type migration struct {
|
||||
name string
|
||||
method func(config *config.Config, args []string) error
|
||||
}
|
||||
|
||||
var migrations = []*cli.Subcommand{
|
||||
&cli.Subcommand{
|
||||
Use: "0.0.X:0.1:placeholder",
|
||||
|
||||
+3
-2
@@ -2,11 +2,12 @@ package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var pprofArgs struct {
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
@@ -28,10 +29,10 @@ func runSignalCmd(config *config.Config, args []string) error {
|
||||
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointSignal,
|
||||
struct {
|
||||
Name string
|
||||
Op string
|
||||
Op string
|
||||
}{
|
||||
Name: args[1],
|
||||
Op: args[0],
|
||||
Op: args[0],
|
||||
},
|
||||
struct{}{},
|
||||
)
|
||||
|
||||
+177
-61
@@ -2,15 +2,6 @@ package client
|
||||
|
||||
import (
|
||||
"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/report"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -19,18 +10,37 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"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
|
||||
val int64
|
||||
}
|
||||
|
||||
type bytesProgressHistory struct {
|
||||
last *byteProgressMeasurement // pointer as poor man's optional
|
||||
last *byteProgressMeasurement // pointer as poor man's optional
|
||||
changeCount int
|
||||
lastChange time.Time
|
||||
bpsAvg float64
|
||||
lastChange time.Time
|
||||
bpsAvg float64
|
||||
}
|
||||
|
||||
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
||||
@@ -38,7 +48,7 @@ func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64
|
||||
if p.last == nil {
|
||||
p.last = &byteProgressMeasurement{
|
||||
time: time.Now(),
|
||||
val: currentVal,
|
||||
val: currentVal,
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
@@ -48,18 +58,17 @@ func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64
|
||||
p.lastChange = time.Now()
|
||||
}
|
||||
|
||||
if time.Now().Sub(p.lastChange) > 3 * time.Second {
|
||||
if time.Since(p.lastChange) > 3*time.Second {
|
||||
p.last = nil
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
|
||||
deltaV := currentVal - p.last.val;
|
||||
deltaT := time.Now().Sub(p.last.time)
|
||||
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.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
||||
|
||||
p.last.time = time.Now()
|
||||
p.last.val = currentVal
|
||||
@@ -75,20 +84,17 @@ type tui struct {
|
||||
report map[string]job.Status
|
||||
err error
|
||||
|
||||
jobFilter string
|
||||
|
||||
replicationProgress map[string]*bytesProgressHistory // by job name
|
||||
}
|
||||
|
||||
func newTui() tui {
|
||||
return tui{
|
||||
replicationProgress: make(map[string]*bytesProgressHistory, 0),
|
||||
replicationProgress: make(map[string]*bytesProgressHistory),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) moveCursor(x, y int) {
|
||||
t.x += x
|
||||
t.y += y
|
||||
}
|
||||
|
||||
const INDENT_MULTIPLIER = 4
|
||||
|
||||
func (t *tui) moveLine(dl int, col int) {
|
||||
@@ -119,7 +125,7 @@ func wrap(s string, width int) string {
|
||||
rem = len(s)
|
||||
}
|
||||
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
|
||||
rem = idx+1
|
||||
rem = idx + 1
|
||||
}
|
||||
untilNewline := strings.TrimRight(s[:rem], "\n\r")
|
||||
s = s[rem:]
|
||||
@@ -135,12 +141,12 @@ func wrap(s string, width int) string {
|
||||
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 {
|
||||
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.write(wrap(whole, width-INDENT_MULTIPLIER*t.indent))
|
||||
t.addIndent(-1)
|
||||
}
|
||||
}
|
||||
@@ -159,9 +165,9 @@ func (t *tui) addIndent(indent int) {
|
||||
t.moveLine(0, 0)
|
||||
}
|
||||
|
||||
|
||||
var statusFlags struct {
|
||||
Raw bool
|
||||
Job string
|
||||
}
|
||||
|
||||
var StatusCmd = &cli.Subcommand{
|
||||
@@ -169,6 +175,7 @@ var StatusCmd = &cli.Subcommand{
|
||||
Short: "show job activity or dump as JSON for monitoring",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
||||
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
||||
},
|
||||
Run: runStatus,
|
||||
}
|
||||
@@ -180,14 +187,17 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
||||
}
|
||||
|
||||
if statusFlags.Raw {
|
||||
resp, err := httpc.Get("http://unix"+daemon.ControlJobEndpointStatus)
|
||||
resp, err := httpc.Get("http://unix" + daemon.ControlJobEndpointStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
||||
io.CopyN(os.Stderr, resp.Body, 4096)
|
||||
_, err := io.CopyN(os.Stderr, resp.Body, 4096)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.Errorf("exit")
|
||||
}
|
||||
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
||||
@@ -200,6 +210,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
||||
t.lock.Lock()
|
||||
t.err = errors.New("Got no report yet")
|
||||
t.lock.Unlock()
|
||||
t.jobFilter = statusFlags.Job
|
||||
|
||||
err = termbox.Init()
|
||||
if err != nil {
|
||||
@@ -226,7 +237,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
go func() {
|
||||
for _ = range ticker.C {
|
||||
for range ticker.C {
|
||||
update()
|
||||
}
|
||||
}()
|
||||
@@ -275,19 +286,29 @@ func (t *tui) draw() {
|
||||
t.write(t.err.Error())
|
||||
} else {
|
||||
//Iterate over map in alphabetical order
|
||||
keys := make([]string, len(t.report))
|
||||
i := 0
|
||||
for k, _ := range t.report {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := t.report[k]
|
||||
keys := make([]string, 0, len(t.report))
|
||||
for k := range t.report {
|
||||
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
||||
continue
|
||||
}
|
||||
if t.jobFilter != "" && k != t.jobFilter {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
if len(keys) == 0 {
|
||||
t.setIndent(0)
|
||||
t.printf("no jobs to display")
|
||||
t.newline()
|
||||
termbox.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
v := t.report[k]
|
||||
|
||||
t.setIndent(0)
|
||||
|
||||
t.printf("Job: %s", k)
|
||||
@@ -322,6 +343,15 @@ func (t *tui) draw() {
|
||||
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 {
|
||||
@@ -334,6 +364,19 @@ func (t *tui) draw() {
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(snapStatus.Pruning)
|
||||
t.addIndent(-1)
|
||||
t.printf("Snapshotting:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(snapStatus.Snapshotting)
|
||||
t.addIndent(-1)
|
||||
} else if v.Type == job.TypeSource {
|
||||
|
||||
st := v.JobSpecific.(*job.PassiveStatus)
|
||||
t.printf("Snapshotting:\n")
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(st.Snapper)
|
||||
t.addIndent(-1)
|
||||
|
||||
} else {
|
||||
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||
t.newline()
|
||||
@@ -363,7 +406,7 @@ func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgress
|
||||
t.newline()
|
||||
}
|
||||
if !rep.WaitReconnectSince.IsZero() {
|
||||
delta := rep.WaitReconnectUntil.Sub(time.Now()).Round(time.Second)
|
||||
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
||||
var until string
|
||||
if rep.WaitReconnectUntil.IsZero() {
|
||||
@@ -386,11 +429,11 @@ func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgress
|
||||
} else {
|
||||
t.printf("Attempt #%d", len(rep.Attempts))
|
||||
if len(rep.Attempts) > 1 {
|
||||
t.printf(". Previous attempts failed with the follwing statuses:")
|
||||
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.printfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
||||
t.newline()
|
||||
}
|
||||
t.addIndent(-1)
|
||||
@@ -462,7 +505,7 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
*pruner.FSReport
|
||||
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 {
|
||||
all = append(all, commonFS{&r.Pending[i], false})
|
||||
}
|
||||
@@ -471,7 +514,8 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
}
|
||||
|
||||
switch state {
|
||||
case pruner.Plan: fallthrough
|
||||
case pruner.Plan:
|
||||
fallthrough
|
||||
case pruner.PlanErr:
|
||||
return
|
||||
}
|
||||
@@ -499,7 +543,7 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
t.write("[")
|
||||
t.write(times("=", progress))
|
||||
t.write(">")
|
||||
t.write(times("-", 80 - progress))
|
||||
t.write(times("-", 80-progress))
|
||||
t.write("]")
|
||||
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||
t.newline()
|
||||
@@ -519,9 +563,9 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
if fs.LastError != "" {
|
||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
||||
t.printf("ERROR:")
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||
} else {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||
}
|
||||
t.newline()
|
||||
continue
|
||||
@@ -531,7 +575,7 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
len(fs.DestroyList), len(fs.SnapshotList))
|
||||
|
||||
if fs.completed {
|
||||
t.printf( "Completed %s\n", pruneRuleActionStr)
|
||||
t.printf("Completed %s\n", pruneRuleActionStr)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -546,6 +590,86 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) renderSnapperReport(r *snapper.Report) {
|
||||
if r == nil {
|
||||
t.printf("<snapshot type does not have a report>\n")
|
||||
return
|
||||
}
|
||||
|
||||
t.printf("Status: %s", r.State)
|
||||
t.newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.printf("Error: %s\n", r.Error)
|
||||
}
|
||||
if !r.SleepUntil.IsZero() {
|
||||
t.printf("Sleep until: %s\n", r.SleepUntil)
|
||||
}
|
||||
|
||||
sort.Slice(r.Progress, func(i, j int) bool {
|
||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||
})
|
||||
|
||||
t.addIndent(1)
|
||||
defer t.addIndent(-1)
|
||||
dur := func(d time.Duration) string {
|
||||
return d.Round(100 * time.Millisecond).String()
|
||||
}
|
||||
|
||||
type row struct {
|
||||
path, state, duration, remainder, hookReport string
|
||||
}
|
||||
var widths struct {
|
||||
path, state, duration int
|
||||
}
|
||||
rows := make([]*row, len(r.Progress))
|
||||
for i, fs := range r.Progress {
|
||||
r := &row{
|
||||
path: fs.Path,
|
||||
state: fs.State.String(),
|
||||
}
|
||||
if fs.HooksHadError {
|
||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||
}
|
||||
switch fs.State {
|
||||
case snapper.SnapPending:
|
||||
r.duration = "..."
|
||||
r.remainder = ""
|
||||
case snapper.SnapStarted:
|
||||
r.duration = dur(time.Since(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
case snapper.SnapDone:
|
||||
fallthrough
|
||||
case snapper.SnapError:
|
||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
}
|
||||
rows[i] = r
|
||||
if len(r.path) > widths.path {
|
||||
widths.path = len(r.path)
|
||||
}
|
||||
if len(r.state) > widths.state {
|
||||
widths.state = len(r.state)
|
||||
}
|
||||
if len(r.duration) > widths.duration {
|
||||
widths.duration = len(r.duration)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
path := rightPad(r.path, widths.path, " ")
|
||||
state := rightPad(r.state, widths.state, " ")
|
||||
duration := rightPad(r.duration, widths.duration, " ")
|
||||
t.printf("%s %s %s", path, state, duration)
|
||||
t.printfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||
if r.hookReport != "" {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||
}
|
||||
t.newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func times(str string, n int) (out string) {
|
||||
for i := 0; i < n; i++ {
|
||||
out += str
|
||||
@@ -557,15 +681,7 @@ func rightPad(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + times(pad, length-len(str))
|
||||
}
|
||||
|
||||
|
||||
func leftPad(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[len(str)-length:]
|
||||
}
|
||||
return times(pad, length-len(str)) + str
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
|
||||
var arrowPositions = `>\|/`
|
||||
@@ -584,7 +700,7 @@ func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||
|
||||
t.write("[")
|
||||
t.write(times("=", completedLength))
|
||||
t.write( string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||
t.write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||
t.write(times("-", length-completedLength))
|
||||
t.write("]")
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"os"
|
||||
|
||||
"github.com/problame/go-netssh"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/problame/go-netssh"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"log"
|
||||
"path"
|
||||
)
|
||||
|
||||
+12
-10
@@ -2,15 +2,17 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
var TestCmd = &cli.Subcommand {
|
||||
var TestCmd = &cli.Subcommand{
|
||||
Use: "test",
|
||||
SetupSubcommands: func() []*cli.Subcommand {
|
||||
return []*cli.Subcommand{testFilter, testPlaceholder}
|
||||
@@ -18,13 +20,13 @@ var TestCmd = &cli.Subcommand {
|
||||
}
|
||||
|
||||
var testFilterArgs struct {
|
||||
job string
|
||||
all bool
|
||||
job string
|
||||
all bool
|
||||
input string
|
||||
}
|
||||
|
||||
var testFilter = &cli.Subcommand{
|
||||
Use: "filesystems --job JOB [--all | --input INPUT]",
|
||||
Use: "filesystems --job JOB [--all | --input INPUT]",
|
||||
Short: "test filesystems filter specified in push or source job",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.StringVar(&testFilterArgs.job, "job", "", "the name of the push or source job")
|
||||
@@ -51,8 +53,10 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
||||
return err
|
||||
}
|
||||
switch j := job.Ret.(type) {
|
||||
case *config.SourceJob: confFilter = j.Filesystems
|
||||
case *config.PushJob: confFilter = j.Filesystems
|
||||
case *config.SourceJob:
|
||||
confFilter = j.Filesystems
|
||||
case *config.PushJob:
|
||||
confFilter = j.Filesystems
|
||||
default:
|
||||
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
||||
}
|
||||
@@ -109,10 +113,8 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
||||
}
|
||||
|
||||
var testPlaceholderArgs struct {
|
||||
action string
|
||||
ds string
|
||||
plv string
|
||||
all bool
|
||||
ds string
|
||||
all bool
|
||||
}
|
||||
|
||||
var testPlaceholder = &cli.Subcommand{
|
||||
|
||||
+7
-5
@@ -2,23 +2,25 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/zrepl/zrepl/cli"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"github.com/zrepl/zrepl/version"
|
||||
"os"
|
||||
)
|
||||
|
||||
var versionArgs struct {
|
||||
Show string
|
||||
Config *config.Config
|
||||
Show string
|
||||
Config *config.Config
|
||||
ConfigErr error
|
||||
}
|
||||
|
||||
var VersionCmd = &cli.Subcommand{
|
||||
Use: "version",
|
||||
Short: "print version of zrepl binary and running daemon",
|
||||
Use: "version",
|
||||
Short: "print version of zrepl binary and running daemon",
|
||||
NoRequireConfig: true,
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)")
|
||||
|
||||
+142
-73
@@ -2,8 +2,6 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/yaml-config"
|
||||
"io/ioutil"
|
||||
"log/syslog"
|
||||
"os"
|
||||
@@ -11,6 +9,9 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/yaml-config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -34,11 +35,16 @@ type JobEnum struct {
|
||||
func (j JobEnum) Name() string {
|
||||
var name string
|
||||
switch v := j.Ret.(type) {
|
||||
case *SnapJob: name = v.Name
|
||||
case *PushJob: name = v.Name
|
||||
case *SinkJob: name = v.Name
|
||||
case *PullJob: name = v.Name
|
||||
case *SourceJob: name = v.Name
|
||||
case *SnapJob:
|
||||
name = v.Name
|
||||
case *PushJob:
|
||||
name = v.Name
|
||||
case *SinkJob:
|
||||
name = v.Name
|
||||
case *PullJob:
|
||||
name = v.Name
|
||||
case *SourceJob:
|
||||
name = v.Name
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown job type %T", v))
|
||||
}
|
||||
@@ -46,38 +52,38 @@ func (j JobEnum) Name() string {
|
||||
}
|
||||
|
||||
type ActiveJob struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Connect ConnectEnum `yaml:"connect"`
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Connect ConnectEnum `yaml:"connect"`
|
||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
}
|
||||
|
||||
type PassiveJob struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Serve ServeEnum `yaml:"serve"`
|
||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Serve ServeEnum `yaml:"serve"`
|
||||
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 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 PushJob struct {
|
||||
ActiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||
ActiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||
}
|
||||
|
||||
type PullJob struct {
|
||||
ActiveJob `yaml:",inline"`
|
||||
RootFS string `yaml:"root_fs"`
|
||||
RootFS string `yaml:"root_fs"`
|
||||
Interval PositiveDurationOrManual `yaml:"interval"`
|
||||
}
|
||||
|
||||
@@ -118,9 +124,9 @@ type SinkJob struct {
|
||||
}
|
||||
|
||||
type SourceJob struct {
|
||||
PassiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||
PassiveJob `yaml:",inline"`
|
||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||
}
|
||||
|
||||
type FilesystemsFilter map[string]bool
|
||||
@@ -130,9 +136,10 @@ type SnapshottingEnum struct {
|
||||
}
|
||||
|
||||
type SnapshottingPeriodic struct {
|
||||
Type string `yaml:"type"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Type string `yaml:"type"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Interval time.Duration `yaml:"interval,positive"`
|
||||
Hooks HookList `yaml:"hooks,optional"`
|
||||
}
|
||||
|
||||
type SnapshottingManual struct {
|
||||
@@ -191,18 +198,18 @@ type ConnectEnum struct {
|
||||
}
|
||||
|
||||
type ConnectCommon struct {
|
||||
Type string `yaml:"type"`
|
||||
Type string `yaml:"type"`
|
||||
}
|
||||
|
||||
type TCPConnect struct {
|
||||
ConnectCommon `yaml:",inline"`
|
||||
Address string `yaml:"address"`
|
||||
Address string `yaml:"address,hostport"`
|
||||
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
||||
}
|
||||
|
||||
type TLSConnect struct {
|
||||
ConnectCommon `yaml:",inline"`
|
||||
Address string `yaml:"address"`
|
||||
Address string `yaml:"address,hostport"`
|
||||
Ca string `yaml:"ca"`
|
||||
Cert string `yaml:"cert"`
|
||||
Key string `yaml:"key"`
|
||||
@@ -223,9 +230,10 @@ type SSHStdinserverConnect struct {
|
||||
}
|
||||
|
||||
type LocalConnect struct {
|
||||
ConnectCommon `yaml:",inline"`
|
||||
ListenerName string `yaml:"listener_name"`
|
||||
ClientIdentity string `yaml:"client_identity"`
|
||||
ConnectCommon `yaml:",inline"`
|
||||
ListenerName string `yaml:"listener_name"`
|
||||
ClientIdentity string `yaml:"client_identity"`
|
||||
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=2s"`
|
||||
}
|
||||
|
||||
type ServeEnum struct {
|
||||
@@ -233,18 +241,18 @@ type ServeEnum struct {
|
||||
}
|
||||
|
||||
type ServeCommon struct {
|
||||
Type string `yaml:"type"`
|
||||
Type string `yaml:"type"`
|
||||
}
|
||||
|
||||
type TCPServe struct {
|
||||
ServeCommon `yaml:",inline"`
|
||||
Listen string `yaml:"listen"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
Clients map[string]string `yaml:"clients"`
|
||||
}
|
||||
|
||||
type TLSServe struct {
|
||||
ServeCommon `yaml:",inline"`
|
||||
Listen string `yaml:"listen"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
Ca string `yaml:"ca"`
|
||||
Cert string `yaml:"cert"`
|
||||
Key string `yaml:"key"`
|
||||
@@ -253,12 +261,12 @@ type TLSServe struct {
|
||||
}
|
||||
|
||||
type StdinserverServer struct {
|
||||
ServeCommon `yaml:",inline"`
|
||||
ServeCommon `yaml:",inline"`
|
||||
ClientIdentities []string `yaml:"client_identities"`
|
||||
}
|
||||
|
||||
type LocalServe struct {
|
||||
ServeCommon `yaml:",inline"`
|
||||
ServeCommon `yaml:",inline"`
|
||||
ListenerName string `yaml:"listener_name"`
|
||||
}
|
||||
|
||||
@@ -267,8 +275,8 @@ type PruningEnum struct {
|
||||
}
|
||||
|
||||
type PruneKeepNotReplicated struct {
|
||||
Type string `yaml:"type"`
|
||||
KeepSnapshotAtCursor bool `yaml:"keep_snapshot_at_cursor,optional,default=true"`
|
||||
Type string `yaml:"type"`
|
||||
KeepSnapshotAtCursor bool `yaml:"keep_snapshot_at_cursor,optional,default=true"`
|
||||
}
|
||||
|
||||
type PruneKeepLastN struct {
|
||||
@@ -277,8 +285,8 @@ type PruneKeepLastN struct {
|
||||
}
|
||||
|
||||
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
||||
Type string `yaml:"type"`
|
||||
Regex string `yaml:"regex"`
|
||||
Type string `yaml:"type"`
|
||||
Regex string `yaml:"regex"`
|
||||
Negate bool `yaml:"negate,optional,default=false"`
|
||||
}
|
||||
|
||||
@@ -301,12 +309,12 @@ type StdoutLoggingOutlet struct {
|
||||
type SyslogLoggingOutlet struct {
|
||||
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 {
|
||||
LoggingOutletCommon `yaml:",inline"`
|
||||
Address string `yaml:"address"`
|
||||
Address string `yaml:"address,hostport"`
|
||||
Net string `yaml:"net,default=tcp"`
|
||||
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
||||
TLS *TCPLoggingOutletTLS `yaml:"tls,optional"`
|
||||
@@ -324,7 +332,7 @@ type MonitoringEnum struct {
|
||||
|
||||
type PrometheusMonitoring struct {
|
||||
Type string `yaml:"type"`
|
||||
Listen string `yaml:"listen"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
}
|
||||
|
||||
type SyslogFacility syslog.Priority
|
||||
@@ -355,6 +363,38 @@ type JobDebugSettings struct {
|
||||
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) {
|
||||
var in struct {
|
||||
Type string
|
||||
@@ -392,7 +432,7 @@ func (t *ConnectEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error)
|
||||
"tcp": &TCPConnect{},
|
||||
"tls": &TLSConnect{},
|
||||
"ssh+stdinserver": &SSHStdinserverConnect{},
|
||||
"local": &LocalConnect{},
|
||||
"local": &LocalConnect{},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -402,7 +442,7 @@ func (t *ServeEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||
"tcp": &TCPServe{},
|
||||
"tls": &TLSServe{},
|
||||
"stdinserver": &StdinserverServer{},
|
||||
"local" : &LocalServe{},
|
||||
"local": &LocalServe{},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -420,7 +460,7 @@ func (t *PruningEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error)
|
||||
func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||
"periodic": &SnapshottingPeriodic{},
|
||||
"manual": &SnapshottingManual{},
|
||||
"manual": &SnapshottingManual{},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -448,31 +488,60 @@ func (t *SyslogFacility) UnmarshalYAML(u func(interface{}, bool) error) (err 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
|
||||
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
|
||||
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{
|
||||
|
||||
@@ -2,11 +2,12 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/syslog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/yaml-config"
|
||||
"log/syslog"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testValidGlobalSection(t *testing.T, s string) *Config {
|
||||
@@ -24,7 +25,7 @@ jobs:
|
||||
`
|
||||
_, err := ParseConfigBytes([]byte(jobdef))
|
||||
require.NoError(t, err)
|
||||
return testValidConfig(t, s + jobdef)
|
||||
return testValidConfig(t, s+jobdef)
|
||||
}
|
||||
|
||||
func TestOutletTypes(t *testing.T) {
|
||||
@@ -71,7 +72,7 @@ global:
|
||||
- type: prometheus
|
||||
listen: ':9091'
|
||||
`)
|
||||
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) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -36,4 +37,4 @@ jobs:
|
||||
- type: last_n
|
||||
count: 1
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSnapshotting(t *testing.T) {
|
||||
@@ -37,7 +38,30 @@ jobs:
|
||||
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
|
||||
|
||||
t.Run("manual", func(t *testing.T) {
|
||||
@@ -51,7 +75,16 @@ jobs:
|
||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
||||
assert.Equal(t, "periodic", snp.Type)
|
||||
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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -39,6 +43,7 @@ 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 {
|
||||
@@ -64,3 +69,20 @@ func testConfig(t *testing.T, input string) (*Config, error) {
|
||||
t.Helper()
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
type RetentionIntervalList []RetentionInterval
|
||||
|
||||
type PruneGrid struct {
|
||||
Type string `yaml:"type"`
|
||||
Grid RetentionIntervalList `yaml:"grid"`
|
||||
Regex string `yaml:"regex"`
|
||||
Type string `yaml:"type"`
|
||||
Grid RetentionIntervalList `yaml:"grid"`
|
||||
Regex string `yaml:"regex"`
|
||||
}
|
||||
|
||||
type RetentionInterval struct {
|
||||
@@ -31,10 +31,6 @@ func (i *RetentionInterval) KeepCount() int {
|
||||
|
||||
const RetentionGridKeepCountAll int = -1
|
||||
|
||||
type RetentionGrid struct {
|
||||
intervals []RetentionInterval
|
||||
}
|
||||
|
||||
func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||
var in string
|
||||
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
|
||||
+59
-25
@@ -12,9 +12,11 @@ import (
|
||||
|
||||
"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/util/envconst"
|
||||
"github.com/zrepl/zrepl/version"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -43,24 +45,24 @@ func (j *controlJob) Status() *job.Status { return &job.Status{Type: job.TypeInt
|
||||
func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||
|
||||
var promControl struct {
|
||||
requestBegin *prometheus.CounterVec
|
||||
requestBegin *prometheus.CounterVec
|
||||
requestFinished *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
|
||||
promControl.requestBegin = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "control",
|
||||
Name: "request_begin",
|
||||
Help: "number of request we started to handle",
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "control",
|
||||
Name: "request_begin",
|
||||
Help: "number of request we started to handle",
|
||||
}, []string{"endpoint"})
|
||||
|
||||
promControl.requestFinished = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "control",
|
||||
Name: "request_finished",
|
||||
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},
|
||||
Namespace: "zrepl",
|
||||
Subsystem: "control",
|
||||
Name: "request_finished",
|
||||
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},
|
||||
}, []string{"endpoint"})
|
||||
registerer.MustRegister(promControl.requestBegin)
|
||||
registerer.MustRegister(promControl.requestFinished)
|
||||
@@ -85,10 +87,16 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
}
|
||||
|
||||
pprofServer := NewPProfServer(ctx)
|
||||
if listen := envconst.String("ZREPL_DAEMON_AUTOSTART_PPROF_SERVER", ""); listen != "" {
|
||||
pprofServer.Control(PprofServerControlMsg{
|
||||
Run: true,
|
||||
HttpListenAddress: listen,
|
||||
})
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
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
|
||||
err := decoder(&msg)
|
||||
if err != nil {
|
||||
@@ -99,22 +107,22 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
}}})
|
||||
|
||||
mux.Handle(ControlJobEndpointVersion,
|
||||
requestLogger{log: log, handler: jsonResponder{func() (interface{}, error) {
|
||||
requestLogger{log: log, handler: jsonResponder{log, func() (interface{}, error) {
|
||||
return version.NewZreplVersionInformation(), nil
|
||||
}}})
|
||||
|
||||
mux.Handle(ControlJobEndpointStatus,
|
||||
// don't log requests to status endpoint, too spammy
|
||||
jsonResponder{func() (interface{}, error) {
|
||||
jsonResponder{log, func() (interface{}, error) {
|
||||
s := j.jobs.status()
|
||||
return s, nil
|
||||
}})
|
||||
|
||||
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 {
|
||||
Name string
|
||||
Op string
|
||||
Op string
|
||||
}
|
||||
var req reqT
|
||||
if decoder(&req) != nil {
|
||||
@@ -136,8 +144,8 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
server := http.Server{
|
||||
Handler: mux,
|
||||
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
||||
WriteTimeout: 1*time.Second,
|
||||
ReadTimeout: 1*time.Second,
|
||||
WriteTimeout: 1 * time.Second,
|
||||
ReadTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
outer:
|
||||
@@ -152,7 +160,10 @@ outer:
|
||||
select {
|
||||
case <-ctx.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
|
||||
case err = <-served:
|
||||
if err != nil {
|
||||
@@ -166,33 +177,50 @@ outer:
|
||||
}
|
||||
|
||||
type jsonResponder struct {
|
||||
log Logger
|
||||
producer func() (interface{}, error)
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
j.log.WithError(err).Error("control handler error")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
io.WriteString(w, err.Error())
|
||||
_, err = io.WriteString(w, err.Error())
|
||||
logIoErr(err)
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = json.NewEncoder(&buf).Encode(res)
|
||||
if err != nil {
|
||||
j.log.WithError(err).Error("control handler json marshal error")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
io.WriteString(w, err.Error())
|
||||
_, err = io.WriteString(w, err.Error())
|
||||
} else {
|
||||
io.Copy(w, &buf)
|
||||
_, err = io.Copy(w, &buf)
|
||||
}
|
||||
logIoErr(err)
|
||||
}
|
||||
|
||||
type jsonDecoder = func(interface{}) error
|
||||
|
||||
type jsonRequestResponder struct {
|
||||
log Logger
|
||||
producer func(decoder jsonDecoder) (interface{}, error)
|
||||
}
|
||||
|
||||
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
|
||||
decoder := func(i interface{}) error {
|
||||
err := json.NewDecoder(r.Body).Decode(&i)
|
||||
@@ -204,22 +232,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 decodeError != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
io.WriteString(w, decodeError.Error())
|
||||
_, err := io.WriteString(w, decodeError.Error())
|
||||
logIoErr(err)
|
||||
return
|
||||
}
|
||||
if producerErr != nil {
|
||||
j.log.WithError(producerErr).Error("control handler error")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
io.WriteString(w, producerErr.Error())
|
||||
_, err := io.WriteString(w, producerErr.Error())
|
||||
logIoErr(err)
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
encodeErr := json.NewEncoder(&buf).Encode(res)
|
||||
if encodeErr != nil {
|
||||
j.log.WithError(producerErr).Error("control handler json marhsal error")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
io.WriteString(w, encodeErr.Error())
|
||||
_, err := io.WriteString(w, encodeErr.Error())
|
||||
logIoErr(err)
|
||||
} else {
|
||||
io.Copy(w, &buf)
|
||||
_, err := io.Copy(w, &buf)
|
||||
logIoErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-12
@@ -3,8 +3,16 @@ package daemon
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||
@@ -12,12 +20,6 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/version"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrapf(err,"cannot build monitorin gjob #%d", i)
|
||||
return errors.Wrapf(err, "cannot build monitorin gjob #%d", i)
|
||||
}
|
||||
jobs.start(ctx, job, true)
|
||||
}
|
||||
|
||||
|
||||
log.Info("starting daemon")
|
||||
|
||||
// start regular jobs
|
||||
@@ -103,7 +104,7 @@ type jobs struct {
|
||||
// m protects all fields below it
|
||||
m sync.RWMutex
|
||||
wakeups map[string]wakeup.Func // by Job.Name
|
||||
resets map[string]reset.Func // by Job.Name
|
||||
resets map[string]reset.Func // by Job.Name
|
||||
jobs map[string]job.Job
|
||||
}
|
||||
|
||||
@@ -116,9 +117,7 @@ func newJobs() *jobs {
|
||||
}
|
||||
|
||||
const (
|
||||
logJobField string = "job"
|
||||
logTaskField string = "task"
|
||||
logSubsysField string = "subsystem"
|
||||
logJobField string = "job"
|
||||
)
|
||||
|
||||
func (s *jobs) wait() <-chan struct{} {
|
||||
|
||||
@@ -2,10 +2,12 @@ package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DatasetMapFilter struct {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
import "testing"
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"strings"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type AnyFSVFilter struct{}
|
||||
@@ -17,7 +18,6 @@ func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err err
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
type PrefixFilter struct {
|
||||
prefix string
|
||||
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))
|
||||
+12
-2
@@ -68,8 +68,7 @@ type activeSideTasks struct {
|
||||
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
||||
a.tasksMtx.Lock()
|
||||
defer a.tasksMtx.Unlock()
|
||||
var copy activeSideTasks
|
||||
copy = a.tasks
|
||||
copy := a.tasks
|
||||
if u == nil {
|
||||
return copy
|
||||
}
|
||||
@@ -84,6 +83,7 @@ type activeMode interface {
|
||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||
Type() Type
|
||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
||||
SnapperReport() *snapper.Report
|
||||
ResetConnectBackoff()
|
||||
}
|
||||
|
||||
@@ -125,6 +125,10 @@ func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
||||
m.snapper.Run(ctx, wakeUpCommon)
|
||||
}
|
||||
|
||||
func (m *modePush) SnapperReport() *snapper.Report {
|
||||
return m.snapper.Report()
|
||||
}
|
||||
|
||||
func (m *modePush) ResetConnectBackoff() {
|
||||
m.setupMtx.Lock()
|
||||
defer m.setupMtx.Unlock()
|
||||
@@ -207,6 +211,10 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modePull) SnapperReport() *snapper.Report {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *modePull) ResetConnectBackoff() {
|
||||
m.setupMtx.Lock()
|
||||
defer m.setupMtx.Unlock()
|
||||
@@ -280,6 +288,7 @@ func (j *ActiveSide) Name() string { return j.name }
|
||||
type ActiveSideStatus struct {
|
||||
Replication *report.Report
|
||||
PruningSender, PruningReceiver *pruner.Report
|
||||
Snapshotting *snapper.Report
|
||||
}
|
||||
|
||||
func (j *ActiveSide) Status() *Status {
|
||||
@@ -296,6 +305,7 @@ func (j *ActiveSide) Status() *Status {
|
||||
if tasks.prunerReceiver != nil {
|
||||
s.PruningReceiver = tasks.prunerReceiver.Report()
|
||||
}
|
||||
s.Snapshotting = j.mode.SnapperReport()
|
||||
return &Status{Type: t, JobSpecific: s}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Code generated by "enumer -type=ActiveSideState"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package job
|
||||
|
||||
import (
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
)
|
||||
|
||||
@@ -24,13 +25,13 @@ func JobsFromConfig(c *config.Config) ([]Job, error) {
|
||||
|
||||
// receiving-side root filesystems must not overlap
|
||||
{
|
||||
rfss := make([]string, len(js))
|
||||
for i, j := range js {
|
||||
rfss := make([]string, 0, len(js))
|
||||
for _, j := range js {
|
||||
jrfs, ok := j.OwnedDatasetSubtreeRoot()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rfss[i] = jrfs.ToString()
|
||||
rfss = append(rfss, jrfs.ToString())
|
||||
}
|
||||
if err := validateReceivingSidesDoNotOverlap(rfss); err != nil {
|
||||
return nil, err
|
||||
|
||||
+16
-11
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -29,7 +30,6 @@ func WithLogger(ctx context.Context, l Logger) context.Context {
|
||||
return context.WithValue(ctx, contextKeyLog, l)
|
||||
}
|
||||
|
||||
|
||||
type Job interface {
|
||||
Name() string
|
||||
Run(ctx context.Context)
|
||||
@@ -44,15 +44,15 @@ type Type string
|
||||
|
||||
const (
|
||||
TypeInternal Type = "internal"
|
||||
TypeSnap Type = "snap"
|
||||
TypePush Type = "push"
|
||||
TypeSink Type = "sink"
|
||||
TypePull Type = "pull"
|
||||
TypeSource Type = "source"
|
||||
TypeSnap Type = "snap"
|
||||
TypePush Type = "push"
|
||||
TypeSink Type = "sink"
|
||||
TypePull Type = "pull"
|
||||
TypeSource Type = "source"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Type Type
|
||||
Type Type
|
||||
JobSpecific interface{}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ func (s *Status) MarshalJSON() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := map[string]json.RawMessage {
|
||||
"type": typeJson,
|
||||
m := map[string]json.RawMessage{
|
||||
"type": typeJson,
|
||||
string(s.Type): jobJSON,
|
||||
}
|
||||
return json.Marshal(m)
|
||||
@@ -94,16 +94,21 @@ func (s *Status) UnmarshalJSON(in []byte) (err error) {
|
||||
var st SnapJobStatus
|
||||
err = json.Unmarshal(jobJSON, &st)
|
||||
s.JobSpecific = &st
|
||||
case TypePull: fallthrough
|
||||
|
||||
case TypePull:
|
||||
fallthrough
|
||||
case TypePush:
|
||||
var st ActiveSideStatus
|
||||
err = json.Unmarshal(jobJSON, &st)
|
||||
s.JobSpecific = &st
|
||||
case TypeSource: fallthrough
|
||||
|
||||
case TypeSource:
|
||||
fallthrough
|
||||
case TypeSink:
|
||||
var st PassiveStatus
|
||||
err = json.Unmarshal(jobJSON, &st)
|
||||
s.JobSpecific = &st
|
||||
|
||||
case TypeInternal:
|
||||
// internal jobs do not report specifics
|
||||
default:
|
||||
|
||||
+14
-3
@@ -27,6 +27,7 @@ type PassiveSide struct {
|
||||
type passiveMode interface {
|
||||
Handler() rpc.Handler
|
||||
RunPeriodic(ctx context.Context)
|
||||
SnapperReport() *snapper.Report // may be nil
|
||||
Type() Type
|
||||
}
|
||||
|
||||
@@ -40,7 +41,8 @@ func (m *modeSink) Handler() rpc.Handler {
|
||||
return endpoint.NewReceiver(m.rootDataset, true)
|
||||
}
|
||||
|
||||
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) {
|
||||
m = &modeSink{}
|
||||
@@ -85,6 +87,10 @@ func (m *modeSource) RunPeriodic(ctx context.Context) {
|
||||
m.snapper.Run(ctx, nil)
|
||||
}
|
||||
|
||||
func (m *modeSource) SnapperReport() *snapper.Report {
|
||||
return m.snapper.Report()
|
||||
}
|
||||
|
||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) {
|
||||
|
||||
s = &PassiveSide{mode: mode, name: in.Name}
|
||||
@@ -97,10 +103,15 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passive
|
||||
|
||||
func (j *PassiveSide) Name() string { return j.name }
|
||||
|
||||
type PassiveStatus struct{}
|
||||
type PassiveStatus struct {
|
||||
Snapper *snapper.Report
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -65,7 +66,8 @@ func (j *SnapJob) RegisterMetrics(registerer prometheus.Registerer) {
|
||||
}
|
||||
|
||||
type SnapJobStatus struct {
|
||||
Pruning *pruner.Report
|
||||
Pruning *pruner.Report
|
||||
Snapshotting *snapper.Report // may be nil
|
||||
}
|
||||
|
||||
func (j *SnapJob) Status() *Status {
|
||||
@@ -74,6 +76,7 @@ func (j *SnapJob) Status() *Status {
|
||||
if j.pruner != nil {
|
||||
s.Pruning = j.pruner.Report()
|
||||
}
|
||||
s.Snapshotting = j.snapper.Report()
|
||||
return &Status{Type: t, JobSpecific: s}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
@@ -37,7 +38,7 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
|
||||
var syslogOutlets, stdoutOutlets int
|
||||
for lei, le := range in {
|
||||
|
||||
outlet, minLevel, err := parseOutlet(le)
|
||||
outlet, minLevel, err := ParseOutlet(le)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
||||
}
|
||||
@@ -72,6 +73,7 @@ const (
|
||||
SubsyEndpoint Subsystem = "endpoint"
|
||||
SubsysPruning Subsystem = "pruning"
|
||||
SubsysSnapshot Subsystem = "snapshot"
|
||||
SubsysHooks Subsystem = "hook"
|
||||
SubsysTransport Subsystem = "transport"
|
||||
SubsysTransportMux Subsystem = "transportmux"
|
||||
SubsysRPC Subsystem = "rpc"
|
||||
@@ -85,6 +87,7 @@ func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Contex
|
||||
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsyEndpoint))
|
||||
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
|
||||
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
||||
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,
|
||||
@@ -123,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) {
|
||||
if common.Level == "" || common.Format == "" {
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-logfmt/logfmt"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -86,7 +88,7 @@ func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
|
||||
prefixFields := []string{JobField, SubsysField}
|
||||
prefixed := make(map[string]bool, len(prefixFields)+2)
|
||||
for _, field := range prefixFields {
|
||||
val, ok := e.Fields[field].(string)
|
||||
val, ok := e.Fields[field]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@@ -159,10 +161,16 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
|
||||
enc := logfmt.NewEncoder(&buf)
|
||||
|
||||
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 {
|
||||
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
|
||||
@@ -179,8 +187,10 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
|
||||
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 {
|
||||
if !prefixed[k] {
|
||||
if err := logfmtTryEncodeKeyval(enc, k, v); err != nil {
|
||||
@@ -199,7 +209,10 @@ func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error
|
||||
case nil: // ok
|
||||
return nil
|
||||
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 errors.Wrapf(err, "cannot encode field '%s'", field)
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"io"
|
||||
"log/syslog"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
)
|
||||
|
||||
type EntryFormatter interface {
|
||||
@@ -28,7 +30,10 @@ func (h WriterOutlet) WriteEntry(entry logger.Entry) error {
|
||||
return err
|
||||
}
|
||||
_, err = h.writer.Write(bytes)
|
||||
h.writer.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = h.writer.Write([]byte("\n"))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -92,8 +97,10 @@ func (h *TCPOutlet) outLoop(retryInterval time.Duration) {
|
||||
conn = nil
|
||||
}
|
||||
}
|
||||
conn.SetWriteDeadline(time.Now().Add(retryInterval))
|
||||
_, err = io.Copy(conn, msg)
|
||||
err = conn.SetWriteDeadline(time.Now().Add(retryInterval))
|
||||
if err == nil {
|
||||
_, err = io.Copy(conn, msg)
|
||||
}
|
||||
if err != nil {
|
||||
retry = time.Now().Add(retryInterval)
|
||||
conn.Close()
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
var DaemonCmd = &cli.Subcommand {
|
||||
var DaemonCmd = &cli.Subcommand{
|
||||
Use: "daemon",
|
||||
Short: "run the zrepl daemon",
|
||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package nethelpers
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func PreparePrivateSockpath(sockpath string) error {
|
||||
|
||||
+10
-2
@@ -7,11 +7,12 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
)
|
||||
|
||||
type pprofServer struct {
|
||||
cc chan PprofServerControlMsg
|
||||
state PprofServerControlMsg
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
@@ -63,7 +64,14 @@ outer:
|
||||
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
|
||||
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,17 @@ package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"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/logger"
|
||||
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type prometheusJob struct {
|
||||
@@ -25,7 +27,7 @@ func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob
|
||||
}
|
||||
|
||||
var prom struct {
|
||||
taskLogEntries *prometheus.CounterVec
|
||||
taskLogEntries *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -63,17 +65,15 @@ func (j *prometheusJob) Run(ctx context.Context) {
|
||||
log.WithError(err).Error("cannot listen")
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
l.Close()
|
||||
}
|
||||
<-ctx.Done()
|
||||
l.Close()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
err = http.Serve(l, mux)
|
||||
if err != nil {
|
||||
if err != nil && ctx.Err() == nil {
|
||||
log.WithError(err).Error("error while serving")
|
||||
}
|
||||
|
||||
@@ -93,4 +93,3 @@ func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
|
||||
prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+38
-50
@@ -3,17 +3,19 @@ package pruner
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/pruning"
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
||||
@@ -53,7 +55,7 @@ type args struct {
|
||||
rules []pruning.KeepRule
|
||||
retryWait time.Duration
|
||||
considerSnapAtCursorReplicated bool
|
||||
promPruneSecs prometheus.Observer
|
||||
promPruneSecs prometheus.Observer
|
||||
}
|
||||
|
||||
type Pruner struct {
|
||||
@@ -64,7 +66,7 @@ type Pruner struct {
|
||||
state State
|
||||
|
||||
// State PlanErr
|
||||
err error
|
||||
err error
|
||||
|
||||
// State Exec
|
||||
execQueue *execQueue
|
||||
@@ -75,7 +77,7 @@ type PrunerFactory struct {
|
||||
receiverRules []pruning.KeepRule
|
||||
retryWait time.Duration
|
||||
considerSnapAtCursorReplicated bool
|
||||
promPruneSecs *prometheus.HistogramVec
|
||||
promPruneSecs *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
type LocalPrunerFactory struct {
|
||||
@@ -84,19 +86,6 @@ type LocalPrunerFactory struct {
|
||||
promPruneSecs *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
func checkContainsKeep1(rules []pruning.KeepRule) error {
|
||||
if len(rules) == 0 {
|
||||
return nil //No keep rules means keep all - ok
|
||||
}
|
||||
for _, e := range rules {
|
||||
switch e.(type) {
|
||||
case *pruning.KeepLastN:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("sender keep rules must contain last_n or be empty so that the last snapshot is definitely kept")
|
||||
}
|
||||
|
||||
func NewLocalPrunerFactory(in config.PruningLocal, promPruneSecs *prometheus.HistogramVec) (*LocalPrunerFactory, error) {
|
||||
rules, err := pruning.RulesFromConfig(in.Keep)
|
||||
if err != nil {
|
||||
@@ -137,11 +126,11 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
||||
considerSnapAtCursorReplicated = considerSnapAtCursorReplicated || !knr.KeepSnapshotAtCursor
|
||||
}
|
||||
f := &PrunerFactory{
|
||||
senderRules: keepRulesSender,
|
||||
receiverRules: keepRulesReceiver,
|
||||
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10 * time.Second),
|
||||
senderRules: keepRulesSender,
|
||||
receiverRules: keepRulesReceiver,
|
||||
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
|
||||
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
|
||||
promPruneSecs: promPruneSecs,
|
||||
promPruneSecs: promPruneSecs,
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
@@ -213,17 +202,17 @@ func (p *Pruner) Prune() {
|
||||
|
||||
func (p *Pruner) prune(args args) {
|
||||
u := func(f func(*Pruner)) {
|
||||
p.mtx.Lock()
|
||||
defer p.mtx.Unlock()
|
||||
f(p)
|
||||
}
|
||||
p.mtx.Lock()
|
||||
defer p.mtx.Unlock()
|
||||
f(p)
|
||||
}
|
||||
// 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 {
|
||||
State string
|
||||
@@ -239,9 +228,9 @@ type FSReport struct {
|
||||
}
|
||||
|
||||
type SnapshotReport struct {
|
||||
Name string
|
||||
Name string
|
||||
Replicated bool
|
||||
Date time.Time
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
func (p *Pruner) Report() *Report {
|
||||
@@ -250,9 +239,9 @@ func (p *Pruner) Report() *Report {
|
||||
|
||||
r := Report{State: p.state.String()}
|
||||
|
||||
if p.err != nil {
|
||||
r.Error = p.err.Error()
|
||||
}
|
||||
if p.err != nil {
|
||||
r.Error = p.err.Error()
|
||||
}
|
||||
|
||||
if p.execQueue != nil {
|
||||
r.Pending, r.Completed = p.execQueue.Report()
|
||||
@@ -268,7 +257,7 @@ func (p *Pruner) State() State {
|
||||
}
|
||||
|
||||
type fs struct {
|
||||
path string
|
||||
path string
|
||||
|
||||
// permanent error during planning
|
||||
planErr error
|
||||
@@ -316,7 +305,7 @@ func (f *fs) Report() FSReport {
|
||||
|
||||
if f.planErr != nil {
|
||||
r.LastError = f.planErr.Error()
|
||||
} else if f.execErrLast != nil {
|
||||
} else if f.execErrLast != nil {
|
||||
r.LastError = f.execErrLast.Error()
|
||||
}
|
||||
|
||||
@@ -326,7 +315,7 @@ func (f *fs) Report() FSReport {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -490,9 +479,9 @@ tfss_loop:
|
||||
})
|
||||
|
||||
for {
|
||||
var pfs *fs
|
||||
var pfs *fs
|
||||
u(func(pruner *Pruner) {
|
||||
pfs = pruner.execQueue.Pop()
|
||||
pfs = pruner.execQueue.Pop()
|
||||
})
|
||||
if pfs == nil {
|
||||
break
|
||||
@@ -516,16 +505,15 @@ tfss_loop:
|
||||
hadErr := false
|
||||
for _, fsr := range rep.Completed {
|
||||
hadErr = hadErr || fsr.SkipReason.NotSkipped() && fsr.LastError != ""
|
||||
}
|
||||
}
|
||||
if hadErr {
|
||||
p.state = ExecErr
|
||||
} else {
|
||||
p.state = Done
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// attempts to exec pfs, puts it back into the queue with the result
|
||||
func doOneAttemptExec(a *args, u updater, pfs *fs) {
|
||||
@@ -558,20 +546,20 @@ func doOneAttemptExec(a *args, u updater, pfs *fs) {
|
||||
err = nil
|
||||
destroyFails := make([]*pdu.DestroySnapshotRes, 0)
|
||||
for _, reqDestroy := range destroyList {
|
||||
res, ok := destroyResults[reqDestroy.Name]
|
||||
if !ok {
|
||||
err = fmt.Errorf("missing destroy-result for %s", reqDestroy.RelName())
|
||||
break
|
||||
} else if res.Error != "" {
|
||||
destroyFails = append(destroyFails, res)
|
||||
}
|
||||
res, ok := destroyResults[reqDestroy.Name]
|
||||
if !ok {
|
||||
err = fmt.Errorf("missing destroy-result for %s", reqDestroy.RelName())
|
||||
break
|
||||
} else if res.Error != "" {
|
||||
destroyFails = append(destroyFails, res)
|
||||
}
|
||||
}
|
||||
if err == nil && len(destroyFails) > 0 {
|
||||
names := make([]string, len(destroyFails))
|
||||
pairs := make([]string, len(destroyFails))
|
||||
allSame := true
|
||||
lastMsg := destroyFails[0].Error
|
||||
for i := 0; i < len(destroyFails); i++{
|
||||
for i := 0; i < len(destroyFails); i++ {
|
||||
allSame = allSame && destroyFails[i].Error == lastMsg
|
||||
relname := destroyFails[i].Snapshot.RelName()
|
||||
names[i] = relname
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
)
|
||||
|
||||
type execQueue struct {
|
||||
mtx sync.Mutex
|
||||
mtx sync.Mutex
|
||||
pending, completed []*fs
|
||||
}
|
||||
|
||||
func newExecQueue(cap int) *execQueue {
|
||||
q := execQueue{
|
||||
pending: make([]*fs, 0, cap),
|
||||
pending: make([]*fs, 0, cap),
|
||||
completed: make([]*fs, 0, cap),
|
||||
}
|
||||
return &q
|
||||
@@ -55,7 +55,7 @@ func (q *execQueue) Pop() *fs {
|
||||
return fs
|
||||
}
|
||||
|
||||
func(q *execQueue) Put(fs *fs, err error, done bool) {
|
||||
func (q *execQueue) Put(fs *fs, err error, done bool) {
|
||||
fs.mtx.Lock()
|
||||
fs.execErrLast = err
|
||||
if done || err != nil {
|
||||
@@ -79,5 +79,4 @@ func(q *execQueue) Put(fs *fs, err error, done bool) {
|
||||
})
|
||||
q.mtx.Unlock()
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Code generated by "enumer -type=State"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package pruner
|
||||
|
||||
import (
|
||||
|
||||
+123
-47
@@ -1,18 +1,20 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
"context"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"fmt"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"sort"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"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/zfs"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=SnapState
|
||||
type SnapState uint
|
||||
@@ -24,18 +26,20 @@ const (
|
||||
SnapError
|
||||
)
|
||||
|
||||
// All fields protected by Snapper.mtx
|
||||
type snapProgress struct {
|
||||
state SnapState
|
||||
|
||||
// SnapStarted, SnapDone, SnapError
|
||||
name string
|
||||
startAt time.Time
|
||||
name string
|
||||
startAt time.Time
|
||||
hookPlan *hooks.Plan
|
||||
|
||||
// SnapDone
|
||||
doneAt time.Time
|
||||
|
||||
// SnapErr
|
||||
err error
|
||||
// SnapErr TODO disambiguate state
|
||||
runResults hooks.PlanReport
|
||||
}
|
||||
|
||||
type args struct {
|
||||
@@ -44,20 +48,22 @@ type args struct {
|
||||
prefix string
|
||||
interval time.Duration
|
||||
fsf *filters.DatasetMapFilter
|
||||
snapshotsTaken chan<-struct{}
|
||||
snapshotsTaken chan<- struct{}
|
||||
hooks *hooks.List
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
type Snapper struct {
|
||||
args args
|
||||
|
||||
mtx sync.Mutex
|
||||
mtx sync.Mutex
|
||||
state State
|
||||
|
||||
// set in state Plan, used in Waiting
|
||||
lastInvocation time.Time
|
||||
|
||||
// valid for state Snapshotting
|
||||
plan map[*zfs.DatasetPath]snapProgress
|
||||
plan map[*zfs.DatasetPath]*snapProgress
|
||||
|
||||
// valid for state SyncUp and Waiting
|
||||
sleepUntil time.Time
|
||||
@@ -70,7 +76,7 @@ type Snapper struct {
|
||||
type State uint
|
||||
|
||||
const (
|
||||
SyncUp State = 1<<iota
|
||||
SyncUp State = 1 << iota
|
||||
SyncUpErrWait
|
||||
Planning
|
||||
Snapshotting
|
||||
@@ -81,13 +87,13 @@ const (
|
||||
|
||||
func (s State) sf() state {
|
||||
m := map[State]state{
|
||||
SyncUp: syncUp,
|
||||
SyncUp: syncUp,
|
||||
SyncUpErrWait: wait,
|
||||
Planning: plan,
|
||||
Snapshotting: snapshot,
|
||||
Waiting: wait,
|
||||
ErrorWait: wait,
|
||||
Stopped: nil,
|
||||
Planning: plan,
|
||||
Snapshotting: snapshot,
|
||||
Waiting: wait,
|
||||
ErrorWait: wait,
|
||||
Stopped: nil,
|
||||
}
|
||||
return m[s]
|
||||
}
|
||||
@@ -122,10 +128,16 @@ func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *con
|
||||
return nil, errors.New("interval must be positive")
|
||||
}
|
||||
|
||||
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "hook config error")
|
||||
}
|
||||
|
||||
args := args{
|
||||
prefix: in.Prefix,
|
||||
prefix: in.Prefix,
|
||||
interval: in.Interval,
|
||||
fsf: fsf,
|
||||
fsf: fsf,
|
||||
hooks: hookList,
|
||||
// ctx and log is set in Run()
|
||||
}
|
||||
|
||||
@@ -140,6 +152,7 @@ func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||
s.args.snapshotsTaken = snapshotsTaken
|
||||
s.args.ctx = ctx
|
||||
s.args.log = getLogger(ctx)
|
||||
s.args.dryRun = false // for future expansion
|
||||
|
||||
u := func(u func(*Snapper)) State {
|
||||
s.mtx.Lock()
|
||||
@@ -199,10 +212,10 @@ func syncUp(a args, u updater) state {
|
||||
if err != nil {
|
||||
return onErr(err, u)
|
||||
}
|
||||
u(func(s *Snapper){
|
||||
u(func(s *Snapper) {
|
||||
s.sleepUntil = syncPoint
|
||||
})
|
||||
t := time.NewTimer(syncPoint.Sub(time.Now()))
|
||||
t := time.NewTimer(time.Until(syncPoint))
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-t.C:
|
||||
@@ -223,24 +236,30 @@ func plan(a args, u updater) state {
|
||||
return onErr(err, u)
|
||||
}
|
||||
|
||||
plan := make(map[*zfs.DatasetPath]snapProgress, len(fss))
|
||||
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
||||
for _, fs := range fss {
|
||||
plan[fs] = snapProgress{state: SnapPending}
|
||||
plan[fs] = &snapProgress{state: SnapPending}
|
||||
}
|
||||
return u(func(s *Snapper) {
|
||||
s.state = Snapshotting
|
||||
s.plan = plan
|
||||
s.err = nil
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func snapshot(a args, u updater) state {
|
||||
|
||||
var plan map[*zfs.DatasetPath]snapProgress
|
||||
var plan map[*zfs.DatasetPath]*snapProgress
|
||||
u(func(snapper *Snapper) {
|
||||
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?
|
||||
for fs, progress := range plan {
|
||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||
@@ -250,27 +269,71 @@ func snapshot(a args, u updater) state {
|
||||
WithField("fs", fs.ToString()).
|
||||
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) {
|
||||
progress.name = snapname
|
||||
progress.startAt = time.Now()
|
||||
progress.hookPlan = plan
|
||||
progress.state = SnapStarted
|
||||
})
|
||||
|
||||
l.Debug("create snapshot")
|
||||
err := zfs.ZFSSnapshot(fs, snapname, false)
|
||||
if err != nil {
|
||||
hadErr = true
|
||||
l.WithError(err).Error("cannot create snapshot")
|
||||
}
|
||||
doneAt := time.Now()
|
||||
|
||||
u(func(snapper *Snapper) {
|
||||
progress.doneAt = doneAt
|
||||
progress.state = SnapDone
|
||||
if err != nil {
|
||||
progress.state = SnapError
|
||||
progress.err = err
|
||||
{
|
||||
l := hooks.GetLogger(a.ctx).WithField("fs", fs.ToString()).WithField("snap", snapname)
|
||||
l.WithField("report", plan.Report().String()).Debug("begin run job plan")
|
||||
plan.Run(hooks.WithLogger(a.ctx, l), a.dryRun)
|
||||
planReport = plan.Report()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
updateFSState:
|
||||
anyFsHadErr = anyFsHadErr || fsHadErr
|
||||
u(func(snapper *Snapper) {
|
||||
progress.doneAt = time.Now()
|
||||
progress.state = SnapDone
|
||||
if fsHadErr {
|
||||
progress.state = SnapError
|
||||
}
|
||||
progress.runResults = planReport
|
||||
})
|
||||
}
|
||||
|
||||
@@ -282,12 +345,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) {
|
||||
if hadErr {
|
||||
if anyFsHadErr {
|
||||
snapper.state = ErrorWait
|
||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
||||
} else {
|
||||
snapper.state = Waiting
|
||||
snapper.err = nil
|
||||
}
|
||||
}).sf()
|
||||
}
|
||||
@@ -306,7 +383,7 @@ func wait(a args, u updater) state {
|
||||
logFunc("enter wait-state after error")
|
||||
})
|
||||
|
||||
t := time.NewTimer(sleepUntil.Sub(time.Now()))
|
||||
t := time.NewTimer(time.Until(sleepUntil))
|
||||
defer t.Stop()
|
||||
|
||||
select {
|
||||
@@ -386,4 +463,3 @@ func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval t
|
||||
return snaptimes[0].time, nil
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package snapper
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
)
|
||||
@@ -17,12 +18,20 @@ type PeriodicOrManual struct {
|
||||
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 {
|
||||
s.s.Run(ctx, wakeUpCommon)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns nil if manual
|
||||
func (s *PeriodicOrManual) Report() *Report {
|
||||
if s.s != nil {
|
||||
return s.s.Report()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.SnapshottingPeriodic:
|
||||
|
||||
@@ -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"
|
||||
|
||||
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 (
|
||||
_SnapState_name_0 = "SnapPendingSnapStarted"
|
||||
_SnapState_name_1 = "SnapDone"
|
||||
|
||||
@@ -4,6 +4,19 @@ package snapper
|
||||
|
||||
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 (
|
||||
_State_name_0 = "SyncUpSyncUpErrWait"
|
||||
_State_name_1 = "Planning"
|
||||
|
||||
+44
-1
@@ -1,3 +1,5 @@
|
||||
.. include:: global.rst.inc
|
||||
|
||||
.. |break_config| replace:: **[CONFIG]**
|
||||
.. |break| replace:: **[BREAK]**
|
||||
.. |bugfix| replace:: [BUG]
|
||||
@@ -25,6 +27,46 @@ We use the following annotations for classifying changes:
|
||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||
* |docs| Change to the documentation.
|
||||
|
||||
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|
|
||||
| For 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
|
||||
---
|
||||
|
||||
@@ -42,7 +84,7 @@ It breaks both configuration and transport format, and thus requires manual inte
|
||||
Notes to Package Maintainers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Notify users about migrations (see changes attributed with |mig| below)
|
||||
* 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.
|
||||
This behavior is independent from the ``stdout`` outlet type.
|
||||
Please make sure the stderr output of the daemon is captured somewhere.
|
||||
@@ -51,6 +93,7 @@ Notes to Package Maintainers
|
||||
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.
|
||||
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
|
||||
~~~~~~~
|
||||
|
||||
@@ -7,10 +7,11 @@ Configuration
|
||||
|
||||
.. toctree::
|
||||
|
||||
configuration/preface
|
||||
configuration/overview
|
||||
configuration/jobs
|
||||
configuration/transports
|
||||
configuration/filter_syntax
|
||||
configuration/snapshotting
|
||||
configuration/prune
|
||||
configuration/logging
|
||||
configuration/monitoring
|
||||
|
||||
+2
-215
@@ -1,222 +1,9 @@
|
||||
.. 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 Types & Replication
|
||||
=======================
|
||||
|
||||
.. _job-overview:
|
||||
|
||||
Overview & Terminology
|
||||
----------------------
|
||||
|
||||
A *job* is the unit of activity tracked by the zrepl daemon.
|
||||
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>` | |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
||||
| | | | | required |
|
||||
| | | | * Workaround for :ref:`source-side pruning <prune-workaround-source-side-pruning>` |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
|
||||
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 maps requests from different client identities to their respective sub-filesystem tree ``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.
|
||||
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||
* 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.
|
||||
Note that there is only one cursor bookmark per filesystem, which prohibits multiple jobs to replicate the same filesystem (:ref:`see below<jobs-multiple-jobs>`).
|
||||
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
|
||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||
|
||||
.. 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``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
|
||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||
|
||||
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 this zrepl job 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`.
|
||||
* Handling snapshotting through a separate ``snap`` job.
|
||||
|
||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: manual
|
||||
...
|
||||
|
||||
.. _jobs-multiple-jobs:
|
||||
|
||||
Multiple Jobs & More than 2 Machines
|
||||
------------------------------------
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss:
|
||||
|
||||
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine.
|
||||
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
|
||||
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
|
||||
|
||||
Explanations & exceptions to above rules are detailed below.
|
||||
|
||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||
|
||||
No Overlapping
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Jobs run independently of each other.
|
||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
|
||||
More subtle race conditions can occur with the :ref:`replication cursor bookmark <replication-cursor-bookmark>`, which currently only exists once per filesystem.
|
||||
|
||||
N push jobs to 1 sink
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
||||
It is thus safe to push to one sink job with different client identities.
|
||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
||||
|
||||
N pull jobs from 1 source
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||
|
||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
||||
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
Job Types in Detail
|
||||
===================
|
||||
|
||||
.. _job-push:
|
||||
|
||||
|
||||
@@ -6,16 +6,16 @@ Miscellaneous
|
||||
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
|
||||
* the ``control`` CLI subcommand connects to a defined control socket
|
||||
* a ``control`` socket that the CLI commands use to interact with the daemon
|
||||
* 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.
|
||||
Therefore we have to make sure they can only be created and accessed by ``zrepl daemon``.
|
||||
In fact, ``zrepl daemon`` will not bind a socket to a path in a directory that is world-accessible.
|
||||
There is no authentication on these sockets except the UNIX permissions.
|
||||
The zrepl daemon will refuse to bind any of the above sockets 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
|
||||
|
||||
|
||||
::
|
||||
|
||||
mkdir -p /var/run/zrepl/stdinserver
|
||||
chmod -R 0700 /var/run/zrepl
|
||||
|
||||
|
||||
Durations & Intervals
|
||||
---------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
|
||||
Overview & Terminology
|
||||
======================
|
||||
|
||||
All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup.
|
||||
The following paths are considered:
|
||||
|
||||
* If set, the location specified via the global ``--config`` flag
|
||||
* ``/etc/zrepl/zrepl.yml``
|
||||
* ``/usr/local/etc/zrepl/zrepl.yml``
|
||||
|
||||
The ``zrepl configcheck`` subcommand can be used to validate the configuration.
|
||||
The command will output nothing and exit with zero status code if the configuration is valid.
|
||||
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
||||
Full example configs such as in the :ref:`tutorial` or the :sampleconf:`/` directory might also be helpful.
|
||||
However, copy-pasting examples is no substitute for reading documentation!
|
||||
|
||||
Config File Structure
|
||||
---------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
global: ...
|
||||
jobs:
|
||||
- name: backup
|
||||
type: push
|
||||
- ...
|
||||
|
||||
zrepl is confgured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
||||
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
||||
The ``jobs`` section is a list of jobs which we are goind to explain now.
|
||||
|
||||
.. _job-overview:
|
||||
|
||||
Jobs \& How They Work Together
|
||||
------------------------------
|
||||
|
||||
A *job* is the unit of activity tracked by the zrepl daemon.
|
||||
The ``type`` of a job determines its role in a replication setup and in snapshot management.
|
||||
Jobs are identified by their ``name``, both in log files and the ``zrepl status`` command.
|
||||
|
||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
||||
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
||||
The passive side responds to requests from the active side after checking its persmissions.
|
||||
|
||||
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
|
||||
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
||||
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Setup name | active side | passive side | use case |
|
||||
+=======================+==============+==================================+====================================================================================+
|
||||
| Push mode | ``push`` | ``sink`` | * Laptop backup |
|
||||
| | (snap) | | * NAS behind NAT to offsite |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
||||
| | | (snap) | * Remote server to NAS behind NAT |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool |
|
||||
| | | with :ref:`local transport <transport-local>` | |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
||||
| | (snap) | | | required |
|
||||
| | | | * Workaround for :ref:`source-side pruning <prune-workaround-source-side-pruning>` |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
|
||||
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 (see below).
|
||||
* 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 maps requests from different client identities to their respective sub-filesystem tree ``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.
|
||||
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||
* 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.
|
||||
Note that there is only one cursor bookmark per filesystem, which prohibits multiple jobs to replicate the same filesystem (:ref:`see below<jobs-multiple-jobs>`).
|
||||
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
|
||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||
|
||||
.. 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`.
|
||||
|
||||
|
||||
.. _jobs-multiple-jobs:
|
||||
|
||||
Multiple Jobs & More than 2 Machines
|
||||
------------------------------------
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss:
|
||||
|
||||
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine.
|
||||
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
|
||||
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
|
||||
|
||||
Explanations & exceptions to above rules are detailed below.
|
||||
|
||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||
|
||||
No Overlapping
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Jobs run independently of each other.
|
||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
|
||||
More subtle race conditions can occur with the :ref:`replication cursor bookmark <replication-cursor-bookmark>`, which currently only exists once per filesystem.
|
||||
|
||||
N push jobs to 1 sink
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
||||
It is thus safe to push to one sink job with different client identities.
|
||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
||||
|
||||
N pull jobs from 1 source
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||
|
||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
||||
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
.. _configuration_preface:
|
||||
|
||||
=======
|
||||
Preface
|
||||
=======
|
||||
|
||||
-----------------------
|
||||
Configuration File Path
|
||||
-----------------------
|
||||
|
||||
zrepl searches for its main configuration file in the following locations (in that order):
|
||||
|
||||
* If set, the location specified via the global ``--config`` flag
|
||||
* ``/etc/zrepl/zrepl.yml``
|
||||
* ``/usr/local/etc/zrepl/zrepl.yml``
|
||||
|
||||
The examples in the :ref:`tutorial` or the :sampleconf:`/` directory should provide a good starting point.
|
||||
|
||||
-------------------
|
||||
Runtime Directories
|
||||
-------------------
|
||||
|
||||
zrepl requires runtime directories for various UNIX sockets --- they are documented in the :ref:`config file<conf-runtime-directories>`.
|
||||
Your package maintainer / init script should take care of creating them.
|
||||
Alternatively, for default settings, the following should to the trick.
|
||||
|
||||
::
|
||||
|
||||
mkdir -p /var/run/zrepl/stdinserver
|
||||
chmod -R 0700 /var/run/zrepl
|
||||
|
||||
|
||||
----------
|
||||
Validating
|
||||
----------
|
||||
|
||||
The config can be validated using the ``zrepl configcheck`` subcommand.
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
.. _job-snapshotting-spec:
|
||||
|
||||
Taking Snaphots
|
||||
===============
|
||||
|
||||
The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
|
||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||
|
||||
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
|
||||
hooks: ...
|
||||
...
|
||||
|
||||
There is also a ``manual`` snapshotting type, which covers the following use cases:
|
||||
|
||||
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
||||
* Handling snapshotting through a separate ``snap`` job.
|
||||
|
||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: manual
|
||||
...
|
||||
|
||||
.. _job-snapshotting-hooks:
|
||||
|
||||
Pre- and Post-Snapshot Hooks
|
||||
----------------------------
|
||||
|
||||
Jobs with `periodic snapshots <job-snapshotting-spec_>`_ can run hooks before and/or after taking the snapshot specified in ``snapshotting.hooks``:
|
||||
Hooks are called per filesystem before and after the snapshot is taken (pre- and post-edge).
|
||||
Pre-edge invocations are in configuration order, post-edge invocations in reverse order, i.e. like a stack.
|
||||
If a pre-snapshot invocation fails, ``err_is_fatal=true`` cuts off subsequent hooks, does not take a snapshot, and only invokes post-edges corresponding to previous successful pre-edges.
|
||||
``err_is_fatal=false`` logs the failed pre-edge invocation but does not affect subsequent hooks nor snapshotting itself.
|
||||
Post-edges are only invoked for hooks whose pre-edges ran without error.
|
||||
Note that hook failures for one filesystem never affect other filesystems.
|
||||
|
||||
The optional ``timeout`` parameter specifies a period after which zrepl will kill the hook process and report an error.
|
||||
The default is 30 seconds and may be specified in any units understood by `time.ParseDuration <https://golang.org/pkg/time/#ParseDuration>`_.
|
||||
|
||||
The optional ``filesystems`` filter which limits the filesystems the hook runs for. This uses the same |filter-spec| as jobs.
|
||||
|
||||
Most hook types take additional parameters, please refer to the respective subsections below.
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 10 70
|
||||
:header-rows: 1
|
||||
|
||||
* - Hook ``type``
|
||||
- Details
|
||||
- Description
|
||||
* - ``command``
|
||||
- :ref:`Details <job-hook-type-command>`
|
||||
- Arbitrary pre- and post snapshot scripts.
|
||||
* - ``postgres-checkpoint``
|
||||
- :ref:`Details <job-hook-type-postgres-checkpoint>`
|
||||
- Execute Postgres ``CHECKPOINT`` SQL command before snapshot.
|
||||
* - ``mysql-lock-tables``
|
||||
- :ref:`Details <job-hook-type-mysql-lock-tables>`
|
||||
- Flush and read-Lock MySQL tables while taking the snapshot.
|
||||
|
||||
.. _job-hook-type-command:
|
||||
|
||||
``command`` Hooks
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
hooks:
|
||||
- type: command
|
||||
path: /etc/zrepl/hooks/zrepl-notify.sh
|
||||
timeout: 30s
|
||||
err_is_fatal: false
|
||||
- type: command
|
||||
path: /etc/zrepl/hooks/special-snapshot.sh
|
||||
filesystems: {
|
||||
"tank/special": true
|
||||
}
|
||||
...
|
||||
|
||||
|
||||
The hook type ``command`` is the only currently supported hook type.
|
||||
Future versions of zrepl may support other hook types.
|
||||
The ``path`` to the hook executables must be absolute (e.g. ``/etc/zrepl/hooks/zrepl-notify.sh``).
|
||||
No arguments may be specified; create a wrapper script if zrepl must call an executable that requires arguments.
|
||||
zrepl will call the hook both before and after the snapshot, but with different values of the ``ZREPL_HOOKTYPE`` environment variable; see below.
|
||||
The process standard output is logged at level INFO. Standard error is logged at level WARN.
|
||||
|
||||
zrepl sets a number of environment variables for the hook processes:
|
||||
|
||||
* ``ZREPL_HOOKTYPE``: either "pre_snapshot" or "post_snapshot"
|
||||
* ``ZREPL_FS``: the ZFS filesystem name being snapshotted
|
||||
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
|
||||
* ``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
|
||||
|
||||
An empty template hook can be found in :sampleconf:`hooks/template.sh`.
|
||||
|
||||
.. _job-hook-type-postgres-checkpoint:
|
||||
|
||||
``postgres-checkpoint`` Hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Connects to a Postgres server and executes the ``CHECKPOINT`` statement pre-snapshot.
|
||||
Checkpointing applies the WAL contents to all data files and syncs the data files to disk.
|
||||
This is not required for a consistent database backup: it merely forward-pays the "cost" of WAL replay to the time of snapshotting instead of at restore.
|
||||
However, the Postgres manual recommends against checkpointing during normal operation.
|
||||
Further, the operation requires Postgres superuser privileges.
|
||||
zrepl users must decide on their own whether this hook is useful for them (it likely isn't).
|
||||
|
||||
.. ATTENTION::
|
||||
Note that WALs and Postgres data directory (with all database data files) must be on the same filesystem to guarantee a correct point-in-time backup with the ZFS snapshot.
|
||||
|
||||
DSN syntax documented here: `<https://godoc.org/github.com/lib/pq>`_
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE USER zrepl_checkpoint PASSWORD yourpasswordhere;
|
||||
ALTER ROLE zrepl_checkpoint SUPERUSER;
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- type: postgres-checkpoint
|
||||
dsn: "host=localhost port=5432 user=postgres password=yourpasswordhere sslmode=disable"
|
||||
filesystems: {
|
||||
"p1/postgres/data11": true
|
||||
}
|
||||
|
||||
.. _job-hook-type-mysql-lock-tables:
|
||||
|
||||
``mysql-lock-tables`` Hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Connects to MySQL and executes
|
||||
|
||||
* pre-snapshot ``FLUSH TABLES WITH READ LOCK`` to lock all tables in all databases in the MySQL server we connect to (`docs <https://dev.mysql.com/doc/refman/8.0/en/flush.html#flush-tables-with-read-lock>`_)
|
||||
* post-snapshot ``UNLOCK TABLES`` reverse above operation.
|
||||
|
||||
Above procedure is documented in the `MySQL manual <https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html>`_
|
||||
as a means to produce a consistent backup of a MySQL DBMS installation (i.e., all databases).
|
||||
|
||||
`DSN syntax <https://github.com/go-sql-driver/mysql#dsn-data-source-name>`_: ``[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]``
|
||||
|
||||
.. ATTENTION::
|
||||
All MySQL databases must be on the same ZFS filesystem to guarantee a consistent point-in-time backup with the ZFS snapshot.
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE USER zrepl_lock_tables IDENTIFIED BY 'yourpasswordhere';
|
||||
GRANT RELOAD ON *.* TO zrepl_lock_tables;
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- type: mysql-lock-tables
|
||||
dsn: "zrepl_lock_tables:yourpasswordhere@tcp(localhost)/"
|
||||
filesystems: {
|
||||
"tank/mysql": true
|
||||
}
|
||||
@@ -345,5 +345,6 @@ The ``client_identity`` is used by the sink as documented above.
|
||||
type: local
|
||||
listener_name: localsink
|
||||
client_identity: local_backup
|
||||
dial_timeout: 2s # optional, 0 for no timeout
|
||||
...
|
||||
|
||||
|
||||
@@ -2,3 +2,25 @@
|
||||
|
||||
..
|
||||
.. |macroname| replace:: macro expansion
|
||||
|
||||
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
||||
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
||||
.. |Language: Go| image:: https://img.shields.io/badge/language-Go-6ad7e5.svg
|
||||
:target: https://golang.org/
|
||||
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
||||
:target: https://zrepl.github.io
|
||||
.. |Donate via PayPal| image:: https://img.shields.io/badge/donate-paypal-yellow.svg
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/receives/zrepl.svg?logo=liberapay
|
||||
:target: https://liberapay.com/zrepl/donate
|
||||
.. |Donate via Patreon| image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow
|
||||
:target: https://www.patreon.com/zrepl
|
||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||
|
||||
|
||||
.. |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>`
|
||||
+31
-19
@@ -5,21 +5,8 @@
|
||||
|
||||
.. include:: global.rst.inc
|
||||
|
||||
|GitHub license| |Language: Go| |User Docs| |Donate via PayPal| |Donate
|
||||
via Liberapay| |Twitter|
|
||||
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
|
||||
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
||||
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
||||
.. |Language: Go| image:: https://img.shields.io/badge/language-Go-6ad7e5.svg
|
||||
:target: https://golang.org/
|
||||
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
||||
:target: https://zrepl.github.io
|
||||
.. |Donate via PayPal| image:: https://img.shields.io/badge/donate-paypal-yellow.svg
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/badge/donate-liberapay-yellow.svg
|
||||
:target: https://liberapay.com/zrepl/donate
|
||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||
|
||||
zrepl - ZFS replication
|
||||
-----------------------
|
||||
@@ -28,12 +15,32 @@ zrepl - ZFS replication
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<div style="margin-bottom: 1em; background: #2e3436; min-height: 6em; max-width: 100%">
|
||||
<a href="https://raw.githubusercontent.com/wiki/zrepl/zrepl/zrepl_0.1_status.mp4" target="_new" >
|
||||
<video title="zrepl status subcommand" loop autoplay style="width: 100%; display: block;" src="https://raw.githubusercontent.com/wiki/zrepl/zrepl/zrepl_0.1_status.mp4"></video>
|
||||
</a>
|
||||
<div style="margin-bottom: 1em; background: #2e3436; color: white; font-size: 0.8em; min-height: 6em; max-width: 100%; overflow: auto;">
|
||||
<pre>
|
||||
Job: prod_to_backups
|
||||
Type: push
|
||||
Replication:
|
||||
Attempt #1
|
||||
Status: fan-out-filesystems
|
||||
Progress: [=========================\----] 246.7 MiB / 264.7 MiB @ 11.5 MiB/s
|
||||
zroot STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||
zroot/ROOT DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||
zroot/ROOT/default STEPPING (step 1/2, 123.4 MiB/129.3 MiB) next: @a => @b
|
||||
zroot/tmp STEPPING (step 1/2, 29.9 KiB/44.2 KiB) next: @a => @b
|
||||
zroot/usr STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||
zroot/usr/home STEPPING (step 1/2, 123.3 MiB/135.3 MiB) next: @a => @b
|
||||
zroot/var STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||
zroot/var/audit DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||
zroot/var/crash DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||
zroot/var/log STEPPING (step 1/2, 22.0 KiB/29.2 KiB) next: @a => @b
|
||||
zroot/var/mail STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||
Pruning Sender:
|
||||
...
|
||||
Pruning Receiver:
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
Getting started
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -56,7 +63,8 @@ Main Features
|
||||
|
||||
* **Automatic snapshot management**
|
||||
|
||||
* [x] Periodic filesystem snapshots
|
||||
* [x] Periodic :ref:`filesystem snapshots <job-snapshotting-spec>`
|
||||
* [x] Support for :ref:`pre- and post-snapshot hooks <job-snapshotting-hooks>` with builtins for MySQL & Postgres
|
||||
* [x] Flexible :ref:`pruning rule system <prune>`
|
||||
|
||||
* [x] Age-based fading (grandfathering scheme)
|
||||
@@ -89,6 +97,7 @@ Contributing
|
||||
|
||||
We are happy about any help we can get!
|
||||
|
||||
* :ref:`Financial Support <supporters>`
|
||||
* Explore the codebase
|
||||
|
||||
* These docs live in the ``docs/`` subdirectory
|
||||
@@ -104,6 +113,8 @@ We are happy about any help we can get!
|
||||
Make sure to read the `Developer Documentation section <https://github.com/zrepl/zrepl>`_ and open new issues or pull requests there.
|
||||
|
||||
|
||||
|
||||
|
||||
Table of Contents
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -119,4 +130,5 @@ Table of Contents
|
||||
pr
|
||||
changelog
|
||||
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||
supporters
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ The following list may be incomplete, feel free to submit a PR with an update:
|
||||
* - FreeBSD
|
||||
- ``pkg install zrepl``
|
||||
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
||||
* - MacOS
|
||||
- ``brew install zrepl``
|
||||
- Available on `homebrew <https://brew.sh>`_
|
||||
* - Others
|
||||
-
|
||||
- Use `binary releases`_ or build from source.
|
||||
@@ -41,7 +44,7 @@ The following list may be incomplete, feel free to submit a PR with an update:
|
||||
Compile From Source
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Producing a release requires **Go 1.9** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
||||
Python and pip3 should probably be installed via your distro's package manager.
|
||||
|
||||
@@ -55,9 +58,9 @@ and serves as a reference for build dependencies and procedure:
|
||||
cd zrepl
|
||||
sudo docker build -t zrepl_build -f build.Dockerfile .
|
||||
sudo docker run -it --rm \
|
||||
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
|
||||
-v "${PWD}:/src" \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
zrepl_build make vendordeps release
|
||||
zrepl_build make release
|
||||
|
||||
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
||||
|
||||
@@ -69,7 +72,7 @@ Alternatively, you can install build dependencies on your local system and then
|
||||
python3 -m venv3
|
||||
source venv3/bin/activate
|
||||
./lazy.sh devsetup
|
||||
make vendordeps release
|
||||
make release
|
||||
|
||||
The Python venv is used for the documentation build dependencies.
|
||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
||||
@@ -83,8 +86,7 @@ Either way, all build results are located in the ``artifacts/`` directory.
|
||||
What next?
|
||||
----------
|
||||
|
||||
Read the :ref:`configuration chapter<configuration_toc>` and optionally create the :ref:`runtime directories <configuration_preface>`.
|
||||
Afterwards, continue with the :ref:`usage chapter<usage>`.
|
||||
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||
|
||||
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
||||
|
||||
|
||||
+2
-2
@@ -54,9 +54,9 @@ echo "building site"
|
||||
set -e
|
||||
sphinx-versioning build \
|
||||
--show-banner \
|
||||
--whitelist-branches 'master' \
|
||||
--whitelist-branches '^master$' \
|
||||
--whitelist-tags '(v)?\d+\.\d+\.\d+$' \
|
||||
--whitelist-tags '0.1.0-rc(3|4)$' \
|
||||
--whitelist-tags '(v)?0.1.0-rc(3|4)$' \
|
||||
docs ./public_git \
|
||||
-- -c sphinxconf # older conf.py throw errors because they used
|
||||
# version = subprocess.show_output(["git", "describe"])
|
||||
|
||||
+27
-17
@@ -1,20 +1,30 @@
|
||||
alabaster==0.7.12
|
||||
Babel==2.6.0
|
||||
certifi==2018.10.15
|
||||
attrs==19.1.0
|
||||
Babel==2.7.0
|
||||
certifi==2019.6.16
|
||||
chardet==3.0.4
|
||||
docutils==0.14
|
||||
idna==2.7
|
||||
Click==7.0
|
||||
colorclass==2.2.0
|
||||
docutils==0.15.2
|
||||
idna==2.8
|
||||
imagesize==1.1.0
|
||||
Jinja2==2.10
|
||||
MarkupSafe==1.1.0
|
||||
packaging==18.0
|
||||
Pygments==2.2.0
|
||||
pyparsing==2.3.0
|
||||
pytz==2018.7
|
||||
requests==2.20.1
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
Sphinx==1.8.2
|
||||
sphinx-rtd-theme==0.4.2
|
||||
sphinxcontrib-websupport==1.1.0
|
||||
urllib3==1.24.1
|
||||
Jinja2==2.10.1
|
||||
MarkupSafe==1.1.1
|
||||
packaging==19.1
|
||||
Pygments==2.4.2
|
||||
pyparsing==2.4.2
|
||||
pytz==2019.2
|
||||
requests==2.22.0
|
||||
six==1.12.0
|
||||
snowballstemmer==1.9.1
|
||||
Sphinx==1.8.5
|
||||
sphinx-rtd-theme==0.4.3
|
||||
sphinxcontrib-applehelp==1.0.1
|
||||
sphinxcontrib-devhelp==1.0.1
|
||||
sphinxcontrib-htmlhelp==1.0.2
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==1.0.2
|
||||
sphinxcontrib-serializinghtml==1.1.3
|
||||
-e git://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
|
||||
sphinxcontrib-websupport==1.1.2
|
||||
urllib3==1.25.3
|
||||
|
||||
@@ -51,7 +51,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'zrepl'
|
||||
copyright = '2017-2018, Christian Schwarz'
|
||||
copyright = '2017-2019, Christian Schwarz'
|
||||
author = 'Christian Schwarz'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
.. include:: global.rst.inc
|
||||
|
||||
.. _supporters:
|
||||
|
||||
|Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
|
||||
zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
You can support maintenance and feature development through one of the services listed above.
|
||||
For commerical support, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
**Thanks for your support!**
|
||||
|
||||
.. NOTE ::
|
||||
|
||||
PayPal takes a relatively high fixed processing fee plus percentage of the donation.
|
||||
Larger less-frequent donations make more sense there.
|
||||
|
||||
Supporters
|
||||
==========
|
||||
|
||||
We would like to thank the following people / organizations for supporting zrepl through monetary and other means:
|
||||
|
||||
* `Ross Williams <https://github.com/overhacked>`_
|
||||
* Mike T.
|
||||
* `Justin Scholz <https://github.com/JMoVS>`_
|
||||
* `InsanePrawn <https://github.com/InsanePrawn>`_
|
||||
* `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
||||
* `Anton Schirg <https://github.com/antonxy>`_
|
||||
@@ -2,6 +2,7 @@ package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
)
|
||||
|
||||
|
||||
+59
-18
@@ -7,8 +7,11 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/util/semaphore"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
@@ -74,16 +77,29 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
||||
|
||||
}
|
||||
|
||||
var maxConcurrentZFSSendSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10))
|
||||
|
||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
||||
_, err := s.filterCheckFS(r.Filesystem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
getLogger(ctx).Debug("acquire concurrent send semaphore")
|
||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||
// => would require handling on the client-side
|
||||
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
|
||||
guard, err := maxConcurrentZFSSendSemaphore.Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer guard.Release()
|
||||
|
||||
si, err := zfs.ZFSSendDry(r.Filesystem, r.From, r.To, "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var expSize int64 = 0 // protocol says 0 means no estimate
|
||||
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
||||
expSize = si.SizeEstimate
|
||||
@@ -319,6 +335,8 @@ func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zf
|
||||
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
||||
}
|
||||
|
||||
var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10))
|
||||
|
||||
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
||||
getLogger(ctx).Debug("incoming Receive")
|
||||
defer receive.Close()
|
||||
@@ -362,6 +380,15 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
||||
}
|
||||
|
||||
if !ph.FSExists {
|
||||
if s.rootWithoutClientComponent.HasPrefix(v.Path) {
|
||||
if v.Path.Length() == 1 {
|
||||
visitErr = fmt.Errorf("pool %q not imported", v.Path.ToString())
|
||||
} else {
|
||||
visitErr = fmt.Errorf("root_fs %q does not exist", s.rootWithoutClientComponent.ToString())
|
||||
}
|
||||
getLogger(ctx).WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
||||
return false
|
||||
}
|
||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||
l.Debug("create placeholder filesystem")
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(v.Path)
|
||||
@@ -395,6 +422,16 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
||||
}
|
||||
}
|
||||
|
||||
getLogger(ctx).Debug("acquire concurrent recv semaphore")
|
||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||
// => would require handling on the client-side
|
||||
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
|
||||
guard, err := maxConcurrentZFSRecvSemaphore.Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer guard.Release()
|
||||
|
||||
getLogger(ctx).WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
|
||||
|
||||
if err := zfs.ZFSRecv(ctx, lp.ToString(), receive, recvOpts); err != nil {
|
||||
@@ -417,30 +454,34 @@ func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapsho
|
||||
}
|
||||
|
||||
func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.FilesystemVersion) (*pdu.DestroySnapshotsRes, error) {
|
||||
fsvs := make([]*zfs.FilesystemVersion, len(snaps))
|
||||
reqs := make([]*zfs.DestroySnapOp, len(snaps))
|
||||
ress := make([]*pdu.DestroySnapshotRes, len(snaps))
|
||||
errs := make([]error, len(snaps))
|
||||
for i, fsv := range snaps {
|
||||
if fsv.Type != pdu.FilesystemVersion_Snapshot {
|
||||
return nil, fmt.Errorf("version %q is not a snapshot", fsv.Name)
|
||||
}
|
||||
var err error
|
||||
fsvs[i], err = fsv.ZFSFilesystemVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
ress[i] = &pdu.DestroySnapshotRes{
|
||||
Snapshot: fsv,
|
||||
// Error set after batch operation
|
||||
}
|
||||
reqs[i] = &zfs.DestroySnapOp{
|
||||
Filesystem: lp.ToString(),
|
||||
Name: fsv.Name,
|
||||
ErrOut: &errs[i],
|
||||
}
|
||||
}
|
||||
res := &pdu.DestroySnapshotsRes{
|
||||
Results: make([]*pdu.DestroySnapshotRes, len(fsvs)),
|
||||
}
|
||||
for i, fsv := range fsvs {
|
||||
err := zfs.ZFSDestroyFilesystemVersion(lp, fsv)
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
res.Results[i] = &pdu.DestroySnapshotRes{
|
||||
Snapshot: pdu.FilesystemVersionFromZFS(fsv),
|
||||
Error: errMsg,
|
||||
zfs.ZFSDestroyFilesystemVersions(reqs)
|
||||
for i := range reqs {
|
||||
if errs[i] != nil {
|
||||
if de, ok := errs[i].(*zfs.DestroySnapshotsError); ok && len(de.Reason) == 1 {
|
||||
ress[i].Error = de.Reason[0]
|
||||
} else {
|
||||
ress[i].Error = errs[i].Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
return &pdu.DestroySnapshotsRes{
|
||||
Results: ress,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
module github.com/zrepl/zrepl
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
|
||||
github.com/alvaroloes/enumer v1.1.1
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff // indirect
|
||||
github.com/gdamore/tcell v1.2.0
|
||||
github.com/go-critic/go-critic v0.3.4 // indirect
|
||||
github.com/go-logfmt/logfmt v0.3.0
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
|
||||
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/uuid v1.1.1
|
||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
|
||||
github.com/kr/pretty v0.1.0
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/mattn/go-isatty v0.0.3
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/montanaflynn/stats v0.5.0
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pkg/profile v1.2.1
|
||||
github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5 // indirect
|
||||
github.com/prometheus/common v0.0.0-20180413074202-d0f7cd64bda4 // indirect
|
||||
github.com/prometheus/procfs v0.0.0-20180408092902-8b1c2da0d56d // indirect
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.4.0 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cobra v0.0.2
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.3.2 // indirect
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/theckman/goconstraint v1.11.0 // indirect
|
||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
|
||||
github.com/yudai/pp v2.0.1+incompatible // indirect
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 // indirect
|
||||
google.golang.org/grpc v1.17.0
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,358 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
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/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
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/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff h1:zk1wwii7uXmI0znwU+lqg+wFL9G5+vm5I+9rv2let60=
|
||||
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff/go.mod h1:yUhRXHewUVJ1k89wHKP68xfzk7kwXUx/DV1nx4EBMbw=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
|
||||
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
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-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4 h1:0suja/iKSDbEIYLbrS/8C7iArJiWpgCNcR+zwAHu7Ig=
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
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/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.1.1/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/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8 h1:+dKzeuiDYbD/Cfi/sEkkNrs6z4/WZ8ZIR8dAbUpjXbU=
|
||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
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/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
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/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
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/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.0 h1:YNOwxxSJzSUARoD9KRZLzM9Y858MNGCOACTvCW9TSAc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
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/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk=
|
||||
github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
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/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
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/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284 h1:GX8YFBfSL6GlKymQZOsQllPbxVamfEYpis7oaJLXYZA=
|
||||
github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284/go.mod h1:XMR9fjP5/wMyFK0Ifk9ZHDFem4Rukm+ChvoYr2nxrgw=
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b h1:ymnAeobXUW0sfmt5n8euMzW4vgdQvZNTagdYMcpK63g=
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5 h1:cLL6NowurKLMfCeQy4tIeph12XNQWgANCNvdyrOYKV4=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180413074202-d0f7cd64bda4 h1:rPmL0pCWefiqV7RjMYuBUrLbMJmNVjRKz82caELPTUQ=
|
||||
github.com/prometheus/common v0.0.0-20180413074202-d0f7cd64bda4/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180408092902-8b1c2da0d56d h1:RCcsxyRr6+/pLg6wr0cUjPovhEhSNOtPh0SOz6u3hGU=
|
||||
github.com/prometheus/procfs v0.0.0-20180408092902-8b1c2da0d56d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
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/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
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.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE=
|
||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
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/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0=
|
||||
github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g=
|
||||
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=
|
||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M=
|
||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190903123849-809f4603401a h1:l37btErUqI1yNmt5mf8kzglbeQfo32+TRacnxthiuck=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190903123849-809f4603401a/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f h1:3MuiGfgMHCSwKUcsuI7ODbi50j+evTB7SsoOBMNC5Fk=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20181106065722-10aee1819953/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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/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-20180828015842-6cd1fcedba52/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=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 h1:yvw+zsSmSM02Z5H3ZdEV7B7Ql7eFrjQTnmByJvK+3J8=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
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=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
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=
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
|
||||
if tty -s; then
|
||||
bold=$(tput bold)
|
||||
normal=$(tput sgr0)
|
||||
@@ -26,20 +27,16 @@ fi
|
||||
CHECKOUTPATH="${GOPATH}/src/github.com/zrepl/zrepl"
|
||||
|
||||
godep() {
|
||||
step "Install go dep using 'go get' to \$GOPATH/bin"
|
||||
go get -u github.com/golang/dep/cmd/dep
|
||||
if ! type dep ; then
|
||||
echo "Unable to install go dep" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
step "Fetching dependencies using 'dep ensure'"
|
||||
dep ensure -v -vendor-only
|
||||
step "go install build dependencies fetched using dep"
|
||||
# these will be in the vendor directory
|
||||
go build -o "$GOPATH/bin/stringer" ./vendor/golang.org/x/tools/cmd/stringer
|
||||
go build -o "$GOPATH/bin/protoc-gen-go" ./vendor/github.com/golang/protobuf/protoc-gen-go
|
||||
go build -o "$GOPATH/bin/enumer" ./vendor/github.com/alvaroloes/enumer
|
||||
if ! type stringer || ! type protoc-gen-go || ! type enumer ; then
|
||||
step "install build dependencies (versions pinned using go.mod and tools.go)"
|
||||
set -x
|
||||
export GO111MODULE=on # otherwise, a checkout of this repo in GOPATH will disable modules on Go 1.12 and earlier
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/stringer" golang.org/x/tools/cmd/stringer
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/protoc-gen-go" github.com/golang/protobuf/protoc-gen-go
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/enumer" github.com/alvaroloes/enumer
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/goimports" golang.org/x/tools/cmd/goimports
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/golangci-lint" github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
set +x
|
||||
if ! type stringer || ! type protoc-gen-go || ! type enumer || ! type goimports || ! type golangci-lint; then
|
||||
echo "Installed dependencies but can't find them in \$PATH, adjust it to contain \$GOPATH/bin" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Level int
|
||||
@@ -66,7 +67,7 @@ func (l Level) Short() string {
|
||||
case Error:
|
||||
return "ERRO"
|
||||
default:
|
||||
return fmt.Sprintf("%s", l)
|
||||
return l.String()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ func (l Level) String() string {
|
||||
case Error:
|
||||
return "error"
|
||||
default:
|
||||
return fmt.Sprintf("%s", string(l))
|
||||
return string(l)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -20,6 +20,7 @@ type Logger interface {
|
||||
WithField(field string, val interface{}) Logger
|
||||
WithFields(fields Fields) Logger
|
||||
WithError(err error) Logger
|
||||
Log(level Level, msg string)
|
||||
Debug(msg string)
|
||||
Info(msg string)
|
||||
Warn(msg string)
|
||||
@@ -66,7 +67,8 @@ func (l *loggerImpl) logInternalError(outlet Outlet, err string) {
|
||||
time.Now(),
|
||||
fields,
|
||||
}
|
||||
l.outlets.GetLoggerErrorOutlet().WriteEntry(entry)
|
||||
// ignore errors at this point (still better than panicking if the error is temporary)
|
||||
_ = l.outlets.GetLoggerErrorOutlet().WriteEntry(entry)
|
||||
}
|
||||
|
||||
func (l *loggerImpl) log(level Level, msg string) {
|
||||
@@ -157,6 +159,10 @@ func (l *loggerImpl) WithError(err error) Logger {
|
||||
return l.WithField(FieldError, val)
|
||||
}
|
||||
|
||||
func (l *loggerImpl) Log(level Level, msg string) {
|
||||
l.log(level, msg)
|
||||
}
|
||||
|
||||
func (l *loggerImpl) Debug(msg string) {
|
||||
l.log(Debug, msg)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package logger_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kr/pretty"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
)
|
||||
|
||||
type TestOutlet struct {
|
||||
|
||||
@@ -13,6 +13,7 @@ func (n nullLogger) ReplaceField(field string, val interface{}) Logger { return
|
||||
func (n nullLogger) WithField(field string, val interface{}) Logger { return n }
|
||||
func (n nullLogger) WithFields(fields Fields) Logger { return n }
|
||||
func (n nullLogger) WithError(err error) Logger { return n }
|
||||
func (nullLogger) Log(level Level, msg string) {}
|
||||
func (nullLogger) Debug(msg string) {}
|
||||
func (nullLogger) Info(msg string) {}
|
||||
func (nullLogger) Warn(msg string) {}
|
||||
|
||||
@@ -5,11 +5,7 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type stderrLogger struct {
|
||||
Logger
|
||||
}
|
||||
|
||||
type stderrLoggerOutlet struct {}
|
||||
type stderrLoggerOutlet struct{}
|
||||
|
||||
func (stderrLoggerOutlet) WriteEntry(entry Entry) error {
|
||||
fmt.Fprintf(os.Stderr, "%#v\n", entry)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/platformtest/tests"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var args struct {
|
||||
createArgs platformtest.ZpoolCreateArgs
|
||||
}
|
||||
flag.StringVar(&args.createArgs.PoolName, "poolname", "", "")
|
||||
flag.StringVar(&args.createArgs.ImagePath, "imagepath", "", "")
|
||||
flag.Int64Var(&args.createArgs.ImageSize, "imagesize", 100*(1<<20), "")
|
||||
flag.StringVar(&args.createArgs.Mountpoint, "mountpoint", "none", "")
|
||||
flag.Parse()
|
||||
|
||||
outlets := logger.NewOutlets()
|
||||
outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
|
||||
LoggingOutletCommon: config.LoggingOutletCommon{
|
||||
Level: "debug",
|
||||
Format: "human",
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
outlets.Add(outlet, level)
|
||||
logger := logger.NewLogger(outlets, 1*time.Second)
|
||||
|
||||
if err := args.createArgs.Validate(); err != nil {
|
||||
logger.Error(err.Error())
|
||||
panic(err)
|
||||
}
|
||||
ctx := platformtest.WithLogger(context.Background(), logger)
|
||||
ex := platformtest.NewEx(logger)
|
||||
|
||||
bold := color.New(color.Bold)
|
||||
for _, c := range tests.Cases {
|
||||
// ATTENTION future parallelism must pass c by value into closure!
|
||||
err := func() error {
|
||||
bold.Printf("BEGIN TEST CASE %s\n", c)
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.createArgs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create test pool")
|
||||
}
|
||||
defer func() {
|
||||
if err := pool.Destroy(ctx, ex); err != nil {
|
||||
fmt.Printf("error destroying test pool: %s", err)
|
||||
}
|
||||
}()
|
||||
ctx := &platformtest.Context{
|
||||
Context: ctx,
|
||||
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
||||
}
|
||||
c(ctx)
|
||||
bold.Printf("DONE TEST CASE %s\n", c)
|
||||
fmt.Println()
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
bold.Printf("TEST CASE FAILED WITH ERROR:\n")
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Idea taken from
|
||||
// https://github.com/openSUSE/umoci/blob/v0.2.1/cmd/umoci/main_test.go
|
||||
//
|
||||
/* How to generate coverage:
|
||||
go test -c -covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/...
|
||||
sudo ../logmockzfs/logzfsenv /tmp/zrepl_platform_test.log /usr/bin/zfs \
|
||||
./harness.test -test.coverprofile=/tmp/harness.out \
|
||||
-test.v __DEVEL--i-heard-you-like-tests \
|
||||
-imagepath /tmp/testpool.img -poolname zreplplatformtest
|
||||
go tool cover -html=/tmp/harness.out -o /tmp/harness.html
|
||||
*/
|
||||
// Merge with existing coverage reports using gocovmerge:
|
||||
// https://github.com/wadey/gocovmerge
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
var (
|
||||
args []string
|
||||
run bool
|
||||
)
|
||||
|
||||
for _, arg := range os.Args {
|
||||
switch {
|
||||
case arg == "__DEVEL--i-heard-you-like-tests":
|
||||
run = true
|
||||
case strings.HasPrefix(arg, "-test"):
|
||||
case strings.HasPrefix(arg, "__DEVEL"):
|
||||
default:
|
||||
args = append(args, arg)
|
||||
}
|
||||
}
|
||||
os.Args = args
|
||||
|
||||
if run {
|
||||
main()
|
||||
}
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ue
|
||||
export ZREPL_MOCK_ZFS_COMMAND_LOG="$1"
|
||||
shift
|
||||
export ZREPL_MOCK_ZFS_PATH="$1"
|
||||
shift
|
||||
export PATH="$(dirname "${BASH_SOURCE[0]}" )":"$PATH"
|
||||
args=("$@")
|
||||
exec "${args[@]}"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user