Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 348ecde574 |
+106
-346
@@ -1,79 +1,73 @@
|
|||||||
version: 2.1
|
version: 2.0
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build:
|
||||||
|
jobs:
|
||||||
|
- build-1.11
|
||||||
|
- build-1.12
|
||||||
|
- build-1.13
|
||||||
|
- build-latest
|
||||||
|
- test-build-in-docker
|
||||||
|
jobs:
|
||||||
|
|
||||||
commands:
|
# build-latest serves as the template
|
||||||
setup-home-local-bin:
|
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
|
||||||
steps:
|
build-latest: &build-latest
|
||||||
- run:
|
description: Builds zrepl
|
||||||
shell: /bin/bash -euo pipefail
|
|
||||||
command: |
|
|
||||||
mkdir -p "$HOME/.local/bin"
|
|
||||||
line='export PATH="$HOME/.local/bin:$PATH"'
|
|
||||||
if grep "$line" $BASH_ENV >/dev/null; then
|
|
||||||
echo "$line" >> $BASH_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
invoke-lazy-sh:
|
|
||||||
parameters:
|
parameters:
|
||||||
subcommand:
|
image:
|
||||||
|
description: "the docker image that the job should use"
|
||||||
type: string
|
type: string
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:latest
|
||||||
|
environment:
|
||||||
|
# required by lazy.sh
|
||||||
|
TERM: xterm
|
||||||
|
working_directory: /go/src/github.com/zrepl/zrepl
|
||||||
steps:
|
steps:
|
||||||
- run:
|
- run:
|
||||||
environment:
|
name: Setup environment variables
|
||||||
TERM: xterm
|
|
||||||
command: ./lazy.sh <<parameters.subcommand>>
|
|
||||||
|
|
||||||
apt-update-and-install-common-deps:
|
|
||||||
steps:
|
|
||||||
- run: sudo apt update && sudo apt install gawk make
|
|
||||||
|
|
||||||
restore-cache-gomod:
|
|
||||||
steps:
|
|
||||||
- restore_cache:
|
|
||||||
key: go-mod-v4-{{ checksum "go.sum" }}
|
|
||||||
save-cache-gomod:
|
|
||||||
steps:
|
|
||||||
- save_cache:
|
|
||||||
key: go-mod-v4-{{ checksum "go.sum" }}
|
|
||||||
paths:
|
|
||||||
- "/go/pkg/mod"
|
|
||||||
|
|
||||||
install-godep:
|
|
||||||
steps:
|
|
||||||
- apt-update-and-install-common-deps
|
|
||||||
- invoke-lazy-sh:
|
|
||||||
subcommand: godep
|
|
||||||
|
|
||||||
install-docdep:
|
|
||||||
steps:
|
|
||||||
- apt-update-and-install-common-deps
|
|
||||||
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
|
|
||||||
- invoke-lazy-sh:
|
|
||||||
subcommand: docdep
|
|
||||||
|
|
||||||
download-and-install-minio-client:
|
|
||||||
steps:
|
|
||||||
- setup-home-local-bin
|
|
||||||
- restore_cache:
|
|
||||||
key: minio-client-v2
|
|
||||||
- run:
|
|
||||||
shell: /bin/bash -eo pipefail
|
|
||||||
command: |
|
command: |
|
||||||
if which mc; then exit 0; fi
|
# used by pip (for docs)
|
||||||
sudo curl -sSL https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2020-08-20T00-23-01Z \
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
|
||||||
-o "$HOME/.local/bin/mc"
|
# we use modules
|
||||||
sudo chmod +x "$HOME/.local/bin/mc"
|
echo 'export GO111MODULE=on' >> $BASH_ENV
|
||||||
- save_cache:
|
|
||||||
key: minio-client-v2
|
- restore_cache:
|
||||||
paths:
|
keys:
|
||||||
- "$HOME/.local/bin/mc"
|
- source
|
||||||
|
- protobuf
|
||||||
|
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- save_cache:
|
||||||
|
key: source
|
||||||
|
paths:
|
||||||
|
- ".git"
|
||||||
|
|
||||||
|
# install deps
|
||||||
|
- run: wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
||||||
|
- run: echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
||||||
|
- run: sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
||||||
|
- save_cache:
|
||||||
|
key: protobuf
|
||||||
|
paths:
|
||||||
|
- "/usr/include/google/protobuf"
|
||||||
|
|
||||||
|
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev gawk
|
||||||
|
- run: ./lazy.sh devsetup
|
||||||
|
|
||||||
|
- run: make zrepl-bin
|
||||||
|
- run: make vet
|
||||||
|
- run: make test
|
||||||
|
- run: make lint
|
||||||
|
- run: make release
|
||||||
|
|
||||||
|
|
||||||
|
- store_artifacts:
|
||||||
|
path: ./artifacts/release
|
||||||
|
when: always
|
||||||
|
|
||||||
upload-minio:
|
|
||||||
parameters:
|
|
||||||
src:
|
|
||||||
type: string
|
|
||||||
dst:
|
|
||||||
type: string
|
|
||||||
steps:
|
|
||||||
- run:
|
- run:
|
||||||
shell: /bin/bash -eo pipefail
|
shell: /bin/bash -eo pipefail
|
||||||
when: always
|
when: always
|
||||||
@@ -84,296 +78,62 @@ commands:
|
|||||||
fi
|
fi
|
||||||
set -u # from now on
|
set -u # from now on
|
||||||
|
|
||||||
|
# Download and install minio
|
||||||
|
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
|
||||||
|
chmod +x ${GOPATH}/bin/mc
|
||||||
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||||
|
|
||||||
# keep in sync with set-github-minio-status
|
|
||||||
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
|
||||||
|
|
||||||
# Upload artifacts
|
# Upload artifacts
|
||||||
mkdir -p ./artifacts
|
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
|
||||||
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
|
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
|
||||||
|
|
||||||
set-github-minio-status:
|
|
||||||
parameters:
|
|
||||||
context:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
minio-dst:
|
|
||||||
type: string
|
|
||||||
steps:
|
|
||||||
- run:
|
|
||||||
shell: /bin/bash -eo pipefail
|
|
||||||
command: |
|
|
||||||
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
|
||||||
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
set -u # from now on
|
|
||||||
|
|
||||||
# keep in sync with with upload-minio command
|
|
||||||
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
|
||||||
# Push Artifact Link to GitHub
|
# Push Artifact Link to GitHub
|
||||||
REPO="zrepl/zrepl"
|
REPO="zrepl/zrepl"
|
||||||
COMMIT="${CIRCLE_SHA1}"
|
COMMIT="${CIRCLE_SHA1}"
|
||||||
JOB_NAME="${CIRCLE_JOB}"
|
JOB_NAME="${CIRCLE_JOB}"
|
||||||
CONTEXT="<<parameters.context>>"
|
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT?access_token=$GITHUB_COMMIT_STATUS_TOKEN" \
|
||||||
DESCRIPTION="<<parameters.description>>"
|
|
||||||
TARGETURL=https://minio.cschwarz.com/minio/"$jobprefix"/"<<parameters.minio-dst>>"
|
|
||||||
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \
|
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
|
|
||||||
-X POST \
|
-X POST \
|
||||||
-d '{"context":"'"$CONTEXT"'", "state": "success", "description":"'"$DESCRIPTION"'", "target_url":"'"$TARGETURL"'"}'
|
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
|
||||||
|
|
||||||
|
build-1.11:
|
||||||
trigger-pipeline:
|
<<: *build-latest
|
||||||
parameters:
|
|
||||||
body_no_shell_subst:
|
|
||||||
type: string
|
|
||||||
steps:
|
|
||||||
- run: |
|
|
||||||
curl -X POST https://circleci.com/api/v2/project/github/zrepl/zrepl/pipeline \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-H 'Accept: application/json' \
|
|
||||||
-H "Circle-Token: $ZREPL_BOT_CIRCLE_TOKEN" \
|
|
||||||
--data '<<parameters.body_no_shell_subst>>'
|
|
||||||
|
|
||||||
parameters:
|
|
||||||
do_ci:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
|
|
||||||
do_release:
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
release_docker_baseimage_tag:
|
|
||||||
type: string
|
|
||||||
default: "1.19"
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
ci:
|
|
||||||
when: << pipeline.parameters.do_ci >>
|
|
||||||
jobs:
|
|
||||||
- quickcheck-docs
|
|
||||||
- quickcheck-go: &quickcheck-go-smoketest
|
|
||||||
name: quickcheck-go-amd64-linux-1.19
|
|
||||||
goversion: &latest-go-release "1.19"
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
- test-go-on-latest-go-release:
|
|
||||||
goversion: *latest-go-release
|
|
||||||
- quickcheck-go:
|
|
||||||
requires:
|
|
||||||
- quickcheck-go-amd64-linux-1.19 #quickcheck-go-smoketest.name
|
|
||||||
matrix: &quickcheck-go-matrix
|
|
||||||
alias: quickcheck-go-matrix
|
|
||||||
parameters:
|
|
||||||
goversion: [*latest-go-release, "1.12"]
|
|
||||||
goos: ["linux", "freebsd"]
|
|
||||||
goarch: ["amd64", "arm64"]
|
|
||||||
exclude:
|
|
||||||
# don't re-do quickcheck-go-smoketest
|
|
||||||
- goversion: *latest-go-release
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
# not supported by Go 1.12
|
|
||||||
- goversion: "1.12"
|
|
||||||
goos: freebsd
|
|
||||||
goarch: arm64
|
|
||||||
|
|
||||||
release:
|
|
||||||
when: << pipeline.parameters.do_release >>
|
|
||||||
jobs:
|
|
||||||
- release-build
|
|
||||||
- release-deb:
|
|
||||||
requires:
|
|
||||||
- release-build
|
|
||||||
- release-rpm:
|
|
||||||
requires:
|
|
||||||
- release-build
|
|
||||||
- release-upload:
|
|
||||||
requires:
|
|
||||||
- release-build
|
|
||||||
- release-deb
|
|
||||||
- release-rpm
|
|
||||||
|
|
||||||
periodic:
|
|
||||||
triggers:
|
|
||||||
- schedule:
|
|
||||||
cron: "00 17 * * *"
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- stable
|
|
||||||
- problame/circleci-build
|
|
||||||
jobs:
|
|
||||||
- periodic-full-pipeline-run
|
|
||||||
|
|
||||||
zrepl.github.io:
|
|
||||||
jobs:
|
|
||||||
- publish-zrepl-github-io:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- stable
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
quickcheck-docs:
|
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/base:2020.08
|
- image: circleci/golang:1.11
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- install-docdep
|
|
||||||
- run: make docs
|
|
||||||
|
|
||||||
- download-and-install-minio-client
|
build-1.12:
|
||||||
- upload-minio:
|
<<: *build-latest
|
||||||
src: artifacts
|
|
||||||
dst: ""
|
|
||||||
- set-github-minio-status:
|
|
||||||
context: artifacts/${CIRCLE_JOB}
|
|
||||||
description: artifacts of CI job ${CIRCLE_JOB}
|
|
||||||
minio-dst: ""
|
|
||||||
|
|
||||||
quickcheck-go:
|
|
||||||
parameters:
|
|
||||||
goversion:
|
|
||||||
type: string
|
|
||||||
goos:
|
|
||||||
type: string
|
|
||||||
goarch:
|
|
||||||
type: string
|
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/go:<<parameters.goversion>>
|
- image: circleci/golang:1.12
|
||||||
|
|
||||||
|
build-1.13:
|
||||||
|
<<: *build-latest
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.13
|
||||||
|
|
||||||
|
# this job tries to mimic the build-in-docker instructions
|
||||||
|
# given in docs/installation.rst
|
||||||
|
#
|
||||||
|
# However, CircleCI doesn't support volume mounts, so we have to copy
|
||||||
|
# the source into the build-container by modifying the Dockerfile
|
||||||
|
test-build-in-docker:
|
||||||
|
description: Check that build-in-docker works
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:latest
|
||||||
environment:
|
environment:
|
||||||
GOOS: <<parameters.goos>>
|
working_directory: /go/src/github.com/zrepl/zrepl
|
||||||
GOARCH: <<parameters.goarch>>
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
- setup_remote_docker
|
||||||
- restore-cache-gomod
|
- run:
|
||||||
- run: go mod download
|
name: (hacky) circleci doesn't allow volume mounts, so copy src to container
|
||||||
- run: cd build && go mod download
|
command: echo "ADD . /src" >> build.Dockerfile
|
||||||
- save-cache-gomod
|
- run:
|
||||||
|
name: (hacky) commit modified Dockerfile to avoid failing git clean check in Makefile
|
||||||
- install-godep
|
command: git -c user.name='circleci' -c user.email='circleci@localhost' commit -m 'CIRCLECI modified Dockerfile with zrepl src' --author 'autoauthor <circleci@localhost>' -- build.Dockerfile
|
||||||
- run: make formatcheck
|
- run:
|
||||||
- run: make generate-platform-test-list
|
name: build the build image (build deps)
|
||||||
- run: make zrepl-bin test-platform-bin
|
command: docker build -t zrepl_build -f build.Dockerfile .
|
||||||
- run: make vet
|
- run:
|
||||||
- run: make lint
|
name: try compiling
|
||||||
|
command: docker run -it zrepl_build make release
|
||||||
- download-and-install-minio-client
|
|
||||||
- run: rm -f artifacts/generate-platform-test-list
|
|
||||||
- store_artifacts:
|
|
||||||
path: artifacts
|
|
||||||
- upload-minio:
|
|
||||||
src: artifacts
|
|
||||||
dst: ""
|
|
||||||
- set-github-minio-status:
|
|
||||||
context: artifacts/${CIRCLE_JOB}
|
|
||||||
description: artifacts of CI job ${CIRCLE_JOB}
|
|
||||||
minio-dst: ""
|
|
||||||
|
|
||||||
test-go-on-latest-go-release:
|
|
||||||
parameters:
|
|
||||||
goversion:
|
|
||||||
type: string
|
|
||||||
docker:
|
|
||||||
- image: cimg/go:<<parameters.goversion>>
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore-cache-gomod
|
|
||||||
- run: make test-go
|
|
||||||
# don't save-cache-gomod here, test-go doesn't pull all the dependencies
|
|
||||||
|
|
||||||
release-build:
|
|
||||||
machine:
|
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: make release-docker RELEASE_DOCKER_BASEIMAGE_TAG=<<pipeline.parameters.release_docker_baseimage_tag>>
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths: [.]
|
|
||||||
release-deb:
|
|
||||||
machine:
|
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: .
|
|
||||||
- run: make debs-docker
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- "artifacts/*.deb"
|
|
||||||
|
|
||||||
release-rpm:
|
|
||||||
machine:
|
|
||||||
image: ubuntu-2004:202201-02
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: .
|
|
||||||
- run: make rpms-docker
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- "artifacts/*.rpm"
|
|
||||||
|
|
||||||
release-upload:
|
|
||||||
docker:
|
|
||||||
- image: cimg/base:2020.08
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: .
|
|
||||||
- store_artifacts:
|
|
||||||
path: artifacts
|
|
||||||
- download-and-install-minio-client
|
|
||||||
- upload-minio:
|
|
||||||
src: artifacts
|
|
||||||
dst: ""
|
|
||||||
- set-github-minio-status:
|
|
||||||
context: artifacts/release
|
|
||||||
description: CI-generated release artifacts
|
|
||||||
minio-dst: ""
|
|
||||||
|
|
||||||
periodic-full-pipeline-run:
|
|
||||||
docker:
|
|
||||||
- image: cimg/base:2020.08
|
|
||||||
steps:
|
|
||||||
- trigger-pipeline:
|
|
||||||
body_no_shell_subst: '{"branch":"<<pipeline.git.branch>>", "parameters": { "do_ci": true, "do_release": true }}'
|
|
||||||
|
|
||||||
publish-zrepl-github-io:
|
|
||||||
docker:
|
|
||||||
- image: cimg/python:3.7
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- invoke-lazy-sh:
|
|
||||||
subcommand: docdep
|
|
||||||
- run:
|
|
||||||
command: |
|
|
||||||
git config --global user.email "zreplbot@cschwarz.com"
|
|
||||||
git config --global user.name "zrepl-github-io-ci"
|
|
||||||
|
|
||||||
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
|
||||||
- run: ssh-add -D
|
|
||||||
# the default circleci ssh config only additional ssh keys for Host !github.com
|
|
||||||
- run:
|
|
||||||
command: |
|
|
||||||
cat > ~/.ssh/config \<<EOF
|
|
||||||
Host *
|
|
||||||
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421
|
|
||||||
EOF
|
|
||||||
- add_ssh_keys:
|
|
||||||
fingerprints:
|
|
||||||
# deploy key for zrepl.github.io
|
|
||||||
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
|
|
||||||
|
|
||||||
- run: bash -x docs/publish.sh -c -a
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
COMMIT="$1"
|
|
||||||
GO_VERSION="$2"
|
|
||||||
|
|
||||||
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
|
|
||||||
-H 'Accept: application/vnd.github.v3+json' \
|
|
||||||
-H "Authorization: token $GITHUB_ACCESS_TOKEN" \
|
|
||||||
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$COMMIT"'", "go_version": "'"$GO_VERSION"'" }}'
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
github: problame
|
|
||||||
patreon: zrepl
|
patreon: zrepl
|
||||||
liberapay: zrepl
|
liberapay: zrepl
|
||||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||||
|
|||||||
@@ -7,10 +7,4 @@ issues:
|
|||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
# Disable staticcheck 'Empty body in an if or else branch' as it's useful
|
|
||||||
# to put a comment into an empty else-clause that explains why whatever
|
|
||||||
# is done in the if-caluse is not necessary if the condition is false.
|
|
||||||
- linters:
|
|
||||||
- staticcheck
|
|
||||||
text: "SA9003:"
|
|
||||||
|
|
||||||
|
|||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
dist: xenial
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
env: # for allow_failures: https://docs.travis-ci.com/user/customizing-the-build/
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
|
||||||
|
- language: go
|
||||||
|
name: "Build in Docker (docs/installation.rst)"
|
||||||
|
script:
|
||||||
|
- sudo docker build -t zrepl_build -f build.Dockerfile .
|
||||||
|
- |
|
||||||
|
sudo docker run -it --rm \
|
||||||
|
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
zrepl_build make vendordeps release
|
||||||
|
|
||||||
|
- &zrepl_build_template
|
||||||
|
language: go
|
||||||
|
go_import_path: github.com/zrepl/zrepl
|
||||||
|
before_install:
|
||||||
|
- wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
||||||
|
- echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
||||||
|
- sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
||||||
|
- ./lazy.sh godep
|
||||||
|
- make vendordeps
|
||||||
|
script:
|
||||||
|
- make
|
||||||
|
- make vet
|
||||||
|
- make test
|
||||||
|
- make lint
|
||||||
|
- make artifacts/zrepl-freebsd-amd64
|
||||||
|
- make artifacts/zrepl-linux-amd64
|
||||||
|
- make artifacts/zrepl-darwin-amd64
|
||||||
|
go:
|
||||||
|
- "1.11"
|
||||||
|
|
||||||
|
- <<: *zrepl_build_template
|
||||||
|
go:
|
||||||
|
- "1.12"
|
||||||
|
|
||||||
|
- <<: *zrepl_build_template
|
||||||
|
go:
|
||||||
|
- "master"
|
||||||
|
|
||||||
|
- &zrepl_docs_template
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.4"
|
||||||
|
install:
|
||||||
|
- sudo apt-get install libgirepository1.0-dev
|
||||||
|
- pip install -r docs/requirements.txt
|
||||||
|
script:
|
||||||
|
- make docs
|
||||||
|
- <<: *zrepl_docs_template
|
||||||
|
python:
|
||||||
|
- "3.5"
|
||||||
|
- <<: *zrepl_docs_template
|
||||||
|
python:
|
||||||
|
- "3.6"
|
||||||
|
- <<: *zrepl_docs_template
|
||||||
|
python:
|
||||||
|
- "3.7"
|
||||||
|
|
||||||
|
|
||||||
|
allow_failures:
|
||||||
|
- <<: *zrepl_build_template
|
||||||
|
go:
|
||||||
|
- "master"
|
||||||
@@ -23,14 +23,9 @@ GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
|||||||
GO_ENV_VARS := GO111MODULE=on
|
GO_ENV_VARS := GO111MODULE=on
|
||||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||||
GO_MOD_READONLY := -mod=readonly
|
GO_MOD_READONLY := -mod=readonly
|
||||||
GO_EXTRA_BUILDFLAGS :=
|
GO_BUILDFLAGS := $(GO_MOD_READONLY)
|
||||||
GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
|
||||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
GOLANGCI_LINT := golangci-lint
|
GOLANGCI_LINT := golangci-lint
|
||||||
GOCOVMERGE := gocovmerge
|
|
||||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.19
|
|
||||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
|
||||||
|
|
||||||
ifneq ($(GOARM),)
|
ifneq ($(GOARM),)
|
||||||
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||||
else
|
else
|
||||||
@@ -49,95 +44,16 @@ printvars:
|
|||||||
|
|
||||||
release: clean
|
release: clean
|
||||||
# no cross-platform support for target test
|
# no cross-platform support for target test
|
||||||
$(MAKE) test-go
|
$(MAKE) test
|
||||||
$(MAKE) bins-all
|
$(MAKE) bins-all
|
||||||
$(MAKE) noarch
|
$(MAKE) noarch
|
||||||
$(MAKE) wrapup-and-checksum
|
$(MAKE) wrapup-and-checksum
|
||||||
$(MAKE) check-git-clean
|
$(MAKE) check-git-clean
|
||||||
ifeq (SIGN, 1)
|
ifeq (SIGN, 1)
|
||||||
$(MAKE) sign
|
$(make) sign
|
||||||
endif
|
endif
|
||||||
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
||||||
|
|
||||||
release-docker: $(ARTIFACTDIR)
|
|
||||||
sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build.Dockerfile > artifacts/release-docker.Dockerfile
|
|
||||||
docker build -t zrepl_release --pull -f artifacts/release-docker.Dockerfile .
|
|
||||||
docker run --rm -i -v $(CURDIR):/src -u $$(id -u):$$(id -g) \
|
|
||||||
zrepl_release \
|
|
||||||
make release GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
|
||||||
|
|
||||||
debs-docker:
|
|
||||||
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=deb
|
|
||||||
rpms-docker:
|
|
||||||
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=rpm
|
|
||||||
_debs_or_rpms_docker: # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
|
||||||
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=amd64
|
|
||||||
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm64
|
|
||||||
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm GOARM=7
|
|
||||||
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=386
|
|
||||||
|
|
||||||
rpm: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
|
||||||
$(eval _ZREPL_RPM_VERSION := $(subst -,.,$(_ZREPL_VERSION)))
|
|
||||||
$(eval _ZREPL_RPM_TOPDIR_ABS := $(CURDIR)/$(ARTIFACTDIR)/rpmbuild)
|
|
||||||
rm -rf "$(_ZREPL_RPM_TOPDIR_ABS)"
|
|
||||||
mkdir "$(_ZREPL_RPM_TOPDIR_ABS)"
|
|
||||||
mkdir -p "$(_ZREPL_RPM_TOPDIR_ABS)"/{SPECS,RPMS,BUILD,BUILDROOT}
|
|
||||||
sed "s/^Version:.*/Version: $(_ZREPL_RPM_VERSION)/g" \
|
|
||||||
packaging/rpm/zrepl.spec > $(_ZREPL_RPM_TOPDIR_ABS)/SPECS/zrepl.spec
|
|
||||||
|
|
||||||
# see /usr/lib/rpm/platform
|
|
||||||
ifeq ($(GOARCH),amd64)
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := x86_64)
|
|
||||||
else ifeq ($(GOARCH), 386)
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := i386)
|
|
||||||
else ifeq ($(GOARCH), arm64)
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := aarch64)
|
|
||||||
else ifeq ($(GOARCH), arm)
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := armv7hl)
|
|
||||||
else
|
|
||||||
$(eval _ZREPL_RPMBUILD_TARGET := $(GOARCH))
|
|
||||||
endif
|
|
||||||
rpmbuild \
|
|
||||||
--build-in-place \
|
|
||||||
--define "_sourcedir $(CURDIR)" \
|
|
||||||
--define "_topdir $(_ZREPL_RPM_TOPDIR_ABS)" \
|
|
||||||
--define "_zrepl_binary_filename zrepl-$(ZREPL_TARGET_TUPLE)" \
|
|
||||||
--target $(_ZREPL_RPMBUILD_TARGET) \
|
|
||||||
-bb "$(_ZREPL_RPM_TOPDIR_ABS)"/SPECS/zrepl.spec
|
|
||||||
cp "$(_ZREPL_RPM_TOPDIR_ABS)"/RPMS/$(_ZREPL_RPMBUILD_TARGET)/zrepl-$(_ZREPL_RPM_VERSION)*.rpm $(ARTIFACTDIR)/
|
|
||||||
|
|
||||||
rpm-docker:
|
|
||||||
docker build -t zrepl_rpm_pkg --pull -f packaging/rpm/Dockerfile .
|
|
||||||
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
|
||||||
zrepl_rpm_pkg \
|
|
||||||
make rpm GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
|
||||||
|
|
||||||
deb: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
|
||||||
|
|
||||||
cp packaging/deb/debian/changelog.template packaging/deb/debian/changelog
|
|
||||||
sed -i 's/DATE_DASH_R_OUTPUT/$(shell date -R)/' packaging/deb/debian/changelog
|
|
||||||
VERSION="$(subst -,.,$(_ZREPL_VERSION))"; \
|
|
||||||
export VERSION="$${VERSION#v}"; \
|
|
||||||
sed -i 's/VERSION/'"$$VERSION"'/' packaging/deb/debian/changelog
|
|
||||||
|
|
||||||
ifeq ($(GOARCH), arm)
|
|
||||||
$(eval DEB_HOST_ARCH := armhf)
|
|
||||||
else ifeq ($(GOARCH), 386)
|
|
||||||
$(eval DEB_HOST_ARCH := i386)
|
|
||||||
else
|
|
||||||
$(eval DEB_HOST_ARCH := $(GOARCH))
|
|
||||||
endif
|
|
||||||
|
|
||||||
export ZREPL_DPKG_ZREPL_BINARY_FILENAME=zrepl-$(ZREPL_TARGET_TUPLE); \
|
|
||||||
dpkg-buildpackage -b --no-sign --host-arch $(DEB_HOST_ARCH)
|
|
||||||
cp ../*.deb artifacts/
|
|
||||||
|
|
||||||
deb-docker:
|
|
||||||
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
|
||||||
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
|
||||||
zrepl_debian_pkg \
|
|
||||||
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
|
||||||
|
|
||||||
# expects `release` target to have run before
|
# expects `release` target to have run before
|
||||||
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||||
wrapup-and-checksum:
|
wrapup-and-checksum:
|
||||||
@@ -149,7 +65,6 @@ wrapup-and-checksum:
|
|||||||
-acf $(NOARCH_TARBALL) \
|
-acf $(NOARCH_TARBALL) \
|
||||||
$(ARTIFACTDIR)/docs/html \
|
$(ARTIFACTDIR)/docs/html \
|
||||||
$(ARTIFACTDIR)/bash_completion \
|
$(ARTIFACTDIR)/bash_completion \
|
||||||
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
|
||||||
$(ARTIFACTDIR)/go_env.txt \
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
dist \
|
dist \
|
||||||
config/samples
|
config/samples
|
||||||
@@ -173,10 +88,6 @@ check-git-clean:
|
|||||||
fi; \
|
fi; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
tag-release:
|
|
||||||
test -n "$(ZREPL_TAG_VERSION)" || exit 1
|
|
||||||
git tag -u E27CA5FC -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
|
|
||||||
|
|
||||||
sign:
|
sign:
|
||||||
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
||||||
--armor \
|
--armor \
|
||||||
@@ -186,15 +97,13 @@ clean: docs-clean
|
|||||||
rm -rf "$(ARTIFACTDIR)"
|
rm -rf "$(ARTIFACTDIR)"
|
||||||
|
|
||||||
##################### BINARIES #####################
|
##################### BINARIES #####################
|
||||||
.PHONY: bins-all lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
|
.PHONY: bins-all lint test vet zrepl-bin platformtest-bin
|
||||||
|
|
||||||
BINS_ALL_TARGETS := zrepl-bin test-platform-bin vet lint
|
BINS_ALL_TARGETS := zrepl-bin platformtest-bin vet lint
|
||||||
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
|
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
|
||||||
bins-all:
|
bins-all:
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm GOARM=7
|
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=arm64
|
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
||||||
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
||||||
@@ -211,49 +120,27 @@ endif
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
|
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
|
||||||
|
test:
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) ./...
|
||||||
vet:
|
vet:
|
||||||
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||||
|
|
||||||
test-go: $(ARTIFACTDIR)
|
|
||||||
rm -f "$(ARTIFACTDIR)/gotest.cover"
|
|
||||||
ifeq ($(COVER),1)
|
|
||||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
|
||||||
-coverpkg github.com/zrepl/zrepl/... \
|
|
||||||
-covermode atomic \
|
|
||||||
-coverprofile "$(ARTIFACTDIR)/gotest.cover" \
|
|
||||||
./...
|
|
||||||
else
|
|
||||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
|
||||||
./...
|
|
||||||
endif
|
|
||||||
|
|
||||||
zrepl-bin:
|
zrepl-bin:
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||||
|
|
||||||
generate-platform-test-list:
|
platformtest-bin:
|
||||||
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" ./platformtest/harness
|
||||||
|
|
||||||
COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
|
##################### DEV TARGETS #####################
|
||||||
cover-platform-bin:
|
# not part of the build, must do that manually
|
||||||
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
.PHONY: generate format platformtest
|
||||||
-c -o "$(COVER_PLATFORM_BIN_PATH)" \
|
|
||||||
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
|
|
||||||
./platformtest/harness
|
|
||||||
cover-platform:
|
|
||||||
# do not track dependency on cover-platform-bin to allow build of binary outside of test VM
|
|
||||||
export _TEST_PLATFORM_CMD="$(COVER_PLATFORM_BIN_PATH) \
|
|
||||||
-test.coverprofile \"$(ARTIFACTDIR)/platformtest.cover\" \
|
|
||||||
-test.v \
|
|
||||||
__DEVEL--i-heard-you-like-tests"; \
|
|
||||||
$(MAKE) _test-or-cover-platform-impl
|
|
||||||
|
|
||||||
TEST_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)
|
generate:
|
||||||
test-platform-bin:
|
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
||||||
$(GO_BUILD) -o "$(TEST_PLATFORM_BIN_PATH)" ./platformtest/harness
|
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
||||||
test-platform:
|
|
||||||
export _TEST_PLATFORM_CMD="\"$(TEST_PLATFORM_BIN_PATH)\""; \
|
format:
|
||||||
$(MAKE) _test-or-cover-platform-impl
|
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
|
||||||
|
|
||||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||||
@@ -261,54 +148,18 @@ ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
|||||||
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
||||||
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
||||||
ZREPL_PLATFORMTEST_ARGS :=
|
ZREPL_PLATFORMTEST_ARGS :=
|
||||||
_test-or-cover-platform-impl: $(ARTIFACTDIR)
|
platformtest: # do not track dependency on platformtest-bin to allow build of platformtest outside of test VM
|
||||||
ifndef _TEST_PLATFORM_CMD
|
|
||||||
$(error _TEST_PLATFORM_CMD is undefined, caller 'cover-platform' or 'test-platform' should have defined it)
|
|
||||||
endif
|
|
||||||
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||||
rm -f "$(ARTIFACTDIR)/platformtest.cover"
|
|
||||||
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||||
$(_TEST_PLATFORM_CMD) \
|
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||||
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||||
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||||
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||||
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
|
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
|
||||||
$(ZREPL_PLATFORMTEST_ARGS)
|
$(ZREPL_PLATFORMTEST_ARGS)
|
||||||
|
|
||||||
cover-merge: $(ARTIFACTDIR)
|
|
||||||
$(GOCOVMERGE) $(ARTIFACTDIR)/platformtest.cover $(ARTIFACTDIR)/gotest.cover > $(ARTIFACTDIR)/merged.cover
|
|
||||||
cover-html: cover-merge
|
|
||||||
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
|
|
||||||
|
|
||||||
cover-full:
|
|
||||||
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
|
||||||
$(MAKE) test-go COVER=1
|
|
||||||
$(MAKE) cover-platform-bin
|
|
||||||
$(MAKE) cover-platform
|
|
||||||
$(MAKE) cover-html
|
|
||||||
|
|
||||||
##################### DEV TARGETS #####################
|
|
||||||
# not part of the build, must do that manually
|
|
||||||
.PHONY: generate formatcheck format
|
|
||||||
|
|
||||||
generate: generate-platform-test-list
|
|
||||||
protoc -I=replication/logic/pdu --go_out=replication/logic/pdu --go-grpc_out=replication/logic/pdu replication/logic/pdu/pdu.proto
|
|
||||||
protoc -I=rpc/grpcclientidentity/example --go_out=rpc/grpcclientidentity/example/pdu --go-grpc_out=rpc/grpcclientidentity/example/pdu rpc/grpcclientidentity/example/grpcauth.proto
|
|
||||||
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
|
||||||
|
|
||||||
GOIMPORTS := goimports -srcdir . -local 'github.com/zrepl/zrepl'
|
|
||||||
FINDSRCFILES := find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go'
|
|
||||||
|
|
||||||
formatcheck:
|
|
||||||
@# goimports doesn't have a knob to exit with non-zero status code if formatting is needed
|
|
||||||
@# see https://go-review.googlesource.com/c/tools/+/237378
|
|
||||||
@ affectedfiles=$$($(GOIMPORTS) -l $(shell $(FINDSRCFILES)) | tee /dev/stderr | wc -l); test "$$affectedfiles" = 0
|
|
||||||
|
|
||||||
format:
|
|
||||||
@ $(GOIMPORTS) -w -d $(shell $(FINDSRCFILES))
|
|
||||||
|
|
||||||
##################### NOARCH #####################
|
##################### NOARCH #####################
|
||||||
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||||
|
|
||||||
|
|
||||||
$(ARTIFACTDIR):
|
$(ARTIFACTDIR):
|
||||||
@@ -316,29 +167,22 @@ $(ARTIFACTDIR):
|
|||||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||||
mkdir -p "$@"
|
mkdir -p "$@"
|
||||||
|
|
||||||
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
|
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
$(ARTIFACTDIR)/bash_completion:
|
$(ARTIFACTDIR)/bash_completion:
|
||||||
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@"
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) bashcomp "$@"
|
||||||
|
|
||||||
$(ARTIFACTDIR)/_zrepl.zsh_completion:
|
|
||||||
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
|
||||||
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/go_env.txt:
|
$(ARTIFACTDIR)/go_env.txt:
|
||||||
$(GO_ENV_VARS) $(GO) env > $@
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
$(GO) version >> $@
|
|
||||||
|
|
||||||
docs: $(ARTIFACTDIR)/docs
|
docs: $(ARTIFACTDIR)/docs
|
||||||
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html
|
make -C docs \
|
||||||
$(MAKE) -C docs \
|
|
||||||
html \
|
html \
|
||||||
BUILDDIR=../artifacts/docs \
|
BUILDDIR=../artifacts/docs \
|
||||||
SPHINXOPTS="-W --keep-going -n"
|
|
||||||
|
|
||||||
docs-clean:
|
docs-clean:
|
||||||
$(MAKE) -C docs \
|
make -C docs \
|
||||||
clean \
|
clean \
|
||||||
BUILDDIR=../artifacts/docs
|
BUILDDIR=../artifacts/docs
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://zrepl.github.io)
|
[](https://zrepl.github.io)
|
||||||
[](https://patreon.com/zrepl)
|
|
||||||
[](https://github.com/sponsors/problame)
|
|
||||||
[](https://liberapay.com/zrepl/donate)
|
|
||||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||||
|
[](https://liberapay.com/zrepl/donate)
|
||||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||||
[](https://matrix.to/#/#zrepl:matrix.org)
|
|
||||||
|
|
||||||
# zrepl
|
# zrepl
|
||||||
zrepl is a one-stop ZFS backup & replication solution.
|
zrepl is a one-stop ZFS backup & replication solution.
|
||||||
@@ -22,92 +19,92 @@ zrepl is a one-stop ZFS backup & replication solution.
|
|||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
1. Does your feature request require default values / some kind of configuration?
|
1. Does you feature request require default values / some kind of configuration?
|
||||||
If so, think of an expressive configuration example.
|
If so, think of an expressive configuration example.
|
||||||
2. Think of at least one use case that generalizes from your concrete application.
|
2. Think of at least one use case that generalizes from your concrete application.
|
||||||
3. Open an issue on GitHub with example conf & use case attached.
|
3. Open an issue on GitHub with example conf & use case attached.
|
||||||
4. **Optional**: [Post a bounty](https://www.bountysource.com/teams/zrepl) on the issue, or [contact Christian Schwarz](https://cschwarz.com) for contract work.
|
|
||||||
|
|
||||||
The above does not apply if you already implemented everything.
|
The above does not apply if you already implemented everything.
|
||||||
Check out the *Coding Workflow* section below for details.
|
Check out the *Coding Workflow* section below for details.
|
||||||
|
|
||||||
## Building, Releasing, Downstream-Packaging
|
## Package Maintainer Information
|
||||||
|
|
||||||
This section provides an overview of the zrepl build & release process.
|
* Follow the steps in `docs/installation.rst -> Compiling from Source` and read the Makefile / shell scripts used in this process.
|
||||||
Check out `docs/installation/compile-from-source.rst` for build-from-source instructions.
|
* Make sure your distro is compatible with the paths in `docs/installation.rst`.
|
||||||
|
|
||||||
### Overview
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Install **build dependencies** using `./lazy.sh devsetup`.
|
|
||||||
`lazy.sh` uses `python3-pip` to fetch the build dependencies for the docs - you might want to use a [venv](https://docs.python.org/3/library/venv.html).
|
|
||||||
If you just want to install the Go dependencies, run `./lazy.sh godep`.
|
|
||||||
|
|
||||||
The **test suite** is split into pure **Go tests** (`make test-go`) and **platform tests** that interact with ZFS and thus generally **require root privileges** (`sudo make test-platform`).
|
|
||||||
Platform tests run on their own pool with the name `zreplplatformtest`, which is created using the file vdev in `/tmp`.
|
|
||||||
|
|
||||||
For a full **code coverage** profile, run `make test-go COVER=1 && sudo make test-platform && make cover-merge`.
|
|
||||||
An HTML report can be generated using `make cover-html`.
|
|
||||||
|
|
||||||
**Code generation** is triggered by `make generate`. Generated code is committed to the source tree.
|
|
||||||
|
|
||||||
### Build & Release Process
|
|
||||||
|
|
||||||
**The `Makefile` is catering to the needs of developers & CI, not distro packagers**.
|
|
||||||
It provides phony targets for
|
|
||||||
* local development (building, running tests, etc)
|
|
||||||
* building a release in Docker (used by the CI & release management)
|
|
||||||
* building .deb and .rpm packages out of the release artifacts.
|
|
||||||
|
|
||||||
**Build tooling & dependencies** are documented as code in `lazy.sh`.
|
|
||||||
Go dependencies are then fetched by the go command and pip dependencies are pinned through a `requirements.txt`.
|
|
||||||
|
|
||||||
**We use CircleCI for continuous integration**.
|
|
||||||
There are two workflows:
|
|
||||||
|
|
||||||
* `ci` runs for every commit / branch / tag pushed to GitHub.
|
|
||||||
It is supposed to run very fast (<5min and provides quick feedback to developers).
|
|
||||||
It runs formatting checks, lints and tests on the most important OSes / architectures.
|
|
||||||
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
|
|
||||||
|
|
||||||
* `release` runs
|
|
||||||
* on manual triggers through the CircleCI API (in order to produce a release)
|
|
||||||
* periodically on `master`
|
|
||||||
Artifacts are published to minio.cschwarz.com (see GitHub Commit Status).
|
|
||||||
|
|
||||||
**Releases** are issued via Git tags + GitHub Releases feature.
|
|
||||||
The procedure to issue a release is as follows:
|
|
||||||
* Issue the source release:
|
|
||||||
* Git tag the release on the `master` branch.
|
|
||||||
* Push the tag.
|
|
||||||
* Run `./docs/publish.sh` to re-build & push zrepl.github.io.
|
|
||||||
* Issue the official binary release:
|
|
||||||
* Run the `release` pipeline (triggered via CircleCI API)
|
|
||||||
* Download the artifacts to the release manager's machine.
|
|
||||||
* Create a GitHub release, edit the changelog, upload all the release artifacts, including .rpm and .deb files.
|
|
||||||
* Issue the GitHub release.
|
|
||||||
* Add the .rpm and .deb files to the official zrepl repos, publish those.
|
|
||||||
|
|
||||||
**Official binary releases are not re-built when Go receives an update. If the Go update is critical to zrepl (e.g. a Go security update that affects zrepl), we'd issue a new source release**.
|
|
||||||
The rationale for this is that whereas distros provide a mechanism for this (`$zrepl_source_release-$distro_package_revision`), GitHub Releases doesn't which means we'd need to update the existing GitHub release's assets, which nobody would notice (no RSS feed updates, etc.).
|
|
||||||
Downstream packagers can read the changelog to determine whether they want to push that minor release into their distro or simply skip it.
|
|
||||||
|
|
||||||
### Additional Notes to Distro Package Maintainers
|
|
||||||
|
|
||||||
* Run the platform tests (Docs -> Usage -> Platform Tests) **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
|
||||||
* Ship a default config that adheres to your distro's `hier` and logging system.
|
* Ship a default config that adheres to your distro's `hier` and logging system.
|
||||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||||
* `dist/systemd` contains a Systemd unit template.
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||||
* Have a look at the `Makefile`'s `ZREPL_VERSION` variable and how it passed to Go's `ldFlags`.
|
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
||||||
This is how `zrepl version` knows what version number to show.
|
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
||||||
Your build system should set the `ldFlags` flags appropriately and add a prefix or suffix that indicates that the given zrepl binary is a distro build, not an official one.
|
* Use `make platformtest` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||||
|
|
||||||
|
## Developer Documentation
|
||||||
|
|
||||||
## Contributing Code
|
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`.
|
||||||
|
|
||||||
|
### Overall Architecture
|
||||||
|
|
||||||
|
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
|
||||||
|
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── artifacts # build artifcats generate by make
|
||||||
|
├── cli # wrapper around CLI package cobra
|
||||||
|
├── client # all subcommands that are not `daemon`
|
||||||
|
├── config # config data types (=> package yaml-config)
|
||||||
|
│ └── samples
|
||||||
|
├── daemon # the implementation of `zrepl daemon` subcommand
|
||||||
|
│ ├── filters
|
||||||
|
│ ├── job # job implementations
|
||||||
|
│ ├── logging # logging outlets + formatters
|
||||||
|
│ ├── nethelpers
|
||||||
|
│ ├── prometheus
|
||||||
|
│ ├── pruner # pruner implementation
|
||||||
|
│ ├── snapper # snapshotter implementation
|
||||||
|
├── docs # sphinx-based documentation
|
||||||
|
├── dist # supplemental material for users & package maintainers
|
||||||
|
│ ├── **/*.rst # documentation in reStructuredText
|
||||||
|
│ ├── sphinxconf
|
||||||
|
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
||||||
|
│ ├── requirements.txt # pip3 requirements to build documentation
|
||||||
|
│ ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo
|
||||||
|
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
||||||
|
├── endpoint # implementation of replication endpoints (=> package replication)
|
||||||
|
├── logger # our own logger package
|
||||||
|
├── pruning # pruning rules (the logic, not the actual execution)
|
||||||
|
│ └── retentiongrid
|
||||||
|
├── replication
|
||||||
|
│ ├── driver # the driver of the replication logic (status reporting, error handling)
|
||||||
|
│ ├── logic # planning & executing replication steps via rpc
|
||||||
|
| | └── pdu # the generated gRPC & protobuf code used in replication (and endpoints)
|
||||||
|
│ └── report # the JSON-serializable report datastructures exposed to the client
|
||||||
|
├── rpc # the hybrid gRPC + ./dataconn RPC client: connects to a remote replication.Endpoint
|
||||||
|
│ ├── dataconn # Bulk data-transfer RPC protocol
|
||||||
|
│ ├── grpcclientidentity # adaptor to inject package transport's 'client identity' concept into gRPC contexts
|
||||||
|
│ ├── netadaptor # adaptor to convert a package transport's Connecter and Listener into net.* primitives
|
||||||
|
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
|
||||||
|
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
||||||
|
├── tlsconf # abstraction for Go TLS server + client config
|
||||||
|
├── transport # transports implementation
|
||||||
|
│ ├── fromconfig
|
||||||
|
│ ├── local
|
||||||
|
│ ├── ssh
|
||||||
|
│ ├── tcp
|
||||||
|
│ └── tls
|
||||||
|
├── util
|
||||||
|
├── vendor # managed by dep
|
||||||
|
├── version # abstraction for versions (filled during build by Makefile)
|
||||||
|
└── zfs # zfs(8) wrappers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coding Workflow
|
||||||
|
|
||||||
* Open an issue when starting to hack on a new feature
|
* Open an issue when starting to hack on a new feature
|
||||||
* Commits should reference the issue they are related to
|
* Commits should reference the issue they are related to
|
||||||
@@ -117,6 +114,9 @@ Downstream packagers can read the changelog to determine whether they want to pu
|
|||||||
|
|
||||||
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
|
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
|
||||||
|
|
||||||
|
* Config-breaking changes must contain a line `BREAK CONFIG` in the commit message
|
||||||
|
* Other breaking changes must contain a line `BREAK` in the commit message
|
||||||
|
|
||||||
### Glossary & Naming Inconsistencies
|
### Glossary & Naming Inconsistencies
|
||||||
|
|
||||||
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
||||||
@@ -133,3 +133,4 @@ variables and types are often named *dataset* when they in fact refer to a *file
|
|||||||
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
|
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
|
||||||
|
|
||||||
However, new contributions & patches should fix naming without further notice in the commit message.
|
However, new contributions & patches should fix naming without further notice in the commit message.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM !SUBSTITUTED_BY_MAKEFILE
|
FROM golang:latest
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
|
|||||||
+43
-5
@@ -3,11 +3,49 @@ module github.com/zrepl/zrepl/build
|
|||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
|
||||||
github.com/alvaroloes/enumer v1.1.1
|
github.com/alvaroloes/enumer v1.1.1
|
||||||
github.com/golangci/golangci-lint v1.50.1
|
github.com/fatih/color v1.7.0 // indirect
|
||||||
|
github.com/go-critic/go-critic v0.3.4 // indirect
|
||||||
|
github.com/gogo/protobuf v1.2.1 // indirect
|
||||||
|
github.com/golang/mock v1.2.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.2.0
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
|
||||||
|
github.com/golangci/golangci-lint v1.17.1
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
|
||||||
|
github.com/golangci/misspell v0.3.4 // indirect
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||||
|
github.com/google/go-cmp v0.3.0 // indirect
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/pkg/errors v0.8.1 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.4.2 // indirect
|
||||||
|
github.com/spf13/afero v1.2.2 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
github.com/spf13/viper v1.3.2 // indirect
|
||||||
golang.org/x/tools v0.2.0
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
|
||||||
google.golang.org/protobuf v1.28.0
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect
|
||||||
|
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
// invalid dates in transitive dependencies (first validated in Go 1.13, didn't fail in earlier Go versions)
|
||||||
|
replace (
|
||||||
|
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 => github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540
|
||||||
|
github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead => github.com/go-critic/go-critic v0.3.5-0.20190210220443-ee9bf5809ead
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6
|
||||||
|
github.com/golangci/go-tools v0.0.0-20180109140146-35a9f45a5db0 => github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0
|
||||||
|
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547
|
||||||
|
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217
|
||||||
|
golang.org/x/tools v0.0.0-20190125232054-379209517ffe => golang.org/x/tools v0.0.0-20190205201329-379209517ffe
|
||||||
|
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34
|
||||||
)
|
)
|
||||||
|
|||||||
+100
-1646
File diff suppressed because it is too large
Load Diff
+1
-5
@@ -1,15 +1,11 @@
|
|||||||
//go:build tools
|
|
||||||
// +build tools
|
// +build tools
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// the lines are parsed by lazy.sh, do not edit
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/alvaroloes/enumer"
|
_ "github.com/alvaroloes/enumer"
|
||||||
|
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||||
_ "github.com/wadey/gocovmerge"
|
|
||||||
_ "golang.org/x/tools/cmd/goimports"
|
_ "golang.org/x/tools/cmd/goimports"
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
_ "golang.org/x/tools/cmd/stringer"
|
||||||
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
|
||||||
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
|
||||||
)
|
)
|
||||||
|
|||||||
+21
-54
@@ -1,15 +1,12 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,55 +19,29 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "One-stop ZFS replication solution",
|
Short: "One-stop ZFS replication solution",
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
var bashcompCmd = &cobra.Command{
|
||||||
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
Use: "bashcomp path/to/out/file",
|
||||||
}
|
Short: "generate bash completions",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
var genCompletionCmd = &cobra.Command{
|
if len(args) != 1 {
|
||||||
Use: "gencompletion",
|
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||||
Short: "generate shell auto-completions",
|
err := cmd.Usage()
|
||||||
}
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
type completionCmdInfo struct {
|
}
|
||||||
genFunc func(outpath string) error
|
os.Exit(1)
|
||||||
help string
|
}
|
||||||
}
|
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
|
||||||
var completionCmdMap = map[string]completionCmdInfo{
|
os.Exit(1)
|
||||||
"zsh": {
|
}
|
||||||
rootCmd.GenZshCompletionFile,
|
|
||||||
" save to file `_zrepl` in your zsh's $fpath",
|
|
||||||
},
|
|
||||||
"bash": {
|
|
||||||
rootCmd.GenBashCompletionFile,
|
|
||||||
" save to a path and source that path in your .bashrc",
|
|
||||||
},
|
},
|
||||||
|
Hidden: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
for sh, info := range completionCmdMap {
|
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
||||||
sh, info := sh, info
|
rootCmd.AddCommand(bashcompCmd)
|
||||||
genCompletionCmd.AddCommand(&cobra.Command{
|
|
||||||
Use: fmt.Sprintf("%s path/to/out/file", sh),
|
|
||||||
Short: fmt.Sprintf("generate %s completions", sh),
|
|
||||||
Example: info.help,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if len(args) != 1 {
|
|
||||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
|
||||||
err := cmd.Usage()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := info.genFunc(args[0]); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error generating %s completion: %s", sh, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
rootCmd.AddCommand(genCompletionCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subcommand struct {
|
type Subcommand struct {
|
||||||
@@ -78,7 +49,7 @@ type Subcommand struct {
|
|||||||
Short string
|
Short string
|
||||||
Example string
|
Example string
|
||||||
NoRequireConfig bool
|
NoRequireConfig bool
|
||||||
Run func(ctx context.Context, subcommand *Subcommand, args []string) error
|
Run func(subcommand *Subcommand, args []string) error
|
||||||
SetupFlags func(f *pflag.FlagSet)
|
SetupFlags func(f *pflag.FlagSet)
|
||||||
SetupSubcommands func() []*Subcommand
|
SetupSubcommands func() []*Subcommand
|
||||||
|
|
||||||
@@ -99,11 +70,7 @@ func (s *Subcommand) Config() *config.Config {
|
|||||||
|
|
||||||
func (s *Subcommand) run(cmd *cobra.Command, args []string) {
|
func (s *Subcommand) run(cmd *cobra.Command, args []string) {
|
||||||
s.tryParseConfig()
|
s.tryParseConfig()
|
||||||
ctx := context.Background()
|
err := s.Run(s, args)
|
||||||
endTask := trace.WithTaskFromStackUpdateCtx(&ctx)
|
|
||||||
defer endTask()
|
|
||||||
err := s.Run(ctx, s, args)
|
|
||||||
endTask()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
+4
-15
@@ -1,7 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -19,9 +18,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var configcheckArgs struct {
|
var configcheckArgs struct {
|
||||||
format string
|
format string
|
||||||
what string
|
what string
|
||||||
skipCertCheck bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ConfigcheckCmd = &cli.Subcommand{
|
var ConfigcheckCmd = &cli.Subcommand{
|
||||||
@@ -30,9 +28,8 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
||||||
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
||||||
f.BoolVar(&configcheckArgs.skipCertCheck, "skip-cert-check", false, "skip checking cert files")
|
|
||||||
},
|
},
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||||
formatMap := map[string]func(interface{}){
|
formatMap := map[string]func(interface{}){
|
||||||
"": func(i interface{}) {},
|
"": func(i interface{}) {},
|
||||||
"pretty": func(i interface{}) {
|
"pretty": func(i interface{}) {
|
||||||
@@ -58,16 +55,8 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hadErr bool
|
var hadErr bool
|
||||||
|
|
||||||
parseFlags := config.ParseFlagsNone
|
|
||||||
|
|
||||||
if configcheckArgs.skipCertCheck {
|
|
||||||
parseFlags |= config.ParseFlagsNoCertCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
// further: try to build jobs
|
// further: try to build jobs
|
||||||
confJobs, err := job.JobsFromConfig(subcommand.Config(), parseFlags)
|
confJobs, err := job.JobsFromConfig(subcommand.Config())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := errors.Wrap(err, "cannot build jobs from config")
|
err := errors.Wrap(err, "cannot build jobs from config")
|
||||||
if configcheckArgs.what == "jobs" {
|
if configcheckArgs.what == "jobs" {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
HoldsCmd = &cli.Subcommand{
|
||||||
|
Use: "holds",
|
||||||
|
Short: "manage holds & step bookmarks",
|
||||||
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
|
return holdsList
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var holdsList = []*cli.Subcommand{
|
||||||
|
&cli.Subcommand{
|
||||||
|
Use: "list [FSFILTER]",
|
||||||
|
Run: doHoldsList,
|
||||||
|
NoRequireConfig: true,
|
||||||
|
Short: `
|
||||||
|
FSFILTER SYNTAX:
|
||||||
|
representation of a 'filesystems' filter statement on the command line
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fsfilterFromCliArg(arg string) (zfs.DatasetFilter, error) {
|
||||||
|
mappings := strings.Split(arg, ",")
|
||||||
|
f := filters.NewDatasetMapFilter(len(mappings), true)
|
||||||
|
for _, m := range mappings {
|
||||||
|
thisMappingErr := fmt.Errorf("expecting comma-separated list of <dataset-pattern>:<ok|!> pairs, got %q", m)
|
||||||
|
lhsrhs := strings.SplitN(m, ":", 2)
|
||||||
|
if len(lhsrhs) != 2 {
|
||||||
|
return nil, thisMappingErr
|
||||||
|
}
|
||||||
|
err := f.Add(lhsrhs[0], lhsrhs[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %s", thisMappingErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.AsFilter(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doHoldsList(sc *cli.Subcommand, args []string) error {
|
||||||
|
var err error
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
return errors.New("this subcommand takes at most one argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter zfs.DatasetFilter
|
||||||
|
if len(args) == 0 {
|
||||||
|
filter = zfs.NoFilter()
|
||||||
|
} else {
|
||||||
|
filter, err = fsfilterFromCliArg(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot parse filesystem filter args")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listing, err := endpoint.ListZFSHoldsAndBookmarks(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return err // context clear by invocation of command
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent(" ", " ")
|
||||||
|
if err := enc.Encode(listing); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+16
-11
@@ -48,13 +48,14 @@ var migratePlaceholder0_1Args struct {
|
|||||||
dryRun bool
|
dryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func doMigratePlaceholder0_1(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return fmt.Errorf("migration does not take arguments, got %v", args)
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := sc.Config()
|
cfg := sc.Config()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot list filesystems")
|
return errors.Wrap(err, "cannot list filesystems")
|
||||||
@@ -98,7 +99,7 @@ func doMigratePlaceholder0_1(ctx context.Context, sc *cli.Subcommand, args []str
|
|||||||
}
|
}
|
||||||
for _, fs := range wi.fss {
|
for _, fs := range wi.fss {
|
||||||
fmt.Printf("\t%q ... ", fs.ToString())
|
fmt.Printf("\t%q ... ", fs.ToString())
|
||||||
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(ctx, fs, migratePlaceholder0_1Args.dryRun)
|
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(fs, migratePlaceholder0_1Args.dryRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error: %s\n", err)
|
fmt.Printf("error: %s\n", err)
|
||||||
} else if !r.NeedsModification {
|
} else if !r.NeedsModification {
|
||||||
@@ -123,19 +124,21 @@ var fail = color.New(color.FgRed)
|
|||||||
|
|
||||||
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
|
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
|
||||||
|
|
||||||
func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
func doMigrateReplicationCursor(sc *cli.Subcommand, args []string) error {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return fmt.Errorf("migration does not take arguments, got %v", args)
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := sc.Config()
|
cfg := sc.Config()
|
||||||
jobs, err := job.JobsFromConfig(cfg, config.ParseFlagsNone)
|
jobs, err := job.JobsFromConfig(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
||||||
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
||||||
return fmt.Errorf("exiting migration after error")
|
return fmt.Errorf("exiting migration after error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
|
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
|
||||||
for i, j := range cfg.Jobs {
|
for i, j := range cfg.Jobs {
|
||||||
if jobs[i].Name() != j.Name() {
|
if jobs[i].Name() != j.Name() {
|
||||||
@@ -162,7 +165,7 @@ func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []
|
|||||||
var hadError bool
|
var hadError bool
|
||||||
for _, fs := range fss {
|
for _, fs := range fss {
|
||||||
|
|
||||||
bold.Printf("INSPECT FILESYSTEM %q\n", fs.ToString())
|
bold.Printf("INSPECT FILESYTEM %q\n", fs.ToString())
|
||||||
|
|
||||||
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
||||||
if err == migrateReplicationCursorSkipSentinel {
|
if err == migrateReplicationCursorSkipSentinel {
|
||||||
@@ -208,15 +211,17 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
|
|||||||
}
|
}
|
||||||
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
||||||
|
|
||||||
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
|
versions, err := zfs.ZFSListFilesystemVersions(fs, nil)
|
||||||
Types: zfs.Bookmarks,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldCursor *zfs.FilesystemVersion
|
var oldCursor *zfs.FilesystemVersion
|
||||||
for i, fsv := range bookmarks {
|
for i, fsv := range versions {
|
||||||
|
if fsv.Type != zfs.Bookmark {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
||||||
if err != endpoint.ErrV1ReplicationCursor {
|
if err != endpoint.ErrV1ReplicationCursor {
|
||||||
continue
|
continue
|
||||||
@@ -227,7 +232,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
|
|||||||
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCursor = &bookmarks[i]
|
oldCursor = &versions[i]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +264,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
|
|||||||
if migrateReplicationCursorArgs.dryRun {
|
if migrateReplicationCursorArgs.dryRun {
|
||||||
succ.Printf("DRY RUN\n")
|
succ.Printf("DRY RUN\n")
|
||||||
} else {
|
} else {
|
||||||
if err := zfs.ZFSDestroyFilesystemVersion(ctx, fs, oldCursor); err != nil {
|
if err := zfs.ZFSDestroyFilesystemVersion(fs, oldCursor); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-4
@@ -1,10 +1,67 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import "github.com/zrepl/zrepl/cli"
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pprofArgs struct {
|
||||||
|
daemon.PprofServerControlMsg
|
||||||
|
}
|
||||||
|
|
||||||
var PprofCmd = &cli.Subcommand{
|
var PprofCmd = &cli.Subcommand{
|
||||||
Use: "pprof",
|
Use: "pprof off | [on TCP_LISTEN_ADDRESS]",
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS",
|
||||||
return []*cli.Subcommand{PprofListenCmd, pprofActivityTraceCmd}
|
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
goto enargs
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "on":
|
||||||
|
pprofArgs.Run = true
|
||||||
|
if len(args) != 2 {
|
||||||
|
return errors.New("must specify TCP_LISTEN_ADDRESS as second positional argument")
|
||||||
|
}
|
||||||
|
pprofArgs.HttpListenAddress = args[1]
|
||||||
|
case "off":
|
||||||
|
if len(args) != 1 {
|
||||||
|
goto enargs
|
||||||
|
}
|
||||||
|
pprofArgs.Run = false
|
||||||
|
}
|
||||||
|
|
||||||
|
RunPProf(subcommand.Config())
|
||||||
|
return nil
|
||||||
|
enargs:
|
||||||
|
return errors.New("invalid number of positional arguments")
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RunPProf(conf *config.Config) {
|
||||||
|
log := log.New(os.Stderr, "", 0)
|
||||||
|
|
||||||
|
die := func() {
|
||||||
|
log.Printf("exiting after error")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("connecting to zrepl daemon")
|
||||||
|
|
||||||
|
httpc, err := controlHttpClient(conf.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error creating http client: %s", err)
|
||||||
|
die()
|
||||||
|
}
|
||||||
|
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPProf, pprofArgs.PprofServerControlMsg, struct{}{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error sending control message: %s", err)
|
||||||
|
die()
|
||||||
|
}
|
||||||
|
log.Printf("finished")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
var pprofActivityTraceCmd = &cli.Subcommand{
|
|
||||||
Use: "activity-trace ZREPL_PPROF_HOST:ZREPL_PPROF_PORT",
|
|
||||||
Short: "attach to zrepl daemon with activated pprof listener and dump an activity-trace to stdout",
|
|
||||||
Run: runPProfActivityTrace,
|
|
||||||
}
|
|
||||||
|
|
||||||
func runPProfActivityTrace(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
|
||||||
log := log.New(os.Stderr, "", 0)
|
|
||||||
|
|
||||||
die := func() {
|
|
||||||
log.Printf("exiting after error")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) != 1 {
|
|
||||||
log.Printf("exactly one positional argument is required")
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
url := "ws://" + args[0] + "/debug/zrepl/activity-trace" // FIXME dont' repeat that
|
|
||||||
|
|
||||||
log.Printf("attaching to activity trace stream %s", url)
|
|
||||||
ws, err := websocket.Dial(url, "", url)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(os.Stdout, ws)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
)
|
|
||||||
|
|
||||||
var pprofListenCmd struct {
|
|
||||||
daemon.PprofServerControlMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
var PprofListenCmd = &cli.Subcommand{
|
|
||||||
Use: "listen off | [on TCP_LISTEN_ADDRESS]",
|
|
||||||
Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS",
|
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
|
||||||
if len(args) < 1 {
|
|
||||||
goto enargs
|
|
||||||
}
|
|
||||||
switch args[0] {
|
|
||||||
case "on":
|
|
||||||
pprofListenCmd.Run = true
|
|
||||||
if len(args) != 2 {
|
|
||||||
return errors.New("must specify TCP_LISTEN_ADDRESS as second positional argument")
|
|
||||||
}
|
|
||||||
pprofListenCmd.HttpListenAddress = args[1]
|
|
||||||
case "off":
|
|
||||||
if len(args) != 1 {
|
|
||||||
goto enargs
|
|
||||||
}
|
|
||||||
pprofListenCmd.Run = false
|
|
||||||
}
|
|
||||||
|
|
||||||
RunPProf(subcommand.Config())
|
|
||||||
return nil
|
|
||||||
enargs:
|
|
||||||
return errors.New("invalid number of positional arguments")
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunPProf(conf *config.Config) {
|
|
||||||
log := log.New(os.Stderr, "", 0)
|
|
||||||
|
|
||||||
die := func() {
|
|
||||||
log.Printf("exiting after error")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("connecting to zrepl daemon")
|
|
||||||
|
|
||||||
httpc, err := controlHttpClient(conf.Global.Control.SockPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error creating http client: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPProf, pprofListenCmd.PprofServerControlMsg, struct{}{})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error sending control message: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
log.Printf("finished")
|
|
||||||
}
|
|
||||||
+1
-3
@@ -1,8 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
@@ -13,7 +11,7 @@ import (
|
|||||||
var SignalCmd = &cli.Subcommand{
|
var SignalCmd = &cli.Subcommand{
|
||||||
Use: "signal [wakeup|reset] JOB",
|
Use: "signal [wakeup|reset] JOB",
|
||||||
Short: "wake up a job from wait state or abort its current invocation",
|
Short: "wake up a job from wait state or abort its current invocation",
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||||
return runSignalCmd(subcommand.Config(), args)
|
return runSignalCmd(subcommand.Config(), args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,810 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
type bytesProgressHistory struct {
|
||||||
|
last *byteProgressMeasurement // pointer as poor man's optional
|
||||||
|
changeCount int
|
||||||
|
lastChange time.Time
|
||||||
|
bpsAvg float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
||||||
|
|
||||||
|
if p.last == nil {
|
||||||
|
p.last = &byteProgressMeasurement{
|
||||||
|
time: time.Now(),
|
||||||
|
val: currentVal,
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.last.val != currentVal {
|
||||||
|
p.changeCount++
|
||||||
|
p.lastChange = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(p.lastChange) > 3*time.Second {
|
||||||
|
p.last = nil
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
deltaV := currentVal - p.last.val
|
||||||
|
deltaT := time.Since(p.last.time)
|
||||||
|
rate := float64(deltaV) / deltaT.Seconds()
|
||||||
|
|
||||||
|
factor := 0.3
|
||||||
|
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
||||||
|
|
||||||
|
p.last.time = time.Now()
|
||||||
|
p.last.val = currentVal
|
||||||
|
|
||||||
|
return int64(p.bpsAvg), p.changeCount
|
||||||
|
}
|
||||||
|
|
||||||
|
type tui struct {
|
||||||
|
x, y int
|
||||||
|
indent int
|
||||||
|
|
||||||
|
lock sync.Mutex //For report and error
|
||||||
|
report map[string]job.Status
|
||||||
|
err error
|
||||||
|
|
||||||
|
jobFilter string
|
||||||
|
|
||||||
|
replicationProgress map[string]*bytesProgressHistory // by job name
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTui() tui {
|
||||||
|
return tui{
|
||||||
|
replicationProgress: make(map[string]*bytesProgressHistory),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const INDENT_MULTIPLIER = 4
|
||||||
|
|
||||||
|
func (t *tui) moveLine(dl int, col int) {
|
||||||
|
t.y += dl
|
||||||
|
t.x = t.indent*INDENT_MULTIPLIER + col
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) write(text string) {
|
||||||
|
for _, c := range text {
|
||||||
|
if c == '\n' {
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
termbox.SetCell(t.x, t.y, c, termbox.ColorDefault, termbox.ColorDefault)
|
||||||
|
t.x += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) printf(text string, a ...interface{}) {
|
||||||
|
t.write(fmt.Sprintf(text, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrap(s string, width int) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for len(s) > 0 {
|
||||||
|
rem := width
|
||||||
|
if rem > len(s) {
|
||||||
|
rem = len(s)
|
||||||
|
}
|
||||||
|
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
|
||||||
|
rem = idx + 1
|
||||||
|
}
|
||||||
|
untilNewline := strings.TrimRight(s[:rem], "\n\r")
|
||||||
|
s = s[rem:]
|
||||||
|
if len(untilNewline) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString(untilNewline)
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return strings.TrimRight(b.String(), "\n\r")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) {
|
||||||
|
whole := fmt.Sprintf(format, a...)
|
||||||
|
width, _ := termbox.Size()
|
||||||
|
if !strings.ContainsAny(whole, "\n\r") && t.x+len(whole) <= width {
|
||||||
|
t.printf(format, a...)
|
||||||
|
} else {
|
||||||
|
t.addIndent(1)
|
||||||
|
t.newline()
|
||||||
|
t.write(wrap(whole, width-INDENT_MULTIPLIER*t.indent))
|
||||||
|
t.addIndent(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) newline() {
|
||||||
|
t.moveLine(1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) setIndent(indent int) {
|
||||||
|
t.indent = indent
|
||||||
|
t.moveLine(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) addIndent(indent int) {
|
||||||
|
t.indent += indent
|
||||||
|
t.moveLine(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusFlags struct {
|
||||||
|
Raw bool
|
||||||
|
Job string
|
||||||
|
}
|
||||||
|
|
||||||
|
var StatusCmd = &cli.Subcommand{
|
||||||
|
Use: "status",
|
||||||
|
Short: "show job activity or dump as JSON for monitoring",
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
||||||
|
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
||||||
|
},
|
||||||
|
Run: runStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStatus(s *cli.Subcommand, args []string) error {
|
||||||
|
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusFlags.Raw {
|
||||||
|
resp, err := httpc.Get("http://unix" + daemon.ControlJobEndpointStatus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
||||||
|
_, err := io.CopyN(os.Stderr, resp.Body, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errors.Errorf("exit")
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := newTui()
|
||||||
|
t.lock.Lock()
|
||||||
|
t.err = errors.New("Got no report yet")
|
||||||
|
t.lock.Unlock()
|
||||||
|
t.jobFilter = statusFlags.Job
|
||||||
|
|
||||||
|
err = termbox.Init()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer termbox.Close()
|
||||||
|
|
||||||
|
update := func() {
|
||||||
|
m := make(map[string]job.Status)
|
||||||
|
|
||||||
|
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
||||||
|
struct{}{},
|
||||||
|
&m,
|
||||||
|
)
|
||||||
|
|
||||||
|
t.lock.Lock()
|
||||||
|
t.err = err2
|
||||||
|
t.report = m
|
||||||
|
t.lock.Unlock()
|
||||||
|
t.draw()
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
go func() {
|
||||||
|
for range ticker.C {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
termbox.HideCursor()
|
||||||
|
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
switch ev := termbox.PollEvent(); ev.Type {
|
||||||
|
case termbox.EventKey:
|
||||||
|
switch ev.Key {
|
||||||
|
case termbox.KeyEsc:
|
||||||
|
break loop
|
||||||
|
case termbox.KeyCtrlC:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
case termbox.EventResize:
|
||||||
|
t.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
|
||||||
|
p, ok := t.replicationProgress[jobName]
|
||||||
|
if !ok {
|
||||||
|
p = &bytesProgressHistory{}
|
||||||
|
t.replicationProgress[jobName] = p
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) draw() {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
||||||
|
t.x = 0
|
||||||
|
t.y = 0
|
||||||
|
t.indent = 0
|
||||||
|
|
||||||
|
if t.err != nil {
|
||||||
|
t.write(t.err.Error())
|
||||||
|
} else {
|
||||||
|
//Iterate over map in alphabetical order
|
||||||
|
keys := make([]string, 0, len(t.report))
|
||||||
|
for k := range t.report {
|
||||||
|
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t.jobFilter != "" && k != t.jobFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
if len(keys) == 0 {
|
||||||
|
t.setIndent(0)
|
||||||
|
t.printf("no jobs to display")
|
||||||
|
t.newline()
|
||||||
|
termbox.Flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
v := t.report[k]
|
||||||
|
|
||||||
|
t.setIndent(0)
|
||||||
|
|
||||||
|
t.printf("Job: %s", k)
|
||||||
|
t.setIndent(1)
|
||||||
|
t.newline()
|
||||||
|
t.printf("Type: %s", v.Type)
|
||||||
|
t.setIndent(1)
|
||||||
|
t.newline()
|
||||||
|
|
||||||
|
if v.Type == job.TypePush || v.Type == job.TypePull {
|
||||||
|
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
||||||
|
if !ok || activeStatus == nil {
|
||||||
|
t.printf("ActiveSideStatus is null")
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.printf("Replication:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgresHistory(k))
|
||||||
|
t.addIndent(-1)
|
||||||
|
|
||||||
|
t.printf("Pruning Sender:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderPrunerReport(activeStatus.PruningSender)
|
||||||
|
t.addIndent(-1)
|
||||||
|
|
||||||
|
t.printf("Pruning Receiver:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderPrunerReport(activeStatus.PruningReceiver)
|
||||||
|
t.addIndent(-1)
|
||||||
|
|
||||||
|
if v.Type == job.TypePush {
|
||||||
|
t.printf("Snapshotting:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderSnapperReport(activeStatus.Snapshotting)
|
||||||
|
t.addIndent(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if v.Type == job.TypeSnap {
|
||||||
|
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||||
|
if !ok || snapStatus == nil {
|
||||||
|
t.printf("SnapJobStatus is null")
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.printf("Pruning snapshots:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderPrunerReport(snapStatus.Pruning)
|
||||||
|
t.addIndent(-1)
|
||||||
|
t.printf("Snapshotting:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderSnapperReport(snapStatus.Snapshotting)
|
||||||
|
t.addIndent(-1)
|
||||||
|
} else if v.Type == job.TypeSource {
|
||||||
|
|
||||||
|
st := v.JobSpecific.(*job.PassiveStatus)
|
||||||
|
t.printf("Snapshotting:\n")
|
||||||
|
t.addIndent(1)
|
||||||
|
t.renderSnapperReport(st.Snapper)
|
||||||
|
t.addIndent(-1)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||||
|
t.newline()
|
||||||
|
asYaml, err := yaml.Marshal(v.JobSpecific)
|
||||||
|
if err != nil {
|
||||||
|
t.printf("Error marshaling status to YAML: %s", err)
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.write(string(asYaml))
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
termbox.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgressHistory) {
|
||||||
|
if rep == nil {
|
||||||
|
t.printf("...\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rep.WaitReconnectError != nil {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
if !rep.WaitReconnectSince.IsZero() {
|
||||||
|
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
||||||
|
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
||||||
|
var until string
|
||||||
|
if rep.WaitReconnectUntil.IsZero() {
|
||||||
|
until = "waiting indefinitely"
|
||||||
|
} else {
|
||||||
|
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
||||||
|
}
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
||||||
|
rep.WaitReconnectSince, until)
|
||||||
|
} else {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO visualize more than the latest attempt by folding all attempts into one
|
||||||
|
if len(rep.Attempts) == 0 {
|
||||||
|
t.printf("no attempts made yet")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
t.printf("Attempt #%d", len(rep.Attempts))
|
||||||
|
if len(rep.Attempts) > 1 {
|
||||||
|
t.printf(". Previous attempts failed with the following statuses:")
|
||||||
|
t.newline()
|
||||||
|
t.addIndent(1)
|
||||||
|
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
t.addIndent(-1)
|
||||||
|
} else {
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latest := rep.Attempts[len(rep.Attempts)-1]
|
||||||
|
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
||||||
|
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
||||||
|
})
|
||||||
|
|
||||||
|
t.printf("Status: %s", latest.State)
|
||||||
|
t.newline()
|
||||||
|
if latest.State == report.AttemptPlanningError {
|
||||||
|
t.printf("Problem: ")
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
||||||
|
t.newline()
|
||||||
|
} else if latest.State == report.AttemptFanOutError {
|
||||||
|
t.printf("Problem: one or more of the filesystems encountered errors")
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||||
|
// Draw global progress bar
|
||||||
|
// Progress: [---------------]
|
||||||
|
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||||
|
rate, changeCount := history.Update(replicated)
|
||||||
|
eta := time.Duration(0)
|
||||||
|
if rate > 0 {
|
||||||
|
eta = time.Duration((expected-replicated)/rate) * time.Second
|
||||||
|
}
|
||||||
|
t.write("Progress: ")
|
||||||
|
t.drawBar(50, replicated, expected, changeCount)
|
||||||
|
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
||||||
|
if eta != 0 {
|
||||||
|
t.write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
if containsInvalidSizeEstimates {
|
||||||
|
t.write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxFSLen int
|
||||||
|
for _, fs := range latest.Filesystems {
|
||||||
|
if len(fs.Info.Name) > maxFSLen {
|
||||||
|
maxFSLen = len(fs.Info.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fs := range latest.Filesystems {
|
||||||
|
t.printFilesystemStatus(fs, false, maxFSLen) // FIXME bring 'active' flag back
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||||
|
if r == nil {
|
||||||
|
t.printf("...\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := pruner.StateString(r.State)
|
||||||
|
if err != nil {
|
||||||
|
t.printf("Status: %q (parse error: %q)\n", r.State, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.printf("Status: %s", state)
|
||||||
|
t.newline()
|
||||||
|
|
||||||
|
if r.Error != "" {
|
||||||
|
t.printf("Error: %s\n", r.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commonFS struct {
|
||||||
|
*pruner.FSReport
|
||||||
|
completed bool
|
||||||
|
}
|
||||||
|
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
||||||
|
for i := range r.Pending {
|
||||||
|
all = append(all, commonFS{&r.Pending[i], false})
|
||||||
|
}
|
||||||
|
for i := range r.Completed {
|
||||||
|
all = append(all, commonFS{&r.Completed[i], true})
|
||||||
|
}
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case pruner.Plan:
|
||||||
|
fallthrough
|
||||||
|
case pruner.PlanErr:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(all) == 0 {
|
||||||
|
t.printf("nothing to do\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalDestroyCount, completedDestroyCount int
|
||||||
|
var maxFSname int
|
||||||
|
for _, fs := range all {
|
||||||
|
totalDestroyCount += len(fs.DestroyList)
|
||||||
|
if fs.completed {
|
||||||
|
completedDestroyCount += len(fs.DestroyList)
|
||||||
|
}
|
||||||
|
if maxFSname < len(fs.Filesystem) {
|
||||||
|
maxFSname = len(fs.Filesystem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// global progress bar
|
||||||
|
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
||||||
|
t.write("Progress: ")
|
||||||
|
t.write("[")
|
||||||
|
t.write(times("=", progress))
|
||||||
|
t.write(">")
|
||||||
|
t.write(times("-", 80-progress))
|
||||||
|
t.write("]")
|
||||||
|
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||||
|
t.newline()
|
||||||
|
|
||||||
|
sort.SliceStable(all, func(i, j int) bool {
|
||||||
|
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw a table-like representation of 'all'
|
||||||
|
for _, fs := range all {
|
||||||
|
t.write(rightPad(fs.Filesystem, maxFSname, " "))
|
||||||
|
t.write(" ")
|
||||||
|
if !fs.SkipReason.NotSkipped() {
|
||||||
|
t.printf("skipped: %s\n", fs.SkipReason)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fs.LastError != "" {
|
||||||
|
if strings.ContainsAny(fs.LastError, "\r\n") {
|
||||||
|
t.printf("ERROR:")
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
||||||
|
} else {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
||||||
|
len(fs.DestroyList), len(fs.SnapshotList))
|
||||||
|
|
||||||
|
if fs.completed {
|
||||||
|
t.printf("Completed %s\n", pruneRuleActionStr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.write("Pending ") // whitespace is padding 10
|
||||||
|
if len(fs.DestroyList) == 1 {
|
||||||
|
t.write(fs.DestroyList[0].Name)
|
||||||
|
} else {
|
||||||
|
t.write(pruneRuleActionStr)
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) renderSnapperReport(r *snapper.Report) {
|
||||||
|
if r == nil {
|
||||||
|
t.printf("<snapshot type does not have a report>\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.printf("Status: %s", r.State)
|
||||||
|
t.newline()
|
||||||
|
|
||||||
|
if r.Error != "" {
|
||||||
|
t.printf("Error: %s\n", r.Error)
|
||||||
|
}
|
||||||
|
if !r.SleepUntil.IsZero() {
|
||||||
|
t.printf("Sleep until: %s\n", r.SleepUntil)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(r.Progress, func(i, j int) bool {
|
||||||
|
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
t.addIndent(1)
|
||||||
|
defer t.addIndent(-1)
|
||||||
|
dur := func(d time.Duration) string {
|
||||||
|
return d.Round(100 * time.Millisecond).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
path, state, duration, remainder, hookReport string
|
||||||
|
}
|
||||||
|
var widths struct {
|
||||||
|
path, state, duration int
|
||||||
|
}
|
||||||
|
rows := make([]*row, len(r.Progress))
|
||||||
|
for i, fs := range r.Progress {
|
||||||
|
r := &row{
|
||||||
|
path: fs.Path,
|
||||||
|
state: fs.State.String(),
|
||||||
|
}
|
||||||
|
if fs.HooksHadError {
|
||||||
|
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||||
|
}
|
||||||
|
switch fs.State {
|
||||||
|
case snapper.SnapPending:
|
||||||
|
r.duration = "..."
|
||||||
|
r.remainder = ""
|
||||||
|
case snapper.SnapStarted:
|
||||||
|
r.duration = dur(time.Since(fs.StartAt))
|
||||||
|
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||||
|
case snapper.SnapDone:
|
||||||
|
fallthrough
|
||||||
|
case snapper.SnapError:
|
||||||
|
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||||
|
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||||
|
}
|
||||||
|
rows[i] = r
|
||||||
|
if len(r.path) > widths.path {
|
||||||
|
widths.path = len(r.path)
|
||||||
|
}
|
||||||
|
if len(r.state) > widths.state {
|
||||||
|
widths.state = len(r.state)
|
||||||
|
}
|
||||||
|
if len(r.duration) > widths.duration {
|
||||||
|
widths.duration = len(r.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range rows {
|
||||||
|
path := rightPad(r.path, widths.path, " ")
|
||||||
|
state := rightPad(r.state, widths.state, " ")
|
||||||
|
duration := rightPad(r.duration, widths.duration, " ")
|
||||||
|
t.printf("%s %s %s", path, state, duration)
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||||
|
if r.hookReport != "" {
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||||
|
}
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func times(str string, n int) (out string) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out += str
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func rightPad(str string, length int, pad string) string {
|
||||||
|
if len(str) > length {
|
||||||
|
return str[:length]
|
||||||
|
}
|
||||||
|
return str + strings.Repeat(pad, length-len(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
var arrowPositions = `>\|/`
|
||||||
|
|
||||||
|
// changeCount = 0 indicates stall / no progresss
|
||||||
|
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||||
|
var completedLength int
|
||||||
|
if totalBytes > 0 {
|
||||||
|
completedLength = int(int64(length) * bytes / totalBytes)
|
||||||
|
if completedLength > length {
|
||||||
|
completedLength = length
|
||||||
|
}
|
||||||
|
} else if totalBytes == bytes {
|
||||||
|
completedLength = length
|
||||||
|
}
|
||||||
|
|
||||||
|
t.write("[")
|
||||||
|
t.write(times("=", completedLength))
|
||||||
|
t.write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
||||||
|
t.write(times("-", length-completedLength))
|
||||||
|
t.write("]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
||||||
|
|
||||||
|
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||||
|
sizeEstimationImpreciseNotice := ""
|
||||||
|
if containsInvalidSizeEstimates {
|
||||||
|
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
||||||
|
}
|
||||||
|
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
||||||
|
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
||||||
|
}
|
||||||
|
|
||||||
|
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
||||||
|
strings.ToUpper(string(rep.State)),
|
||||||
|
rep.CurrentStep, len(rep.Steps),
|
||||||
|
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||||
|
sizeEstimationImpreciseNotice,
|
||||||
|
)
|
||||||
|
|
||||||
|
activeIndicator := " "
|
||||||
|
if active {
|
||||||
|
activeIndicator = "*"
|
||||||
|
}
|
||||||
|
t.printf("%s %s %s ",
|
||||||
|
activeIndicator,
|
||||||
|
rightPad(rep.Info.Name, maxFS, " "),
|
||||||
|
status)
|
||||||
|
|
||||||
|
next := ""
|
||||||
|
if err := rep.Error(); err != nil {
|
||||||
|
next = err.Err
|
||||||
|
} else if rep.State != report.FilesystemDone {
|
||||||
|
if nextStep := rep.NextStep(); nextStep != nil {
|
||||||
|
if nextStep.IsIncremental() {
|
||||||
|
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||||
|
} else {
|
||||||
|
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
||||||
|
}
|
||||||
|
attribs := []string{}
|
||||||
|
|
||||||
|
if nextStep.Info.Resumed {
|
||||||
|
attribs = append(attribs, "resumed")
|
||||||
|
}
|
||||||
|
|
||||||
|
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||||
|
|
||||||
|
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||||
|
} else {
|
||||||
|
next = "" // individual FSes may still be in planning state
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
t.printfDrawIndentedAndWrappedIfMultiline("%s", next)
|
||||||
|
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ByteCountBinary(b int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if b < unit {
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanizeDuration(duration time.Duration) string {
|
||||||
|
days := int64(duration.Hours() / 24)
|
||||||
|
hours := int64(math.Mod(duration.Hours(), 24))
|
||||||
|
minutes := int64(math.Mod(duration.Minutes(), 60))
|
||||||
|
seconds := int64(math.Mod(duration.Seconds(), 60))
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
force := false
|
||||||
|
chunks := []int64{days, hours, minutes, seconds}
|
||||||
|
for i, chunk := range chunks {
|
||||||
|
if force || chunk > 0 {
|
||||||
|
padding := 0
|
||||||
|
if force {
|
||||||
|
padding = 2
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
h *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(network, addr string) (*Client, error) {
|
|
||||||
httpc, err := makeControlHttpClient(func(_ context.Context) (net.Conn, error) { return net.Dial(network, addr) })
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Client{httpc}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Status() (s daemon.Status, _ error) {
|
|
||||||
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus,
|
|
||||||
struct{}{},
|
|
||||||
&s,
|
|
||||||
)
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) StatusRaw() ([]byte, error) {
|
|
||||||
var r json.RawMessage
|
|
||||||
err := jsonRequestResponse(c.h, daemon.ControlJobEndpointStatus, struct{}{}, &r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) signal(job, sig string) error {
|
|
||||||
return jsonRequestResponse(c.h, daemon.ControlJobEndpointSignal,
|
|
||||||
struct {
|
|
||||||
Name string
|
|
||||||
Op string
|
|
||||||
}{
|
|
||||||
Name: job,
|
|
||||||
Op: sig,
|
|
||||||
},
|
|
||||||
struct{}{},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SignalWakeup(job string) error {
|
|
||||||
return c.signal(job, "wakeup")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SignalReset(job string) error {
|
|
||||||
return c.signal(job, "reset")
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeControlHttpClient(dialfunc func(context.Context) (net.Conn, error)) (client *http.Client, err error) {
|
|
||||||
return &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
return dialfunc(ctx)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonRequestResponse(c *http.Client, endpoint string, req interface{}, res interface{}) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
encodeErr := json.NewEncoder(&buf).Encode(req)
|
|
||||||
if encodeErr != nil {
|
|
||||||
return encodeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
hreq, err := http.NewRequest("POST", "http://unix"+endpoint, &buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
hreq.Header.Set("Content-Type", "application/json")
|
|
||||||
// Prevent EOF errors when client request frequency and server keepalive close are at the same time.
|
|
||||||
// Found this by watching http.Server.ConnState changes, then found
|
|
||||||
// https://stackoverflow.com/questions/17714494/golang-http-request-results-in-eof-errors-when-making-multiple-requests-successi
|
|
||||||
// Note: The issue seems even more prounounced with local TCP sockets than unix domain sockets. So, I used that for debugging.
|
|
||||||
hreq.Close = true
|
|
||||||
resp, err := c.Do(hreq)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
var msg bytes.Buffer
|
|
||||||
_, _ = io.CopyN(&msg, resp.Body, 4096) // ignore error, just display what we got
|
|
||||||
return errors.Errorf("%s", msg.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
decodeError := json.NewDecoder(resp.Body).Decode(&res)
|
|
||||||
if decodeError != nil {
|
|
||||||
return decodeError
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/client/status/client"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
"github.com/zrepl/zrepl/util/choices"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
Status() (daemon.Status, error)
|
|
||||||
StatusRaw() ([]byte, error)
|
|
||||||
SignalWakeup(job string) error
|
|
||||||
SignalReset(job string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type statusFlags struct {
|
|
||||||
Mode choices.Choices
|
|
||||||
Job string
|
|
||||||
Delay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusv2Flags statusFlags
|
|
||||||
|
|
||||||
type statusv2Mode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
StatusV2ModeInteractive statusv2Mode = 1 + iota
|
|
||||||
StatusV2ModeDump
|
|
||||||
StatusV2ModeRaw
|
|
||||||
StatusV2ModeLegacy
|
|
||||||
)
|
|
||||||
|
|
||||||
var Subcommand = &cli.Subcommand{
|
|
||||||
Use: "status",
|
|
||||||
Short: "retrieve & display daemon status information",
|
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
|
||||||
statusv2Flags.Mode.Init(
|
|
||||||
"interactive", StatusV2ModeInteractive,
|
|
||||||
"dump", StatusV2ModeDump,
|
|
||||||
"raw", StatusV2ModeRaw,
|
|
||||||
"legacy", StatusV2ModeLegacy,
|
|
||||||
)
|
|
||||||
statusv2Flags.Mode.SetTypeString("mode")
|
|
||||||
statusv2Flags.Mode.SetDefaultValue(StatusV2ModeInteractive)
|
|
||||||
f.Var(&statusv2Flags.Mode, "mode", statusv2Flags.Mode.Usage())
|
|
||||||
f.StringVar(&statusv2Flags.Job, "job", "", "only show specified job (works in \"dump\" and \"interactive\" mode)")
|
|
||||||
f.DurationVarP(&statusv2Flags.Delay, "delay", "d", 1*time.Second, "use -d 3s for 3 seconds delay (minimum delay is 1s)")
|
|
||||||
},
|
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
|
||||||
return runStatusV2Command(ctx, subcommand.Config(), args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runStatusV2Command(ctx context.Context, config *config.Config, args []string) error {
|
|
||||||
|
|
||||||
c, err := client.New("unix", config.Global.Control.SockPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "connect to daemon socket at %q", config.Global.Control.SockPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
|
||||||
|
|
||||||
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump && mode != StatusV2ModeRaw {
|
|
||||||
dumpmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
rawmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeRaw)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return errors.Errorf("error: stdout is not a tty, please use --mode %s or --mode %s", dumpmode, rawmode)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch mode {
|
|
||||||
case StatusV2ModeInteractive:
|
|
||||||
return interactive(c, statusv2Flags)
|
|
||||||
case StatusV2ModeDump:
|
|
||||||
return dump(c, statusv2Flags.Job)
|
|
||||||
case StatusV2ModeRaw:
|
|
||||||
return raw(c)
|
|
||||||
case StatusV2ModeLegacy:
|
|
||||||
return legacy(c, statusv2Flags)
|
|
||||||
default:
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dump(c Client, job string) error {
|
|
||||||
s, err := c.Status()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if job != "" {
|
|
||||||
if _, ok := s.Jobs[job]; !ok {
|
|
||||||
return errors.Errorf("job %q not found", job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width := (1 << 31) - 1
|
|
||||||
wrap := false
|
|
||||||
hline := strings.Repeat("-", 80)
|
|
||||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
|
||||||
wrap = true
|
|
||||||
screen, err := tcell.NewScreen()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "get terminal dimensions")
|
|
||||||
}
|
|
||||||
if err := screen.Init(); err != nil {
|
|
||||||
return errors.Wrap(err, "init screen")
|
|
||||||
}
|
|
||||||
width, _ = screen.Size()
|
|
||||||
screen.Fini()
|
|
||||||
hline = strings.Repeat("-", width)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := viewmodel.New()
|
|
||||||
params := viewmodel.Params{
|
|
||||||
Report: s.Jobs,
|
|
||||||
ReportFetchError: nil,
|
|
||||||
SelectedJob: nil,
|
|
||||||
FSFilter: func(s string) bool { return true },
|
|
||||||
DetailViewWidth: width,
|
|
||||||
DetailViewWrap: wrap,
|
|
||||||
ShortKeybindingOverview: "",
|
|
||||||
}
|
|
||||||
m.Update(params)
|
|
||||||
for _, j := range m.Jobs() {
|
|
||||||
if job != "" && j.Name() != job {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
params.SelectedJob = j
|
|
||||||
m.Update(params)
|
|
||||||
fmt.Println(m.SelectedJob().FullDescription())
|
|
||||||
if job != "" {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
fmt.Println(hline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
|
||||||
tview "gitlab.com/tslocum/cview"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func interactive(c Client, flag statusFlags) error {
|
|
||||||
|
|
||||||
// Set this so we don't overwrite the default terminal colors
|
|
||||||
// See https://github.com/rivo/tview/blob/master/styles.go
|
|
||||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
|
||||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
|
||||||
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
|
||||||
tview.Styles.BorderColor = tcell.ColorDefault
|
|
||||||
app := tview.NewApplication()
|
|
||||||
|
|
||||||
jobDetailSplit := tview.NewFlex()
|
|
||||||
jobMenu := tview.NewTreeView()
|
|
||||||
jobMenuRoot := tview.NewTreeNode("jobs")
|
|
||||||
jobMenuRoot.SetSelectable(true)
|
|
||||||
jobMenu.SetRoot(jobMenuRoot)
|
|
||||||
jobMenu.SetCurrentNode(jobMenuRoot)
|
|
||||||
jobMenu.SetSelectedTextColor(tcell.ColorGreen)
|
|
||||||
jobTextDetail := tview.NewTextView()
|
|
||||||
jobTextDetail.SetWrap(false)
|
|
||||||
|
|
||||||
jobMenu.SetBorder(true)
|
|
||||||
jobTextDetail.SetBorder(true)
|
|
||||||
|
|
||||||
toolbarSplit := tview.NewFlex()
|
|
||||||
toolbarSplit.SetDirection(tview.FlexRow)
|
|
||||||
inputBarContainer := tview.NewFlex()
|
|
||||||
fsFilterInput := tview.NewInputField()
|
|
||||||
fsFilterInput.SetBorder(false)
|
|
||||||
fsFilterInput.SetFieldBackgroundColor(tcell.ColorDefault)
|
|
||||||
inputBarLabel := tview.NewTextView()
|
|
||||||
inputBarLabel.SetText("[::b]FILTER ")
|
|
||||||
inputBarLabel.SetDynamicColors(true)
|
|
||||||
inputBarContainer.AddItem(inputBarLabel, 7, 1, false)
|
|
||||||
inputBarContainer.AddItem(fsFilterInput, 0, 10, false)
|
|
||||||
toolbarSplit.AddItem(inputBarContainer, 1, 0, false)
|
|
||||||
toolbarSplit.AddItem(jobDetailSplit, 0, 10, false)
|
|
||||||
|
|
||||||
bottombar := tview.NewFlex()
|
|
||||||
bottombar.SetDirection(tview.FlexColumn)
|
|
||||||
bottombarDateView := tview.NewTextView()
|
|
||||||
bottombar.AddItem(bottombarDateView, len(time.Now().String()), 0, false)
|
|
||||||
bottomBarStatus := tview.NewTextView()
|
|
||||||
bottomBarStatus.SetDynamicColors(true)
|
|
||||||
bottomBarStatus.SetTextAlign(tview.AlignRight)
|
|
||||||
bottombar.AddItem(bottomBarStatus, 0, 10, false)
|
|
||||||
toolbarSplit.AddItem(bottombar, 1, 0, false)
|
|
||||||
|
|
||||||
tabbableWithJobMenu := []tview.Primitive{jobMenu, jobTextDetail, fsFilterInput}
|
|
||||||
tabbableWithoutJobMenu := []tview.Primitive{jobTextDetail, fsFilterInput}
|
|
||||||
var tabbable []tview.Primitive
|
|
||||||
tabbableActiveIndex := 0
|
|
||||||
tabbableRedraw := func() {
|
|
||||||
if len(tabbable) == 0 {
|
|
||||||
app.SetFocus(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tabbableActiveIndex >= len(tabbable) {
|
|
||||||
app.SetFocus(tabbable[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.SetFocus(tabbable[tabbableActiveIndex])
|
|
||||||
}
|
|
||||||
tabbableCycle := func() {
|
|
||||||
if len(tabbable) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tabbableActiveIndex = (tabbableActiveIndex + 1) % len(tabbable)
|
|
||||||
app.SetFocus(tabbable[tabbableActiveIndex])
|
|
||||||
tabbableRedraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
jobMenuVisisble := false
|
|
||||||
reconfigureJobDetailSplit := func(setJobMenuVisible bool) {
|
|
||||||
if jobMenuVisisble == setJobMenuVisible {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jobMenuVisisble = setJobMenuVisible
|
|
||||||
if setJobMenuVisible {
|
|
||||||
jobDetailSplit.RemoveItem(jobTextDetail)
|
|
||||||
jobDetailSplit.AddItem(jobMenu, 0, 1, true)
|
|
||||||
jobDetailSplit.AddItem(jobTextDetail, 0, 5, false)
|
|
||||||
tabbable = tabbableWithJobMenu
|
|
||||||
} else {
|
|
||||||
jobDetailSplit.RemoveItem(jobMenu)
|
|
||||||
tabbable = tabbableWithoutJobMenu
|
|
||||||
}
|
|
||||||
tabbableRedraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
showModal := func(m *tview.Modal, modalDoneFunc func(idx int, label string)) {
|
|
||||||
preModalFocus := app.GetFocus()
|
|
||||||
m.SetDoneFunc(func(idx int, label string) {
|
|
||||||
if modalDoneFunc != nil {
|
|
||||||
modalDoneFunc(idx, label)
|
|
||||||
}
|
|
||||||
app.SetRoot(toolbarSplit, true)
|
|
||||||
app.SetFocus(preModalFocus)
|
|
||||||
app.Draw()
|
|
||||||
})
|
|
||||||
app.SetRoot(m, true)
|
|
||||||
app.Draw()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.SetRoot(toolbarSplit, true)
|
|
||||||
// initial focus
|
|
||||||
tabbableActiveIndex = len(tabbable)
|
|
||||||
tabbableCycle()
|
|
||||||
reconfigureJobDetailSplit(true)
|
|
||||||
|
|
||||||
m := viewmodel.New()
|
|
||||||
params := &viewmodel.Params{
|
|
||||||
Report: nil,
|
|
||||||
SelectedJob: nil,
|
|
||||||
FSFilter: func(_ string) bool { return true },
|
|
||||||
DetailViewWidth: 100,
|
|
||||||
DetailViewWrap: false,
|
|
||||||
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]W[::-] wrap lines [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
|
||||||
}
|
|
||||||
paramsMtx := &sync.Mutex{}
|
|
||||||
var redraw func()
|
|
||||||
viewmodelupdate := func(cb func(*viewmodel.Params)) {
|
|
||||||
paramsMtx.Lock()
|
|
||||||
defer paramsMtx.Unlock()
|
|
||||||
cb(params)
|
|
||||||
m.Update(*params)
|
|
||||||
}
|
|
||||||
redraw = func() {
|
|
||||||
jobs := m.Jobs()
|
|
||||||
if flag.Job != "" {
|
|
||||||
job_found := false
|
|
||||||
for _, job := range jobs {
|
|
||||||
if strings.Compare(flag.Job, job.Name()) == 0 {
|
|
||||||
jobs = []*viewmodel.Job{job}
|
|
||||||
job_found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !job_found {
|
|
||||||
jobs = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
redrawJobsList := false
|
|
||||||
var selectedJobN *tview.TreeNode
|
|
||||||
if len(jobMenuRoot.GetChildren()) == len(jobs) {
|
|
||||||
for i, jobN := range jobMenuRoot.GetChildren() {
|
|
||||||
if jobN.GetReference().(*viewmodel.Job) != jobs[i] {
|
|
||||||
redrawJobsList = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if jobN.GetReference().(*viewmodel.Job) == m.SelectedJob() {
|
|
||||||
selectedJobN = jobN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
redrawJobsList = true
|
|
||||||
}
|
|
||||||
if redrawJobsList {
|
|
||||||
selectedJobN = nil
|
|
||||||
children := make([]*tview.TreeNode, len(jobs))
|
|
||||||
for i := range jobs {
|
|
||||||
jobN := tview.NewTreeNode(jobs[i].JobTreeTitle())
|
|
||||||
jobN.SetReference(jobs[i])
|
|
||||||
jobN.SetSelectable(true)
|
|
||||||
children[i] = jobN
|
|
||||||
jobN.SetSelectedFunc(func() {
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.SelectedJob = jobN.GetReference().(*viewmodel.Job)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if jobs[i] == m.SelectedJob() {
|
|
||||||
selectedJobN = jobN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jobMenuRoot.SetChildren(children)
|
|
||||||
}
|
|
||||||
|
|
||||||
if selectedJobN != nil && jobMenu.GetCurrentNode() != selectedJobN {
|
|
||||||
jobMenu.SetCurrentNode(selectedJobN)
|
|
||||||
} else if selectedJobN == nil {
|
|
||||||
// select something, otherwise selection breaks (likely bug in tview)
|
|
||||||
jobMenu.SetCurrentNode(jobMenuRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
if selJ := m.SelectedJob(); selJ != nil {
|
|
||||||
jobTextDetail.SetText(selJ.FullDescription())
|
|
||||||
} else {
|
|
||||||
jobTextDetail.SetText("please select a job")
|
|
||||||
}
|
|
||||||
|
|
||||||
bottombardatestring := m.DateString()
|
|
||||||
bottombarDateView.SetText(bottombardatestring)
|
|
||||||
bottombar.ResizeItem(bottombarDateView, len(bottombardatestring), 0)
|
|
||||||
|
|
||||||
bottomBarStatus.SetText(m.BottomBarStatus())
|
|
||||||
|
|
||||||
app.Draw()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
app.Suspend(func() {
|
|
||||||
panic(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
st, err := c.Status()
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.Report = st.Jobs
|
|
||||||
p.ReportFetchError = err
|
|
||||||
})
|
|
||||||
app.QueueUpdateDraw(redraw)
|
|
||||||
|
|
||||||
time.Sleep(flag.Delay)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
jobMenu.SetChangedFunc(func(jobN *tview.TreeNode) {
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.SelectedJob, _ = jobN.GetReference().(*viewmodel.Job)
|
|
||||||
})
|
|
||||||
redraw()
|
|
||||||
jobTextDetail.ScrollToBeginning()
|
|
||||||
})
|
|
||||||
jobMenu.SetSelectedFunc(func(jobN *tview.TreeNode) {
|
|
||||||
app.SetFocus(jobTextDetail)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
_, _, p.DetailViewWidth, _ = jobTextDetail.GetInnerRect()
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
app.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
|
||||||
if e.Key() == tcell.KeyTab {
|
|
||||||
tabbableCycle()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && app.GetFocus() == fsFilterInput {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && e.Rune() == '/' {
|
|
||||||
if app.GetFocus() != fsFilterInput {
|
|
||||||
app.SetFocus(fsFilterInput)
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && e.Rune() == 'M' {
|
|
||||||
reconfigureJobDetailSplit(!jobMenuVisisble)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && e.Rune() == 'q' {
|
|
||||||
app.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Key() == tcell.KeyRune && e.Rune() == 'S' {
|
|
||||||
job, ok := jobMenu.GetCurrentNode().GetReference().(*viewmodel.Job)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
signals := []string{"wakeup", "reset"}
|
|
||||||
clientFuncs := []func(job string) error{c.SignalWakeup, c.SignalReset}
|
|
||||||
sigMod := tview.NewModal()
|
|
||||||
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
|
||||||
sigMod.SetBorder(true)
|
|
||||||
sigMod.GetForm().SetButtonTextColorFocused(tcell.ColorGreen)
|
|
||||||
sigMod.AddButtons(signals)
|
|
||||||
sigMod.SetText(fmt.Sprintf("Send a signal to job %q", job.Name()))
|
|
||||||
showModal(sigMod, func(idx int, _ string) {
|
|
||||||
go func() {
|
|
||||||
if idx == -1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := clientFuncs[idx](job.Name())
|
|
||||||
if err != nil {
|
|
||||||
app.QueueUpdate(func() {
|
|
||||||
me := tview.NewModal()
|
|
||||||
me.SetText(fmt.Sprintf("signal error: %s", err))
|
|
||||||
me.AddButtons([]string{"Close"})
|
|
||||||
showModal(me, nil)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return e
|
|
||||||
})
|
|
||||||
|
|
||||||
fsFilterInput.SetChangedFunc(func(searchterm string) {
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.FSFilter = func(fs string) bool {
|
|
||||||
r, err := regexp.Compile(searchterm)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return r.MatchString(fs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
redraw()
|
|
||||||
jobTextDetail.ScrollToBeginning()
|
|
||||||
})
|
|
||||||
fsFilterInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
||||||
if event.Key() == tcell.KeyEnter {
|
|
||||||
app.SetFocus(jobTextDetail)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
})
|
|
||||||
|
|
||||||
jobTextDetail.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'w' {
|
|
||||||
// toggle wrapping
|
|
||||||
viewmodelupdate(func(p *viewmodel.Params) {
|
|
||||||
p.DetailViewWrap = !p.DetailViewWrap
|
|
||||||
})
|
|
||||||
redraw()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
})
|
|
||||||
|
|
||||||
return app.Run()
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
tview "gitlab.com/tslocum/cview"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/client/status/viewmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func legacy(c Client, flag statusFlags) error {
|
|
||||||
|
|
||||||
// Set this so we don't overwrite the default terminal colors
|
|
||||||
// See https://github.com/rivo/tview/blob/master/styles.go
|
|
||||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
|
||||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDefault
|
|
||||||
tview.Styles.PrimaryTextColor = tcell.ColorDefault
|
|
||||||
tview.Styles.BorderColor = tcell.ColorDefault
|
|
||||||
app := tview.NewApplication()
|
|
||||||
|
|
||||||
textView := tview.NewTextView()
|
|
||||||
textView.SetWrap(true)
|
|
||||||
textView.SetScrollable(true) // so that it allows us to set scroll position
|
|
||||||
textView.SetScrollBarVisibility(tview.ScrollBarNever)
|
|
||||||
|
|
||||||
app.SetRoot(textView, true)
|
|
||||||
|
|
||||||
width := (1 << 31) - 1
|
|
||||||
wrap := false
|
|
||||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
|
||||||
wrap = true
|
|
||||||
screen, err := tcell.NewScreen()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "get terminal dimensions")
|
|
||||||
}
|
|
||||||
if err := screen.Init(); err != nil {
|
|
||||||
return errors.Wrap(err, "init screen")
|
|
||||||
}
|
|
||||||
width, _ = screen.Size()
|
|
||||||
screen.Fini()
|
|
||||||
}
|
|
||||||
|
|
||||||
paramsMtx := &sync.Mutex{}
|
|
||||||
params := viewmodel.Params{
|
|
||||||
Report: nil,
|
|
||||||
ReportFetchError: nil,
|
|
||||||
SelectedJob: nil,
|
|
||||||
FSFilter: func(s string) bool { return true },
|
|
||||||
DetailViewWidth: width,
|
|
||||||
DetailViewWrap: wrap,
|
|
||||||
ShortKeybindingOverview: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw := func() {
|
|
||||||
textView.Clear()
|
|
||||||
|
|
||||||
paramsMtx.Lock()
|
|
||||||
defer paramsMtx.Unlock()
|
|
||||||
|
|
||||||
if params.ReportFetchError != nil {
|
|
||||||
fmt.Fprintln(textView, params.ReportFetchError.Error())
|
|
||||||
} else if params.Report != nil {
|
|
||||||
m := viewmodel.New()
|
|
||||||
m.Update(params)
|
|
||||||
for _, j := range m.Jobs() {
|
|
||||||
if flag.Job != "" && j.Name() != flag.Job {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
params.SelectedJob = j
|
|
||||||
m.Update(params)
|
|
||||||
fmt.Fprintln(textView, m.SelectedJob().FullDescription())
|
|
||||||
if flag.Job != "" {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
hline := strings.Repeat("-", params.DetailViewWidth)
|
|
||||||
fmt.Fprintln(textView, hline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(textView, "waiting for request results")
|
|
||||||
}
|
|
||||||
textView.ScrollToBeginning()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
|
|
||||||
// sync resizes to `params`
|
|
||||||
paramsMtx.Lock()
|
|
||||||
_, _, newWidth, _ := textView.GetInnerRect()
|
|
||||||
if newWidth != params.DetailViewWidth {
|
|
||||||
params.DetailViewWidth = newWidth
|
|
||||||
app.QueueUpdateDraw(redraw)
|
|
||||||
}
|
|
||||||
paramsMtx.Unlock()
|
|
||||||
|
|
||||||
textView.ScrollToBeginning() // has the effect of inhibiting user scrolls
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
app.Suspend(func() {
|
|
||||||
panic(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
st, err := c.Status()
|
|
||||||
paramsMtx.Lock()
|
|
||||||
params.Report = st.Jobs
|
|
||||||
params.ReportFetchError = err
|
|
||||||
paramsMtx.Unlock()
|
|
||||||
|
|
||||||
app.QueueUpdateDraw(redraw)
|
|
||||||
|
|
||||||
time.Sleep(flag.Delay)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return app.Run()
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func raw(c Client) error {
|
|
||||||
b, err := c.StatusRaw()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(os.Stdout, bytes.NewReader(b)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package viewmodel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ByteCountBinaryUint(b uint64) string {
|
|
||||||
if b > math.MaxInt64 {
|
|
||||||
panic(b)
|
|
||||||
}
|
|
||||||
return ByteCountBinary(int64(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ByteCountBinary(b int64) string {
|
|
||||||
const unit = 1024
|
|
||||||
if b < unit {
|
|
||||||
return fmt.Sprintf("%d B", b)
|
|
||||||
}
|
|
||||||
div, exp := unit, 0
|
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package viewmodel
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type byteProgressMeasurement struct {
|
|
||||||
time time.Time
|
|
||||||
val uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
type bytesProgressHistory struct {
|
|
||||||
last *byteProgressMeasurement // pointer as poor man's optional
|
|
||||||
changeCount int
|
|
||||||
lastChange time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *bytesProgressHistory) Update(currentVal uint64) (bytesPerSecondAvg int64, changeCount int) {
|
|
||||||
|
|
||||||
if p.last == nil {
|
|
||||||
p.last = &byteProgressMeasurement{
|
|
||||||
time: time.Now(),
|
|
||||||
val: currentVal,
|
|
||||||
}
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.last.val != currentVal {
|
|
||||||
p.changeCount++
|
|
||||||
p.lastChange = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Since(p.lastChange) > 3*time.Second {
|
|
||||||
p.last = nil
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var deltaV int64
|
|
||||||
if currentVal >= p.last.val {
|
|
||||||
deltaV = int64(currentVal - p.last.val)
|
|
||||||
} else {
|
|
||||||
deltaV = -int64(p.last.val - currentVal)
|
|
||||||
}
|
|
||||||
deltaT := time.Since(p.last.time)
|
|
||||||
rate := float64(deltaV) / deltaT.Seconds()
|
|
||||||
|
|
||||||
p.last.time = time.Now()
|
|
||||||
p.last.val = currentVal
|
|
||||||
|
|
||||||
return int64(rate), p.changeCount
|
|
||||||
}
|
|
||||||
@@ -1,656 +0,0 @@
|
|||||||
package viewmodel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
yaml "github.com/zrepl/yaml-config"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/client/status/viewmodel/stringbuilder"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
|
||||||
)
|
|
||||||
|
|
||||||
type M struct {
|
|
||||||
jobs map[string]*Job
|
|
||||||
jobsList []*Job
|
|
||||||
selectedJob *Job
|
|
||||||
dateString string
|
|
||||||
bottomBarStatus string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Job struct {
|
|
||||||
// long-lived
|
|
||||||
name string
|
|
||||||
byteProgress *bytesProgressHistory
|
|
||||||
|
|
||||||
lastStatus *job.Status
|
|
||||||
fulldescription string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *M {
|
|
||||||
return &M{
|
|
||||||
jobs: make(map[string]*Job),
|
|
||||||
jobsList: make([]*Job, 0),
|
|
||||||
selectedJob: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterFunc func(string) bool
|
|
||||||
|
|
||||||
type Params struct {
|
|
||||||
Report map[string]*job.Status
|
|
||||||
ReportFetchError error
|
|
||||||
SelectedJob *Job
|
|
||||||
FSFilter FilterFunc `validate:"required"`
|
|
||||||
DetailViewWidth int `validate:"gte=1"`
|
|
||||||
DetailViewWrap bool
|
|
||||||
ShortKeybindingOverview string
|
|
||||||
}
|
|
||||||
|
|
||||||
var validate = validator.New()
|
|
||||||
|
|
||||||
func (m *M) Update(p Params) {
|
|
||||||
|
|
||||||
if err := validate.Struct(p); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.ReportFetchError != nil {
|
|
||||||
m.bottomBarStatus = fmt.Sprintf("[red::]status fetch: %s", p.ReportFetchError)
|
|
||||||
} else {
|
|
||||||
m.bottomBarStatus = p.ShortKeybindingOverview
|
|
||||||
for jobname, st := range p.Report {
|
|
||||||
// TODO handle job renames & deletions
|
|
||||||
j, ok := m.jobs[jobname]
|
|
||||||
if !ok {
|
|
||||||
j = &Job{
|
|
||||||
name: jobname,
|
|
||||||
byteProgress: &bytesProgressHistory{},
|
|
||||||
}
|
|
||||||
m.jobs[jobname] = j
|
|
||||||
m.jobsList = append(m.jobsList, j)
|
|
||||||
}
|
|
||||||
j.lastStatus = st
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter out internal jobs
|
|
||||||
var jobsList []*Job
|
|
||||||
for _, j := range m.jobsList {
|
|
||||||
if daemon.IsInternalJobName(j.name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
jobsList = append(jobsList, j)
|
|
||||||
}
|
|
||||||
m.jobsList = jobsList
|
|
||||||
|
|
||||||
// determinism!
|
|
||||||
sort.Slice(m.jobsList, func(i, j int) bool {
|
|
||||||
return strings.Compare(m.jobsList[i].name, m.jobsList[j].name) < 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// try to not lose the selected job
|
|
||||||
m.selectedJob = nil
|
|
||||||
for _, j := range m.jobsList {
|
|
||||||
j.updateFullDescription(p)
|
|
||||||
if j == p.SelectedJob {
|
|
||||||
m.selectedJob = j
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.dateString = time.Now().Format(time.RFC3339)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *M) BottomBarStatus() string { return m.bottomBarStatus }
|
|
||||||
|
|
||||||
func (m *M) Jobs() []*Job { return m.jobsList }
|
|
||||||
|
|
||||||
// may be nil
|
|
||||||
func (m *M) SelectedJob() *Job { return m.selectedJob }
|
|
||||||
|
|
||||||
func (m *M) DateString() string { return m.dateString }
|
|
||||||
|
|
||||||
func (j *Job) updateFullDescription(p Params) {
|
|
||||||
width := p.DetailViewWidth
|
|
||||||
if !p.DetailViewWrap {
|
|
||||||
width = 10000000 // FIXME
|
|
||||||
}
|
|
||||||
b := stringbuilder.New(stringbuilder.Config{
|
|
||||||
IndentMultiplier: 3,
|
|
||||||
Width: width,
|
|
||||||
})
|
|
||||||
drawJob(b, j.name, j.lastStatus, j.byteProgress, p.FSFilter)
|
|
||||||
j.fulldescription = b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Job) JobTreeTitle() string {
|
|
||||||
return j.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Job) FullDescription() string {
|
|
||||||
return j.fulldescription
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Job) Name() string {
|
|
||||||
return j.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawJob(t *stringbuilder.B, name string, v *job.Status, history *bytesProgressHistory, fsfilter FilterFunc) {
|
|
||||||
|
|
||||||
t.Printf("Job: %s\n", name)
|
|
||||||
t.Printf("Type: %s\n\n", v.Type)
|
|
||||||
|
|
||||||
if v.Type == job.TypePush || v.Type == job.TypePull {
|
|
||||||
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
|
||||||
if !ok || activeStatus == nil {
|
|
||||||
t.Printf("ActiveSideStatus is null")
|
|
||||||
t.Newline()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Printf("Replication:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderReplicationReport(t, activeStatus.Replication, history, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
|
|
||||||
t.Printf("Pruning Sender:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderPrunerReport(t, activeStatus.PruningSender, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
|
|
||||||
t.Printf("Pruning Receiver:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderPrunerReport(t, activeStatus.PruningReceiver, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
|
|
||||||
if v.Type == job.TypePush {
|
|
||||||
t.Printf("Snapshotting:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderSnapperReport(t, activeStatus.Snapshotting, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if v.Type == job.TypeSnap {
|
|
||||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
|
||||||
if !ok || snapStatus == nil {
|
|
||||||
t.Printf("SnapJobStatus is null")
|
|
||||||
t.Newline()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Printf("Pruning snapshots:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderPrunerReport(t, snapStatus.Pruning, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
t.Printf("Snapshotting:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
renderSnapperReport(t, snapStatus.Snapshotting, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
} else if v.Type == job.TypeSource {
|
|
||||||
|
|
||||||
st := v.JobSpecific.(*job.PassiveStatus)
|
|
||||||
t.Printf("Snapshotting:\n")
|
|
||||||
t.AddIndent(1)
|
|
||||||
renderSnapperReport(t, st.Snapper, fsfilter)
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
t.Printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
|
||||||
t.Newline()
|
|
||||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
|
||||||
if err != nil {
|
|
||||||
t.Printf("Error marshaling status to YAML: %s", err)
|
|
||||||
t.Newline()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Write(string(asYaml))
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, maxFS int) {
|
|
||||||
|
|
||||||
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
|
||||||
sizeEstimationImpreciseNotice := ""
|
|
||||||
if containsInvalidSizeEstimates {
|
|
||||||
sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
|
|
||||||
}
|
|
||||||
if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
|
|
||||||
sizeEstimationImpreciseNotice = " (step lacks size estimation)"
|
|
||||||
}
|
|
||||||
|
|
||||||
userVisisbleCurrentStep, userVisibleTotalSteps := rep.CurrentStep, len(rep.Steps)
|
|
||||||
// `.CurrentStep` is == len(rep.Steps) if all steps are done.
|
|
||||||
// Until then, it's an index into .Steps that starts at 0.
|
|
||||||
// For the user, we want it to start at 1.
|
|
||||||
if rep.CurrentStep >= len(rep.Steps) {
|
|
||||||
// rep.CurrentStep is what we want to show.
|
|
||||||
// We check for >= and not == for robustness.
|
|
||||||
} else {
|
|
||||||
// We're not done yet, so, make step count start at 1
|
|
||||||
// (The `.State` is included in the output, indicating we're not done yet)
|
|
||||||
userVisisbleCurrentStep = rep.CurrentStep + 1
|
|
||||||
}
|
|
||||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
|
|
||||||
strings.ToUpper(string(rep.State)),
|
|
||||||
userVisisbleCurrentStep, userVisibleTotalSteps,
|
|
||||||
ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected),
|
|
||||||
sizeEstimationImpreciseNotice,
|
|
||||||
)
|
|
||||||
|
|
||||||
activeIndicator := " "
|
|
||||||
if rep.BlockedOn == report.FsBlockedOnNothing &&
|
|
||||||
(rep.State == report.FilesystemPlanning || rep.State == report.FilesystemStepping) {
|
|
||||||
activeIndicator = "*"
|
|
||||||
}
|
|
||||||
t.AddIndent(1)
|
|
||||||
t.Printf("%s %s %s ",
|
|
||||||
activeIndicator,
|
|
||||||
stringbuilder.RightPad(rep.Info.Name, maxFS, " "),
|
|
||||||
status)
|
|
||||||
|
|
||||||
next := ""
|
|
||||||
if err := rep.Error(); err != nil {
|
|
||||||
next = err.Err
|
|
||||||
} else if rep.State != report.FilesystemDone {
|
|
||||||
if nextStep := rep.NextStep(); nextStep != nil {
|
|
||||||
if nextStep.IsIncremental() {
|
|
||||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
|
||||||
} else {
|
|
||||||
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
|
|
||||||
}
|
|
||||||
attribs := []string{}
|
|
||||||
|
|
||||||
if nextStep.Info.Resumed {
|
|
||||||
attribs = append(attribs, "resumed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(attribs) > 0 {
|
|
||||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next = "" // individual FSes may still be in planning state
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
t.Printf("%s", next)
|
|
||||||
|
|
||||||
t.AddIndent(-1)
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *bytesProgressHistory, fsfilter FilterFunc) {
|
|
||||||
if rep == nil {
|
|
||||||
t.Printf("...\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if rep.WaitReconnectError != nil {
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
if !rep.WaitReconnectSince.IsZero() {
|
|
||||||
delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
|
|
||||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
|
||||||
var until string
|
|
||||||
if rep.WaitReconnectUntil.IsZero() {
|
|
||||||
until = "waiting indefinitely"
|
|
||||||
} else {
|
|
||||||
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
|
||||||
}
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
|
||||||
rep.WaitReconnectSince, until)
|
|
||||||
} else {
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO visualize more than the latest attempt by folding all attempts into one
|
|
||||||
if len(rep.Attempts) == 0 {
|
|
||||||
t.Printf("no attempts made yet")
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
t.Printf("Attempt #%d", len(rep.Attempts))
|
|
||||||
if len(rep.Attempts) > 1 {
|
|
||||||
t.Printf(". Previous attempts failed with the following statuses:")
|
|
||||||
t.AddIndentAndNewline(1)
|
|
||||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)\n", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
|
||||||
}
|
|
||||||
t.AddIndentAndNewline(-1)
|
|
||||||
} else {
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
latest := rep.Attempts[len(rep.Attempts)-1]
|
|
||||||
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
|
||||||
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
|
||||||
})
|
|
||||||
|
|
||||||
// apply filter
|
|
||||||
filtered := make([]*report.FilesystemReport, 0, len(latest.Filesystems))
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
if !fsfilter(fs.Info.Name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, fs)
|
|
||||||
}
|
|
||||||
latest.Filesystems = filtered
|
|
||||||
|
|
||||||
t.Printf("Status: %s", latest.State)
|
|
||||||
t.Newline()
|
|
||||||
if !latest.FinishAt.IsZero() {
|
|
||||||
t.Printf("Last Run: %s (lasted %s)\n", latest.FinishAt.Round(time.Second), latest.FinishAt.Sub(latest.StartAt).Round(time.Second))
|
|
||||||
} else {
|
|
||||||
t.Printf("Started: %s (lasting %s)\n", latest.StartAt.Round(time.Second), time.Since(latest.StartAt).Round(time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
if latest.State == report.AttemptPlanningError {
|
|
||||||
t.Printf("Problem: ")
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
|
||||||
t.Newline()
|
|
||||||
} else if latest.State == report.AttemptFanOutError {
|
|
||||||
t.Printf("Problem: one or more of the filesystems encountered errors")
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
|
||||||
// Draw global progress bar
|
|
||||||
// Progress: [---------------]
|
|
||||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
|
||||||
rate, changeCount := history.Update(replicated)
|
|
||||||
eta := time.Duration(0)
|
|
||||||
if rate > 0 {
|
|
||||||
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Write("Progress: ")
|
|
||||||
t.DrawBar(50, replicated, expected, changeCount)
|
|
||||||
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
|
|
||||||
if eta != 0 {
|
|
||||||
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
if containsInvalidSizeEstimates {
|
|
||||||
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(latest.Filesystems) == 0 {
|
|
||||||
t.Write("NOTE: no filesystems were considered for replication!")
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxFSLen int
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
if len(fs.Info.Name) > maxFSLen {
|
|
||||||
maxFSLen = len(fs.Info.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
printFilesystemStatus(t, fs, maxFSLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func humanizeDuration(duration time.Duration) string {
|
|
||||||
days := int64(duration.Hours() / 24)
|
|
||||||
hours := int64(math.Mod(duration.Hours(), 24))
|
|
||||||
minutes := int64(math.Mod(duration.Minutes(), 60))
|
|
||||||
seconds := int64(math.Mod(duration.Seconds(), 60))
|
|
||||||
|
|
||||||
var parts []string
|
|
||||||
|
|
||||||
force := false
|
|
||||||
chunks := []int64{days, hours, minutes, seconds}
|
|
||||||
for i, chunk := range chunks {
|
|
||||||
if force || chunk > 0 {
|
|
||||||
padding := 0
|
|
||||||
if force {
|
|
||||||
padding = 2
|
|
||||||
}
|
|
||||||
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
|
||||||
force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
|
||||||
if r == nil {
|
|
||||||
t.Printf("...\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := pruner.StateString(r.State)
|
|
||||||
if err != nil {
|
|
||||||
t.Printf("Status: %q (parse error: %q)\n", r.State, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Printf("Status: %s", state)
|
|
||||||
t.Newline()
|
|
||||||
|
|
||||||
if r.Error != "" {
|
|
||||||
t.Printf("Error: %s\n", r.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type commonFS struct {
|
|
||||||
*pruner.FSReport
|
|
||||||
completed bool
|
|
||||||
}
|
|
||||||
all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
|
|
||||||
for i := range r.Pending {
|
|
||||||
all = append(all, commonFS{&r.Pending[i], false})
|
|
||||||
}
|
|
||||||
for i := range r.Completed {
|
|
||||||
all = append(all, commonFS{&r.Completed[i], true})
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter all
|
|
||||||
filtered := make([]commonFS, 0, len(all))
|
|
||||||
for _, fs := range all {
|
|
||||||
if fsfilter(fs.FSReport.Filesystem) {
|
|
||||||
filtered = append(filtered, fs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
all = filtered
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case pruner.Plan:
|
|
||||||
fallthrough
|
|
||||||
case pruner.PlanErr:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(all) == 0 {
|
|
||||||
t.Printf("nothing to do\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalDestroyCount, completedDestroyCount int
|
|
||||||
var maxFSname int
|
|
||||||
for _, fs := range all {
|
|
||||||
totalDestroyCount += len(fs.DestroyList)
|
|
||||||
if fs.completed {
|
|
||||||
completedDestroyCount += len(fs.DestroyList)
|
|
||||||
}
|
|
||||||
if maxFSname < len(fs.Filesystem) {
|
|
||||||
maxFSname = len(fs.Filesystem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// global progress bar
|
|
||||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
|
||||||
t.Write("Progress: ")
|
|
||||||
t.Write("[")
|
|
||||||
t.Write(stringbuilder.Times("=", progress))
|
|
||||||
t.Write(">")
|
|
||||||
t.Write(stringbuilder.Times("-", 80-progress))
|
|
||||||
t.Write("]")
|
|
||||||
t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
|
||||||
t.Newline()
|
|
||||||
|
|
||||||
sort.SliceStable(all, func(i, j int) bool {
|
|
||||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
// Draw a table-like representation of 'all'
|
|
||||||
for _, fs := range all {
|
|
||||||
t.Write(stringbuilder.RightPad(fs.Filesystem, maxFSname, " "))
|
|
||||||
t.Write(" ")
|
|
||||||
if !fs.SkipReason.NotSkipped() {
|
|
||||||
t.Printf("skipped: %s\n", fs.SkipReason)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fs.LastError != "" {
|
|
||||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
|
||||||
t.Printf("ERROR:")
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
|
||||||
} else {
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
|
||||||
len(fs.DestroyList), len(fs.SnapshotList))
|
|
||||||
|
|
||||||
if fs.completed {
|
|
||||||
t.Printf("Completed %s\n", pruneRuleActionStr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Write("Pending ") // whitespace is padding 10
|
|
||||||
if len(fs.DestroyList) == 1 {
|
|
||||||
t.Write(fs.DestroyList[0].Name)
|
|
||||||
} else {
|
|
||||||
t.Write(pruneRuleActionStr)
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
|
|
||||||
if r == nil {
|
|
||||||
t.Printf("<no snapshotting report available>\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Printf("Type: %s\n", r.Type)
|
|
||||||
if r.Periodic != nil {
|
|
||||||
renderSnapperReportPeriodic(t, r.Periodic, fsfilter)
|
|
||||||
} else if r.Cron != nil {
|
|
||||||
renderSnapperReportCron(t, r.Cron, fsfilter)
|
|
||||||
} else {
|
|
||||||
t.Printf("<no details available>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderSnapperReportPeriodic(t *stringbuilder.B, r *snapper.PeriodicReport, fsfilter FilterFunc) {
|
|
||||||
t.Printf("Status: %s", r.State)
|
|
||||||
t.Newline()
|
|
||||||
|
|
||||||
if r.Error != "" {
|
|
||||||
t.Printf("Error: %s\n", r.Error)
|
|
||||||
}
|
|
||||||
if !r.SleepUntil.IsZero() {
|
|
||||||
t.Printf("Sleep until: %s\n", r.SleepUntil)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderSnapperReportCron(t *stringbuilder.B, r *snapper.CronReport, fsfilter FilterFunc) {
|
|
||||||
t.Printf("State: %s\n", r.State)
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if r.WakeupTime.After(now) {
|
|
||||||
t.Printf("Sleep until: %s (%s remaining)\n", r.WakeupTime, r.WakeupTime.Sub(now).Round(time.Second))
|
|
||||||
} else {
|
|
||||||
t.Printf("Started: %s (lasting %s)\n", r.WakeupTime, now.Sub(r.WakeupTime).Round(time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.ReportFilesystem, fsfilter FilterFunc) {
|
|
||||||
sort.Slice(fss, func(i, j int) bool {
|
|
||||||
return strings.Compare(fss[i].Path, fss[j].Path) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
dur := func(d time.Duration) string {
|
|
||||||
return d.Round(100 * time.Millisecond).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type row struct {
|
|
||||||
path, state, duration, remainder, hookReport string
|
|
||||||
}
|
|
||||||
var widths struct {
|
|
||||||
path, state, duration int
|
|
||||||
}
|
|
||||||
rows := make([]*row, 0, len(fss))
|
|
||||||
for _, fs := range fss {
|
|
||||||
if !fsfilter(fs.Path) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r := &row{
|
|
||||||
path: fs.Path,
|
|
||||||
state: fs.State.String(),
|
|
||||||
}
|
|
||||||
if fs.HooksHadError {
|
|
||||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
|
||||||
}
|
|
||||||
switch fs.State {
|
|
||||||
case snapper.SnapPending:
|
|
||||||
r.duration = "..."
|
|
||||||
r.remainder = ""
|
|
||||||
case snapper.SnapStarted:
|
|
||||||
r.duration = dur(time.Since(fs.StartAt))
|
|
||||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
|
||||||
case snapper.SnapDone:
|
|
||||||
fallthrough
|
|
||||||
case snapper.SnapError:
|
|
||||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
|
||||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
|
||||||
}
|
|
||||||
rows = append(rows, r)
|
|
||||||
if len(r.path) > widths.path {
|
|
||||||
widths.path = len(r.path)
|
|
||||||
}
|
|
||||||
if len(r.state) > widths.state {
|
|
||||||
widths.state = len(r.state)
|
|
||||||
}
|
|
||||||
if len(r.duration) > widths.duration {
|
|
||||||
widths.duration = len(r.duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range rows {
|
|
||||||
path := stringbuilder.RightPad(r.path, widths.path, " ")
|
|
||||||
state := stringbuilder.RightPad(r.state, widths.state, " ")
|
|
||||||
duration := stringbuilder.RightPad(r.duration, widths.duration, " ")
|
|
||||||
t.Printf("%s %s %s", path, state, duration)
|
|
||||||
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
|
||||||
if r.hookReport != "" {
|
|
||||||
t.AddIndent(1)
|
|
||||||
t.Newline()
|
|
||||||
t.Printf("%s", r.hookReport)
|
|
||||||
t.AddIndent(-1)
|
|
||||||
}
|
|
||||||
t.Newline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package stringbuilder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
|
||||||
|
|
||||||
type B struct {
|
|
||||||
// const
|
|
||||||
indentMultiplier int
|
|
||||||
|
|
||||||
// mut
|
|
||||||
sb *strings.Builder
|
|
||||||
indent int
|
|
||||||
width int
|
|
||||||
x, y int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
IndentMultiplier int `validate:"gte=1"`
|
|
||||||
Width int `validate:"gte=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var validate = validator.New()
|
|
||||||
|
|
||||||
func New(config Config) *B {
|
|
||||||
|
|
||||||
if err := validate.Struct(config); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &B{sb: &strings.Builder{}, width: config.Width, indentMultiplier: config.IndentMultiplier}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *B) String() string { return b.sb.String() }
|
|
||||||
|
|
||||||
func (w *B) Newline() {
|
|
||||||
w.Write("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *B) PrintfDrawIndentedAndWrappedIfMultiline(format string, args ...interface{}) {
|
|
||||||
whole := fmt.Sprintf(format, args...)
|
|
||||||
if strings.ContainsAny(whole, "\n\r") {
|
|
||||||
w.AddIndent(1)
|
|
||||||
defer w.AddIndent(-1)
|
|
||||||
}
|
|
||||||
w.Write(whole)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *B) Printf(format string, args ...interface{}) {
|
|
||||||
whole := fmt.Sprintf(format, args...)
|
|
||||||
w.Write(whole)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *B) AddIndent(delta int) {
|
|
||||||
t.indent += delta * t.indentMultiplier
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *B) AddIndentAndNewline(delta int) {
|
|
||||||
t.indent += delta * t.indentMultiplier
|
|
||||||
t.Write("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *B) Write(s string) {
|
|
||||||
for _, c := range s {
|
|
||||||
if c == '\n' {
|
|
||||||
fmt.Fprint(w.sb, "\n")
|
|
||||||
w.x = 0
|
|
||||||
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
|
||||||
w.x = w.indent
|
|
||||||
w.y++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if w.x >= w.width {
|
|
||||||
fmt.Fprint(w.sb, "\n")
|
|
||||||
w.x = 0
|
|
||||||
fmt.Fprint(w.sb, Times(" ", w.indent-w.x))
|
|
||||||
w.x = w.indent
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w.sb, "%c", c)
|
|
||||||
w.x++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Times(str string, n int) (out string) {
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
out += str
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func RightPad(str string, length int, pad string) string {
|
|
||||||
if len(str) > length {
|
|
||||||
return str[:length]
|
|
||||||
}
|
|
||||||
return str + strings.Repeat(pad, length-len(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
// changeCount = 0 indicates stall / no progress
|
|
||||||
func (w *B) DrawBar(length int, bytes, totalBytes uint64, changeCount int) {
|
|
||||||
const arrowPositions = `>\|/`
|
|
||||||
var completedLength int
|
|
||||||
if totalBytes > 0 {
|
|
||||||
completedLength = int(uint64(length) * bytes / totalBytes)
|
|
||||||
if completedLength > length {
|
|
||||||
completedLength = length
|
|
||||||
}
|
|
||||||
} else if totalBytes == bytes {
|
|
||||||
completedLength = length
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write("[")
|
|
||||||
w.Write(Times("=", completedLength))
|
|
||||||
w.Write(string(arrowPositions[changeCount%len(arrowPositions)]))
|
|
||||||
w.Write(Times("-", length-completedLength))
|
|
||||||
w.Write("]")
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
var StdinserverCmd = &cli.Subcommand{
|
var StdinserverCmd = &cli.Subcommand{
|
||||||
Use: "stdinserver CLIENT_IDENTITY",
|
Use: "stdinserver CLIENT_IDENTITY",
|
||||||
Short: "stdinserver transport mode (started from authorized_keys file as forced command)",
|
Short: "stdinserver transport mode (started from authorized_keys file as forced command)",
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||||
return runStdinserver(subcommand.Config(), args)
|
return runStdinserver(subcommand.Config(), args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-18
@@ -39,7 +39,7 @@ var testFilter = &cli.Subcommand{
|
|||||||
Run: runTestFilterCmd,
|
Run: runTestFilterCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTestFilterCmd(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
if testFilterArgs.job == "" {
|
if testFilterArgs.job == "" {
|
||||||
return fmt.Errorf("must specify --job flag")
|
return fmt.Errorf("must specify --job flag")
|
||||||
@@ -60,8 +60,6 @@ func runTestFilterCmd(ctx context.Context, subcommand *cli.Subcommand, args []st
|
|||||||
confFilter = j.Filesystems
|
confFilter = j.Filesystems
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
confFilter = j.Filesystems
|
confFilter = j.Filesystems
|
||||||
case *config.SnapJob:
|
|
||||||
confFilter = j.Filesystems
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
return fmt.Errorf("job type %T does not have filesystems filter", j)
|
||||||
}
|
}
|
||||||
@@ -75,7 +73,7 @@ func runTestFilterCmd(ctx context.Context, subcommand *cli.Subcommand, args []st
|
|||||||
if testFilterArgs.input != "" {
|
if testFilterArgs.input != "" {
|
||||||
fsnames = []string{testFilterArgs.input}
|
fsnames = []string{testFilterArgs.input}
|
||||||
} else {
|
} else {
|
||||||
out, err := zfs.ZFSList(ctx, []string{"name"})
|
out, err := zfs.ZFSList([]string{"name"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not list ZFS filesystems: %s", err)
|
return fmt.Errorf("could not list ZFS filesystems: %s", err)
|
||||||
}
|
}
|
||||||
@@ -136,15 +134,13 @@ var testPlaceholder = &cli.Subcommand{
|
|||||||
Run: runTestPlaceholder,
|
Run: runTestPlaceholder,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
var checkDPs []*zfs.DatasetPath
|
var checkDPs []*zfs.DatasetPath
|
||||||
var datasetWasExplicitArgument bool
|
|
||||||
|
|
||||||
// all actions first
|
// all actions first
|
||||||
if testPlaceholderArgs.all {
|
if testPlaceholderArgs.all {
|
||||||
datasetWasExplicitArgument = false
|
out, err := zfs.ZFSList([]string{"name"})
|
||||||
out, err := zfs.ZFSList(ctx, []string{"name"})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not list ZFS filesystems")
|
return errors.Wrap(err, "could not list ZFS filesystems")
|
||||||
}
|
}
|
||||||
@@ -156,7 +152,6 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
|||||||
checkDPs = append(checkDPs, dp)
|
checkDPs = append(checkDPs, dp)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
datasetWasExplicitArgument = true
|
|
||||||
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -169,17 +164,12 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
|
|||||||
|
|
||||||
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
|
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
|
||||||
for _, dp := range checkDPs {
|
for _, dp := range checkDPs {
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, dp)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(dp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot get placeholder state")
|
return errors.Wrap(err, "cannot get placeholder state")
|
||||||
}
|
}
|
||||||
if !ph.FSExists {
|
if !ph.FSExists {
|
||||||
if datasetWasExplicitArgument {
|
panic("placeholder state inconsistent: filesystem " + ph.FS + " must exist in this context")
|
||||||
return errors.Errorf("filesystem %q does not exist", ph.FS)
|
|
||||||
} else {
|
|
||||||
// got deleted between ZFSList and ZFSGetFilesystemPlaceholderState
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is := "yes"
|
is := "yes"
|
||||||
if !ph.IsPlaceholder {
|
if !ph.IsPlaceholder {
|
||||||
@@ -203,11 +193,11 @@ var testDecodeResumeToken = &cli.Subcommand{
|
|||||||
Run: runTestDecodeResumeTokenCmd,
|
Run: runTestDecodeResumeTokenCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTestDecodeResumeTokenCmd(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
func runTestDecodeResumeTokenCmd(subcommand *cli.Subcommand, args []string) error {
|
||||||
if testDecodeResumeTokenArgs.token == "" {
|
if testDecodeResumeTokenArgs.token == "" {
|
||||||
return fmt.Errorf("token argument must be specified")
|
return fmt.Errorf("token argument must be specified")
|
||||||
}
|
}
|
||||||
token, err := zfs.ParseResumeToken(ctx, testDecodeResumeTokenArgs.token)
|
token, err := zfs.ParseResumeToken(context.Background(), testDecodeResumeTokenArgs.token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ var VersionCmd = &cli.Subcommand{
|
|||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)")
|
f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)")
|
||||||
},
|
},
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||||
versionArgs.Config = subcommand.Config()
|
versionArgs.Config = subcommand.Config()
|
||||||
versionArgs.ConfigErr = subcommand.ConfigParsingError()
|
versionArgs.ConfigErr = subcommand.ConfigParsingError()
|
||||||
return runVersionCmd()
|
return runVersionCmd()
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ZFSAbstractionsCmd = &cli.Subcommand{
|
|
||||||
Use: "zfs-abstraction",
|
|
||||||
Short: "manage abstractions that zrepl builds on top of ZFS",
|
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
|
||||||
return []*cli.Subcommand{
|
|
||||||
zabsCmdList,
|
|
||||||
zabsCmdReleaseAll,
|
|
||||||
zabsCmdReleaseStale,
|
|
||||||
zabsCmdCreate,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// a common set of CLI flags that map to the fields of an
|
|
||||||
// endpoint.ListZFSHoldsAndBookmarksQuery
|
|
||||||
type zabsFilterFlags struct {
|
|
||||||
Filesystems FilesystemsFilterFlag
|
|
||||||
Job JobIDFlag
|
|
||||||
Types AbstractionTypesFlag
|
|
||||||
Concurrency int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// produce a query from the CLI flags
|
|
||||||
func (f zabsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) {
|
|
||||||
q := endpoint.ListZFSHoldsAndBookmarksQuery{
|
|
||||||
FS: f.Filesystems.FlagValue(),
|
|
||||||
What: f.Types.FlagValue(),
|
|
||||||
JobID: f.Job.FlagValue(),
|
|
||||||
Concurrency: f.Concurrency,
|
|
||||||
}
|
|
||||||
return q, q.Validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string) {
|
|
||||||
// Note: the default value is defined in the .FlagValue methods
|
|
||||||
s.Var(&f.Filesystems, "fs", fmt.Sprintf("only %s holds on the specified filesystem [default: all filesystems] [comma-separated list of <dataset-pattern>:<ok|!> pairs]", verb))
|
|
||||||
s.Var(&f.Job, "job", fmt.Sprintf("only %s holds created by the specified job [default: any job]", verb))
|
|
||||||
|
|
||||||
variants := make([]string, 0, len(endpoint.AbstractionTypesAll))
|
|
||||||
for v := range endpoint.AbstractionTypesAll {
|
|
||||||
variants = append(variants, string(v))
|
|
||||||
}
|
|
||||||
sort.Strings(variants)
|
|
||||||
variantsJoined := strings.Join(variants, "|")
|
|
||||||
s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined))
|
|
||||||
|
|
||||||
s.Int64VarP(&f.Concurrency, "concurrency", "p", 1, "number of concurrently queried filesystems")
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobIDFlag struct{ J *endpoint.JobID }
|
|
||||||
|
|
||||||
func (f *JobIDFlag) Set(s string) error {
|
|
||||||
if len(s) == 0 {
|
|
||||||
*f = JobIDFlag{J: nil}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
jobID, err := endpoint.MakeJobID(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*f = JobIDFlag{J: &jobID}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (f JobIDFlag) Type() string { return "job-ID" }
|
|
||||||
func (f JobIDFlag) String() string { return fmt.Sprint(f.J) }
|
|
||||||
func (f JobIDFlag) FlagValue() *endpoint.JobID { return f.J }
|
|
||||||
|
|
||||||
type AbstractionTypesFlag map[endpoint.AbstractionType]bool
|
|
||||||
|
|
||||||
func (f *AbstractionTypesFlag) Set(s string) error {
|
|
||||||
ats, err := endpoint.AbstractionTypeSetFromStrings(strings.Split(s, ","))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*f = AbstractionTypesFlag(ats)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (f AbstractionTypesFlag) Type() string { return "abstraction-type" }
|
|
||||||
func (f AbstractionTypesFlag) String() string {
|
|
||||||
return endpoint.AbstractionTypeSet(f).String()
|
|
||||||
}
|
|
||||||
func (f AbstractionTypesFlag) FlagValue() map[endpoint.AbstractionType]bool {
|
|
||||||
if len(f) > 0 {
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
return endpoint.AbstractionTypesAll
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilesystemsFilterFlag struct {
|
|
||||||
F endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (flag *FilesystemsFilterFlag) Set(s string) error {
|
|
||||||
mappings := strings.Split(s, ",")
|
|
||||||
if len(mappings) == 1 && !strings.Contains(mappings[0], ":") {
|
|
||||||
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
|
||||||
FS: &mappings[0],
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f := filters.NewDatasetMapFilter(len(mappings), true)
|
|
||||||
for _, m := range mappings {
|
|
||||||
thisMappingErr := fmt.Errorf("expecting comma-separated list of <dataset-pattern>:<ok|!> pairs, got %q", m)
|
|
||||||
lhsrhs := strings.SplitN(m, ":", 2)
|
|
||||||
if len(lhsrhs) != 2 {
|
|
||||||
return thisMappingErr
|
|
||||||
}
|
|
||||||
err := f.Add(lhsrhs[0], lhsrhs[1])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %s", thisMappingErr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
|
||||||
Filter: f,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (flag FilesystemsFilterFlag) Type() string { return "filesystem filter spec" }
|
|
||||||
func (flag FilesystemsFilterFlag) String() string {
|
|
||||||
return fmt.Sprintf("%v", flag.F)
|
|
||||||
}
|
|
||||||
func (flag FilesystemsFilterFlag) FlagValue() endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter {
|
|
||||||
var z FilesystemsFilterFlag
|
|
||||||
if flag == z {
|
|
||||||
return endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{Filter: zfs.NoFilter()}
|
|
||||||
}
|
|
||||||
return flag.F
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import "github.com/zrepl/zrepl/cli"
|
|
||||||
|
|
||||||
var zabsCmdCreate = &cli.Subcommand{
|
|
||||||
Use: "create",
|
|
||||||
NoRequireConfig: true,
|
|
||||||
Short: `create zrepl ZFS abstractions (mostly useful for debugging & development, users should not need to use this command)`,
|
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
|
||||||
return []*cli.Subcommand{
|
|
||||||
zabsCmdCreateStepHold,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
var zabsCreateStepHoldFlags struct {
|
|
||||||
target string
|
|
||||||
jobid JobIDFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
var zabsCmdCreateStepHold = &cli.Subcommand{
|
|
||||||
Use: "step",
|
|
||||||
Run: doZabsCreateStep,
|
|
||||||
NoRequireConfig: true,
|
|
||||||
Short: `create a step hold or bookmark`,
|
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
|
||||||
f.StringVarP(&zabsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held")
|
|
||||||
f.VarP(&zabsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func doZabsCreateStep(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
|
||||||
if len(args) > 0 {
|
|
||||||
return errors.New("subcommand takes no arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
f := &zabsCreateStepHoldFlags
|
|
||||||
|
|
||||||
fs, _, _, err := zfs.DecomposeVersionString(f.target)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "%q invalid target", f.target)
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.jobid.FlagValue() == nil {
|
|
||||||
return errors.Errorf("jobid must be set")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := zfs.ZFSGetFilesystemVersion(ctx, f.target)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "get info about target %q", f.target)
|
|
||||||
}
|
|
||||||
|
|
||||||
step, err := endpoint.HoldStep(ctx, fs, v, *f.jobid.FlagValue())
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "create step hold")
|
|
||||||
}
|
|
||||||
fmt.Println(step.String())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
|
||||||
)
|
|
||||||
|
|
||||||
var zabsListFlags struct {
|
|
||||||
Filter zabsFilterFlags
|
|
||||||
Json bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var zabsCmdList = &cli.Subcommand{
|
|
||||||
Use: "list",
|
|
||||||
Short: `list zrepl ZFS abstractions`,
|
|
||||||
Run: doZabsList,
|
|
||||||
NoRequireConfig: true,
|
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
|
||||||
zabsListFlags.Filter.registerZabsFilterFlags(f, "list")
|
|
||||||
f.BoolVar(&zabsListFlags.Json, "json", false, "emit JSON")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
return errors.New("this subcommand takes no positional arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
q, err := zabsListFlags.Filter.Query()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "invalid filter specification on command line")
|
|
||||||
}
|
|
||||||
|
|
||||||
abstractions, errors, drainDone, err := endpoint.ListAbstractionsStreamed(ctx, q)
|
|
||||||
if err != nil {
|
|
||||||
return err // context clear by invocation of command
|
|
||||||
}
|
|
||||||
defer drainDone()
|
|
||||||
|
|
||||||
var line chainlock.L
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
defer wg.Wait()
|
|
||||||
|
|
||||||
// print results
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
for a := range abstractions {
|
|
||||||
func() {
|
|
||||||
defer line.Lock().Unlock()
|
|
||||||
if zabsListFlags.Json {
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
if err := enc.Encode(a); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
} else {
|
|
||||||
fmt.Println(a)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// print errors to stderr
|
|
||||||
errorColor := color.New(color.FgRed)
|
|
||||||
var errorsSlice []endpoint.ListAbstractionsError
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for err := range errors {
|
|
||||||
func() {
|
|
||||||
defer line.Lock().Unlock()
|
|
||||||
errorsSlice = append(errorsSlice, err)
|
|
||||||
errorColor.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
wg.Wait()
|
|
||||||
if len(errorsSlice) > 0 {
|
|
||||||
errorColor.Add(color.Bold).Fprintf(os.Stderr, "there were errors in listing the abstractions")
|
|
||||||
return fmt.Errorf("")
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
)
|
|
||||||
|
|
||||||
// shared between release-all and release-step
|
|
||||||
var zabsReleaseFlags struct {
|
|
||||||
Filter zabsFilterFlags
|
|
||||||
Json bool
|
|
||||||
DryRun bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerZabsReleaseFlags(s *pflag.FlagSet) {
|
|
||||||
zabsReleaseFlags.Filter.registerZabsFilterFlags(s, "release")
|
|
||||||
s.BoolVar(&zabsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed")
|
|
||||||
s.BoolVar(&zabsReleaseFlags.DryRun, "dry-run", false, "do a dry-run")
|
|
||||||
}
|
|
||||||
|
|
||||||
var zabsCmdReleaseAll = &cli.Subcommand{
|
|
||||||
Use: "release-all",
|
|
||||||
Run: doZabsReleaseAll,
|
|
||||||
NoRequireConfig: true,
|
|
||||||
Short: `(DANGEROUS) release ALL zrepl ZFS abstractions (mostly useful for uninstalling zrepl completely or for "de-zrepl-ing" a filesystem)`,
|
|
||||||
SetupFlags: registerZabsReleaseFlags,
|
|
||||||
}
|
|
||||||
|
|
||||||
var zabsCmdReleaseStale = &cli.Subcommand{
|
|
||||||
Use: "release-stale",
|
|
||||||
Run: doZabsReleaseStale,
|
|
||||||
NoRequireConfig: true,
|
|
||||||
Short: `release stale zrepl ZFS abstractions (useful if zrepl has a bug and does not do it by itself)`,
|
|
||||||
SetupFlags: registerZabsReleaseFlags,
|
|
||||||
}
|
|
||||||
|
|
||||||
func doZabsReleaseAll(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
return errors.New("this subcommand takes no positional arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
q, err := zabsReleaseFlags.Filter.Query()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "invalid filter specification on command line")
|
|
||||||
}
|
|
||||||
|
|
||||||
abstractions, listErrors, err := endpoint.ListAbstractions(ctx, q)
|
|
||||||
if err != nil {
|
|
||||||
return err // context clear by invocation of command
|
|
||||||
}
|
|
||||||
if len(listErrors) > 0 {
|
|
||||||
color.New(color.FgRed).Fprintf(os.Stderr, "there were errors in listing the abstractions:\n%s\n", listErrors)
|
|
||||||
// proceed anyways with rest of abstractions
|
|
||||||
}
|
|
||||||
|
|
||||||
return doZabsRelease_Common(ctx, abstractions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func doZabsReleaseStale(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
return errors.New("this subcommand takes no positional arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
q, err := zabsReleaseFlags.Filter.Query()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "invalid filter specification on command line")
|
|
||||||
}
|
|
||||||
|
|
||||||
stalenessInfo, err := endpoint.ListStale(ctx, q)
|
|
||||||
if err != nil {
|
|
||||||
return err // context clear by invocation of command
|
|
||||||
}
|
|
||||||
|
|
||||||
return doZabsRelease_Common(ctx, stalenessInfo.Stale)
|
|
||||||
}
|
|
||||||
|
|
||||||
func doZabsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error {
|
|
||||||
|
|
||||||
if zabsReleaseFlags.DryRun {
|
|
||||||
if zabsReleaseFlags.Json {
|
|
||||||
m, err := json.MarshalIndent(destroy, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stdout.Write(m); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
} else {
|
|
||||||
for _, a := range destroy {
|
|
||||||
fmt.Printf("would destroy %s\n", a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
outcome := endpoint.BatchDestroy(ctx, destroy)
|
|
||||||
hadErr := false
|
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
colorErr := color.New(color.FgRed)
|
|
||||||
printfSuccess := color.New(color.FgGreen).FprintfFunc()
|
|
||||||
printfSection := color.New(color.Bold).FprintfFunc()
|
|
||||||
|
|
||||||
for res := range outcome {
|
|
||||||
hadErr = hadErr || res.DestroyErr != nil
|
|
||||||
if zabsReleaseFlags.Json {
|
|
||||||
err := enc.Encode(res)
|
|
||||||
if err != nil {
|
|
||||||
colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
printfSection(os.Stdout, "destroy %s ...", res.Abstraction)
|
|
||||||
if res.DestroyErr != nil {
|
|
||||||
colorErr.Fprintf(os.Stdout, " failed:\n%s\n", res.DestroyErr)
|
|
||||||
} else {
|
|
||||||
printfSuccess(os.Stdout, " OK\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hadErr {
|
|
||||||
colorErr.Add(color.Bold).Fprintf(os.Stderr, "there were errors in destroying the abstractions")
|
|
||||||
return fmt.Errorf("")
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
-114
@@ -11,18 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/robfig/cron/v3"
|
|
||||||
"github.com/zrepl/yaml-config"
|
"github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/datasizeunit"
|
|
||||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ParseFlags uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
ParseFlagsNone ParseFlags = 0
|
|
||||||
ParseFlagsNoCertCheck ParseFlags = 1 << iota
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -63,17 +52,11 @@ func (j JobEnum) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActiveJob struct {
|
type ActiveJob struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Connect ConnectEnum `yaml:"connect"`
|
Connect ConnectEnum `yaml:"connect"`
|
||||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
|
||||||
ConflictResolution *ConflictResolution `yaml:"conflict_resolution,optional,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConflictResolution struct {
|
|
||||||
InitialReplication string `yaml:"initial_replication,optional,default=most_recent"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PassiveJob struct {
|
type PassiveJob struct {
|
||||||
@@ -93,60 +76,27 @@ type SnapJob struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SendOptions struct {
|
type SendOptions struct {
|
||||||
Encrypted bool `yaml:"encrypted,optional,default=false"`
|
Encrypted bool `yaml:"encrypted"`
|
||||||
Raw bool `yaml:"raw,optional,default=false"`
|
}
|
||||||
SendProperties bool `yaml:"send_properties,optional,default=false"`
|
|
||||||
BackupProperties bool `yaml:"backup_properties,optional,default=false"`
|
|
||||||
LargeBlocks bool `yaml:"large_blocks,optional,default=false"`
|
|
||||||
Compressed bool `yaml:"compressed,optional,default=false"`
|
|
||||||
EmbeddedData bool `yaml:"embedded_data,optional,default=false"`
|
|
||||||
Saved bool `yaml:"saved,optional,default=false"`
|
|
||||||
|
|
||||||
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
var _ yaml.Defaulter = (*SendOptions)(nil)
|
||||||
|
|
||||||
|
func (l *SendOptions) SetDefault() {
|
||||||
|
*l = SendOptions{Encrypted: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecvOptions struct {
|
type RecvOptions struct {
|
||||||
// Note: we cannot enforce encrypted recv as the ZFS cli doesn't provide a mechanism for it
|
// Note: we cannot enforce encrypted recv as the ZFS cli doesn't provide a mechanism for it
|
||||||
// Encrypted bool `yaml:"may_encrypted"`
|
// Encrypted bool `yaml:"may_encrypted"`
|
||||||
|
|
||||||
// Future:
|
// Future:
|
||||||
// Reencrypt bool `yaml:"reencrypt"`
|
// Reencrypt bool `yaml:"reencrypt"`
|
||||||
|
|
||||||
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
|
||||||
|
|
||||||
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
|
||||||
|
|
||||||
Placeholder *PlaceholderRecvOptions `yaml:"placeholder,fromdefaults"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ yaml.Unmarshaler = &datasizeunit.Bits{}
|
var _ yaml.Defaulter = (*RecvOptions)(nil)
|
||||||
|
|
||||||
type BandwidthLimit struct {
|
func (l *RecvOptions) SetDefault() {
|
||||||
Max datasizeunit.Bits `yaml:"max,default=-1 B"`
|
*l = RecvOptions{}
|
||||||
BucketCapacity datasizeunit.Bits `yaml:"bucket_capacity,default=128 KiB"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Replication struct {
|
|
||||||
Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"`
|
|
||||||
Concurrency *ReplicationOptionsConcurrency `yaml:"concurrency,optional,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReplicationOptionsProtection struct {
|
|
||||||
Initial string `yaml:"initial,optional,default=guarantee_resumability"`
|
|
||||||
Incremental string `yaml:"incremental,optional,default=guarantee_resumability"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReplicationOptionsConcurrency struct {
|
|
||||||
Steps int `yaml:"steps,optional,default=1"`
|
|
||||||
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PropertyRecvOptions struct {
|
|
||||||
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
|
||||||
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaceholderRecvOptions struct {
|
|
||||||
Encryption string `yaml:"encryption,default=unspecified"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PushJob struct {
|
type PushJob struct {
|
||||||
@@ -156,9 +106,6 @@ type PushJob struct {
|
|||||||
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *PushJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
|
|
||||||
func (j *PushJob) GetSendOptions() *SendOptions { return j.Send }
|
|
||||||
|
|
||||||
type PullJob struct {
|
type PullJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
RootFS string `yaml:"root_fs"`
|
RootFS string `yaml:"root_fs"`
|
||||||
@@ -166,10 +113,6 @@ type PullJob struct {
|
|||||||
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *PullJob) GetRootFS() string { return j.RootFS }
|
|
||||||
func (j *PullJob) GetAppendClientIdentity() bool { return false }
|
|
||||||
func (j *PullJob) GetRecvOptions() *RecvOptions { return j.Recv }
|
|
||||||
|
|
||||||
type PositiveDurationOrManual struct {
|
type PositiveDurationOrManual struct {
|
||||||
Interval time.Duration
|
Interval time.Duration
|
||||||
Manual bool
|
Manual bool
|
||||||
@@ -207,10 +150,6 @@ type SinkJob struct {
|
|||||||
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SinkJob) GetRootFS() string { return j.RootFS }
|
|
||||||
func (j *SinkJob) GetAppendClientIdentity() bool { return true }
|
|
||||||
func (j *SinkJob) GetRecvOptions() *RecvOptions { return j.Recv }
|
|
||||||
|
|
||||||
type SourceJob struct {
|
type SourceJob struct {
|
||||||
PassiveJob `yaml:",inline"`
|
PassiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
@@ -218,9 +157,6 @@ type SourceJob struct {
|
|||||||
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SourceJob) GetFilesystems() FilesystemsFilter { return j.Filesystems }
|
|
||||||
func (j *SourceJob) GetSendOptions() *SendOptions { return j.Send }
|
|
||||||
|
|
||||||
type FilesystemsFilter map[string]bool
|
type FilesystemsFilter map[string]bool
|
||||||
|
|
||||||
type SnapshottingEnum struct {
|
type SnapshottingEnum struct {
|
||||||
@@ -234,38 +170,6 @@ type SnapshottingPeriodic struct {
|
|||||||
Hooks HookList `yaml:"hooks,optional"`
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronSpec struct {
|
|
||||||
Schedule cron.Schedule
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ yaml.Unmarshaler = &CronSpec{}
|
|
||||||
|
|
||||||
func (s *CronSpec) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
|
||||||
var specString string
|
|
||||||
if err := unmarshal(&specString, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use standard cron format.
|
|
||||||
// Disable the various "descriptors" (@daily, etc)
|
|
||||||
// They are just aliases to "top of hour", "midnight", etc.
|
|
||||||
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.SecondOptional)
|
|
||||||
|
|
||||||
sched, err := parser.Parse(specString)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "cron syntax invalid")
|
|
||||||
}
|
|
||||||
s.Schedule = sched
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshottingCron struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Prefix string `yaml:"prefix"`
|
|
||||||
Cron CronSpec `yaml:"cron"`
|
|
||||||
Hooks HookList `yaml:"hooks,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshottingManual struct {
|
type SnapshottingManual struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
}
|
}
|
||||||
@@ -408,7 +312,6 @@ type PruneKeepNotReplicated struct {
|
|||||||
type PruneKeepLastN struct {
|
type PruneKeepLastN struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Count int `yaml:"count"`
|
Count int `yaml:"count"`
|
||||||
Regex string `yaml:"regex,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
||||||
@@ -589,7 +492,6 @@ func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err e
|
|||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||||
"periodic": &SnapshottingPeriodic{},
|
"periodic": &SnapshottingPeriodic{},
|
||||||
"manual": &SnapshottingManual{},
|
"manual": &SnapshottingManual{},
|
||||||
"cron": &SnapshottingCron{},
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -718,7 +620,7 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
|
|||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
||||||
|
|
||||||
func parsePositiveDuration(e string) (d time.Duration, err error) {
|
func parsePostitiveDuration(e string) (d time.Duration, err error) {
|
||||||
comps := durationStringRegex.FindStringSubmatch(e)
|
comps := durationStringRegex.FindStringSubmatch(e)
|
||||||
if len(comps) != 3 {
|
if len(comps) != 3 {
|
||||||
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type A struct {
|
|
||||||
B *B `yaml:"b,optional,fromdefaults"`
|
|
||||||
A1 string `yaml:"a1,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type B struct {
|
|
||||||
C *C `yaml:"c,optional,fromdefaults"`
|
|
||||||
D string `yaml:"d,default=ddd"`
|
|
||||||
E string `yaml:"e,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type C struct {
|
|
||||||
Q string `yaml:"q,optional"`
|
|
||||||
R string `yaml:"r,default=r"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDepFromDefaults(t *testing.T) {
|
|
||||||
|
|
||||||
type testcase struct {
|
|
||||||
name string
|
|
||||||
yaml string
|
|
||||||
expect *A
|
|
||||||
}
|
|
||||||
|
|
||||||
tcs := []testcase{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
yaml: `{}`,
|
|
||||||
expect: &A{
|
|
||||||
B: &B{
|
|
||||||
C: &C{
|
|
||||||
R: "r",
|
|
||||||
},
|
|
||||||
D: "ddd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "a1 set",
|
|
||||||
yaml: `{"a1":"blah"}`,
|
|
||||||
expect: &A{
|
|
||||||
A1: "blah",
|
|
||||||
B: &B{
|
|
||||||
C: &C{
|
|
||||||
R: "r",
|
|
||||||
},
|
|
||||||
D: "ddd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "D set",
|
|
||||||
yaml: `
|
|
||||||
b:
|
|
||||||
d: 4d
|
|
||||||
`,
|
|
||||||
expect: &A{
|
|
||||||
B: &B{
|
|
||||||
D: "4d",
|
|
||||||
C: &C{
|
|
||||||
R: "r",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for tci := range tcs {
|
|
||||||
t.Run(fmt.Sprintf("%d-%s", tci, tcs[tci].name), func(t *testing.T) {
|
|
||||||
tc := tcs[tci]
|
|
||||||
|
|
||||||
var a A
|
|
||||||
err := yaml.UnmarshalStrict([]byte(tc.yaml), &a)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, tc.expect, &a)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
clients: {
|
clients: {
|
||||||
"10.0.0.1":"foo"
|
"10.0.0.1":"foo"
|
||||||
}
|
}
|
||||||
root_fs: zroot/foo
|
root_fs: zoot/foo
|
||||||
`
|
`
|
||||||
_, err := ParseConfigBytes([]byte(jobdef))
|
_, err := ParseConfigBytes([]byte(jobdef))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -70,9 +70,9 @@ func TestPrometheusMonitoring(t *testing.T) {
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9811'
|
listen: ':9091'
|
||||||
`)
|
`)
|
||||||
assert.Equal(t, ":9811", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRecvOptions(t *testing.T) {
|
|
||||||
tmpl := `
|
|
||||||
jobs:
|
|
||||||
- name: foo
|
|
||||||
type: pull
|
|
||||||
connect:
|
|
||||||
type: local
|
|
||||||
listener_name: foo
|
|
||||||
client_identity: bar
|
|
||||||
root_fs: "zreplplatformtest"
|
|
||||||
%s
|
|
||||||
interval: manual
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
recv_properties_empty := `
|
|
||||||
recv:
|
|
||||||
properties:
|
|
||||||
`
|
|
||||||
|
|
||||||
recv_inherit_empty := `
|
|
||||||
recv:
|
|
||||||
properties:
|
|
||||||
inherit:
|
|
||||||
`
|
|
||||||
|
|
||||||
recv_inherit := `
|
|
||||||
recv:
|
|
||||||
properties:
|
|
||||||
inherit:
|
|
||||||
- testprop
|
|
||||||
`
|
|
||||||
|
|
||||||
recv_override_empty := `
|
|
||||||
recv:
|
|
||||||
properties:
|
|
||||||
override:
|
|
||||||
`
|
|
||||||
|
|
||||||
recv_override := `
|
|
||||||
recv:
|
|
||||||
properties:
|
|
||||||
override:
|
|
||||||
testprop2: "test123"
|
|
||||||
`
|
|
||||||
|
|
||||||
recv_override_and_inherit := `
|
|
||||||
recv:
|
|
||||||
properties:
|
|
||||||
inherit:
|
|
||||||
- testprop
|
|
||||||
override:
|
|
||||||
testprop2: "test123"
|
|
||||||
`
|
|
||||||
|
|
||||||
recv_empty := `
|
|
||||||
recv: {}
|
|
||||||
`
|
|
||||||
|
|
||||||
recv_not_specified := `
|
|
||||||
`
|
|
||||||
|
|
||||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
|
||||||
|
|
||||||
t.Run("recv_inherit_empty", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(recv_inherit_empty))
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("recv_inherit", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(recv_inherit))
|
|
||||||
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
|
||||||
assert.NotEmpty(t, inherit)
|
|
||||||
assert.Contains(t, inherit, zfsprop.Property("testprop"))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("recv_override_empty", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(recv_override_empty))
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("recv_override", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(recv_override))
|
|
||||||
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
|
||||||
require.Len(t, override, 1)
|
|
||||||
require.Equal(t, "test123", override["testprop2"])
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("recv_override_and_inherit", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(recv_override_and_inherit))
|
|
||||||
inherit := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Inherit
|
|
||||||
override := c.Jobs[0].Ret.(*PullJob).Recv.Properties.Override
|
|
||||||
assert.NotEmpty(t, inherit)
|
|
||||||
assert.Contains(t, inherit, zfsprop.Property("testprop"))
|
|
||||||
assert.NotEmpty(t, override)
|
|
||||||
assert.Equal(t, "test123", override["testprop2"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("recv_properties_empty", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(recv_properties_empty))
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("recv_empty", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(recv_empty))
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("send_not_specified", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(recv_not_specified))
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
+10
-82
@@ -38,41 +38,7 @@ jobs:
|
|||||||
encrypted: true
|
encrypted: true
|
||||||
`
|
`
|
||||||
|
|
||||||
raw_true := `
|
encrypted_unspecified := `
|
||||||
send:
|
|
||||||
raw: true
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
raw_false := `
|
|
||||||
send:
|
|
||||||
raw: false
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
raw_and_encrypted := `
|
|
||||||
send:
|
|
||||||
encrypted: true
|
|
||||||
raw: true
|
|
||||||
`
|
|
||||||
|
|
||||||
properties_and_encrypted := `
|
|
||||||
send:
|
|
||||||
encrypted: true
|
|
||||||
send_properties: true
|
|
||||||
`
|
|
||||||
|
|
||||||
properties_true := `
|
|
||||||
send:
|
|
||||||
send_properties: true
|
|
||||||
`
|
|
||||||
|
|
||||||
properties_false := `
|
|
||||||
send:
|
|
||||||
send_properties: false
|
|
||||||
`
|
|
||||||
|
|
||||||
send_empty := `
|
|
||||||
send: {}
|
send: {}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -80,66 +46,28 @@ jobs:
|
|||||||
`
|
`
|
||||||
|
|
||||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
var c *Config
|
||||||
|
|
||||||
t.Run("encrypted_false", func(t *testing.T) {
|
t.Run("encrypted_false", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(encrypted_false))
|
c = testValidConfig(t, fill(encrypted_false))
|
||||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
assert.Equal(t, false, encrypted)
|
assert.Equal(t, false, encrypted)
|
||||||
})
|
})
|
||||||
t.Run("encrypted_true", func(t *testing.T) {
|
t.Run("encrypted_true", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(encrypted_true))
|
c = testValidConfig(t, fill(encrypted_true))
|
||||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
assert.Equal(t, true, encrypted)
|
assert.Equal(t, true, encrypted)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("send_empty", func(t *testing.T) {
|
t.Run("encrypted_unspecified", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(send_empty))
|
c, err := testConfig(t, fill(encrypted_unspecified))
|
||||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
assert.Error(t, err)
|
||||||
assert.Equal(t, false, encrypted)
|
assert.Nil(t, c)
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("properties_and_encrypted", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(properties_and_encrypted))
|
|
||||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
|
||||||
properties := c.Jobs[0].Ret.(*PushJob).Send.SendProperties
|
|
||||||
assert.Equal(t, true, encrypted)
|
|
||||||
assert.Equal(t, true, properties)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("properties_false", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(properties_false))
|
|
||||||
properties := c.Jobs[0].Ret.(*PushJob).Send.SendProperties
|
|
||||||
assert.Equal(t, false, properties)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("properties_true", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(properties_true))
|
|
||||||
properties := c.Jobs[0].Ret.(*PushJob).Send.SendProperties
|
|
||||||
assert.Equal(t, true, properties)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("raw_true", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(raw_true))
|
|
||||||
raw := c.Jobs[0].Ret.(*PushJob).Send.Raw
|
|
||||||
assert.Equal(t, true, raw)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("raw_false", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(raw_false))
|
|
||||||
raw := c.Jobs[0].Ret.(*PushJob).Send.Raw
|
|
||||||
assert.Equal(t, false, raw)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("raw_and_encrypted", func(t *testing.T) {
|
|
||||||
c := testValidConfig(t, fill(raw_and_encrypted))
|
|
||||||
raw := c.Jobs[0].Ret.(*PushJob).Send.Raw
|
|
||||||
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
|
||||||
assert.Equal(t, true, raw)
|
|
||||||
assert.Equal(t, true, encrypted)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("send_not_specified", func(t *testing.T) {
|
t.Run("send_not_specified", func(t *testing.T) {
|
||||||
c := testValidConfig(t, fill(send_not_specified))
|
c, err := testConfig(t, fill(send_not_specified))
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+2
-54
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
||||||
@@ -43,9 +42,8 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// template must be a template/text template with a single '{{ . }}' as placeholder for val
|
// template must be a template/text template with a single '{{ . }}' as placehodler for val
|
||||||
//
|
//nolint[:deadcode,unused]
|
||||||
//nolint:deadcode,unused
|
|
||||||
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
||||||
tmp, err := template.New("master").Parse(tmpl)
|
tmp, err := template.New("master").Parse(tmpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -88,53 +86,3 @@ func TestTrimSpaceEachLineAndPad(t *testing.T) {
|
|||||||
`
|
`
|
||||||
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
|
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCronSpec(t *testing.T) {
|
|
||||||
|
|
||||||
expectAccept := []string{
|
|
||||||
`"* * * * *"`,
|
|
||||||
`"0-10 * * * *"`,
|
|
||||||
`"* 0-5,8,12 * * *"`,
|
|
||||||
}
|
|
||||||
|
|
||||||
expectFail := []string{
|
|
||||||
`* * * *`,
|
|
||||||
``,
|
|
||||||
`23`,
|
|
||||||
`"@reboot"`,
|
|
||||||
`"@every 1h30m"`,
|
|
||||||
`"@daily"`,
|
|
||||||
`* * * * * *`,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, input := range expectAccept {
|
|
||||||
t.Run(input, func(t *testing.T) {
|
|
||||||
s := fmt.Sprintf("spec: %s\n", input)
|
|
||||||
var v struct {
|
|
||||||
Spec CronSpec
|
|
||||||
}
|
|
||||||
v.Spec.Schedule = nil
|
|
||||||
t.Logf("input:\n%s", s)
|
|
||||||
err := yaml.UnmarshalStrict([]byte(s), &v)
|
|
||||||
t.Logf("error: %T %s", err, err)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, v.Spec.Schedule)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, input := range expectFail {
|
|
||||||
t.Run(input, func(t *testing.T) {
|
|
||||||
s := fmt.Sprintf("spec: %s\n", input)
|
|
||||||
var v struct {
|
|
||||||
Spec CronSpec
|
|
||||||
}
|
|
||||||
v.Spec.Schedule = nil
|
|
||||||
t.Logf("input: %q", s)
|
|
||||||
err := yaml.UnmarshalStrict([]byte(s), &v)
|
|
||||||
t.Logf("error: %T %s", err, err)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, v.Spec.Schedule)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
intervals, err := ParseRetentionIntervalSpec(in)
|
intervals, err := parseRetentionGridIntervalsString(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
|
|||||||
return nil, fmt.Errorf("contains factor <= 0")
|
return nil, fmt.Errorf("contains factor <= 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
duration, err := parsePositiveDuration(comps[2])
|
duration, err := parsePostitiveDuration(comps[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseRetentionIntervalSpec(s string) (intervals []RetentionInterval, err error) {
|
func parseRetentionGridIntervalsString(s string) (intervals []RetentionInterval, err error) {
|
||||||
|
|
||||||
ges := strings.Split(s, "|")
|
ges := strings.Split(s, "|")
|
||||||
intervals = make([]RetentionInterval, 0, 7*len(ges))
|
intervals = make([]RetentionInterval, 0, 7*len(ges))
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
jobs:
|
|
||||||
- type: sink
|
|
||||||
name: "limited_sink"
|
|
||||||
root_fs: "fs0"
|
|
||||||
recv:
|
|
||||||
bandwidth_limit:
|
|
||||||
max: 12345 B
|
|
||||||
serve:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
|
|
||||||
- type: push
|
|
||||||
name: "limited_push"
|
|
||||||
connect:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
client_identity: local_backup
|
|
||||||
filesystems: {
|
|
||||||
"root<": true,
|
|
||||||
}
|
|
||||||
send:
|
|
||||||
bandwidth_limit:
|
|
||||||
max: 54321 B
|
|
||||||
bucket_capacity: 1024 B
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: last_n
|
|
||||||
count: 1
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 1
|
|
||||||
|
|
||||||
- type: sink
|
|
||||||
name: "nolimit_sink"
|
|
||||||
root_fs: "fs1"
|
|
||||||
serve:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# This config serves as an example for a local zrepl installation that
|
|
||||||
# backups the entire zpool `system` to `backuppool/zrepl/sink`
|
|
||||||
#
|
|
||||||
# The requirements covered by this setup are described in the zrepl documentation's
|
|
||||||
# quick start section which inlines this example.
|
|
||||||
#
|
|
||||||
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
|
||||||
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_drive`
|
|
||||||
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
|
||||||
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
|
||||||
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
# this job takes care of snapshot creation + pruning
|
|
||||||
- name: snapjob
|
|
||||||
type: snap
|
|
||||||
filesystems: {
|
|
||||||
"system<": true,
|
|
||||||
}
|
|
||||||
# create snapshots with prefix `zrepl_` every 15 minutes
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
interval: 15m
|
|
||||||
prefix: zrepl_
|
|
||||||
pruning:
|
|
||||||
keep:
|
|
||||||
# fade-out scheme for snapshots starting with `zrepl_`
|
|
||||||
# - keep all created in the last hour
|
|
||||||
# - then destroy snapshots such that we keep 24 each 1 hour apart
|
|
||||||
# - then destroy snapshots such that we keep 14 each 1 day apart
|
|
||||||
# - then destroy all older snapshots
|
|
||||||
- type: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 14x1d
|
|
||||||
regex: "^zrepl_.*"
|
|
||||||
# keep all snapshots that don't have the `zrepl_` prefix
|
|
||||||
- type: regex
|
|
||||||
negate: true
|
|
||||||
regex: "^zrepl_.*"
|
|
||||||
|
|
||||||
# This job pushes to the local sink defined in job `backuppool_sink`.
|
|
||||||
# We trigger replication manually from the command line / udev rules using
|
|
||||||
# `zrepl signal wakeup push_to_drive`
|
|
||||||
- type: push
|
|
||||||
name: push_to_drive
|
|
||||||
connect:
|
|
||||||
type: local
|
|
||||||
listener_name: backuppool_sink
|
|
||||||
client_identity: myhostname
|
|
||||||
filesystems: {
|
|
||||||
"system<": true
|
|
||||||
}
|
|
||||||
send:
|
|
||||||
encrypted: true
|
|
||||||
replication:
|
|
||||||
protection:
|
|
||||||
initial: guarantee_resumability
|
|
||||||
# Downgrade protection to guarantee_incremental which uses zfs bookmarks instead of zfs holds.
|
|
||||||
# Thus, when we yank out the backup drive during replication
|
|
||||||
# - we might not be able to resume the interrupted replication step because the partially received `to` snapshot of a `from`->`to` step may be pruned any time
|
|
||||||
# - but in exchange we get back the disk space allocated by `to` when we prune it
|
|
||||||
# - and because we still have the bookmarks created by `guarantee_incremental`, we can still do incremental replication of `from`->`to2` in the future
|
|
||||||
incremental: guarantee_incremental
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
pruning:
|
|
||||||
# no-op prune rule on sender (keep all snapshots), job `snapshot` takes care of this
|
|
||||||
keep_sender:
|
|
||||||
- type: regex
|
|
||||||
regex: ".*"
|
|
||||||
# retain
|
|
||||||
keep_receiver:
|
|
||||||
# longer retention on the backup drive, we have more space there
|
|
||||||
- type: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 360x1d
|
|
||||||
regex: "^zrepl_.*"
|
|
||||||
# retain all non-zrepl snapshots on the backup drive
|
|
||||||
- type: regex
|
|
||||||
negate: true
|
|
||||||
regex: "^zrepl_.*"
|
|
||||||
|
|
||||||
# This job receives from job `push_to_drive` into `backuppool/zrepl/sink/myhostname`
|
|
||||||
- type: sink
|
|
||||||
name: backuppool_sink
|
|
||||||
root_fs: "backuppool/zrepl/sink"
|
|
||||||
serve:
|
|
||||||
type: local
|
|
||||||
listener_name: backuppool_sink
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
jobs:
|
|
||||||
- name: sink
|
|
||||||
type: sink
|
|
||||||
serve:
|
|
||||||
type: tls
|
|
||||||
listen: ":8888"
|
|
||||||
ca: "/etc/zrepl/prod.crt"
|
|
||||||
cert: "/etc/zrepl/backups.crt"
|
|
||||||
key: "/etc/zrepl/backups.key"
|
|
||||||
client_cns:
|
|
||||||
- "prod"
|
|
||||||
root_fs: "storage/zrepl/sink"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
jobs:
|
|
||||||
- name: prod_to_backups
|
|
||||||
type: push
|
|
||||||
connect:
|
|
||||||
type: tls
|
|
||||||
address: "backups.example.com:8888"
|
|
||||||
ca: /etc/zrepl/backups.crt
|
|
||||||
cert: /etc/zrepl/prod.crt
|
|
||||||
key: /etc/zrepl/prod.key
|
|
||||||
server_cn: "backups"
|
|
||||||
filesystems: {
|
|
||||||
"zroot<": true,
|
|
||||||
"zroot/var/tmp<": false,
|
|
||||||
"zroot/usr/home/paranoid": false
|
|
||||||
}
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: not_replicated
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
keep_receiver:
|
|
||||||
- type: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
|
|
||||||
regex: "^zrepl_"
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
jobs:
|
|
||||||
# Separate job for snapshots and pruning
|
|
||||||
- name: snapshots
|
|
||||||
type: snap
|
|
||||||
filesystems:
|
|
||||||
'tank<': true # all filesystems
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
pruning:
|
|
||||||
keep:
|
|
||||||
# Keep non-zrepl snapshots
|
|
||||||
- type: regex
|
|
||||||
negate: true
|
|
||||||
regex: '^zrepl_'
|
|
||||||
# Time-based snapshot retention
|
|
||||||
- type: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 12x30d
|
|
||||||
regex: '^zrepl_'
|
|
||||||
|
|
||||||
# Source job for target B
|
|
||||||
- name: target_b
|
|
||||||
type: source
|
|
||||||
serve:
|
|
||||||
type: tls
|
|
||||||
listen: :8888
|
|
||||||
ca: /etc/zrepl/b.example.com.crt
|
|
||||||
cert: /etc/zrepl/a.example.com.crt
|
|
||||||
key: /etc/zrepl/a.example.com.key
|
|
||||||
client_cns:
|
|
||||||
- b.example.com
|
|
||||||
filesystems:
|
|
||||||
'tank<': true # all filesystems
|
|
||||||
# Snapshots are handled by the separate snap job
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
|
|
||||||
# Source job for target C
|
|
||||||
- name: target_c
|
|
||||||
type: source
|
|
||||||
serve:
|
|
||||||
type: tls
|
|
||||||
listen: :8889
|
|
||||||
ca: /etc/zrepl/c.example.com.crt
|
|
||||||
cert: /etc/zrepl/a.example.com.crt
|
|
||||||
key: /etc/zrepl/a.example.com.key
|
|
||||||
client_cns:
|
|
||||||
- c.example.com
|
|
||||||
filesystems:
|
|
||||||
'tank<': true # all filesystems
|
|
||||||
# Snapshots are handled by the separate snap job
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
|
|
||||||
# Source jobs for remaining targets. Each one should listen on a different port
|
|
||||||
# and reference the correct certificate and client CN.
|
|
||||||
# - name: target_c
|
|
||||||
# ...
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
jobs:
|
|
||||||
# Pull from source server A
|
|
||||||
- name: source_a
|
|
||||||
type: pull
|
|
||||||
connect:
|
|
||||||
type: tls
|
|
||||||
# Use the correct port for this specific client (eg. B is 8888, C is 8889, etc.)
|
|
||||||
address: a.example.com:8888
|
|
||||||
ca: /etc/zrepl/a.example.com.crt
|
|
||||||
# Use the correct key pair for this specific client
|
|
||||||
cert: /etc/zrepl/b.example.com.crt
|
|
||||||
key: /etc/zrepl/b.example.com.key
|
|
||||||
server_cn: a.example.com
|
|
||||||
root_fs: pool0/backup
|
|
||||||
interval: 10m
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
# Source does the pruning in its snap job
|
|
||||||
- type: regex
|
|
||||||
regex: '.*'
|
|
||||||
# Receiver-side pruning can be configured as desired on each target server
|
|
||||||
keep_receiver:
|
|
||||||
# Keep non-zrepl snapshots
|
|
||||||
- type: regex
|
|
||||||
negate: true
|
|
||||||
regex: '^zrepl_'
|
|
||||||
# Time-based snapshot retention
|
|
||||||
- type: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 12x30d
|
|
||||||
regex: '^zrepl_'
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
jobs:
|
|
||||||
- name: snapjob
|
|
||||||
type: snap
|
|
||||||
filesystems: {
|
|
||||||
"tank<": true,
|
|
||||||
}
|
|
||||||
snapshotting:
|
|
||||||
type: cron
|
|
||||||
prefix: zrepl_snapjob_
|
|
||||||
cron: "*/5 * * * *"
|
|
||||||
pruning:
|
|
||||||
keep:
|
|
||||||
- type: last_n
|
|
||||||
count: 60
|
|
||||||
@@ -5,11 +5,7 @@ jobs:
|
|||||||
type: tcp
|
type: tcp
|
||||||
listen: "0.0.0.0:8888"
|
listen: "0.0.0.0:8888"
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "mysql01",
|
"192.168.122.123" : "client1"
|
||||||
"192.168.122.42" : "mx01",
|
|
||||||
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
|
||||||
"10.23.42.0/24": "cluster-*",
|
|
||||||
"fde4:8dba:82e1::/64": "san-*",
|
|
||||||
}
|
}
|
||||||
filesystems: {
|
filesystems: {
|
||||||
"<": true,
|
"<": true,
|
||||||
|
|||||||
+5
-16
@@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -21,7 +20,6 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type controlJob struct {
|
type controlJob struct {
|
||||||
@@ -66,7 +64,7 @@ func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "control",
|
Subsystem: "control",
|
||||||
Name: "request_finished",
|
Name: "request_finished",
|
||||||
Help: "time it took a request to finish",
|
Help: "time it took a request to finih",
|
||||||
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3, 10e-3, 100e-3, 200e-3, 400e-3, 800e-3, 1, 10, 20},
|
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3, 10e-3, 100e-3, 200e-3, 400e-3, 800e-3, 1, 10, 20},
|
||||||
}, []string{"endpoint"})
|
}, []string{"endpoint"})
|
||||||
registerer.MustRegister(promControl.requestBegin)
|
registerer.MustRegister(promControl.requestBegin)
|
||||||
@@ -119,16 +117,7 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
mux.Handle(ControlJobEndpointStatus,
|
mux.Handle(ControlJobEndpointStatus,
|
||||||
// don't log requests to status endpoint, too spammy
|
// don't log requests to status endpoint, too spammy
|
||||||
jsonResponder{log, func() (interface{}, error) {
|
jsonResponder{log, func() (interface{}, error) {
|
||||||
jobs := j.jobs.status()
|
s := j.jobs.status()
|
||||||
globalZFS := zfscmd.GetReport()
|
|
||||||
envconstReport := envconst.GetReport()
|
|
||||||
s := Status{
|
|
||||||
Jobs: jobs,
|
|
||||||
Global: GlobalStatus{
|
|
||||||
ZFSCmds: globalZFS,
|
|
||||||
Envconst: envconstReport,
|
|
||||||
OsEnviron: os.Environ(),
|
|
||||||
}}
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -158,8 +147,8 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
server := http.Server{
|
server := http.Server{
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
|
||||||
WriteTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_WRITE_TIMEOUT", 1*time.Second),
|
WriteTimeout: 1 * time.Second,
|
||||||
ReadTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_SERVER_READ_TIMEOUT", 1*time.Second),
|
ReadTimeout: 1 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
@@ -261,7 +250,7 @@ func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
encodeErr := json.NewEncoder(&buf).Encode(res)
|
encodeErr := json.NewEncoder(&buf).Encode(res)
|
||||||
if encodeErr != nil {
|
if encodeErr != nil {
|
||||||
j.log.WithError(producerErr).Error("control handler json marshal error")
|
j.log.WithError(producerErr).Error("control handler json marhsal error")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, err := io.WriteString(w, encodeErr.Error())
|
_, err := io.WriteString(w, encodeErr.Error())
|
||||||
logIoErr(err)
|
logIoErr(err)
|
||||||
|
|||||||
+16
-52
@@ -3,7 +3,6 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,10 +13,6 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||||
@@ -25,11 +20,11 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(ctx context.Context, conf *config.Config) error {
|
func Run(conf *config.Config) error {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
@@ -39,19 +34,12 @@ func Run(ctx context.Context, conf *config.Config) error {
|
|||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// The math/rand package is used presently for generating trace IDs, we
|
|
||||||
// seed it with the current time and pid so that the IDs are mostly
|
|
||||||
// unique.
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
rand.Seed(int64(os.Getpid()))
|
|
||||||
|
|
||||||
outlets, err := logging.OutletsFromConfig(*conf.Global.Logging)
|
outlets, err := logging.OutletsFromConfig(*conf.Global.Logging)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot build logging from config")
|
return errors.Wrap(err, "cannot build logging from config")
|
||||||
}
|
}
|
||||||
outlets.Add(newPrometheusLogOutlet(), logger.Debug)
|
|
||||||
|
|
||||||
confJobs, err := job.JobsFromConfig(conf, config.ParseFlagsNone)
|
confJobs, err := job.JobsFromConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot build jobs from config")
|
return errors.Wrap(err, "cannot build jobs from config")
|
||||||
}
|
}
|
||||||
@@ -59,23 +47,14 @@ func Run(ctx context.Context, conf *config.Config) error {
|
|||||||
log := logger.NewLogger(outlets, 1*time.Second)
|
log := logger.NewLogger(outlets, 1*time.Second)
|
||||||
log.Info(version.NewZreplVersionInformation().String())
|
log.Info(version.NewZreplVersionInformation().String())
|
||||||
|
|
||||||
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
|
|
||||||
trace.RegisterCallback(trace.Callback{
|
|
||||||
OnBegin: func(ctx context.Context) { logging.GetLogger(ctx, logging.SubsysTraceData).Debug("begin span") },
|
|
||||||
OnEnd: func(ctx context.Context, spanInfo trace.SpanInfo) {
|
|
||||||
logging.
|
|
||||||
GetLogger(ctx, logging.SubsysTraceData).
|
|
||||||
WithField("duration_s", spanInfo.EndedAt().Sub(spanInfo.StartedAt()).Seconds()).
|
|
||||||
Debug("finished span " + spanInfo.TaskAndSpanStack(trace.SpanStackKindAnnotation))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, job := range confJobs {
|
for _, job := range confJobs {
|
||||||
if IsInternalJobName(job.Name()) {
|
if IsInternalJobName(job.Name()) {
|
||||||
panic(fmt.Sprintf("internal job name used for config job '%s'", job.Name())) //FIXME
|
panic(fmt.Sprintf("internal job name used for config job '%s'", job.Name())) //FIXME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx = job.WithLogger(ctx, log)
|
||||||
|
|
||||||
jobs := newJobs()
|
jobs := newJobs()
|
||||||
|
|
||||||
// start control socket
|
// start control socket
|
||||||
@@ -102,12 +81,6 @@ func Run(ctx context.Context, conf *config.Config) error {
|
|||||||
jobs.start(ctx, job, true)
|
jobs.start(ctx, job, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// register global (=non job-local) metrics
|
|
||||||
version.PrometheusRegister(prometheus.DefaultRegisterer)
|
|
||||||
zfscmd.RegisterMetrics(prometheus.DefaultRegisterer)
|
|
||||||
trace.RegisterMetrics(prometheus.DefaultRegisterer)
|
|
||||||
endpoint.RegisterMetrics(prometheus.DefaultRegisterer)
|
|
||||||
|
|
||||||
log.Info("starting daemon")
|
log.Info("starting daemon")
|
||||||
|
|
||||||
// start regular jobs
|
// start regular jobs
|
||||||
@@ -121,8 +94,6 @@ func Run(ctx context.Context, conf *config.Config) error {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.WithError(ctx.Err()).Info("context finished")
|
log.WithError(ctx.Err()).Info("context finished")
|
||||||
}
|
}
|
||||||
log.Info("waiting for jobs to finish")
|
|
||||||
<-jobs.wait()
|
|
||||||
log.Info("daemon exiting")
|
log.Info("daemon exiting")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -145,26 +116,18 @@ func newJobs() *jobs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
logJobField string = "job"
|
||||||
|
)
|
||||||
|
|
||||||
func (s *jobs) wait() <-chan struct{} {
|
func (s *jobs) wait() <-chan struct{} {
|
||||||
ch := make(chan struct{})
|
ch := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
s.wg.Wait()
|
s.wg.Wait()
|
||||||
close(ch)
|
|
||||||
}()
|
}()
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
type Status struct {
|
|
||||||
Jobs map[string]*job.Status
|
|
||||||
Global GlobalStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
type GlobalStatus struct {
|
|
||||||
ZFSCmds *zfscmd.Report
|
|
||||||
Envconst *envconst.Report
|
|
||||||
OsEnviron []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *jobs) status() map[string]*job.Status {
|
func (s *jobs) status() map[string]*job.Status {
|
||||||
s.m.RLock()
|
s.m.RLock()
|
||||||
defer s.m.RUnlock()
|
defer s.m.RUnlock()
|
||||||
@@ -226,8 +189,9 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
s.m.Lock()
|
s.m.Lock()
|
||||||
defer s.m.Unlock()
|
defer s.m.Unlock()
|
||||||
|
|
||||||
ctx = logging.WithInjectedField(ctx, logging.JobField, j.Name())
|
jobLog := job.GetLogger(ctx).
|
||||||
|
WithField(logJobField, j.Name()).
|
||||||
|
WithOutlet(newPrometheusLogOutlet(j.Name()), logger.Debug)
|
||||||
jobName := j.Name()
|
jobName := j.Name()
|
||||||
if !internal && IsInternalJobName(jobName) {
|
if !internal && IsInternalJobName(jobName) {
|
||||||
panic(fmt.Sprintf("internal job name used for non-internal job %s", jobName))
|
panic(fmt.Sprintf("internal job name used for non-internal job %s", jobName))
|
||||||
@@ -242,7 +206,7 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
j.RegisterMetrics(prometheus.DefaultRegisterer)
|
j.RegisterMetrics(prometheus.DefaultRegisterer)
|
||||||
|
|
||||||
s.jobs[jobName] = j
|
s.jobs[jobName] = j
|
||||||
ctx = zfscmd.WithJobID(ctx, j.Name())
|
ctx = job.WithLogger(ctx, jobLog)
|
||||||
ctx, wakeup := wakeup.Context(ctx)
|
ctx, wakeup := wakeup.Context(ctx)
|
||||||
ctx, resetFunc := reset.Context(ctx)
|
ctx, resetFunc := reset.Context(ctx)
|
||||||
s.wakeups[jobName] = wakeup
|
s.wakeups[jobName] = wakeup
|
||||||
@@ -251,8 +215,8 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer s.wg.Done()
|
defer s.wg.Done()
|
||||||
job.GetLogger(ctx).Info("starting job")
|
jobLog.Info("starting job")
|
||||||
defer job.GetLogger(ctx).Info("job exited")
|
defer jobLog.Info("job exited")
|
||||||
j.Run(ctx)
|
j.Run(ctx)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func (m DatasetMapFilter) Filter(p *zfs.DatasetPath) (pass bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct a new filter-only DatasetMapFilter from a mapping
|
// Construct a new filter-only DatasetMapFilter from a mapping
|
||||||
// The new filter allows exactly those paths that were not forbidden by the mapping.
|
// The new filter allows excactly those paths that were not forbidden by the mapping.
|
||||||
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
||||||
|
|
||||||
if m.filterMode {
|
if m.filterMode {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnyFSVFilter struct{}
|
||||||
|
|
||||||
|
func NewAnyFSVFilter() AnyFSVFilter {
|
||||||
|
return AnyFSVFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ zfs.FilesystemVersionFilter = AnyFSVFilter{}
|
||||||
|
|
||||||
|
func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrefixFilter struct {
|
||||||
|
prefix string
|
||||||
|
fstype zfs.VersionType
|
||||||
|
fstypeSet bool // optionals anyone?
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ zfs.FilesystemVersionFilter = &PrefixFilter{}
|
||||||
|
|
||||||
|
func NewPrefixFilter(prefix string) *PrefixFilter {
|
||||||
|
return &PrefixFilter{prefix: prefix}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTypedPrefixFilter(prefix string, versionType zfs.VersionType) *PrefixFilter {
|
||||||
|
return &PrefixFilter{prefix, versionType, true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *PrefixFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
|
||||||
|
fstypeMatches := (!f.fstypeSet || t == f.fstype)
|
||||||
|
prefixMatches := strings.HasPrefix(name, f.prefix)
|
||||||
|
return fstypeMatches && prefixMatches, nil
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
//
|
//
|
||||||
// This package also provides all supported hook type implementations and abstractions around them.
|
// This package also provides all supported hook type implementations and abstractions around them.
|
||||||
//
|
//
|
||||||
// # Use For Other Kinds Of ExpectStepReports
|
// Use For Other Kinds Of ExpectStepReports
|
||||||
//
|
//
|
||||||
// This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication:
|
// This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication:
|
||||||
//
|
//
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// The hook implementations should move out of this package.
|
// The hook implementations should move out of this package.
|
||||||
// However, there is a lot of tight coupling which to untangle isn't worth it ATM.
|
// However, there is a lot of tight coupling which to untangle isn't worth it ATM.
|
||||||
//
|
//
|
||||||
// # How This Package Is Used By Package Snapper
|
// How This Package Is Used By Package Snapper
|
||||||
//
|
//
|
||||||
// Deserialize a config.List using ListFromConfig().
|
// Deserialize a config.List using ListFromConfig().
|
||||||
// Then it MUST filter the list to only contain hooks for a particular filesystem using
|
// Then it MUST filter the list to only contain hooks for a particular filesystem using
|
||||||
@@ -30,4 +30,5 @@
|
|||||||
// Command hooks make it available in the environment variable ZREPL_DRYRUN.
|
// Command hooks make it available in the environment variable ZREPL_DRYRUN.
|
||||||
//
|
//
|
||||||
// Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status").
|
// Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status").
|
||||||
|
//
|
||||||
package hooks
|
package hooks
|
||||||
|
|||||||
@@ -6,17 +6,29 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeyLog contextKey = 0
|
||||||
|
)
|
||||||
|
|
||||||
type Logger = logger.Logger
|
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 { return getLogger(ctx) }
|
||||||
|
|
||||||
func getLogger(ctx context.Context) Logger {
|
func getLogger(ctx context.Context) Logger {
|
||||||
return logging.GetLogger(ctx, logging.SubsysHooks)
|
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
return logger.NewNullLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
||||||
|
|||||||
@@ -93,14 +93,7 @@ func (r *CommandHookReport) String() string {
|
|||||||
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
|
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg string
|
return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
|
||||||
if r.Err == nil {
|
|
||||||
msg = "command hook"
|
|
||||||
} else {
|
|
||||||
msg = fmt.Sprintf("command hook failed with %q", r.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s: \"%s\"", msg, cmdLine.String()) // no %q to make copy-pastable
|
|
||||||
}
|
}
|
||||||
func (r *CommandHookReport) Error() string {
|
func (r *CommandHookReport) Error() string {
|
||||||
if r.Err == nil {
|
if r.Err == nil {
|
||||||
|
|||||||
@@ -17,22 +17,19 @@ import (
|
|||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hook to implement the following recommmendation from MySQL docs
|
|
||||||
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
|
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
|
||||||
//
|
//
|
||||||
// Making Backups Using a File System Snapshot:
|
// Making Backups Using a File System Snapshot:
|
||||||
//
|
//
|
||||||
// If you are using a Veritas file system, you can make a backup like this:
|
// If you are using a Veritas file system, you can make a backup like this:
|
||||||
//
|
//
|
||||||
// From a client program, execute FLUSH TABLES WITH READ LOCK.
|
// From a client program, execute FLUSH TABLES WITH READ LOCK.
|
||||||
// From another shell, execute mount vxfs snapshot.
|
// From another shell, execute mount vxfs snapshot.
|
||||||
// From the first client, execute UNLOCK TABLES.
|
// From the first client, execute UNLOCK TABLES.
|
||||||
// Copy files from the snapshot.
|
// Copy files from the snapshot.
|
||||||
// Unmount the snapshot.
|
// Unmount the snapshot.
|
||||||
//
|
//
|
||||||
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
||||||
//
|
|
||||||
|
|
||||||
type MySQLLockTables struct {
|
type MySQLLockTables struct {
|
||||||
errIsFatal bool
|
errIsFatal bool
|
||||||
connector sqldriver.Connector
|
connector sqldriver.Connector
|
||||||
|
|||||||
@@ -11,11 +11,8 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
@@ -72,9 +69,6 @@ func curry(f comparisonAssertionFunc, expected interface{}, right bool) (ret val
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHooks(t *testing.T) {
|
func TestHooks(t *testing.T) {
|
||||||
ctx, end := trace.WithTaskFromStack(context.Background())
|
|
||||||
defer end()
|
|
||||||
|
|
||||||
testFSName := "testpool/testdataset"
|
testFSName := "testpool/testdataset"
|
||||||
testSnapshotName := "testsnap"
|
testSnapshotName := "testsnap"
|
||||||
|
|
||||||
@@ -161,7 +155,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
},
|
},
|
||||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
expectStep{
|
expectStep{
|
||||||
@@ -185,7 +179,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
},
|
},
|
||||||
expectStep{
|
expectStep{
|
||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
@@ -234,7 +228,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Post,
|
ExpectedEdge: hooks.Post,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -267,7 +261,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
},
|
},
|
||||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
expectStep{
|
expectStep{
|
||||||
@@ -295,7 +289,7 @@ jobs:
|
|||||||
ExpectedEdge: hooks.Pre,
|
ExpectedEdge: hooks.Pre,
|
||||||
ExpectStatus: hooks.StepErr,
|
ExpectStatus: hooks.StepErr,
|
||||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||||
},
|
},
|
||||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||||
expectStep{
|
expectStep{
|
||||||
@@ -424,8 +418,9 @@ jobs:
|
|||||||
|
|
||||||
cbReached = false
|
cbReached = false
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
if testing.Verbose() && !tt.SuppressOutput {
|
if testing.Verbose() && !tt.SuppressOutput {
|
||||||
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
|
ctx = hooks.WithLogger(ctx, log)
|
||||||
}
|
}
|
||||||
plan.Run(ctx, false)
|
plan.Run(ctx, false)
|
||||||
report := plan.Report()
|
report := plan.Report()
|
||||||
|
|||||||
+54
-131
@@ -8,14 +8,12 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/common/log"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
"github.com/zrepl/zrepl/daemon/job/reset"
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -34,15 +32,11 @@ type ActiveSide struct {
|
|||||||
name endpoint.JobID
|
name endpoint.JobID
|
||||||
connecter transport.Connecter
|
connecter transport.Connecter
|
||||||
|
|
||||||
replicationDriverConfig driver.Config
|
|
||||||
|
|
||||||
prunerFactory *pruner.PrunerFactory
|
prunerFactory *pruner.PrunerFactory
|
||||||
|
|
||||||
promRepStateSecs *prometheus.HistogramVec // labels: state
|
promRepStateSecs *prometheus.HistogramVec // labels: state
|
||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||||
promReplicationErrors prometheus.Gauge
|
|
||||||
promLastSuccessful prometheus.Gauge
|
|
||||||
|
|
||||||
tasksMtx sync.Mutex
|
tasksMtx sync.Mutex
|
||||||
tasks activeSideTasks
|
tasks activeSideTasks
|
||||||
@@ -85,7 +79,7 @@ func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type activeMode interface {
|
type activeMode interface {
|
||||||
ConnectEndpoints(ctx context.Context, connecter transport.Connecter)
|
ConnectEndpoints(rpcLoggers rpc.Loggers, connecter transport.Connecter)
|
||||||
DisconnectEndpoints()
|
DisconnectEndpoints()
|
||||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||||
Type() Type
|
Type() Type
|
||||||
@@ -101,17 +95,17 @@ type modePush struct {
|
|||||||
receiver *rpc.Client
|
receiver *rpc.Client
|
||||||
senderConfig *endpoint.SenderConfig
|
senderConfig *endpoint.SenderConfig
|
||||||
plannerPolicy *logic.PlannerPolicy
|
plannerPolicy *logic.PlannerPolicy
|
||||||
snapper snapper.Snapper
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
||||||
m.setupMtx.Lock()
|
m.setupMtx.Lock()
|
||||||
defer m.setupMtx.Unlock()
|
defer m.setupMtx.Unlock()
|
||||||
if m.receiver != nil || m.sender != nil {
|
if m.receiver != nil || m.sender != nil {
|
||||||
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
||||||
}
|
}
|
||||||
m.sender = endpoint.NewSender(*m.senderConfig)
|
m.sender = endpoint.NewSender(*m.senderConfig)
|
||||||
m.receiver = rpc.NewClient(connecter, rpc.GetLoggersOrPanic(ctx))
|
m.receiver = rpc.NewClient(connecter, loggers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) DisconnectEndpoints() {
|
func (m *modePush) DisconnectEndpoints() {
|
||||||
@@ -137,8 +131,7 @@ func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) SnapperReport() *snapper.Report {
|
func (m *modePush) SnapperReport() *snapper.Report {
|
||||||
r := m.snapper.Report()
|
return m.snapper.Report()
|
||||||
return &r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) ResetConnectBackoff() {
|
func (m *modePush) ResetConnectBackoff() {
|
||||||
@@ -151,33 +144,22 @@ func (m *modePush) ResetConnectBackoff() {
|
|||||||
|
|
||||||
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||||
m := &modePush{}
|
m := &modePush{}
|
||||||
var err error
|
|
||||||
|
|
||||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "sender config")
|
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
||||||
}
|
}
|
||||||
|
|
||||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
if err != nil {
|
FSF: fsf,
|
||||||
return nil, errors.Wrap(err, "field `replication`")
|
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||||
|
JobID: jobID,
|
||||||
}
|
}
|
||||||
|
|
||||||
conflictResolution, err := logic.ConflictResolutionFromConfig(in.ConflictResolution)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "field `conflict_resolution`")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.plannerPolicy = &logic.PlannerPolicy{
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
ConflictResolution: conflictResolution,
|
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||||
ReplicationConfig: replicationConfig,
|
|
||||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
|
||||||
}
|
|
||||||
if err := m.plannerPolicy.Validate(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build planner policy")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,18 +171,19 @@ type modePull struct {
|
|||||||
receiver *endpoint.Receiver
|
receiver *endpoint.Receiver
|
||||||
receiverConfig endpoint.ReceiverConfig
|
receiverConfig endpoint.ReceiverConfig
|
||||||
sender *rpc.Client
|
sender *rpc.Client
|
||||||
|
rootFS *zfs.DatasetPath
|
||||||
plannerPolicy *logic.PlannerPolicy
|
plannerPolicy *logic.PlannerPolicy
|
||||||
interval config.PositiveDurationOrManual
|
interval config.PositiveDurationOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePull) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
||||||
m.setupMtx.Lock()
|
m.setupMtx.Lock()
|
||||||
defer m.setupMtx.Unlock()
|
defer m.setupMtx.Unlock()
|
||||||
if m.receiver != nil || m.sender != nil {
|
if m.receiver != nil || m.sender != nil {
|
||||||
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
||||||
}
|
}
|
||||||
m.receiver = endpoint.NewReceiver(m.receiverConfig)
|
m.receiver = endpoint.NewReceiver(m.receiverConfig)
|
||||||
m.sender = rpc.NewClient(connecter, rpc.GetLoggersOrPanic(ctx))
|
m.sender = rpc.NewClient(connecter, loggers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePull) DisconnectEndpoints() {
|
func (m *modePull) DisconnectEndpoints() {
|
||||||
@@ -262,44 +245,32 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
|||||||
m = &modePull{}
|
m = &modePull{}
|
||||||
m.interval = in.Interval
|
m.interval = in.Interval
|
||||||
|
|
||||||
replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication)
|
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "field `replication`")
|
return nil, errors.New("RootFS is not a valid zfs filesystem path")
|
||||||
}
|
}
|
||||||
|
if m.rootFS.Length() <= 0 {
|
||||||
conflictResolution, err := logic.ConflictResolutionFromConfig(in.ConflictResolution)
|
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "field `conflict_resolution`")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.plannerPolicy = &logic.PlannerPolicy{
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
ConflictResolution: conflictResolution,
|
EncryptedSend: logic.DontCare,
|
||||||
ReplicationConfig: replicationConfig,
|
|
||||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
|
||||||
}
|
|
||||||
if err := m.plannerPolicy.Validate(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build planner policy")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
m.receiverConfig = endpoint.ReceiverConfig{
|
||||||
if err != nil {
|
JobID: jobID,
|
||||||
return nil, err
|
RootWithoutClientComponent: m.rootFS,
|
||||||
|
AppendClientIdentity: false, // !
|
||||||
|
UpdateLastReceivedHold: true,
|
||||||
|
}
|
||||||
|
if err := m.receiverConfig.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func replicationDriverConfigFromConfig(in *config.Replication) (c driver.Config, err error) {
|
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
||||||
c = driver.Config{
|
|
||||||
StepQueueConcurrency: in.Concurrency.Steps,
|
|
||||||
MaxAttempts: envconst.Int("ZREPL_REPLICATION_MAX_ATTEMPTS", 3),
|
|
||||||
ReconnectHardFailTimeout: envconst.Duration("ZREPL_REPLICATION_RECONNECT_HARD_FAIL_TIMEOUT", 10*time.Minute),
|
|
||||||
}
|
|
||||||
err = c.Validate()
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, parseFlags config.ParseFlags) (j *ActiveSide, err error) {
|
|
||||||
|
|
||||||
j = &ActiveSide{}
|
j = &ActiveSide{}
|
||||||
j.name, err = endpoint.MakeJobID(in.Name)
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
@@ -333,22 +304,8 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, p
|
|||||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
Help: "number of bytes replicated from sender to receiver per filesystem",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"filesystem"})
|
}, []string{"filesystem"})
|
||||||
j.promReplicationErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "replication",
|
|
||||||
Name: "filesystem_errors",
|
|
||||||
Help: "number of filesystems that failed replication in the latest replication attempt, or -1 if the job failed before enumerating the filesystems",
|
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
|
||||||
})
|
|
||||||
j.promLastSuccessful = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "replication",
|
|
||||||
Name: "last_successful",
|
|
||||||
Help: "timestamp of last successful replication",
|
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
|
||||||
})
|
|
||||||
|
|
||||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect, parseFlags)
|
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build client")
|
return nil, errors.Wrap(err, "cannot build client")
|
||||||
}
|
}
|
||||||
@@ -365,11 +322,6 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}, p
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
j.replicationDriverConfig, err = replicationDriverConfigFromConfig(in.Replication)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build replication driver config")
|
|
||||||
}
|
|
||||||
|
|
||||||
return j, nil
|
return j, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,8 +329,6 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
registerer.MustRegister(j.promRepStateSecs)
|
registerer.MustRegister(j.promRepStateSecs)
|
||||||
registerer.MustRegister(j.promPruneSecs)
|
registerer.MustRegister(j.promPruneSecs)
|
||||||
registerer.MustRegister(j.promBytesReplicated)
|
registerer.MustRegister(j.promBytesReplicated)
|
||||||
registerer.MustRegister(j.promReplicationErrors)
|
|
||||||
registerer.MustRegister(j.promLastSuccessful)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Name() string { return j.name.String() }
|
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||||
@@ -413,7 +363,7 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|||||||
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return pull.receiverConfig.RootWithoutClientComponent.Copy(), true
|
return pull.rootFS.Copy(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||||
@@ -425,31 +375,16 @@ func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
|||||||
return push.senderConfig
|
return push.senderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// The active side of a replication uses one end (sender or receiver)
|
|
||||||
// directly by method invocation, without going through a transport that
|
|
||||||
// provides a client identity.
|
|
||||||
// However, in order to avoid the need to distinguish between direct-method-invocating
|
|
||||||
// clients and RPC client, we use an invalid client identity as a sentinel value.
|
|
||||||
func FakeActiveSideDirectMethodInvocationClientIdentity(jobId endpoint.JobID) string {
|
|
||||||
return fmt.Sprintf("<local><active><job><client><identity><job=%q>", jobId.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *ActiveSide) Run(ctx context.Context) {
|
func (j *ActiveSide) Run(ctx context.Context) {
|
||||||
ctx, endTask := trace.WithTaskAndSpan(ctx, "active-side-job", j.Name())
|
|
||||||
defer endTask()
|
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, endpoint.ClientIdentityKey, FakeActiveSideDirectMethodInvocationClientIdentity(j.name))
|
|
||||||
|
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
|
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
|
|
||||||
periodicDone := make(chan struct{})
|
periodicDone := make(chan struct{})
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
|
go j.mode.RunPeriodic(ctx, periodicDone)
|
||||||
defer endTask()
|
|
||||||
go j.mode.RunPeriodic(periodicCtx, periodicDone)
|
|
||||||
|
|
||||||
invocationCount := 0
|
invocationCount := 0
|
||||||
outer:
|
outer:
|
||||||
@@ -465,15 +400,17 @@ outer:
|
|||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
|
invLog := log.WithField("invocation", invocationCount)
|
||||||
j.do(invocationCtx)
|
j.do(WithLogger(ctx, invLog))
|
||||||
endSpan()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) do(ctx context.Context) {
|
func (j *ActiveSide) do(ctx context.Context) {
|
||||||
|
|
||||||
j.mode.ConnectEndpoints(ctx, j.connecter)
|
log := GetLogger(ctx)
|
||||||
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
|
loggers := rpc.GetLoggersOrPanic(ctx) // filled by WithSubsystemLoggers
|
||||||
|
j.mode.ConnectEndpoints(loggers, j.connecter)
|
||||||
defer j.mode.DisconnectEndpoints()
|
defer j.mode.DisconnectEndpoints()
|
||||||
|
|
||||||
// allow cancellation of an invocation (this function)
|
// allow cancellation of an invocation (this function)
|
||||||
@@ -496,30 +433,20 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
ctx, endSpan := trace.WithSpan(ctx, "replication")
|
|
||||||
ctx, repCancel := context.WithCancel(ctx)
|
ctx, repCancel := context.WithCancel(ctx)
|
||||||
var repWait driver.WaitFunc
|
var repWait driver.WaitFunc
|
||||||
j.updateTasks(func(tasks *activeSideTasks) {
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
// reset it
|
// reset it
|
||||||
*tasks = activeSideTasks{}
|
*tasks = activeSideTasks{}
|
||||||
tasks.replicationCancel = func() { repCancel(); endSpan() }
|
tasks.replicationCancel = repCancel
|
||||||
tasks.replicationReport, repWait = replication.Do(
|
tasks.replicationReport, repWait = replication.Do(
|
||||||
ctx, j.replicationDriverConfig, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||||
)
|
)
|
||||||
tasks.state = ActiveSideReplicating
|
tasks.state = ActiveSideReplicating
|
||||||
})
|
})
|
||||||
GetLogger(ctx).Info("start replication")
|
log.Info("start replication")
|
||||||
repWait(true) // wait blocking
|
repWait(true) // wait blocking
|
||||||
repCancel() // always cancel to free up context resources
|
repCancel() // always cancel to free up context resources
|
||||||
|
|
||||||
replicationReport := j.tasks.replicationReport()
|
|
||||||
var numErrors = replicationReport.GetFailedFilesystemsCountInLatestAttempt()
|
|
||||||
j.promReplicationErrors.Set(float64(numErrors))
|
|
||||||
if numErrors == 0 {
|
|
||||||
j.promLastSuccessful.SetToCurrentTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
endSpan()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -528,18 +455,16 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
ctx, endSpan := trace.WithSpan(ctx, "prune_sender")
|
|
||||||
ctx, senderCancel := context.WithCancel(ctx)
|
ctx, senderCancel := context.WithCancel(ctx)
|
||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||||
tasks.prunerSenderCancel = func() { senderCancel(); endSpan() }
|
tasks.prunerSenderCancel = senderCancel
|
||||||
tasks.state = ActiveSidePruneSender
|
tasks.state = ActiveSidePruneSender
|
||||||
})
|
})
|
||||||
GetLogger(ctx).Info("start pruning sender")
|
log.Info("start pruning sender")
|
||||||
tasks.prunerSender.Prune()
|
tasks.prunerSender.Prune()
|
||||||
GetLogger(ctx).Info("finished pruning sender")
|
log.Info("finished pruning sender")
|
||||||
senderCancel()
|
senderCancel()
|
||||||
endSpan()
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
select {
|
select {
|
||||||
@@ -547,18 +472,16 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
ctx, endSpan := trace.WithSpan(ctx, "prune_recever")
|
|
||||||
ctx, receiverCancel := context.WithCancel(ctx)
|
ctx, receiverCancel := context.WithCancel(ctx)
|
||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
||||||
tasks.prunerReceiverCancel = func() { receiverCancel(); endSpan() }
|
tasks.prunerReceiverCancel = receiverCancel
|
||||||
tasks.state = ActiveSidePruneReceiver
|
tasks.state = ActiveSidePruneReceiver
|
||||||
})
|
})
|
||||||
GetLogger(ctx).Info("start pruning receiver")
|
log.Info("start pruning receiver")
|
||||||
tasks.prunerReceiver.Prune()
|
tasks.prunerReceiver.Prune()
|
||||||
GetLogger(ctx).Info("finished pruning receiver")
|
log.Info("finished pruning receiver")
|
||||||
receiverCancel()
|
receiverCancel()
|
||||||
endSpan()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
j.updateTasks(func(tasks *activeSideTasks) {
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/transport"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFakeActiveSideDirectMethodInvocationClientIdentityDoesNotPassValidityTest(t *testing.T) {
|
|
||||||
jobid, err := endpoint.MakeJobID("validjobname")
|
|
||||||
require.NoError(t, err)
|
|
||||||
clientIdentity := FakeActiveSideDirectMethodInvocationClientIdentity(jobid)
|
|
||||||
t.Logf("%v", clientIdentity)
|
|
||||||
err = transport.ValidateClientIdentity(clientIdentity)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
@@ -8,13 +8,12 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func JobsFromConfig(c *config.Config, parseFlags config.ParseFlags) ([]Job, error) {
|
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
||||||
js := make([]Job, len(c.Jobs))
|
js := make([]Job, len(c.Jobs))
|
||||||
for i := range c.Jobs {
|
for i := range c.Jobs {
|
||||||
j, err := buildJob(c.Global, c.Jobs[i], parseFlags)
|
j, err := buildJob(c.Global, c.Jobs[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -42,19 +41,19 @@ func JobsFromConfig(c *config.Config, parseFlags config.ParseFlags) ([]Job, erro
|
|||||||
return js, nil
|
return js, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildJob(c *config.Global, in config.JobEnum, parseFlags config.ParseFlags) (j Job, err error) {
|
func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
||||||
cannotBuildJob := func(e error, name string) (Job, error) {
|
cannotBuildJob := func(e error, name string) (Job, error) {
|
||||||
return nil, errors.Wrapf(e, "cannot build job %q", name)
|
return nil, errors.Wrapf(e, "cannot build job %q", name)
|
||||||
}
|
}
|
||||||
// FIXME prettify this
|
// FIXME prettify this
|
||||||
switch v := in.Ret.(type) {
|
switch v := in.Ret.(type) {
|
||||||
case *config.SinkJob:
|
case *config.SinkJob:
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v, parseFlags)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.SourceJob:
|
case *config.SourceJob:
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, v, parseFlags)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -64,12 +63,12 @@ func buildJob(c *config.Global, in config.JobEnum, parseFlags config.ParseFlags)
|
|||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
j, err = activeSide(c, &v.ActiveJob, v, parseFlags)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PullJob:
|
case *config.PullJob:
|
||||||
j, err = activeSide(c, &v.ActiveJob, v, parseFlags)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -108,13 +107,3 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBandwidthLimitConfig(in *config.BandwidthLimit) (c bandwidthlimit.Config, _ error) {
|
|
||||||
if in.Max.ToBytes() > 0 && int64(in.Max.ToBytes()) == 0 {
|
|
||||||
return c, fmt.Errorf("bandwidth limit `max` is too small, must at least specify one byte")
|
|
||||||
}
|
|
||||||
return bandwidthlimit.Config{
|
|
||||||
Max: int64(in.Max.ToBytes()),
|
|
||||||
BucketCapacity: int64(in.BucketCapacity.ToBytes()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SendingJobConfig interface {
|
|
||||||
GetFilesystems() config.FilesystemsFilter
|
|
||||||
GetSendOptions() *config.SendOptions // must not be nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.SenderConfig, error) {
|
|
||||||
|
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.GetFilesystems())
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
|
||||||
}
|
|
||||||
sendOpts := in.GetSendOptions()
|
|
||||||
bwlim, err := buildBandwidthLimitConfig(sendOpts.BandwidthLimit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build bandwith limit config")
|
|
||||||
}
|
|
||||||
|
|
||||||
sc := &endpoint.SenderConfig{
|
|
||||||
FSF: fsf,
|
|
||||||
JobID: jobID,
|
|
||||||
|
|
||||||
Encrypt: &nodefault.Bool{B: sendOpts.Encrypted},
|
|
||||||
SendRaw: sendOpts.Raw,
|
|
||||||
SendProperties: sendOpts.SendProperties,
|
|
||||||
SendBackupProperties: sendOpts.BackupProperties,
|
|
||||||
SendLargeBlocks: sendOpts.LargeBlocks,
|
|
||||||
SendCompressed: sendOpts.Compressed,
|
|
||||||
SendEmbeddedData: sendOpts.EmbeddedData,
|
|
||||||
SendSaved: sendOpts.Saved,
|
|
||||||
|
|
||||||
BandwidthLimit: bwlim,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sc.Validate(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build sender config")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReceivingJobConfig interface {
|
|
||||||
GetRootFS() string
|
|
||||||
GetAppendClientIdentity() bool
|
|
||||||
GetRecvOptions() *config.RecvOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoint.ReceiverConfig, err error) {
|
|
||||||
rootFs, err := zfs.NewDatasetPath(in.GetRootFS())
|
|
||||||
if err != nil {
|
|
||||||
return rc, errors.New("root_fs is not a valid zfs filesystem path")
|
|
||||||
}
|
|
||||||
if rootFs.Length() <= 0 {
|
|
||||||
return rc, errors.New("root_fs must not be empty") // duplicates error check of receiver
|
|
||||||
}
|
|
||||||
|
|
||||||
recvOpts := in.GetRecvOptions()
|
|
||||||
|
|
||||||
bwlim, err := buildBandwidthLimitConfig(recvOpts.BandwidthLimit)
|
|
||||||
if err != nil {
|
|
||||||
return rc, errors.Wrap(err, "cannot build bandwith limit config")
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholderEncryption, err := endpoint.PlaceholderCreationEncryptionPropertyString(recvOpts.Placeholder.Encryption)
|
|
||||||
if err != nil {
|
|
||||||
options := []string{}
|
|
||||||
for _, v := range endpoint.PlaceholderCreationEncryptionPropertyValues() {
|
|
||||||
options = append(options, endpoint.PlaceholderCreationEncryptionProperty(v).String())
|
|
||||||
}
|
|
||||||
return rc, errors.Errorf("placeholder encryption value %q is invalid, must be one of %s",
|
|
||||||
recvOpts.Placeholder.Encryption, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
rc = endpoint.ReceiverConfig{
|
|
||||||
JobID: jobID,
|
|
||||||
RootWithoutClientComponent: rootFs,
|
|
||||||
AppendClientIdentity: in.GetAppendClientIdentity(),
|
|
||||||
|
|
||||||
InheritProperties: recvOpts.Properties.Inherit,
|
|
||||||
OverrideProperties: recvOpts.Properties.Override,
|
|
||||||
|
|
||||||
BandwidthLimit: bwlim,
|
|
||||||
|
|
||||||
PlaceholderEncryption: placeholderEncryption,
|
|
||||||
}
|
|
||||||
if err := rc.Validate(); err != nil {
|
|
||||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
|
||||||
}
|
|
||||||
|
|
||||||
return rc, nil
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,8 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@@ -95,7 +92,7 @@ jobs:
|
|||||||
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
||||||
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
||||||
require.NotNil(t, conf)
|
require.NotNil(t, conf)
|
||||||
jobs, err := JobsFromConfig(conf, config.ParseFlagsNone)
|
jobs, err := JobsFromConfig(conf)
|
||||||
|
|
||||||
if c.valid {
|
if c.valid {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -111,208 +108,3 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|
||||||
paths, err := filepath.Glob("../../config/samples/*")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("glob failed: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type additionalCheck struct {
|
|
||||||
state int
|
|
||||||
test func(t *testing.T, jobs []Job)
|
|
||||||
}
|
|
||||||
additionalChecks := map[string]*additionalCheck{
|
|
||||||
"bandwidth_limit.yml": {test: testSampleConfig_BandwidthLimit},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range paths {
|
|
||||||
|
|
||||||
if path.Ext(p) != ".yml" {
|
|
||||||
t.Logf("skipping file %s", p)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := path.Base(p)
|
|
||||||
t.Logf("checking for presence additonal checks for file %q", filename)
|
|
||||||
additionalCheck := additionalChecks[filename]
|
|
||||||
if additionalCheck == nil {
|
|
||||||
t.Logf("no additional checks")
|
|
||||||
} else {
|
|
||||||
t.Logf("additional check present")
|
|
||||||
additionalCheck.state = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(p, func(t *testing.T) {
|
|
||||||
c, err := config.ParseConfig(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error parsing %s:\n%+v", p, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("file: %s", p)
|
|
||||||
t.Log(pretty.Sprint(c))
|
|
||||||
|
|
||||||
jobs, err := JobsFromConfig(c, config.ParseFlagsNoCertCheck)
|
|
||||||
t.Logf("jobs: %#v", jobs)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
if additionalCheck != nil {
|
|
||||||
additionalCheck.test(t, jobs)
|
|
||||||
additionalCheck.state = 2
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
for basename, c := range additionalChecks {
|
|
||||||
if c.state == 0 {
|
|
||||||
panic("univisited additional check " + basename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSampleConfig_BandwidthLimit(t *testing.T, jobs []Job) {
|
|
||||||
require.Len(t, jobs, 3)
|
|
||||||
|
|
||||||
{
|
|
||||||
limitedSink, ok := jobs[0].(*PassiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[0])
|
|
||||||
limitedSinkMode, ok := limitedSink.mode.(*modeSink)
|
|
||||||
require.True(t, ok, "%T", limitedSink)
|
|
||||||
|
|
||||||
assert.Equal(t, int64(12345), limitedSinkMode.receiverConfig.BandwidthLimit.Max)
|
|
||||||
assert.Equal(t, int64(1<<17), limitedSinkMode.receiverConfig.BandwidthLimit.BucketCapacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
limitedPush, ok := jobs[1].(*ActiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[1])
|
|
||||||
limitedPushMode, ok := limitedPush.mode.(*modePush)
|
|
||||||
require.True(t, ok, "%T", limitedPush)
|
|
||||||
|
|
||||||
assert.Equal(t, int64(54321), limitedPushMode.senderConfig.BandwidthLimit.Max)
|
|
||||||
assert.Equal(t, int64(1024), limitedPushMode.senderConfig.BandwidthLimit.BucketCapacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
unlimitedSink, ok := jobs[2].(*PassiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[2])
|
|
||||||
unlimitedSinkMode, ok := unlimitedSink.mode.(*modeSink)
|
|
||||||
require.True(t, ok, "%T", unlimitedSink)
|
|
||||||
|
|
||||||
max := unlimitedSinkMode.receiverConfig.BandwidthLimit.Max
|
|
||||||
assert.Less(t, max, int64(0), max, "unlimited mode <=> negative value for .Max, see bandwidthlimit.Config")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplicationOptions(t *testing.T) {
|
|
||||||
tmpl := `
|
|
||||||
jobs:
|
|
||||||
- name: foo
|
|
||||||
type: push
|
|
||||||
connect:
|
|
||||||
type: local
|
|
||||||
listener_name: foo
|
|
||||||
client_identity: bar
|
|
||||||
filesystems: {"<": true}
|
|
||||||
%s
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
`
|
|
||||||
|
|
||||||
type Test struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expectOk func(t *testing.T, a *ActiveSide, m *modePush)
|
|
||||||
expectError bool
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []Test{
|
|
||||||
{
|
|
||||||
name: "defaults",
|
|
||||||
input: `
|
|
||||||
replication: {}
|
|
||||||
`,
|
|
||||||
expectOk: func(t *testing.T, a *ActiveSide, m *modePush) {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "steps_zero",
|
|
||||||
input: `
|
|
||||||
replication:
|
|
||||||
concurrency:
|
|
||||||
steps: 0
|
|
||||||
`,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "size_estimates_zero",
|
|
||||||
input: `
|
|
||||||
replication:
|
|
||||||
concurrency:
|
|
||||||
size_estimates: 0
|
|
||||||
`,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom_values",
|
|
||||||
input: `
|
|
||||||
replication:
|
|
||||||
concurrency:
|
|
||||||
steps: 23
|
|
||||||
size_estimates: 42
|
|
||||||
`,
|
|
||||||
expectOk: func(t *testing.T, a *ActiveSide, m *modePush) {
|
|
||||||
assert.Equal(t, 23, a.replicationDriverConfig.StepQueueConcurrency)
|
|
||||||
assert.Equal(t, 42, m.plannerPolicy.SizeEstimationConcurrency)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative_values_forbidden",
|
|
||||||
input: `
|
|
||||||
replication:
|
|
||||||
concurrency:
|
|
||||||
steps: -23
|
|
||||||
size_estimates: -42
|
|
||||||
`,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
|
||||||
|
|
||||||
for _, ts := range tests {
|
|
||||||
t.Run(ts.name, func(t *testing.T) {
|
|
||||||
assert.True(t, (ts.expectError) != (ts.expectOk != nil))
|
|
||||||
|
|
||||||
cstr := fill(ts.input)
|
|
||||||
t.Logf("testing config:\n%s", cstr)
|
|
||||||
c, err := config.ParseConfigBytes([]byte(cstr))
|
|
||||||
require.NoError(t, err)
|
|
||||||
jobs, err := JobsFromConfig(c, config.ParseFlagsNone)
|
|
||||||
if ts.expectOk != nil {
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, c)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, jobs, 1)
|
|
||||||
a := jobs[0].(*ActiveSide)
|
|
||||||
m := a.mode.(*modePush)
|
|
||||||
ts.expectOk(t, a, m)
|
|
||||||
} else if ts.expectError {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
t.Fatalf("test must define expectOk or expectError")
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
+14
-2
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -15,8 +14,21 @@ import (
|
|||||||
|
|
||||||
type Logger = logger.Logger
|
type Logger = logger.Logger
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeyLog contextKey = iota
|
||||||
|
)
|
||||||
|
|
||||||
func GetLogger(ctx context.Context) Logger {
|
func GetLogger(ctx context.Context) Logger {
|
||||||
return logging.GetLogger(ctx, logging.SubsysJob)
|
if l, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
return logger.NewNullLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(ctx context.Context, l Logger) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyLog, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Job interface {
|
type Job interface {
|
||||||
|
|||||||
+30
-26
@@ -7,9 +7,8 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -48,9 +47,19 @@ func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
|||||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||||
m = &modeSink{}
|
m = &modeSink{}
|
||||||
|
|
||||||
m.receiverConfig, err = buildReceiverConfig(in, jobID)
|
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.receiverConfig = endpoint.ReceiverConfig{
|
||||||
|
JobID: jobID,
|
||||||
|
RootWithoutClientComponent: rootDataset,
|
||||||
|
AppendClientIdentity: true, // !
|
||||||
|
UpdateLastReceivedHold: true,
|
||||||
|
}
|
||||||
|
if err := m.receiverConfig.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -58,19 +67,23 @@ func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.Job
|
|||||||
|
|
||||||
type modeSource struct {
|
type modeSource struct {
|
||||||
senderConfig *endpoint.SenderConfig
|
senderConfig *endpoint.SenderConfig
|
||||||
snapper snapper.Snapper
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
||||||
// FIXME exact dedup of modePush
|
// FIXME exact dedup of modePush
|
||||||
m = &modeSource{}
|
m = &modeSource{}
|
||||||
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
m.senderConfig, err = buildSenderConfig(in, jobID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "send options")
|
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
||||||
|
}
|
||||||
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
|
FSF: fsf,
|
||||||
|
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||||
|
JobID: jobID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil {
|
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,11 +101,10 @@ func (m *modeSource) RunPeriodic(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSource) SnapperReport() *snapper.Report {
|
func (m *modeSource) SnapperReport() *snapper.Report {
|
||||||
r := m.snapper.Report()
|
return m.snapper.Report()
|
||||||
return &r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}, parseFlags config.ParseFlags) (s *PassiveSide, err error) {
|
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}) (s *PassiveSide, err error) {
|
||||||
|
|
||||||
s = &PassiveSide{}
|
s = &PassiveSide{}
|
||||||
|
|
||||||
@@ -111,7 +123,7 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob in
|
|||||||
return nil, err // no wrapping necessary
|
return nil, err // no wrapping necessary
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve, parseFlags); err != nil {
|
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build listener factory")
|
return nil, errors.Wrap(err, "cannot build listener factory")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +164,12 @@ func (j *PassiveSide) SenderConfig() *endpoint.SenderConfig {
|
|||||||
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||||
|
|
||||||
func (j *PassiveSide) Run(ctx context.Context) {
|
func (j *PassiveSide) Run(ctx context.Context) {
|
||||||
ctx, endTask := trace.WithTaskAndSpan(ctx, "passive-side-job", j.Name())
|
|
||||||
defer endTask()
|
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
{
|
{
|
||||||
ctx, endTask := trace.WithTask(ctx, "periodic") // shadowing
|
ctx, cancel := context.WithCancel(ctx) // shadowing
|
||||||
defer endTask()
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go j.mode.RunPeriodic(ctx)
|
go j.mode.RunPeriodic(ctx)
|
||||||
}
|
}
|
||||||
@@ -169,14 +179,8 @@ func (j *PassiveSide) Run(ctx context.Context) {
|
|||||||
panic(fmt.Sprintf("implementation error: j.mode.Handler() returned nil: %#v", j))
|
panic(fmt.Sprintf("implementation error: j.mode.Handler() returned nil: %#v", j))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxInterceptor := func(handlerCtx context.Context, info rpc.HandlerContextInterceptorData, handler func(ctx context.Context)) {
|
ctxInterceptor := func(handlerCtx context.Context) context.Context {
|
||||||
// the handlerCtx is clean => need to inherit logging and tracing config from job context
|
return logging.WithSubsystemLoggers(handlerCtx, log)
|
||||||
handlerCtx = logging.WithInherit(handlerCtx, ctx)
|
|
||||||
handlerCtx = trace.WithInherit(handlerCtx, ctx)
|
|
||||||
|
|
||||||
handlerCtx, endTask := trace.WithTaskAndSpan(handlerCtx, "handler", fmt.Sprintf("job=%q client=%q method=%q", j.Name(), info.ClientIdentity(), info.FullMethod()))
|
|
||||||
defer endTask()
|
|
||||||
handler(handlerCtx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rpcLoggers := rpc.GetLoggersOrPanic(ctx) // WithSubsystemLoggers above
|
rpcLoggers := rpc.GetLoggersOrPanic(ctx) // WithSubsystemLoggers above
|
||||||
|
|||||||
+13
-33
@@ -2,20 +2,15 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -26,14 +21,13 @@ import (
|
|||||||
type SnapJob struct {
|
type SnapJob struct {
|
||||||
name endpoint.JobID
|
name endpoint.JobID
|
||||||
fsfilter zfs.DatasetFilter
|
fsfilter zfs.DatasetFilter
|
||||||
snapper snapper.Snapper
|
snapper *snapper.PeriodicOrManual
|
||||||
|
|
||||||
prunerFactory *pruner.LocalPrunerFactory
|
prunerFactory *pruner.LocalPrunerFactory
|
||||||
|
|
||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
||||||
|
|
||||||
prunerMtx sync.Mutex
|
pruner *pruner.Pruner
|
||||||
pruner *pruner.Pruner
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SnapJob) Name() string { return j.name.String() }
|
func (j *SnapJob) Name() string { return j.name.String() }
|
||||||
@@ -81,13 +75,10 @@ type SnapJobStatus struct {
|
|||||||
func (j *SnapJob) Status() *Status {
|
func (j *SnapJob) Status() *Status {
|
||||||
s := &SnapJobStatus{}
|
s := &SnapJobStatus{}
|
||||||
t := j.Type()
|
t := j.Type()
|
||||||
j.prunerMtx.Lock()
|
|
||||||
if j.pruner != nil {
|
if j.pruner != nil {
|
||||||
s.Pruning = j.pruner.Report()
|
s.Pruning = j.pruner.Report()
|
||||||
}
|
}
|
||||||
j.prunerMtx.Unlock()
|
s.Snapshotting = j.snapper.Report()
|
||||||
r := j.snapper.Report()
|
|
||||||
s.Snapshotting = &r
|
|
||||||
return &Status{Type: t, JobSpecific: s}
|
return &Status{Type: t, JobSpecific: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,18 +89,15 @@ func (j *SnapJob) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|||||||
func (j *SnapJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
func (j *SnapJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
func (j *SnapJob) Run(ctx context.Context) {
|
func (j *SnapJob) Run(ctx context.Context) {
|
||||||
ctx, endTask := trace.WithTaskAndSpan(ctx, "snap-job", j.Name())
|
|
||||||
defer endTask()
|
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
|
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
|
|
||||||
periodicDone := make(chan struct{})
|
periodicDone := make(chan struct{})
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
periodicCtx, endTask := trace.WithTask(ctx, "snapshotting")
|
go j.snapper.Run(ctx, periodicDone)
|
||||||
defer endTask()
|
|
||||||
go j.snapper.Run(periodicCtx, periodicDone)
|
|
||||||
|
|
||||||
invocationCount := 0
|
invocationCount := 0
|
||||||
outer:
|
outer:
|
||||||
@@ -124,10 +112,8 @@ outer:
|
|||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
|
invLog := log.WithField("invocation", invocationCount)
|
||||||
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
|
j.doPrune(WithLogger(ctx, invLog))
|
||||||
j.doPrune(invocationCtx)
|
|
||||||
endSpan()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +124,7 @@ outer:
|
|||||||
// TODO:
|
// TODO:
|
||||||
// This is a work-around for the current package daemon/pruner
|
// This is a work-around for the current package daemon/pruner
|
||||||
// and package pruning.Snapshot limitation: they require the
|
// and package pruning.Snapshot limitation: they require the
|
||||||
// `Replicated` getter method be present, but obviously,
|
// `Replicated` getter method be present, but obviously,
|
||||||
// a local job like SnapJob can't deliver on that.
|
// a local job like SnapJob can't deliver on that.
|
||||||
// But the pruner.Pruner gives up on an FS if no replication
|
// But the pruner.Pruner gives up on an FS if no replication
|
||||||
// cursor is present, which is why this pruner returns the
|
// cursor is present, which is why this pruner returns the
|
||||||
@@ -148,7 +134,7 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
|||||||
target pruner.Target
|
target pruner.Target
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ pruner.Sender = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
|
||||||
|
|
||||||
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||||
fsvReq := &pdu.ListFilesystemVersionsReq{
|
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||||
@@ -175,21 +161,15 @@ func (h alwaysUpToDateReplicationCursorHistory) ListFilesystems(ctx context.Cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (j *SnapJob) doPrune(ctx context.Context) {
|
func (j *SnapJob) doPrune(ctx context.Context) {
|
||||||
ctx, endSpan := trace.WithSpan(ctx, "snap-job-do-prune")
|
|
||||||
defer endSpan()
|
|
||||||
log := GetLogger(ctx)
|
log := GetLogger(ctx)
|
||||||
|
ctx = logging.WithSubsystemLoggers(ctx, log)
|
||||||
sender := endpoint.NewSender(endpoint.SenderConfig{
|
sender := endpoint.NewSender(endpoint.SenderConfig{
|
||||||
JobID: j.name,
|
JobID: j.name,
|
||||||
FSF: j.fsfilter,
|
FSF: j.fsfilter,
|
||||||
// FIXME the following config fields are irrelevant for SnapJob
|
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||||
// because the endpoint is only used as pruner.Target.
|
Encrypt: &zfs.NilBool{B: true},
|
||||||
// However, the implementation requires them to be set.
|
|
||||||
Encrypt: &nodefault.Bool{B: true},
|
|
||||||
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
|
||||||
})
|
})
|
||||||
j.prunerMtx.Lock()
|
|
||||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
j.prunerMtx.Unlock()
|
|
||||||
log.Info("start pruning")
|
log.Info("start pruning")
|
||||||
j.pruner.Prune()
|
j.pruner.Prune()
|
||||||
log.Info("finished pruning")
|
log.Info("finished pruning")
|
||||||
|
|||||||
@@ -11,9 +11,17 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/replication/driver"
|
||||||
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
|
"github.com/zrepl/zrepl/rpc"
|
||||||
|
"github.com/zrepl/zrepl/rpc/transportmux"
|
||||||
"github.com/zrepl/zrepl/tlsconf"
|
"github.com/zrepl/zrepl/tlsconf"
|
||||||
|
"github.com/zrepl/zrepl/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
|
||||||
@@ -61,10 +69,8 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
|
|||||||
type Subsystem string
|
type Subsystem string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SubsysMeta Subsystem = "meta"
|
|
||||||
SubsysJob Subsystem = "job"
|
|
||||||
SubsysReplication Subsystem = "repl"
|
SubsysReplication Subsystem = "repl"
|
||||||
SubsysEndpoint Subsystem = "endpoint"
|
SubsyEndpoint Subsystem = "endpoint"
|
||||||
SubsysPruning Subsystem = "pruning"
|
SubsysPruning Subsystem = "pruning"
|
||||||
SubsysSnapshot Subsystem = "snapshot"
|
SubsysSnapshot Subsystem = "snapshot"
|
||||||
SubsysHooks Subsystem = "hook"
|
SubsysHooks Subsystem = "hook"
|
||||||
@@ -73,104 +79,29 @@ const (
|
|||||||
SubsysRPC Subsystem = "rpc"
|
SubsysRPC Subsystem = "rpc"
|
||||||
SubsysRPCControl Subsystem = "rpc.ctrl"
|
SubsysRPCControl Subsystem = "rpc.ctrl"
|
||||||
SubsysRPCData Subsystem = "rpc.data"
|
SubsysRPCData Subsystem = "rpc.data"
|
||||||
SubsysZFSCmd Subsystem = "zfs.cmd"
|
|
||||||
SubsysTraceData Subsystem = "trace.data"
|
|
||||||
SubsysPlatformtest Subsystem = "platformtest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var AllSubsystems = []Subsystem{
|
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
|
||||||
SubsysMeta,
|
ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
||||||
SubsysJob,
|
ctx = driver.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
||||||
SubsysReplication,
|
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsyEndpoint))
|
||||||
SubsysEndpoint,
|
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
|
||||||
SubsysPruning,
|
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
||||||
SubsysSnapshot,
|
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
|
||||||
SubsysHooks,
|
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
||||||
SubsysTransport,
|
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
||||||
SubsysTransportMux,
|
ctx = rpc.WithLoggers(ctx,
|
||||||
SubsysRPC,
|
rpc.Loggers{
|
||||||
SubsysRPCControl,
|
General: log.WithField(SubsysField, SubsysRPC),
|
||||||
SubsysRPCData,
|
Control: log.WithField(SubsysField, SubsysRPCControl),
|
||||||
SubsysZFSCmd,
|
Data: log.WithField(SubsysField, SubsysRPCData),
|
||||||
SubsysTraceData,
|
},
|
||||||
SubsysPlatformtest,
|
)
|
||||||
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
type injectedField struct {
|
func LogSubsystem(log logger.Logger, subsys Subsystem) logger.Logger {
|
||||||
field string
|
return log.ReplaceField(SubsysField, subsys)
|
||||||
value interface{}
|
|
||||||
parent *injectedField
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithInjectedField(ctx context.Context, field string, value interface{}) context.Context {
|
|
||||||
var parent *injectedField
|
|
||||||
parentI := ctx.Value(contextKeyInjectedField)
|
|
||||||
if parentI != nil {
|
|
||||||
parent = parentI.(*injectedField)
|
|
||||||
}
|
|
||||||
// TODO sanity-check `field` now
|
|
||||||
this := &injectedField{field, value, parent}
|
|
||||||
return context.WithValue(ctx, contextKeyInjectedField, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
func iterInjectedFields(ctx context.Context, cb func(field string, value interface{})) {
|
|
||||||
injI := ctx.Value(contextKeyInjectedField)
|
|
||||||
if injI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inj := injI.(*injectedField)
|
|
||||||
for ; inj != nil; inj = inj.parent {
|
|
||||||
cb(inj.field, inj.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubsystemLoggers map[Subsystem]logger.Logger
|
|
||||||
|
|
||||||
func SubsystemLoggersWithUniversalLogger(l logger.Logger) SubsystemLoggers {
|
|
||||||
loggers := make(SubsystemLoggers)
|
|
||||||
for _, s := range AllSubsystems {
|
|
||||||
loggers[s] = l
|
|
||||||
}
|
|
||||||
return loggers
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithLoggers(ctx context.Context, loggers SubsystemLoggers) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKeyLoggers, loggers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLoggers(ctx context.Context) SubsystemLoggers {
|
|
||||||
loggers, ok := ctx.Value(contextKeyLoggers).(SubsystemLoggers)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return loggers
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogger(ctx context.Context, subsys Subsystem) logger.Logger {
|
|
||||||
return getLoggerImpl(ctx, subsys, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLoggerImpl(ctx context.Context, subsys Subsystem, panicIfEnded bool) logger.Logger {
|
|
||||||
loggers, ok := ctx.Value(contextKeyLoggers).(SubsystemLoggers)
|
|
||||||
if !ok || loggers == nil {
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
|
||||||
l, ok := loggers[subsys]
|
|
||||||
if !ok {
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
l = l.WithField(SubsysField, subsys)
|
|
||||||
|
|
||||||
l = l.WithField(SpanField, trace.GetSpanStackOrDefault(ctx, *trace.StackKindId, "NOSPAN"))
|
|
||||||
|
|
||||||
fields := make(logger.Fields)
|
|
||||||
iterInjectedFields(ctx, func(field string, value interface{}) {
|
|
||||||
fields[field] = value
|
|
||||||
})
|
|
||||||
l = l.WithFields(fields)
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package logging
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextKeyLoggers contextKey = 1 + iota
|
|
||||||
contextKeyInjectedField
|
|
||||||
)
|
|
||||||
|
|
||||||
var contextKeys = []contextKey{
|
|
||||||
contextKeyLoggers,
|
|
||||||
contextKeyInjectedField,
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithInherit(ctx, inheritFrom context.Context) context.Context {
|
|
||||||
for _, k := range contextKeys {
|
|
||||||
if v := inheritFrom.Value(k); v != nil {
|
|
||||||
ctx = context.WithValue(ctx, k, v) // no shadow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@ const (
|
|||||||
const (
|
const (
|
||||||
JobField string = "job"
|
JobField string = "job"
|
||||||
SubsysField string = "subsystem"
|
SubsysField string = "subsystem"
|
||||||
SpanField string = "span"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetadataFlags int64
|
type MetadataFlags int64
|
||||||
@@ -86,7 +85,7 @@ func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
|
|||||||
fmt.Fprintf(&line, "[%s]", col.Sprint(e.Level.Short()))
|
fmt.Fprintf(&line, "[%s]", col.Sprint(e.Level.Short()))
|
||||||
}
|
}
|
||||||
|
|
||||||
prefixFields := []string{JobField, SubsysField, SpanField}
|
prefixFields := []string{JobField, SubsysField}
|
||||||
prefixed := make(map[string]bool, len(prefixFields)+2)
|
prefixed := make(map[string]bool, len(prefixFields)+2)
|
||||||
for _, field := range prefixFields {
|
for _, field := range prefixFields {
|
||||||
val, ok := e.Fields[field]
|
val, ok := e.Fields[field]
|
||||||
@@ -175,8 +174,8 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// at least try and put job and task in front
|
// at least try and put job and task in front
|
||||||
prefixed := make(map[string]bool, 3)
|
prefixed := make(map[string]bool, 2)
|
||||||
prefix := []string{JobField, SubsysField, SpanField}
|
prefix := []string{JobField, SubsysField}
|
||||||
for _, pf := range prefix {
|
for _, pf := range prefix {
|
||||||
v, ok := e.Fields[pf]
|
v, ok := e.Fields[pf]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -212,7 +211,7 @@ func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error
|
|||||||
case logfmt.ErrUnsupportedValueType:
|
case logfmt.ErrUnsupportedValueType:
|
||||||
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot encode unsupported value type Go type")
|
return errors.Wrap(err, "cannot encode unsuuported value type Go type")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func NewTCPOutlet(formatter EntryFormatter, network, address string, tlsConfig *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previous is in io.Copy()
|
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previos is in io.Copy()
|
||||||
|
|
||||||
o := &TCPOutlet{
|
o := &TCPOutlet{
|
||||||
formatter: formatter,
|
formatter: formatter,
|
||||||
|
|||||||
@@ -1,422 +0,0 @@
|
|||||||
// package trace provides activity tracing via ctx through Tasks and Spans
|
|
||||||
//
|
|
||||||
// # Basic Concepts
|
|
||||||
//
|
|
||||||
// Tracing can be used to identify where a piece of code spends its time.
|
|
||||||
//
|
|
||||||
// The Go standard library provides package runtime/trace which is useful to identify CPU bottlenecks or
|
|
||||||
// to understand what happens inside the Go runtime.
|
|
||||||
// However, it is not ideal for application level tracing, in particular if those traces should be understandable
|
|
||||||
// to tech-savvy users (albeit not developers).
|
|
||||||
//
|
|
||||||
// This package provides the concept of Tasks and Spans to express what activity is happening within an application:
|
|
||||||
// - Neither task nor span is really tangible but instead contained within the context.Context tree
|
|
||||||
// - Tasks represent concurrent activity (i.e. goroutines).
|
|
||||||
// - Spans represent a semantic stack trace within a task.
|
|
||||||
// As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task:
|
|
||||||
//
|
|
||||||
// go func(ctx context.Context) {
|
|
||||||
// ctx, endTask = WithTask(ctx, "what-happens-inside-the-child-task")
|
|
||||||
// defer endTask()
|
|
||||||
// // ...
|
|
||||||
// }(ctx)
|
|
||||||
//
|
|
||||||
// Within the task, you can open up a hierarchy of spans.
|
|
||||||
// In contrast to tasks, which have can multiple concurrently running child tasks,
|
|
||||||
// spans must nest and not cross the goroutine boundary.
|
|
||||||
//
|
|
||||||
// ctx, endSpan = WithSpan(ctx, "copy-dir")
|
|
||||||
// defer endSpan()
|
|
||||||
// for _, f := range dir.Files() {
|
|
||||||
// func() {
|
|
||||||
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
|
||||||
// defer endspan()
|
|
||||||
// b, _ := ioutil.ReadFile(f)
|
|
||||||
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
|
||||||
// }()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// In combination:
|
|
||||||
//
|
|
||||||
// ctx, endTask = WithTask(ctx, "copy-dirs")
|
|
||||||
// defer endTask()
|
|
||||||
// for i := range dirs {
|
|
||||||
// go func(dir string) {
|
|
||||||
// ctx, endTask := WithTask(ctx, "copy-dir")
|
|
||||||
// defer endTask()
|
|
||||||
// for _, f := range filesIn(dir) {
|
|
||||||
// func() {
|
|
||||||
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
|
||||||
// defer endspan()
|
|
||||||
// b, _ := ioutil.ReadFile(f)
|
|
||||||
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
|
||||||
// }()
|
|
||||||
// }
|
|
||||||
// }()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Note that a span ends at the time you call endSpan - not before and not after that.
|
|
||||||
// If you violate the stack-like nesting of spans by forgetting an endSpan() invocation,
|
|
||||||
// the out-of-order endSpan() will panic.
|
|
||||||
//
|
|
||||||
// A similar rule applies to the endTask closure returned by WithTask:
|
|
||||||
// If a task has live child tasks at the time you call endTask(), the call will panic.
|
|
||||||
//
|
|
||||||
// Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output.
|
|
||||||
//
|
|
||||||
// # Best Practices For Naming Tasks And Spans
|
|
||||||
//
|
|
||||||
// Tasks should always have string constants as names, and must not contain the `#` character. WHy?
|
|
||||||
// First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace.
|
|
||||||
// Also, the package appends `#NUM` for each concurrently running instance of a task name.
|
|
||||||
// Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an
|
|
||||||
// infinite number of horizontal bars in the visualization.
|
|
||||||
//
|
|
||||||
// # Chrome-compatible Tracefile Support
|
|
||||||
//
|
|
||||||
// The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file
|
|
||||||
// that can be loaded into chrome://tracing .
|
|
||||||
// Apart from function GetSpanStackOrDefault, this is the main benefit of this package.
|
|
||||||
//
|
|
||||||
// First, there is a convenience environment variable 'ZREPL_ACTIVITY_TRACE' that can be set to an output path.
|
|
||||||
// From process start onward, a trace is written to that path.
|
|
||||||
//
|
|
||||||
// More consumers can attach to the activity trace through the ChrometraceClientWebsocketHandler websocket handler.
|
|
||||||
//
|
|
||||||
// If a write error is encountered with any consumer (including the env-var based one), the consumer is closed and
|
|
||||||
// will not receive further trace output.
|
|
||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
runtimedebug "runtime/debug"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
|
||||||
)
|
|
||||||
|
|
||||||
var metrics struct {
|
|
||||||
activeTasks prometheus.Gauge
|
|
||||||
}
|
|
||||||
var taskNamer *uniqueConcurrentTaskNamer = newUniqueTaskNamer()
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
metrics.activeTasks = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "trace",
|
|
||||||
Name: "active_tasks",
|
|
||||||
Help: "number of active (tracing-level) tasks in the daemon",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterMetrics(r prometheus.Registerer) {
|
|
||||||
r.MustRegister(metrics.activeTasks)
|
|
||||||
}
|
|
||||||
|
|
||||||
type traceNode struct {
|
|
||||||
// NOTE: members with prefix debug are only valid if the package variable debugEnabled is true.
|
|
||||||
|
|
||||||
id string
|
|
||||||
annotation string
|
|
||||||
parentTask *traceNode
|
|
||||||
debugCreationStack string // debug: stack trace of when the traceNode was created
|
|
||||||
|
|
||||||
mtx chainlock.L
|
|
||||||
|
|
||||||
activeChildTasks int32 // only for task nodes, insignificant for span nodes
|
|
||||||
debugActiveChildTasks map[*traceNode]bool // debug: set of active child tasks
|
|
||||||
parentSpan *traceNode
|
|
||||||
activeChildSpan *traceNode // nil if task or span doesn't have an active child span
|
|
||||||
|
|
||||||
startedAt time.Time
|
|
||||||
endedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *traceNode) StartedAt() time.Time { return s.startedAt }
|
|
||||||
func (s *traceNode) EndedAt() time.Time { return s.endedAt }
|
|
||||||
|
|
||||||
// caller must hold mtx
|
|
||||||
func (s *traceNode) debugString() string {
|
|
||||||
if !debugEnabled {
|
|
||||||
return "<debugEnabled=false>"
|
|
||||||
}
|
|
||||||
return pretty.Sprint(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returned from WithTask or WithSpan.
|
|
||||||
// Must be called once the task or span ends.
|
|
||||||
// See package-level docs for nesting rules.
|
|
||||||
// Wrong call order / forgetting to call it will result in panics.
|
|
||||||
type DoneFunc func()
|
|
||||||
|
|
||||||
var ErrTaskStillHasActiveChildTasks = fmt.Errorf("end task: task still has active child tasks")
|
|
||||||
|
|
||||||
// Start a new root task or create a child task of an existing task.
|
|
||||||
//
|
|
||||||
// This is required when starting a new goroutine and
|
|
||||||
// passing an existing task context to it.
|
|
||||||
//
|
|
||||||
// taskName should be a constantand must not contain '#'
|
|
||||||
//
|
|
||||||
// The implementation ensures that,
|
|
||||||
// if multiple tasks with the same name exist simultaneously,
|
|
||||||
// a unique suffix is appended to uniquely identify the task opened with this function.
|
|
||||||
func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc) {
|
|
||||||
|
|
||||||
var parentTask *traceNode
|
|
||||||
nodeI := ctx.Value(contextKeyTraceNode)
|
|
||||||
if nodeI != nil {
|
|
||||||
node := nodeI.(*traceNode)
|
|
||||||
if node.parentSpan != nil {
|
|
||||||
parentTask = node.parentTask
|
|
||||||
} else {
|
|
||||||
parentTask = node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// find the first ancestor that hasn't ended yet (nil if need be)
|
|
||||||
if parentTask != nil {
|
|
||||||
parentTask.mtx.Lock()
|
|
||||||
}
|
|
||||||
for parentTask != nil && !parentTask.endedAt.IsZero() {
|
|
||||||
thisParent := parentTask
|
|
||||||
parentTask = parentTask.parentTask
|
|
||||||
// lock parent first such that it isn't modified by other callers to thisParent
|
|
||||||
if parentTask != nil {
|
|
||||||
parentTask.mtx.Lock()
|
|
||||||
}
|
|
||||||
thisParent.mtx.Unlock()
|
|
||||||
}
|
|
||||||
// invariant: either parentTask != nil and we hold the lock on parentTask, or parentTask is nil
|
|
||||||
|
|
||||||
taskName, taskNameDone := taskNamer.UniqueConcurrentTaskName(taskName)
|
|
||||||
|
|
||||||
this := &traceNode{
|
|
||||||
id: genID(),
|
|
||||||
annotation: taskName,
|
|
||||||
parentTask: parentTask,
|
|
||||||
activeChildTasks: 0,
|
|
||||||
parentSpan: nil,
|
|
||||||
activeChildSpan: nil,
|
|
||||||
|
|
||||||
startedAt: time.Now(),
|
|
||||||
endedAt: time.Time{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if debugEnabled {
|
|
||||||
this.debugCreationStack = string(runtimedebug.Stack())
|
|
||||||
this.debugActiveChildTasks = map[*traceNode]bool{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if parentTask != nil {
|
|
||||||
this.parentTask.activeChildTasks++
|
|
||||||
if debugEnabled {
|
|
||||||
this.parentTask.debugActiveChildTasks[this] = true
|
|
||||||
}
|
|
||||||
parentTask.mtx.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, contextKeyTraceNode, this)
|
|
||||||
|
|
||||||
chrometraceBeginTask(this)
|
|
||||||
|
|
||||||
metrics.activeTasks.Inc()
|
|
||||||
|
|
||||||
endTaskFunc := func() {
|
|
||||||
|
|
||||||
// only hold locks while manipulating the tree
|
|
||||||
// (trace writer might block too long and unlike spans, tasks are updated concurrently)
|
|
||||||
alreadyEnded := func() (alreadyEnded bool) {
|
|
||||||
if this.parentTask != nil {
|
|
||||||
defer this.parentTask.mtx.Lock().Unlock()
|
|
||||||
}
|
|
||||||
defer this.mtx.Lock().Unlock()
|
|
||||||
|
|
||||||
if this.activeChildTasks != 0 {
|
|
||||||
if debugEnabled {
|
|
||||||
// the debugString can be quite long and panic won't print it completely
|
|
||||||
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks:\n%s\n", this.debugString())
|
|
||||||
}
|
|
||||||
panic(errors.WithMessagef(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks (run daemon with env var %s=1 for more details)\n", this.activeChildTasks, debugEnabledEnvVar))
|
|
||||||
}
|
|
||||||
|
|
||||||
// support idempotent task ends
|
|
||||||
if !this.endedAt.IsZero() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
this.endedAt = time.Now()
|
|
||||||
|
|
||||||
if this.parentTask != nil {
|
|
||||||
this.parentTask.activeChildTasks--
|
|
||||||
if debugEnabled {
|
|
||||||
delete(this.parentTask.debugActiveChildTasks, this)
|
|
||||||
}
|
|
||||||
if this.parentTask.activeChildTasks < 0 {
|
|
||||||
if debugEnabled {
|
|
||||||
// the debugString can be quite long and panic won't print it completely
|
|
||||||
fmt.Fprintf(os.Stderr, "going to panic due to activeChildTasks < 0:\n%s\n", this.parentTask.debugString())
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("impl error: parent task with negative activeChildTasks count: %v", this.parentTask.activeChildTasks))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}()
|
|
||||||
if alreadyEnded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
chrometraceEndTask(this)
|
|
||||||
|
|
||||||
metrics.activeTasks.Dec()
|
|
||||||
|
|
||||||
taskNameDone()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx, endTaskFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrAlreadyActiveChildSpan = fmt.Errorf("create child span: span already has an active child span")
|
|
||||||
var ErrSpanStillHasActiveChildSpan = fmt.Errorf("end span: span still has active child spans")
|
|
||||||
|
|
||||||
// Start a new span.
|
|
||||||
// Important: ctx must have an active task (see WithTask)
|
|
||||||
func WithSpan(ctx context.Context, annotation string) (context.Context, DoneFunc) {
|
|
||||||
var parentSpan, parentTask *traceNode
|
|
||||||
nodeI := ctx.Value(contextKeyTraceNode)
|
|
||||||
if nodeI != nil {
|
|
||||||
parentSpan = nodeI.(*traceNode)
|
|
||||||
if parentSpan.parentSpan == nil {
|
|
||||||
parentTask = parentSpan
|
|
||||||
} else {
|
|
||||||
parentTask = parentSpan.parentTask
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic("must be called from within a task")
|
|
||||||
}
|
|
||||||
|
|
||||||
this := &traceNode{
|
|
||||||
id: genID(),
|
|
||||||
annotation: annotation,
|
|
||||||
parentTask: parentTask,
|
|
||||||
parentSpan: parentSpan,
|
|
||||||
activeChildSpan: nil,
|
|
||||||
|
|
||||||
startedAt: time.Now(),
|
|
||||||
endedAt: time.Time{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if debugEnabled {
|
|
||||||
this.debugCreationStack = string(runtimedebug.Stack())
|
|
||||||
}
|
|
||||||
|
|
||||||
parentSpan.mtx.HoldWhile(func() {
|
|
||||||
if parentSpan.activeChildSpan != nil {
|
|
||||||
panic(ErrAlreadyActiveChildSpan)
|
|
||||||
}
|
|
||||||
parentSpan.activeChildSpan = this
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, contextKeyTraceNode, this)
|
|
||||||
chrometraceBeginSpan(this)
|
|
||||||
callbackEndSpan := callbackBeginSpan(ctx)
|
|
||||||
|
|
||||||
endTaskFunc := func() {
|
|
||||||
|
|
||||||
defer parentSpan.mtx.Lock().Unlock()
|
|
||||||
if parentSpan.activeChildSpan != this && this.endedAt.IsZero() {
|
|
||||||
panic("impl error: activeChildSpan should not change while != nil because there can only be one")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer this.mtx.Lock().Unlock()
|
|
||||||
if this.activeChildSpan != nil {
|
|
||||||
panic(ErrSpanStillHasActiveChildSpan)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !this.endedAt.IsZero() {
|
|
||||||
return // support idempotent span ends
|
|
||||||
}
|
|
||||||
|
|
||||||
parentSpan.activeChildSpan = nil
|
|
||||||
this.endedAt = time.Now()
|
|
||||||
|
|
||||||
chrometraceEndSpan(this)
|
|
||||||
callbackEndSpan(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx, endTaskFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
type StackKind struct {
|
|
||||||
symbolizeTask func(t *traceNode) string
|
|
||||||
symbolizeSpan func(s *traceNode) string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
StackKindId = &StackKind{
|
|
||||||
symbolizeTask: func(t *traceNode) string { return t.id },
|
|
||||||
symbolizeSpan: func(s *traceNode) string { return s.id },
|
|
||||||
}
|
|
||||||
SpanStackKindCombined = &StackKind{
|
|
||||||
symbolizeTask: func(t *traceNode) string { return fmt.Sprintf("(%s %q)", t.id, t.annotation) },
|
|
||||||
symbolizeSpan: func(s *traceNode) string { return fmt.Sprintf("(%s %q)", s.id, s.annotation) },
|
|
||||||
}
|
|
||||||
SpanStackKindAnnotation = &StackKind{
|
|
||||||
symbolizeTask: func(t *traceNode) string { return t.annotation },
|
|
||||||
symbolizeSpan: func(s *traceNode) string { return s.annotation },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (n *traceNode) task() *traceNode {
|
|
||||||
task := n.parentTask
|
|
||||||
if n.parentSpan == nil {
|
|
||||||
task = n
|
|
||||||
}
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *traceNode) TaskName() string {
|
|
||||||
task := n.task()
|
|
||||||
return task.annotation
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *traceNode) TaskAndSpanStack(kind *StackKind) (spanIdStack string) {
|
|
||||||
task := this.task()
|
|
||||||
|
|
||||||
var spansInTask []*traceNode
|
|
||||||
for s := this; s != nil; s = s.parentSpan {
|
|
||||||
spansInTask = append(spansInTask, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
var tasks []*traceNode
|
|
||||||
for t := task; t != nil; t = t.parentTask {
|
|
||||||
tasks = append(tasks, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
var taskIdsRev []string
|
|
||||||
for i := len(tasks) - 1; i >= 0; i-- {
|
|
||||||
taskIdsRev = append(taskIdsRev, kind.symbolizeTask(tasks[i]))
|
|
||||||
}
|
|
||||||
|
|
||||||
var spanIdsRev []string
|
|
||||||
for i := len(spansInTask) - 1; i >= 0; i-- {
|
|
||||||
spanIdsRev = append(spanIdsRev, kind.symbolizeSpan(spansInTask[i]))
|
|
||||||
}
|
|
||||||
|
|
||||||
taskStack := strings.Join(taskIdsRev, "$")
|
|
||||||
return fmt.Sprintf("%s$%s", taskStack, strings.Join(spanIdsRev, "."))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSpanStackOrDefault(ctx context.Context, kind StackKind, def string) string {
|
|
||||||
if nI := ctx.Value(contextKeyTraceNode); nI != nil {
|
|
||||||
n := nI.(*traceNode)
|
|
||||||
return n.TaskAndSpanStack(StackKindId)
|
|
||||||
} else {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SpanInfo interface {
|
|
||||||
StartedAt() time.Time
|
|
||||||
EndedAt() time.Time
|
|
||||||
TaskAndSpanStack(kind *StackKind) string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Callback struct {
|
|
||||||
OnBegin func(ctx context.Context)
|
|
||||||
OnEnd func(ctx context.Context, spanInfo SpanInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
var callbacks struct {
|
|
||||||
mtx chainlock.L
|
|
||||||
cs []Callback
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterCallback(c Callback) {
|
|
||||||
callbacks.mtx.HoldWhile(func() {
|
|
||||||
callbacks.cs = append(callbacks.cs, c)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func callbackBeginSpan(ctx context.Context) func(SpanInfo) {
|
|
||||||
// capture the current state of callbacks into a local variable
|
|
||||||
// this is safe because the slice is append-only and immutable
|
|
||||||
|
|
||||||
// (it is important that a callback registered _after_ callbackBeginSpin is called does not get called on OnEnd)
|
|
||||||
|
|
||||||
var cbs []Callback
|
|
||||||
callbacks.mtx.HoldWhile(func() {
|
|
||||||
cbs = callbacks.cs
|
|
||||||
})
|
|
||||||
for _, cb := range cbs {
|
|
||||||
if cb.OnBegin != nil {
|
|
||||||
cb.OnBegin(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return func(spanInfo SpanInfo) {
|
|
||||||
for _, cb := range cbs {
|
|
||||||
if cb.OnEnd != nil {
|
|
||||||
cb.OnEnd(ctx, spanInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
// The functions in this file are concerned with the generation
|
|
||||||
// of trace files based on the information from WithTask and WithSpan.
|
|
||||||
//
|
|
||||||
// The emitted trace files are open-ended array of JSON objects
|
|
||||||
// that follow the Chrome trace file format:
|
|
||||||
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
|
|
||||||
//
|
|
||||||
// The emitted JSON can be loaded into Chrome's chrome://tracing view.
|
|
||||||
//
|
|
||||||
// The trace file can be written to a file whose path is specified in an env file,
|
|
||||||
// and be written to web sockets established on ChrometraceHttpHandler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
)
|
|
||||||
|
|
||||||
var chrometracePID string
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var err error
|
|
||||||
chrometracePID, err = os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
chrometracePID = fmt.Sprintf("%q", chrometracePID)
|
|
||||||
}
|
|
||||||
|
|
||||||
type chrometraceEvent struct {
|
|
||||||
Cat string `json:"cat,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Stack []string `json:"stack,omitempty"`
|
|
||||||
Phase string `json:"ph"`
|
|
||||||
TimestampUnixMicroseconds int64 `json:"ts"`
|
|
||||||
DurationMicroseconds int64 `json:"dur,omitempty"`
|
|
||||||
Pid string `json:"pid"`
|
|
||||||
Tid string `json:"tid"`
|
|
||||||
Id string `json:"id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func chrometraceBeginSpan(s *traceNode) {
|
|
||||||
taskName := s.TaskName()
|
|
||||||
chrometraceWrite(chrometraceEvent{
|
|
||||||
Name: s.annotation,
|
|
||||||
Phase: "B",
|
|
||||||
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000,
|
|
||||||
Pid: chrometracePID,
|
|
||||||
Tid: taskName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func chrometraceEndSpan(s *traceNode) {
|
|
||||||
taskName := s.TaskName()
|
|
||||||
chrometraceWrite(chrometraceEvent{
|
|
||||||
Name: s.annotation,
|
|
||||||
Phase: "E",
|
|
||||||
TimestampUnixMicroseconds: s.endedAt.UnixNano() / 1000,
|
|
||||||
Pid: chrometracePID,
|
|
||||||
Tid: taskName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var chrometraceFlowId uint64
|
|
||||||
|
|
||||||
func chrometraceBeginTask(s *traceNode) {
|
|
||||||
chrometraceBeginSpan(s)
|
|
||||||
|
|
||||||
if s.parentTask == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// beginning of a task that has a parent
|
|
||||||
// => use flow events to link parent and child
|
|
||||||
|
|
||||||
flowId := atomic.AddUint64(&chrometraceFlowId, 1)
|
|
||||||
flowIdStr := fmt.Sprintf("%x", flowId)
|
|
||||||
|
|
||||||
parentTask := s.parentTask.TaskName()
|
|
||||||
chrometraceWrite(chrometraceEvent{
|
|
||||||
Cat: "task", // seems to be necessary, otherwise the GUI shows some `indexOf` JS error
|
|
||||||
Name: "child-task",
|
|
||||||
Phase: "s",
|
|
||||||
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000, // yes, the child's timestamp (=> from-point of the flow line is at right x-position of parent's bar)
|
|
||||||
Pid: chrometracePID,
|
|
||||||
Tid: parentTask,
|
|
||||||
Id: flowIdStr,
|
|
||||||
})
|
|
||||||
|
|
||||||
childTask := s.TaskName()
|
|
||||||
if parentTask == childTask {
|
|
||||||
panic(parentTask)
|
|
||||||
}
|
|
||||||
chrometraceWrite(chrometraceEvent{
|
|
||||||
Cat: "task", // seems to be necessary, otherwise the GUI shows some `indexOf` JS error
|
|
||||||
Name: "child-task",
|
|
||||||
Phase: "f",
|
|
||||||
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000,
|
|
||||||
Pid: chrometracePID,
|
|
||||||
Tid: childTask,
|
|
||||||
Id: flowIdStr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func chrometraceEndTask(s *traceNode) {
|
|
||||||
chrometraceEndSpan(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
type chrometraceConsumerRegistration struct {
|
|
||||||
w io.Writer
|
|
||||||
// errored must have capacity 1, the writer thread will send to it non-blocking, then close it
|
|
||||||
errored chan error
|
|
||||||
}
|
|
||||||
|
|
||||||
var chrometraceConsumers struct {
|
|
||||||
register chan chrometraceConsumerRegistration
|
|
||||||
consumers map[chrometraceConsumerRegistration]bool
|
|
||||||
write chan []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
chrometraceConsumers.register = make(chan chrometraceConsumerRegistration)
|
|
||||||
chrometraceConsumers.consumers = make(map[chrometraceConsumerRegistration]bool)
|
|
||||||
chrometraceConsumers.write = make(chan []byte)
|
|
||||||
go func() {
|
|
||||||
kickConsumer := func(c chrometraceConsumerRegistration, err error) {
|
|
||||||
debug("chrometrace kicking consumer %#v after error %v", c, err)
|
|
||||||
select {
|
|
||||||
case c.errored <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
close(c.errored)
|
|
||||||
delete(chrometraceConsumers.consumers, c)
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case reg := <-chrometraceConsumers.register:
|
|
||||||
debug("registered chrometrace consumer %#v", reg)
|
|
||||||
chrometraceConsumers.consumers[reg] = true
|
|
||||||
n, err := reg.w.Write([]byte("[\n"))
|
|
||||||
if err != nil {
|
|
||||||
kickConsumer(reg, err)
|
|
||||||
} else if n != 2 {
|
|
||||||
kickConsumer(reg, fmt.Errorf("short write: %v", n))
|
|
||||||
}
|
|
||||||
// successfully registered
|
|
||||||
|
|
||||||
case buf := <-chrometraceConsumers.write:
|
|
||||||
debug("chrometrace write request: %s", string(buf))
|
|
||||||
var r bytes.Reader
|
|
||||||
for c := range chrometraceConsumers.consumers {
|
|
||||||
r.Reset(buf)
|
|
||||||
n, err := io.Copy(c.w, &r)
|
|
||||||
debug("chrometrace wrote n=%v bytes to consumer %#v", n, c)
|
|
||||||
if err != nil {
|
|
||||||
kickConsumer(c, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func chrometraceWrite(i interface{}) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err := json.NewEncoder(&buf).Encode(i)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
buf.WriteString(",")
|
|
||||||
chrometraceConsumers.write <- buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ChrometraceClientWebsocketHandler(conn *websocket.Conn) {
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
defer wg.Wait()
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
r := bufio.NewReader(conn)
|
|
||||||
_, _, _ = r.ReadLine() // ignore errors
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
errored := make(chan error, 1)
|
|
||||||
chrometraceConsumers.register <- chrometraceConsumerRegistration{
|
|
||||||
w: conn,
|
|
||||||
errored: errored,
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
<-errored
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
var chrometraceFileConsumerPath = envconst.String("ZREPL_ACTIVITY_TRACE", "")
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if chrometraceFileConsumerPath != "" {
|
|
||||||
var err error
|
|
||||||
f, err := os.Create(chrometraceFileConsumerPath)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
errored := make(chan error, 1)
|
|
||||||
chrometraceConsumers.register <- chrometraceConsumerRegistration{
|
|
||||||
w: f,
|
|
||||||
errored: errored,
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
<-errored
|
|
||||||
f.Close()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextKeyTraceNode contextKey = 1 + iota
|
|
||||||
)
|
|
||||||
|
|
||||||
var contextKeys = []contextKey{
|
|
||||||
contextKeyTraceNode,
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithInherit inherits the task hierarchy from inheritFrom into ctx.
|
|
||||||
// The returned context is a child of ctx, but its task and span are those of inheritFrom.
|
|
||||||
//
|
|
||||||
// Note that in most use cases, callers most likely want to call WithTask since it will most likely
|
|
||||||
// be in some sort of connection handler context.
|
|
||||||
func WithInherit(ctx, inheritFrom context.Context) context.Context {
|
|
||||||
for _, k := range contextKeys {
|
|
||||||
if v := inheritFrom.Value(k); v != nil {
|
|
||||||
ctx = context.WithValue(ctx, k, v) // no shadow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// use like this:
|
|
||||||
//
|
|
||||||
// defer WithSpanFromStackUpdateCtx(&existingCtx)()
|
|
||||||
func WithSpanFromStackUpdateCtx(ctx *context.Context) DoneFunc {
|
|
||||||
childSpanCtx, end := WithSpan(*ctx, getMyCallerOrPanic())
|
|
||||||
*ctx = childSpanCtx
|
|
||||||
return end
|
|
||||||
}
|
|
||||||
|
|
||||||
// derive task name from call stack (caller's name)
|
|
||||||
func WithTaskFromStack(ctx context.Context) (context.Context, DoneFunc) {
|
|
||||||
return WithTask(ctx, getMyCallerOrPanic())
|
|
||||||
}
|
|
||||||
|
|
||||||
// derive task name from call stack (caller's name) and update *ctx
|
|
||||||
// to point to be the child task ctx
|
|
||||||
func WithTaskFromStackUpdateCtx(ctx *context.Context) DoneFunc {
|
|
||||||
child, end := WithTask(*ctx, getMyCallerOrPanic())
|
|
||||||
*ctx = child
|
|
||||||
return end
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a task and a span within it in one call
|
|
||||||
func WithTaskAndSpan(ctx context.Context, task string, span string) (context.Context, DoneFunc) {
|
|
||||||
ctx, endTask := WithTask(ctx, task)
|
|
||||||
ctx, endSpan := WithSpan(ctx, fmt.Sprintf("%s %s", task, span))
|
|
||||||
return ctx, func() {
|
|
||||||
endSpan()
|
|
||||||
endTask()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a span during which several child tasks are spawned using the `add` function
|
|
||||||
//
|
|
||||||
// IMPORTANT FOR USERS: Caller must ensure that the capturing behavior is correct, the Go linter doesn't catch this.
|
|
||||||
func WithTaskGroup(ctx context.Context, taskGroup string) (_ context.Context, add func(f func(context.Context)), waitEnd DoneFunc) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
ctx, endSpan := WithSpan(ctx, taskGroup)
|
|
||||||
add = func(f func(context.Context)) {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
ctx, endTask := WithTask(ctx, taskGroup)
|
|
||||||
defer endTask()
|
|
||||||
f(ctx)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
waitEnd = func() {
|
|
||||||
wg.Wait()
|
|
||||||
endSpan()
|
|
||||||
}
|
|
||||||
return ctx, add, waitEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMyCallerOrPanic() string {
|
|
||||||
pc, _, _, ok := runtime.Caller(2)
|
|
||||||
if !ok {
|
|
||||||
panic("cannot get caller")
|
|
||||||
}
|
|
||||||
details := runtime.FuncForPC(pc)
|
|
||||||
if ok && details != nil {
|
|
||||||
const prefix = "github.com/zrepl/zrepl"
|
|
||||||
return strings.TrimPrefix(strings.TrimPrefix(details.Name(), prefix), "/")
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetCallerOrPanic(t *testing.T) {
|
|
||||||
withStackFromCtxMock := func() string {
|
|
||||||
return getMyCallerOrPanic()
|
|
||||||
}
|
|
||||||
ret := withStackFromCtxMock()
|
|
||||||
// zrepl prefix is stripped
|
|
||||||
assert.Equal(t, "daemon/logging/trace.TestGetCallerOrPanic", ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithTaskGroupRunTasksConcurrently(t *testing.T) {
|
|
||||||
|
|
||||||
// spawn a task group where each task waits for the other to start
|
|
||||||
// => without concurrency, they would hang
|
|
||||||
|
|
||||||
rootCtx, endRoot := WithTaskFromStack(context.Background())
|
|
||||||
defer endRoot()
|
|
||||||
|
|
||||||
_, add, waitEnd := WithTaskGroup(rootCtx, "test-task-group")
|
|
||||||
|
|
||||||
schedulerTimeout := 2 * time.Second
|
|
||||||
timeout := time.After(schedulerTimeout)
|
|
||||||
var hadTimeout uint32
|
|
||||||
started0, started1 := make(chan struct{}), make(chan struct{})
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
i := i // capture by copy
|
|
||||||
add(func(ctx context.Context) {
|
|
||||||
switch i {
|
|
||||||
case 0:
|
|
||||||
close(started0)
|
|
||||||
select {
|
|
||||||
case <-started1:
|
|
||||||
case <-timeout:
|
|
||||||
atomic.AddUint32(&hadTimeout, 1)
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
close(started1)
|
|
||||||
select {
|
|
||||||
case <-started0:
|
|
||||||
case <-timeout:
|
|
||||||
atomic.AddUint32(&hadTimeout, 1)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
waitEnd()
|
|
||||||
assert.Zero(t, hadTimeout, "either bad impl or scheduler timeout (which is %v)", schedulerTimeout)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
)
|
|
||||||
|
|
||||||
const debugEnabledEnvVar = "ZREPL_TRACE_DEBUG_ENABLED"
|
|
||||||
|
|
||||||
var debugEnabled = envconst.Bool(debugEnabledEnvVar, false)
|
|
||||||
|
|
||||||
func debug(format string, args ...interface{}) {
|
|
||||||
if !debugEnabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
)
|
|
||||||
|
|
||||||
var genIdNumBytes = envconst.Int("ZREPL_TRACE_ID_NUM_BYTES", 3)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if genIdNumBytes < 1 {
|
|
||||||
panic("trace node id byte length must be at least 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func genID() string {
|
|
||||||
var out strings.Builder
|
|
||||||
enc := base64.NewEncoder(base64.RawStdEncoding, &out)
|
|
||||||
buf := make([]byte, genIdNumBytes)
|
|
||||||
for i := 0; i < len(buf); {
|
|
||||||
n, err := rand.Read(buf[i:])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
i += n
|
|
||||||
}
|
|
||||||
n, err := enc.Write(buf[:])
|
|
||||||
if err != nil || n != len(buf) {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := enc.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return out.String()
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gitchander/permutation"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRegularSpanUsage(t *testing.T) {
|
|
||||||
root, endRoot := WithTask(context.Background(), "root")
|
|
||||||
defer endRoot()
|
|
||||||
|
|
||||||
s1, endS1 := WithSpan(root, "parent")
|
|
||||||
s2, endS2 := WithSpan(s1, "child")
|
|
||||||
_, endS3 := WithSpan(s2, "grand-child")
|
|
||||||
require.NotPanics(t, func() { endS3() })
|
|
||||||
require.NotPanics(t, func() { endS2() })
|
|
||||||
|
|
||||||
// reuse
|
|
||||||
_, endS4 := WithSpan(s1, "child-2")
|
|
||||||
require.NotPanics(t, func() { endS4() })
|
|
||||||
|
|
||||||
// close parent
|
|
||||||
require.NotPanics(t, func() { endS1() })
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultipleActiveChildSpansNotAllowed(t *testing.T) {
|
|
||||||
root, endRoot := WithTask(context.Background(), "root")
|
|
||||||
defer endRoot()
|
|
||||||
|
|
||||||
s1, _ := WithSpan(root, "s1")
|
|
||||||
_, endS2 := WithSpan(s1, "s1-child1")
|
|
||||||
|
|
||||||
require.PanicsWithValue(t, ErrAlreadyActiveChildSpan, func() {
|
|
||||||
_, _ = WithSpan(s1, "s1-child2")
|
|
||||||
})
|
|
||||||
|
|
||||||
endS2()
|
|
||||||
|
|
||||||
require.NotPanics(t, func() {
|
|
||||||
_, _ = WithSpan(s1, "s1-child2")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForkingChildSpansNotAllowed(t *testing.T) {
|
|
||||||
root, endRoot := WithTask(context.Background(), "root")
|
|
||||||
defer endRoot()
|
|
||||||
|
|
||||||
s1, _ := WithSpan(root, "s1")
|
|
||||||
sc, endSC := WithSpan(s1, "s1-child")
|
|
||||||
_, _ = WithSpan(sc, "s1-child-child")
|
|
||||||
|
|
||||||
require.PanicsWithValue(t, ErrSpanStillHasActiveChildSpan, func() {
|
|
||||||
endSC()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegularTaskUsage(t *testing.T) {
|
|
||||||
// assert concurrent activities on different tasks can end in any order
|
|
||||||
closeOrder := []int{0, 1, 2}
|
|
||||||
closeOrders := permutation.New(permutation.IntSlice(closeOrder))
|
|
||||||
for closeOrders.Next() {
|
|
||||||
t.Run(fmt.Sprintf("%v", closeOrder), func(t *testing.T) {
|
|
||||||
root, endRoot := WithTask(context.Background(), "root")
|
|
||||||
defer endRoot()
|
|
||||||
|
|
||||||
c1, endC1 := WithTask(root, "c1")
|
|
||||||
defer endC1()
|
|
||||||
c2, endC2 := WithTask(root, "c2")
|
|
||||||
defer endC2()
|
|
||||||
|
|
||||||
// begin 3 concurrent activities
|
|
||||||
_, endAR := WithSpan(root, "aR")
|
|
||||||
_, endAC1 := WithSpan(c1, "aC1")
|
|
||||||
_, endAC2 := WithSpan(c2, "aC2")
|
|
||||||
|
|
||||||
endFuncs := []DoneFunc{endAR, endAC1, endAC2}
|
|
||||||
for _, i := range closeOrder {
|
|
||||||
require.NotPanics(t, func() {
|
|
||||||
endFuncs[i]()
|
|
||||||
}, "%v", i)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTaskEndWithActiveChildTaskNotAllowed(t *testing.T) {
|
|
||||||
root, _ := WithTask(context.Background(), "root")
|
|
||||||
c, endC := WithTask(root, "child")
|
|
||||||
_, _ = WithTask(c, "grand-child")
|
|
||||||
func() {
|
|
||||||
defer func() {
|
|
||||||
r := recover()
|
|
||||||
require.NotNil(t, r)
|
|
||||||
err, ok := r.(error)
|
|
||||||
require.True(t, ok)
|
|
||||||
require.Equal(t, ErrTaskStillHasActiveChildTasks, errors.Cause(err))
|
|
||||||
}()
|
|
||||||
endC()
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIdempotentEndTask(t *testing.T) {
|
|
||||||
_, end := WithTask(context.Background(), "root")
|
|
||||||
end()
|
|
||||||
require.NotPanics(t, func() { end() })
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSpansPanicIfNoParentTask(t *testing.T) {
|
|
||||||
require.Panics(t, func() { WithSpan(context.Background(), "taskless-span") })
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIdempotentEndSpan(t *testing.T) {
|
|
||||||
root, _ := WithTask(context.Background(), "root")
|
|
||||||
_, end := WithSpan(root, "span")
|
|
||||||
end()
|
|
||||||
require.NotPanics(t, func() { end() })
|
|
||||||
}
|
|
||||||
|
|
||||||
func logAndGetTraceNode(t *testing.T, descr string, ctx context.Context) *traceNode {
|
|
||||||
n, ok := ctx.Value(contextKeyTraceNode).(*traceNode)
|
|
||||||
require.True(t, ok)
|
|
||||||
t.Logf("% 20s %p %#v", descr, n, n)
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWhiteboxHierachy(t *testing.T) {
|
|
||||||
root, e1 := WithTask(context.Background(), "root")
|
|
||||||
rootN := logAndGetTraceNode(t, "root", root)
|
|
||||||
assert.Nil(t, rootN.parentTask)
|
|
||||||
assert.Nil(t, rootN.parentSpan)
|
|
||||||
|
|
||||||
child, e2 := WithSpan(root, "child")
|
|
||||||
childN := logAndGetTraceNode(t, "child", child)
|
|
||||||
assert.Equal(t, rootN, childN.parentTask)
|
|
||||||
assert.Equal(t, rootN, childN.parentSpan)
|
|
||||||
|
|
||||||
grandchild, e3 := WithSpan(child, "grandchild")
|
|
||||||
grandchildN := logAndGetTraceNode(t, "grandchild", grandchild)
|
|
||||||
assert.Equal(t, rootN, grandchildN.parentTask)
|
|
||||||
assert.Equal(t, childN, grandchildN.parentSpan)
|
|
||||||
|
|
||||||
gcTask, e4 := WithTask(grandchild, "grandchild-task")
|
|
||||||
gcTaskN := logAndGetTraceNode(t, "grandchild-task", gcTask)
|
|
||||||
assert.Equal(t, rootN, gcTaskN.parentTask)
|
|
||||||
assert.Nil(t, gcTaskN.parentSpan)
|
|
||||||
|
|
||||||
// it is allowed that a child task outlives the _span_ in which it was created
|
|
||||||
// (albeit not its parent task)
|
|
||||||
e3()
|
|
||||||
e2()
|
|
||||||
gcTaskSpan, e5 := WithSpan(gcTask, "granschild-task-span")
|
|
||||||
gcTaskSpanN := logAndGetTraceNode(t, "granschild-task-span", gcTaskSpan)
|
|
||||||
assert.Equal(t, gcTaskN, gcTaskSpanN.parentTask)
|
|
||||||
assert.Equal(t, gcTaskN, gcTaskSpanN.parentSpan)
|
|
||||||
e5()
|
|
||||||
|
|
||||||
e4()
|
|
||||||
e1()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOrphanTasksBecomeNewRootWhenAllAncestorsAreDead(t *testing.T) {
|
|
||||||
parent, e1 := WithTask(context.Background(), "parent")
|
|
||||||
_ = logAndGetTraceNode(t, "parent-task", parent)
|
|
||||||
e1()
|
|
||||||
|
|
||||||
var child context.Context
|
|
||||||
var e2 DoneFunc
|
|
||||||
require.NotPanics(t, func() {
|
|
||||||
child, e2 = WithTask(parent, "child")
|
|
||||||
})
|
|
||||||
childN := logAndGetTraceNode(t, "child-task", child)
|
|
||||||
assert.Nil(t, childN.parentTask)
|
|
||||||
|
|
||||||
grandchild, _ := WithTask(child, "grandchild")
|
|
||||||
grandchildN := logAndGetTraceNode(t, "grandchild-task", grandchild)
|
|
||||||
assert.Equal(t, childN, grandchildN.parentTask)
|
|
||||||
|
|
||||||
require.Panics(t, func() { e2() }, "if the parent was alive at creation of child task, wrong termination order remains a panicable offense though")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOrphanTaskBecomesChildToNearestLiveAncestor(t *testing.T) {
|
|
||||||
parent, e1 := WithTask(context.Background(), "parent")
|
|
||||||
parentN := logAndGetTraceNode(t, "parent-task", parent)
|
|
||||||
|
|
||||||
child, e2 := WithTask(parent, "child")
|
|
||||||
childN := logAndGetTraceNode(t, "child-task", child)
|
|
||||||
assert.Equal(t, parentN, childN.parentTask)
|
|
||||||
e2()
|
|
||||||
|
|
||||||
grandchild, e3 := WithTask(child, "grandchild")
|
|
||||||
grandchildN := logAndGetTraceNode(t, "grandchild-task", grandchild)
|
|
||||||
assert.Equal(t, parentN, grandchildN.parentTask) // child is already dead
|
|
||||||
|
|
||||||
require.NotPanics(t, func() {
|
|
||||||
e3()
|
|
||||||
e1()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/willf/bitset"
|
|
||||||
)
|
|
||||||
|
|
||||||
type uniqueConcurrentTaskNamer struct {
|
|
||||||
mtx sync.Mutex
|
|
||||||
active map[string]*bitset.BitSet
|
|
||||||
}
|
|
||||||
|
|
||||||
// bitvecLengthGauge may be nil
|
|
||||||
func newUniqueTaskNamer() *uniqueConcurrentTaskNamer {
|
|
||||||
return &uniqueConcurrentTaskNamer{
|
|
||||||
active: make(map[string]*bitset.BitSet),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// appends `#%d` to `name` such that until `done` is called,
|
|
||||||
// it is guaranteed that `#%d` is not returned a second time for the same `name`
|
|
||||||
func (namer *uniqueConcurrentTaskNamer) UniqueConcurrentTaskName(name string) (uniqueName string, done func()) {
|
|
||||||
if strings.Contains(name, "#") {
|
|
||||||
panic(name)
|
|
||||||
}
|
|
||||||
namer.mtx.Lock()
|
|
||||||
act, ok := namer.active[name]
|
|
||||||
if !ok {
|
|
||||||
act = bitset.New(64) // FIXME magic const
|
|
||||||
namer.active[name] = act
|
|
||||||
}
|
|
||||||
id, ok := act.NextClear(0)
|
|
||||||
if !ok {
|
|
||||||
// if !ok, all bits are 1 and act.Len() returns the next bit
|
|
||||||
id = act.Len()
|
|
||||||
// FIXME unbounded growth without reclamation
|
|
||||||
}
|
|
||||||
act.Set(id)
|
|
||||||
namer.mtx.Unlock()
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s#%d", name, id), func() {
|
|
||||||
namer.mtx.Lock()
|
|
||||||
defer namer.mtx.Unlock()
|
|
||||||
act, ok := namer.active[name]
|
|
||||||
if !ok {
|
|
||||||
panic("must be initialized upon entry")
|
|
||||||
}
|
|
||||||
act.Clear(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/willf/bitset"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBitsetFeaturesForUniqueConcurrentTaskNamer(t *testing.T) {
|
|
||||||
var b bitset.BitSet
|
|
||||||
require.Equal(t, uint(0), b.Len())
|
|
||||||
require.Equal(t, uint(0), b.Count())
|
|
||||||
|
|
||||||
b.Set(0)
|
|
||||||
require.Equal(t, uint(1), b.Len())
|
|
||||||
require.Equal(t, uint(1), b.Count())
|
|
||||||
|
|
||||||
b.Set(8)
|
|
||||||
require.Equal(t, uint(9), b.Len())
|
|
||||||
require.Equal(t, uint(2), b.Count())
|
|
||||||
|
|
||||||
b.Set(1)
|
|
||||||
require.Equal(t, uint(9), b.Len())
|
|
||||||
require.Equal(t, uint(3), b.Count())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUniqueConcurrentTaskNamer(t *testing.T) {
|
|
||||||
namer := newUniqueTaskNamer()
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
const N = 8128
|
|
||||||
const Q = 23
|
|
||||||
|
|
||||||
var fails uint32
|
|
||||||
var m sync.Map
|
|
||||||
wg.Add(N)
|
|
||||||
for i := 0; i < N; i++ {
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
name := fmt.Sprintf("%d", i/Q)
|
|
||||||
uniqueName, done := namer.UniqueConcurrentTaskName(name)
|
|
||||||
act, _ := m.LoadOrStore(uniqueName, i)
|
|
||||||
if act.(int) != i {
|
|
||||||
atomic.AddUint32(&fails, 1)
|
|
||||||
}
|
|
||||||
m.Delete(uniqueName)
|
|
||||||
done()
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
require.Equal(t, uint32(0), fails)
|
|
||||||
}
|
|
||||||
+2
-4
@@ -1,8 +1,6 @@
|
|||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
)
|
)
|
||||||
@@ -12,7 +10,7 @@ type Logger = logger.Logger
|
|||||||
var DaemonCmd = &cli.Subcommand{
|
var DaemonCmd = &cli.Subcommand{
|
||||||
Use: "daemon",
|
Use: "daemon",
|
||||||
Short: "run the zrepl daemon",
|
Short: "run the zrepl daemon",
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
Run: func(subcommand *cli.Subcommand, args []string) error {
|
||||||
return Run(ctx, subcommand.Config())
|
return Run(subcommand.Config())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type pprofServer struct {
|
type pprofServer struct {
|
||||||
@@ -68,8 +64,6 @@ outer:
|
|||||||
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
|
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
|
||||||
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
|
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
|
||||||
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
|
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
|
||||||
mux.Handle("/metrics", promhttp.Handler())
|
|
||||||
mux.Handle("/debug/zrepl/activity-trace", websocket.Handler(trace.ChrometraceClientWebsocketHandler))
|
|
||||||
go func() {
|
go func() {
|
||||||
err := http.Serve(s.listener, mux)
|
err := http.Serve(s.listener, mux)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
||||||
@@ -87,19 +86,16 @@ func (j *prometheusJob) Run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type prometheusJobOutlet struct {
|
type prometheusJobOutlet struct {
|
||||||
|
jobName string
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ logger.Outlet = prometheusJobOutlet{}
|
var _ logger.Outlet = prometheusJobOutlet{}
|
||||||
|
|
||||||
func newPrometheusLogOutlet() prometheusJobOutlet {
|
func newPrometheusLogOutlet(jobName string) prometheusJobOutlet {
|
||||||
return prometheusJobOutlet{}
|
return prometheusJobOutlet{jobName}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
|
func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
|
||||||
jobFieldVal, ok := entry.Fields[logging.JobField].(string)
|
prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc()
|
||||||
if !ok {
|
|
||||||
jobFieldVal = "_nojobid"
|
|
||||||
}
|
|
||||||
prom.taskLogEntries.WithLabelValues(jobFieldVal, entry.Level.String()).Inc()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-31
@@ -12,28 +12,19 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/pruning"
|
"github.com/zrepl/zrepl/pruning"
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The sender in the replication setup.
|
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
// The pruner uses the Sender to determine which of the Target's filesystems need to be pruned.
|
type History interface {
|
||||||
// Also, it asks the Sender about the replication cursor of each filesystem
|
|
||||||
// to enable the 'not_replicated' pruning rule.
|
|
||||||
//
|
|
||||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
|
||||||
type Sender interface {
|
|
||||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pruning target, i.e., on which snapshots are destroyed.
|
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
// This can be a replication sender or receiver.
|
|
||||||
//
|
|
||||||
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
|
||||||
type Target interface {
|
type Target interface {
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||||
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||||
@@ -44,19 +35,23 @@ type Logger = logger.Logger
|
|||||||
|
|
||||||
type contextKey int
|
type contextKey int
|
||||||
|
|
||||||
const (
|
const contextKeyLogger contextKey = 0
|
||||||
contextKeyPruneSide contextKey = 1 + iota
|
|
||||||
)
|
func WithLogger(ctx context.Context, log Logger) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyLogger, log)
|
||||||
|
}
|
||||||
|
|
||||||
func GetLogger(ctx context.Context) Logger {
|
func GetLogger(ctx context.Context) Logger {
|
||||||
pruneSide := ctx.Value(contextKeyPruneSide).(string)
|
if l, ok := ctx.Value(contextKeyLogger).(Logger); ok {
|
||||||
return logging.GetLogger(ctx, logging.SubsysPruning).WithField("prune_side", pruneSide)
|
return l
|
||||||
|
}
|
||||||
|
return logger.NewNullLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
target Target
|
target Target
|
||||||
sender Sender
|
receiver History
|
||||||
rules []pruning.KeepRule
|
rules []pruning.KeepRule
|
||||||
retryWait time.Duration
|
retryWait time.Duration
|
||||||
considerSnapAtCursorReplicated bool
|
considerSnapAtCursorReplicated bool
|
||||||
@@ -140,12 +135,12 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
WithLogger(ctx, GetLogger(ctx).WithField("prune_side", "sender")),
|
||||||
target,
|
target,
|
||||||
sender,
|
receiver,
|
||||||
f.senderRules,
|
f.senderRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
f.considerSnapAtCursorReplicated,
|
f.considerSnapAtCursorReplicated,
|
||||||
@@ -156,12 +151,12 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, se
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, sender Sender) *Pruner {
|
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
WithLogger(ctx, GetLogger(ctx).WithField("prune_side", "receiver")),
|
||||||
target,
|
target,
|
||||||
sender,
|
receiver,
|
||||||
f.receiverRules,
|
f.receiverRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // senseless here anyways
|
false, // senseless here anyways
|
||||||
@@ -172,12 +167,12 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, history Sender) *Pruner {
|
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
ctx,
|
||||||
target,
|
target,
|
||||||
history,
|
receiver,
|
||||||
f.keepRules,
|
f.keepRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
false, // considerSnapAtCursorReplicated is not relevant for local pruning
|
||||||
@@ -351,9 +346,9 @@ func (s snapshot) Date() time.Time { return s.date }
|
|||||||
|
|
||||||
func doOneAttempt(a *args, u updater) {
|
func doOneAttempt(a *args, u updater) {
|
||||||
|
|
||||||
ctx, target, sender := a.ctx, a.target, a.sender
|
ctx, target, receiver := a.ctx, a.target, a.receiver
|
||||||
|
|
||||||
sfssres, err := sender.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u(func(p *Pruner) {
|
u(func(p *Pruner) {
|
||||||
p.state = PlanErr
|
p.state = PlanErr
|
||||||
@@ -418,7 +413,7 @@ tfss_loop:
|
|||||||
rcReq := &pdu.ReplicationCursorReq{
|
rcReq := &pdu.ReplicationCursorReq{
|
||||||
Filesystem: tfs.Path,
|
Filesystem: tfs.Path,
|
||||||
}
|
}
|
||||||
rc, err := sender.ReplicationCursor(ctx, rcReq)
|
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
@@ -464,7 +459,7 @@ tfss_loop:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if preCursor {
|
if preCursor {
|
||||||
pfsPlanErrAndLog(fmt.Errorf("prune target has no snapshot that corresponds to sender replication cursor bookmark"), "")
|
pfsPlanErrAndLog(fmt.Errorf("replication cursor not found in prune target filesystem versions"), "")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func cronFromConfig(fsf zfs.DatasetFilter, in config.SnapshottingCron) (*Cron, error) {
|
|
||||||
|
|
||||||
hooksList, err := hooks.ListFromConfig(&in.Hooks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "hook config error")
|
|
||||||
}
|
|
||||||
planArgs := planArgs{
|
|
||||||
prefix: in.Prefix,
|
|
||||||
hooks: hooksList,
|
|
||||||
}
|
|
||||||
return &Cron{config: in, fsf: fsf, planArgs: planArgs}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cron struct {
|
|
||||||
config config.SnapshottingCron
|
|
||||||
fsf zfs.DatasetFilter
|
|
||||||
planArgs planArgs
|
|
||||||
|
|
||||||
mtx sync.RWMutex
|
|
||||||
|
|
||||||
running bool
|
|
||||||
wakeupTime time.Time // zero value means uninit
|
|
||||||
lastError error
|
|
||||||
lastPlan *plan
|
|
||||||
wakeupWhileRunningCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Cron) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|
||||||
|
|
||||||
t := time.NewTimer(0)
|
|
||||||
defer func() {
|
|
||||||
if !t.Stop() {
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
now := time.Now()
|
|
||||||
s.mtx.Lock()
|
|
||||||
s.wakeupTime = s.config.Cron.Schedule.Next(now)
|
|
||||||
s.mtx.Unlock()
|
|
||||||
|
|
||||||
// Re-arm the timer.
|
|
||||||
// Need to Stop before Reset, see docs.
|
|
||||||
if !t.Stop() {
|
|
||||||
// Use non-blocking read from timer channel
|
|
||||||
// because, except for the first loop iteration,
|
|
||||||
// the channel is already drained
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.Reset(s.wakeupTime.Sub(now))
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
getLogger(ctx).Debug("cron timer fired")
|
|
||||||
s.mtx.Lock()
|
|
||||||
if s.running {
|
|
||||||
getLogger(ctx).Warn("snapshotting triggered according to cron rules but previous snapshotting is not done; not taking a snapshot this time")
|
|
||||||
s.wakeupWhileRunningCount++
|
|
||||||
s.mtx.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.lastError = nil
|
|
||||||
s.lastPlan = nil
|
|
||||||
s.wakeupWhileRunningCount = 0
|
|
||||||
s.running = true
|
|
||||||
s.mtx.Unlock()
|
|
||||||
go func() {
|
|
||||||
err := s.do(ctx)
|
|
||||||
s.mtx.Lock()
|
|
||||||
s.lastError = err
|
|
||||||
s.running = false
|
|
||||||
s.mtx.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case snapshotsTaken <- struct{}{}:
|
|
||||||
default:
|
|
||||||
if snapshotsTaken != nil {
|
|
||||||
getLogger(ctx).Warn("callback channel is full, discarding snapshot update event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Cron) do(ctx context.Context) error {
|
|
||||||
fss, err := zfs.ZFSListMapping(ctx, s.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "cannot list filesystems")
|
|
||||||
}
|
|
||||||
p := makePlan(s.planArgs, fss)
|
|
||||||
|
|
||||||
s.mtx.Lock()
|
|
||||||
s.lastPlan = p
|
|
||||||
s.lastError = nil
|
|
||||||
s.mtx.Unlock()
|
|
||||||
|
|
||||||
ok := p.execute(ctx, false)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("one or more snapshots could not be created, check logs for details")
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronState string
|
|
||||||
|
|
||||||
const (
|
|
||||||
CronStateRunning CronState = "running"
|
|
||||||
CronStateWaiting CronState = "waiting"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CronReport struct {
|
|
||||||
State CronState
|
|
||||||
WakeupTime time.Time
|
|
||||||
Errors []string
|
|
||||||
Progress []*ReportFilesystem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Cron) Report() Report {
|
|
||||||
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
|
|
||||||
r := CronReport{}
|
|
||||||
|
|
||||||
r.WakeupTime = s.wakeupTime
|
|
||||||
|
|
||||||
if s.running {
|
|
||||||
r.State = CronStateRunning
|
|
||||||
} else {
|
|
||||||
r.State = CronStateWaiting
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.lastError != nil {
|
|
||||||
r.Errors = append(r.Errors, s.lastError.Error())
|
|
||||||
}
|
|
||||||
if s.wakeupWhileRunningCount > 0 {
|
|
||||||
r.Errors = append(r.Errors, fmt.Sprintf("cron frequency is too high; snapshots were not taken %d times", s.wakeupWhileRunningCount))
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Progress = nil
|
|
||||||
if s.lastPlan != nil {
|
|
||||||
r.Progress = s.lastPlan.report()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Report{Type: TypeCron, Cron: &r}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCronLibraryWorks(t *testing.T) {
|
|
||||||
|
|
||||||
type testCase struct {
|
|
||||||
spec string
|
|
||||||
in time.Time
|
|
||||||
expect time.Time
|
|
||||||
}
|
|
||||||
dhm := func(day, hour, minutes int) time.Time {
|
|
||||||
return time.Date(2022, 7, day, hour, minutes, 0, 0, time.UTC)
|
|
||||||
}
|
|
||||||
hm := func(hour, minutes int) time.Time {
|
|
||||||
return dhm(23, hour, minutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
tcs := []testCase{
|
|
||||||
{"0-10 * * * *", dhm(17, 1, 10), dhm(17, 2, 0)},
|
|
||||||
{"0-10 * * * *", dhm(17, 23, 10), dhm(18, 0, 0)},
|
|
||||||
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
|
|
||||||
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
|
|
||||||
|
|
||||||
{"1,3,5 * * * *", hm(1, 1), hm(1, 3)},
|
|
||||||
{"1,3,5 * * * *", hm(1, 2), hm(1, 3)},
|
|
||||||
{"1,3,5 * * * *", hm(1, 3), hm(1, 5)},
|
|
||||||
{"1,3,5 * * * *", hm(1, 5), hm(2, 1)},
|
|
||||||
|
|
||||||
{"* 0-5,8,12 * * *", hm(0, 0), hm(0, 1)},
|
|
||||||
{"* 0-5,8,12 * * *", hm(4, 59), hm(5, 0)},
|
|
||||||
{"* 0-5,8,12 * * *", hm(5, 0), hm(5, 1)},
|
|
||||||
{"* 0-5,8,12 * * *", hm(5, 59), hm(8, 0)},
|
|
||||||
{"* 0-5,8,12 * * *", hm(8, 59), hm(12, 0)},
|
|
||||||
|
|
||||||
// https://github.com/zrepl/zrepl/pull/614#issuecomment-1188358989
|
|
||||||
{"53 17,18,19 * * *", dhm(23, 17, 52), dhm(23, 17, 53)},
|
|
||||||
{"53 17,18,19 * * *", dhm(23, 17, 53), dhm(23, 18, 53)},
|
|
||||||
{"53 17,18,19 * * *", dhm(23, 18, 53), dhm(23, 19, 53)},
|
|
||||||
{"53 17,18,19 * * *", dhm(23, 19, 53), dhm(24 /* ! */, 17, 53)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range tcs {
|
|
||||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
||||||
var s struct {
|
|
||||||
Cron config.CronSpec `yaml:"cron"`
|
|
||||||
}
|
|
||||||
inp := fmt.Sprintf("cron: %q", tc.spec)
|
|
||||||
fmt.Println("spec is ", inp)
|
|
||||||
err := yaml.UnmarshalStrict([]byte(inp), &s)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
actual := s.Cron.Schedule.Next(tc.in)
|
|
||||||
assert.Equal(t, tc.expect, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type planArgs struct {
|
|
||||||
prefix string
|
|
||||||
hooks *hooks.List
|
|
||||||
}
|
|
||||||
|
|
||||||
type plan struct {
|
|
||||||
mtx chainlock.L
|
|
||||||
args planArgs
|
|
||||||
snaps map[*zfs.DatasetPath]*snapProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
func makePlan(args planArgs, fss []*zfs.DatasetPath) *plan {
|
|
||||||
snaps := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
|
||||||
for _, fs := range fss {
|
|
||||||
snaps[fs] = &snapProgress{state: SnapPending}
|
|
||||||
}
|
|
||||||
return &plan{snaps: snaps, args: args}
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:generate stringer -type=SnapState
|
|
||||||
type SnapState uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
SnapPending SnapState = 1 << iota
|
|
||||||
SnapStarted
|
|
||||||
SnapDone
|
|
||||||
SnapError
|
|
||||||
)
|
|
||||||
|
|
||||||
// All fields protected by Snapper.mtx
|
|
||||||
type snapProgress struct {
|
|
||||||
state SnapState
|
|
||||||
|
|
||||||
// SnapStarted, SnapDone, SnapError
|
|
||||||
name string
|
|
||||||
startAt time.Time
|
|
||||||
hookPlan *hooks.Plan
|
|
||||||
|
|
||||||
// SnapDone
|
|
||||||
doneAt time.Time
|
|
||||||
|
|
||||||
// SnapErr TODO disambiguate state
|
|
||||||
runResults hooks.PlanReport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plan *plan) execute(ctx context.Context, dryRun bool) (ok bool) {
|
|
||||||
|
|
||||||
hookMatchCount := make(map[hooks.Hook]int, len(*plan.args.hooks))
|
|
||||||
for _, h := range *plan.args.hooks {
|
|
||||||
hookMatchCount[h] = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
anyFsHadErr := false
|
|
||||||
// TODO channel programs -> allow a little jitter?
|
|
||||||
for fs, progress := range plan.snaps {
|
|
||||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
|
||||||
snapname := fmt.Sprintf("%s%s", plan.args.prefix, suffix)
|
|
||||||
|
|
||||||
ctx := logging.WithInjectedField(ctx, "fs", fs.ToString())
|
|
||||||
ctx = logging.WithInjectedField(ctx, "snap", snapname)
|
|
||||||
|
|
||||||
hookEnvExtra := hooks.Env{
|
|
||||||
hooks.EnvFS: fs.ToString(),
|
|
||||||
hooks.EnvSnapshot: snapname,
|
|
||||||
}
|
|
||||||
|
|
||||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
|
|
||||||
l := getLogger(ctx)
|
|
||||||
l.Debug("create snapshot")
|
|
||||||
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Error("cannot create snapshot")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
|
||||||
|
|
||||||
fsHadErr := false
|
|
||||||
var hookPlanReport hooks.PlanReport
|
|
||||||
var hookPlan *hooks.Plan
|
|
||||||
{
|
|
||||||
filteredHooks, err := plan.args.hooks.CopyFilteredForFilesystem(fs)
|
|
||||||
if err != nil {
|
|
||||||
getLogger(ctx).WithError(err).Error("unexpected filter error")
|
|
||||||
fsHadErr = true
|
|
||||||
goto updateFSState
|
|
||||||
}
|
|
||||||
// account for running hooks
|
|
||||||
for _, h := range filteredHooks {
|
|
||||||
hookMatchCount[h] = hookMatchCount[h] + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var planErr error
|
|
||||||
hookPlan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
|
||||||
if planErr != nil {
|
|
||||||
fsHadErr = true
|
|
||||||
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
|
|
||||||
goto updateFSState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plan.mtx.HoldWhile(func() {
|
|
||||||
progress.name = snapname
|
|
||||||
progress.startAt = time.Now()
|
|
||||||
progress.hookPlan = hookPlan
|
|
||||||
progress.state = SnapStarted
|
|
||||||
})
|
|
||||||
|
|
||||||
{
|
|
||||||
getLogger(ctx).WithField("report", hookPlan.Report().String()).Debug("begin run job plan")
|
|
||||||
hookPlan.Run(ctx, dryRun)
|
|
||||||
hookPlanReport = hookPlan.Report()
|
|
||||||
fsHadErr = hookPlanReport.HadError() // not just fatal errors
|
|
||||||
if fsHadErr {
|
|
||||||
getLogger(ctx).WithField("report", hookPlanReport.String()).Error("end run job plan with error")
|
|
||||||
} else {
|
|
||||||
getLogger(ctx).WithField("report", hookPlanReport.String()).Info("end run job plan successful")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFSState:
|
|
||||||
anyFsHadErr = anyFsHadErr || fsHadErr
|
|
||||||
plan.mtx.HoldWhile(func() {
|
|
||||||
progress.doneAt = time.Now()
|
|
||||||
progress.state = SnapDone
|
|
||||||
if fsHadErr {
|
|
||||||
progress.state = SnapError
|
|
||||||
}
|
|
||||||
progress.runResults = hookPlanReport
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for h, mc := range hookMatchCount {
|
|
||||||
if mc == 0 {
|
|
||||||
hookIdx := -1
|
|
||||||
for idx, ah := range *plan.args.hooks {
|
|
||||||
if ah == h {
|
|
||||||
hookIdx = idx
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getLogger(ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !anyFsHadErr
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReportFilesystem struct {
|
|
||||||
Path string
|
|
||||||
State SnapState
|
|
||||||
|
|
||||||
// Valid in SnapStarted and later
|
|
||||||
SnapName string
|
|
||||||
StartAt time.Time
|
|
||||||
Hooks string
|
|
||||||
HooksHadError bool
|
|
||||||
|
|
||||||
// Valid in SnapDone | SnapError
|
|
||||||
DoneAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plan *plan) report() []*ReportFilesystem {
|
|
||||||
plan.mtx.Lock()
|
|
||||||
defer plan.mtx.Unlock()
|
|
||||||
|
|
||||||
pReps := make([]*ReportFilesystem, 0, len(plan.snaps))
|
|
||||||
for fs, p := range plan.snaps {
|
|
||||||
var hooksStr string
|
|
||||||
var hooksHadError bool
|
|
||||||
if p.hookPlan != nil {
|
|
||||||
hooksStr, hooksHadError = p.report()
|
|
||||||
}
|
|
||||||
pReps = append(pReps, &ReportFilesystem{
|
|
||||||
Path: fs.ToString(),
|
|
||||||
State: p.state,
|
|
||||||
SnapName: p.name,
|
|
||||||
StartAt: p.startAt,
|
|
||||||
DoneAt: p.doneAt,
|
|
||||||
Hooks: hooksStr,
|
|
||||||
HooksHadError: hooksHadError,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(pReps, func(i, j int) bool {
|
|
||||||
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
return pReps
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *snapProgress) report() (hooksStr string, hooksHadError bool) {
|
|
||||||
hr := p.hookPlan.Report()
|
|
||||||
// FIXME: technically this belongs into client
|
|
||||||
// but we can't serialize hooks.Step ATM
|
|
||||||
rightPad := func(str string, length int, pad string) string {
|
|
||||||
if len(str) > length {
|
|
||||||
return str[:length]
|
|
||||||
}
|
|
||||||
return str + strings.Repeat(pad, length-len(str))
|
|
||||||
}
|
|
||||||
hooksHadError = hr.HadError()
|
|
||||||
rows := make([][]string, len(hr))
|
|
||||||
const numCols = 4
|
|
||||||
lens := make([]int, numCols)
|
|
||||||
for i, e := range hr {
|
|
||||||
rows[i] = make([]string, numCols)
|
|
||||||
rows[i][0] = fmt.Sprintf("%d", i+1)
|
|
||||||
rows[i][1] = e.Status.String()
|
|
||||||
runTime := "..."
|
|
||||||
if e.Status != hooks.StepPending {
|
|
||||||
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
|
|
||||||
}
|
|
||||||
rows[i][2] = runTime
|
|
||||||
rows[i][3] = ""
|
|
||||||
if e.Report != nil {
|
|
||||||
rows[i][3] = e.Report.String()
|
|
||||||
}
|
|
||||||
for j, col := range lens {
|
|
||||||
if len(rows[i][j]) > col {
|
|
||||||
lens[j] = len(rows[i][j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rowsFlat := make([]string, len(hr))
|
|
||||||
for i, r := range rows {
|
|
||||||
colsPadded := make([]string, len(r))
|
|
||||||
for j, c := range r[:len(r)-1] {
|
|
||||||
colsPadded[j] = rightPad(c, lens[j], " ")
|
|
||||||
}
|
|
||||||
colsPadded[len(r)-1] = r[len(r)-1]
|
|
||||||
rowsFlat[i] = strings.Join(colsPadded, " ")
|
|
||||||
}
|
|
||||||
hooksStr = strings.Join(rowsFlat, "\n")
|
|
||||||
|
|
||||||
return hooksStr, hooksHadError
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type manual struct{}
|
|
||||||
|
|
||||||
func (s *manual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *manual) Report() Report {
|
|
||||||
return Report{Type: TypeManual, Manual: &struct{}{}}
|
|
||||||
}
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func periodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Periodic, error) {
|
|
||||||
if in.Prefix == "" {
|
|
||||||
return nil, errors.New("prefix must not be empty")
|
|
||||||
}
|
|
||||||
if in.Interval <= 0 {
|
|
||||||
return nil, errors.New("interval must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "hook config error")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := periodicArgs{
|
|
||||||
interval: in.Interval,
|
|
||||||
fsf: fsf,
|
|
||||||
planArgs: planArgs{
|
|
||||||
prefix: in.Prefix,
|
|
||||||
hooks: hookList,
|
|
||||||
},
|
|
||||||
// ctx and log is set in Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Periodic{state: SyncUp, args: args}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type periodicArgs struct {
|
|
||||||
ctx context.Context
|
|
||||||
interval time.Duration
|
|
||||||
fsf zfs.DatasetFilter
|
|
||||||
planArgs planArgs
|
|
||||||
snapshotsTaken chan<- struct{}
|
|
||||||
dryRun bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Periodic struct {
|
|
||||||
args periodicArgs
|
|
||||||
|
|
||||||
mtx sync.Mutex
|
|
||||||
state State
|
|
||||||
|
|
||||||
// set in state Plan, used in Waiting
|
|
||||||
lastInvocation time.Time
|
|
||||||
|
|
||||||
// valid for state Snapshotting
|
|
||||||
plan *plan
|
|
||||||
|
|
||||||
// valid for state SyncUp and Waiting
|
|
||||||
sleepUntil time.Time
|
|
||||||
|
|
||||||
// valid for state Err
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:generate stringer -type=State
|
|
||||||
type State uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
SyncUp State = 1 << iota
|
|
||||||
SyncUpErrWait
|
|
||||||
Planning
|
|
||||||
Snapshotting
|
|
||||||
Waiting
|
|
||||||
ErrorWait
|
|
||||||
Stopped
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s State) sf() state {
|
|
||||||
m := map[State]state{
|
|
||||||
SyncUp: periodicStateSyncUp,
|
|
||||||
SyncUpErrWait: periodicStateWait,
|
|
||||||
Planning: periodicStatePlan,
|
|
||||||
Snapshotting: periodicStateSnapshot,
|
|
||||||
Waiting: periodicStateWait,
|
|
||||||
ErrorWait: periodicStateWait,
|
|
||||||
Stopped: nil,
|
|
||||||
}
|
|
||||||
return m[s]
|
|
||||||
}
|
|
||||||
|
|
||||||
type updater func(u func(*Periodic)) State
|
|
||||||
type state func(a periodicArgs, u updater) state
|
|
||||||
|
|
||||||
func (s *Periodic) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
|
||||||
getLogger(ctx).Debug("start")
|
|
||||||
defer getLogger(ctx).Debug("stop")
|
|
||||||
|
|
||||||
s.args.snapshotsTaken = snapshotsTaken
|
|
||||||
s.args.ctx = ctx
|
|
||||||
s.args.dryRun = false // for future expansion
|
|
||||||
|
|
||||||
u := func(u func(*Periodic)) State {
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
if u != nil {
|
|
||||||
u(s)
|
|
||||||
}
|
|
||||||
return s.state
|
|
||||||
}
|
|
||||||
|
|
||||||
var st state = periodicStateSyncUp
|
|
||||||
|
|
||||||
for st != nil {
|
|
||||||
pre := u(nil)
|
|
||||||
st = st(s.args, u)
|
|
||||||
post := u(nil)
|
|
||||||
getLogger(ctx).
|
|
||||||
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
|
||||||
Debug("state transition")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func onErr(err error, u updater) state {
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.err = err
|
|
||||||
preState := s.state
|
|
||||||
switch s.state {
|
|
||||||
case SyncUp:
|
|
||||||
s.state = SyncUpErrWait
|
|
||||||
case Planning:
|
|
||||||
fallthrough
|
|
||||||
case Snapshotting:
|
|
||||||
s.state = ErrorWait
|
|
||||||
}
|
|
||||||
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func onMainCtxDone(ctx context.Context, u updater) state {
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.err = ctx.Err()
|
|
||||||
s.state = Stopped
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func periodicStateSyncUp(a periodicArgs, u updater) state {
|
|
||||||
u(func(snapper *Periodic) {
|
|
||||||
snapper.lastInvocation = time.Now()
|
|
||||||
})
|
|
||||||
fss, err := listFSes(a.ctx, a.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
syncPoint, err := findSyncPoint(a.ctx, fss, a.planArgs.prefix, a.interval)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
u(func(s *Periodic) {
|
|
||||||
s.sleepUntil = syncPoint
|
|
||||||
})
|
|
||||||
t := time.NewTimer(time.Until(syncPoint))
|
|
||||||
defer t.Stop()
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.state = Planning
|
|
||||||
}).sf()
|
|
||||||
case <-a.ctx.Done():
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func periodicStatePlan(a periodicArgs, u updater) state {
|
|
||||||
u(func(snapper *Periodic) {
|
|
||||||
snapper.lastInvocation = time.Now()
|
|
||||||
})
|
|
||||||
fss, err := listFSes(a.ctx, a.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
p := makePlan(a.planArgs, fss)
|
|
||||||
return u(func(s *Periodic) {
|
|
||||||
s.state = Snapshotting
|
|
||||||
s.plan = p
|
|
||||||
s.err = nil
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func periodicStateSnapshot(a periodicArgs, u updater) state {
|
|
||||||
|
|
||||||
var plan *plan
|
|
||||||
u(func(snapper *Periodic) {
|
|
||||||
plan = snapper.plan
|
|
||||||
})
|
|
||||||
|
|
||||||
ok := plan.execute(a.ctx, false)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case a.snapshotsTaken <- struct{}{}:
|
|
||||||
default:
|
|
||||||
if a.snapshotsTaken != nil {
|
|
||||||
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return u(func(snapper *Periodic) {
|
|
||||||
if !ok {
|
|
||||||
snapper.state = ErrorWait
|
|
||||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
|
||||||
} else {
|
|
||||||
snapper.state = Waiting
|
|
||||||
snapper.err = nil
|
|
||||||
}
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func periodicStateWait(a periodicArgs, u updater) state {
|
|
||||||
var sleepUntil time.Time
|
|
||||||
u(func(snapper *Periodic) {
|
|
||||||
lastTick := snapper.lastInvocation
|
|
||||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
|
||||||
sleepUntil = snapper.sleepUntil
|
|
||||||
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
|
||||||
logFunc := log.Debug
|
|
||||||
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
|
||||||
logFunc = log.Error
|
|
||||||
}
|
|
||||||
logFunc("enter wait-state after error")
|
|
||||||
})
|
|
||||||
|
|
||||||
t := time.NewTimer(time.Until(sleepUntil))
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return u(func(snapper *Periodic) {
|
|
||||||
snapper.state = Planning
|
|
||||||
}).sf()
|
|
||||||
case <-a.ctx.Done():
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
|
||||||
return zfs.ZFSListMapping(ctx, mf)
|
|
||||||
}
|
|
||||||
|
|
||||||
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
|
||||||
|
|
||||||
// see docs/snapshotting.rst
|
|
||||||
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
|
||||||
|
|
||||||
const (
|
|
||||||
prioHasVersions int = iota
|
|
||||||
prioNoVersions
|
|
||||||
)
|
|
||||||
|
|
||||||
type snapTime struct {
|
|
||||||
ds *zfs.DatasetPath
|
|
||||||
prio int // lower is higher
|
|
||||||
time time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fss) == 0 {
|
|
||||||
return time.Now(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
snaptimes := make([]snapTime, 0, len(fss))
|
|
||||||
hardErrs := 0
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
getLogger(ctx).Debug("examine filesystem state to find sync point")
|
|
||||||
for _, d := range fss {
|
|
||||||
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
|
|
||||||
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
|
|
||||||
if err == findSyncPointFSNoFilesystemVersionsErr {
|
|
||||||
snaptimes = append(snaptimes, snapTime{
|
|
||||||
ds: d,
|
|
||||||
prio: prioNoVersions,
|
|
||||||
time: now,
|
|
||||||
})
|
|
||||||
} else if err != nil {
|
|
||||||
hardErrs++
|
|
||||||
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
|
||||||
} else {
|
|
||||||
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
|
||||||
snaptimes = append(snaptimes, snapTime{
|
|
||||||
ds: d,
|
|
||||||
prio: prioHasVersions,
|
|
||||||
time: syncPoint,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hardErrs == len(fss) {
|
|
||||||
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(snaptimes) == 0 {
|
|
||||||
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort ascending by (prio,time)
|
|
||||||
// => those filesystems with versions win over those without any
|
|
||||||
sort.Slice(snaptimes, func(i, j int) bool {
|
|
||||||
if snaptimes[i].prio == snaptimes[j].prio {
|
|
||||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
|
||||||
}
|
|
||||||
return snaptimes[i].prio < snaptimes[j].prio
|
|
||||||
})
|
|
||||||
|
|
||||||
winnerSyncPoint := snaptimes[0].time
|
|
||||||
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
|
|
||||||
l.Info("determined sync point")
|
|
||||||
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
|
||||||
for _, st := range snaptimes {
|
|
||||||
if st.prio == prioNoVersions {
|
|
||||||
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return snaptimes[0].time, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
|
||||||
|
|
||||||
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
|
|
||||||
Types: zfs.Snapshots,
|
|
||||||
ShortnamePrefix: prefix,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
|
||||||
}
|
|
||||||
if len(fsvs) <= 0 {
|
|
||||||
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort versions by creation
|
|
||||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
|
||||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
|
||||||
})
|
|
||||||
|
|
||||||
latest := fsvs[len(fsvs)-1]
|
|
||||||
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
|
|
||||||
|
|
||||||
since := now.Sub(latest.Creation)
|
|
||||||
if since < 0 {
|
|
||||||
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
|
||||||
}
|
|
||||||
|
|
||||||
return latest.Creation.Add(interval), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type PeriodicReport struct {
|
|
||||||
State State
|
|
||||||
// valid in state SyncUp and Waiting
|
|
||||||
SleepUntil time.Time
|
|
||||||
// valid in state Err
|
|
||||||
Error string
|
|
||||||
// valid in state Snapshotting
|
|
||||||
Progress []*ReportFilesystem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Periodic) Report() Report {
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
|
|
||||||
var progress []*ReportFilesystem = nil
|
|
||||||
if s.plan != nil {
|
|
||||||
progress = s.plan.report()
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &PeriodicReport{
|
|
||||||
State: s.state,
|
|
||||||
SleepUntil: s.sleepUntil,
|
|
||||||
Error: errOrEmptyString(s.err),
|
|
||||||
Progress: progress,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Report{Type: TypePeriodic, Periodic: r}
|
|
||||||
}
|
|
||||||
+490
-22
@@ -3,40 +3,508 @@ package snapper
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Type string
|
//go:generate stringer -type=SnapState
|
||||||
|
type SnapState uint
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypePeriodic Type = "periodic"
|
SnapPending SnapState = 1 << iota
|
||||||
TypeCron Type = "cron"
|
SnapStarted
|
||||||
TypeManual Type = "manual"
|
SnapDone
|
||||||
|
SnapError
|
||||||
)
|
)
|
||||||
|
|
||||||
type Snapper interface {
|
// All fields protected by Snapper.mtx
|
||||||
Run(ctx context.Context, snapshotsTaken chan<- struct{})
|
type snapProgress struct {
|
||||||
Report() Report
|
state SnapState
|
||||||
|
|
||||||
|
// SnapStarted, SnapDone, SnapError
|
||||||
|
name string
|
||||||
|
startAt time.Time
|
||||||
|
hookPlan *hooks.Plan
|
||||||
|
|
||||||
|
// SnapDone
|
||||||
|
doneAt time.Time
|
||||||
|
|
||||||
|
// SnapErr TODO disambiguate state
|
||||||
|
runResults hooks.PlanReport
|
||||||
}
|
}
|
||||||
|
|
||||||
type Report struct {
|
type args struct {
|
||||||
Type Type
|
ctx context.Context
|
||||||
Periodic *PeriodicReport
|
log Logger
|
||||||
Cron *CronReport
|
prefix string
|
||||||
Manual *struct{}
|
interval time.Duration
|
||||||
|
fsf *filters.DatasetMapFilter
|
||||||
|
snapshotsTaken chan<- struct{}
|
||||||
|
hooks *hooks.List
|
||||||
|
dryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (Snapper, error) {
|
type Snapper struct {
|
||||||
switch v := in.Ret.(type) {
|
args args
|
||||||
case *config.SnapshottingPeriodic:
|
|
||||||
return periodicFromConfig(g, fsf, v)
|
mtx sync.Mutex
|
||||||
case *config.SnapshottingCron:
|
state State
|
||||||
return cronFromConfig(fsf, *v)
|
|
||||||
case *config.SnapshottingManual:
|
// set in state Plan, used in Waiting
|
||||||
return &manual{}, nil
|
lastInvocation time.Time
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown snapshotting type %T", v)
|
// valid for state Snapshotting
|
||||||
|
plan map[*zfs.DatasetPath]*snapProgress
|
||||||
|
|
||||||
|
// valid for state SyncUp and Waiting
|
||||||
|
sleepUntil time.Time
|
||||||
|
|
||||||
|
// valid for state Err
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate stringer -type=State
|
||||||
|
type State uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
SyncUp State = 1 << iota
|
||||||
|
SyncUpErrWait
|
||||||
|
Planning
|
||||||
|
Snapshotting
|
||||||
|
Waiting
|
||||||
|
ErrorWait
|
||||||
|
Stopped
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s State) sf() state {
|
||||||
|
m := map[State]state{
|
||||||
|
SyncUp: syncUp,
|
||||||
|
SyncUpErrWait: wait,
|
||||||
|
Planning: plan,
|
||||||
|
Snapshotting: snapshot,
|
||||||
|
Waiting: wait,
|
||||||
|
ErrorWait: wait,
|
||||||
|
Stopped: nil,
|
||||||
|
}
|
||||||
|
return m[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
type updater func(u func(*Snapper)) State
|
||||||
|
type state func(a args, u updater) state
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeyLog contextKey = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger = logger.Logger
|
||||||
|
|
||||||
|
func WithLogger(ctx context.Context, log Logger) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyLog, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogger(ctx context.Context) Logger {
|
||||||
|
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
return logger.NewNullLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
||||||
|
if in.Prefix == "" {
|
||||||
|
return nil, errors.New("prefix must not be empty")
|
||||||
|
}
|
||||||
|
if in.Interval <= 0 {
|
||||||
|
return nil, errors.New("interval must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "hook config error")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := args{
|
||||||
|
prefix: in.Prefix,
|
||||||
|
interval: in.Interval,
|
||||||
|
fsf: fsf,
|
||||||
|
hooks: hookList,
|
||||||
|
// ctx and log is set in Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Snapper{state: SyncUp, args: args}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||||
|
|
||||||
|
getLogger(ctx).Debug("start")
|
||||||
|
defer getLogger(ctx).Debug("stop")
|
||||||
|
|
||||||
|
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()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
if u != nil {
|
||||||
|
u(s)
|
||||||
|
}
|
||||||
|
return s.state
|
||||||
|
}
|
||||||
|
|
||||||
|
var st state = syncUp
|
||||||
|
|
||||||
|
for st != nil {
|
||||||
|
pre := u(nil)
|
||||||
|
st = st(s.args, u)
|
||||||
|
post := u(nil)
|
||||||
|
getLogger(ctx).
|
||||||
|
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
||||||
|
Debug("state transition")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func onErr(err error, u updater) state {
|
||||||
|
return u(func(s *Snapper) {
|
||||||
|
s.err = err
|
||||||
|
preState := s.state
|
||||||
|
switch s.state {
|
||||||
|
case SyncUp:
|
||||||
|
s.state = SyncUpErrWait
|
||||||
|
case Planning:
|
||||||
|
fallthrough
|
||||||
|
case Snapshotting:
|
||||||
|
s.state = ErrorWait
|
||||||
|
}
|
||||||
|
s.args.log.WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
||||||
|
}).sf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func onMainCtxDone(ctx context.Context, u updater) state {
|
||||||
|
return u(func(s *Snapper) {
|
||||||
|
s.err = ctx.Err()
|
||||||
|
s.state = Stopped
|
||||||
|
}).sf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncUp(a args, u updater) state {
|
||||||
|
u(func(snapper *Snapper) {
|
||||||
|
snapper.lastInvocation = time.Now()
|
||||||
|
})
|
||||||
|
fss, err := listFSes(a.ctx, a.fsf)
|
||||||
|
if err != nil {
|
||||||
|
return onErr(err, u)
|
||||||
|
}
|
||||||
|
syncPoint, err := findSyncPoint(a.log, fss, a.prefix, a.interval)
|
||||||
|
if err != nil {
|
||||||
|
return onErr(err, u)
|
||||||
|
}
|
||||||
|
u(func(s *Snapper) {
|
||||||
|
s.sleepUntil = syncPoint
|
||||||
|
})
|
||||||
|
t := time.NewTimer(time.Until(syncPoint))
|
||||||
|
defer t.Stop()
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return u(func(s *Snapper) {
|
||||||
|
s.state = Planning
|
||||||
|
}).sf()
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return onMainCtxDone(a.ctx, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func plan(a args, u updater) state {
|
||||||
|
u(func(snapper *Snapper) {
|
||||||
|
snapper.lastInvocation = time.Now()
|
||||||
|
})
|
||||||
|
fss, err := listFSes(a.ctx, a.fsf)
|
||||||
|
if err != nil {
|
||||||
|
return onErr(err, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
||||||
|
for _, fs := range fss {
|
||||||
|
plan[fs] = &snapProgress{state: SnapPending}
|
||||||
|
}
|
||||||
|
return u(func(s *Snapper) {
|
||||||
|
s.state = Snapshotting
|
||||||
|
s.plan = plan
|
||||||
|
s.err = nil
|
||||||
|
}).sf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(a args, u updater) state {
|
||||||
|
|
||||||
|
var plan map[*zfs.DatasetPath]*snapProgress
|
||||||
|
u(func(snapper *Snapper) {
|
||||||
|
plan = snapper.plan
|
||||||
|
})
|
||||||
|
|
||||||
|
hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
|
||||||
|
for _, h := range *a.hooks {
|
||||||
|
hookMatchCount[h] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
anyFsHadErr := false
|
||||||
|
// TODO channel programs -> allow a little jitter?
|
||||||
|
for fs, progress := range plan {
|
||||||
|
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||||
|
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
||||||
|
|
||||||
|
l := a.log.
|
||||||
|
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 := 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case a.snapshotsTaken <- struct{}{}:
|
||||||
|
default:
|
||||||
|
if a.snapshotsTaken != nil {
|
||||||
|
a.log.Warn("callback channel is full, discarding snapshot update event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for h, mc := range hookMatchCount {
|
||||||
|
if mc == 0 {
|
||||||
|
hookIdx := -1
|
||||||
|
for idx, ah := range *a.hooks {
|
||||||
|
if ah == h {
|
||||||
|
hookIdx = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 anyFsHadErr {
|
||||||
|
snapper.state = ErrorWait
|
||||||
|
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
||||||
|
} else {
|
||||||
|
snapper.state = Waiting
|
||||||
|
snapper.err = nil
|
||||||
|
}
|
||||||
|
}).sf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func wait(a args, u updater) state {
|
||||||
|
var sleepUntil time.Time
|
||||||
|
u(func(snapper *Snapper) {
|
||||||
|
lastTick := snapper.lastInvocation
|
||||||
|
snapper.sleepUntil = lastTick.Add(a.interval)
|
||||||
|
sleepUntil = snapper.sleepUntil
|
||||||
|
log := a.log.WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
||||||
|
logFunc := log.Debug
|
||||||
|
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
||||||
|
logFunc = log.Error
|
||||||
|
}
|
||||||
|
logFunc("enter wait-state after error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t := time.NewTimer(time.Until(sleepUntil))
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
return u(func(snapper *Snapper) {
|
||||||
|
snapper.state = Planning
|
||||||
|
}).sf()
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return onMainCtxDone(a.ctx, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) {
|
||||||
|
return zfs.ZFSListMapping(ctx, mf)
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
||||||
|
|
||||||
|
// see docs/snapshotting.rst
|
||||||
|
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
||||||
|
|
||||||
|
const (
|
||||||
|
prioHasVersions int = iota
|
||||||
|
prioNoVersions
|
||||||
|
)
|
||||||
|
|
||||||
|
type snapTime struct {
|
||||||
|
ds *zfs.DatasetPath
|
||||||
|
prio int // lower is higher
|
||||||
|
time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fss) == 0 {
|
||||||
|
return time.Now(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
snaptimes := make([]snapTime, 0, len(fss))
|
||||||
|
hardErrs := 0
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
log.Debug("examine filesystem state to find sync point")
|
||||||
|
for _, d := range fss {
|
||||||
|
l := log.WithField("fs", d.ToString())
|
||||||
|
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(l, now, interval, prefix, d)
|
||||||
|
if err == findSyncPointFSNoFilesystemVersionsErr {
|
||||||
|
snaptimes = append(snaptimes, snapTime{
|
||||||
|
ds: d,
|
||||||
|
prio: prioNoVersions,
|
||||||
|
time: now,
|
||||||
|
})
|
||||||
|
} else if err != nil {
|
||||||
|
hardErrs++
|
||||||
|
l.WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
||||||
|
} else {
|
||||||
|
l.WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
||||||
|
snaptimes = append(snaptimes, snapTime{
|
||||||
|
ds: d,
|
||||||
|
prio: prioHasVersions,
|
||||||
|
time: syncPoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hardErrs == len(fss) {
|
||||||
|
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(snaptimes) == 0 {
|
||||||
|
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort ascending by (prio,time)
|
||||||
|
// => those filesystems with versions win over those without any
|
||||||
|
sort.Slice(snaptimes, func(i, j int) bool {
|
||||||
|
if snaptimes[i].prio == snaptimes[j].prio {
|
||||||
|
return snaptimes[i].time.Before(snaptimes[j].time)
|
||||||
|
}
|
||||||
|
return snaptimes[i].prio < snaptimes[j].prio
|
||||||
|
})
|
||||||
|
|
||||||
|
winnerSyncPoint := snaptimes[0].time
|
||||||
|
l := log.WithField("syncPoint", winnerSyncPoint.String())
|
||||||
|
l.Info("determined sync point")
|
||||||
|
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
||||||
|
for _, st := range snaptimes {
|
||||||
|
if st.prio == prioNoVersions {
|
||||||
|
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snaptimes[0].time, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
||||||
|
|
||||||
|
func findSyncPointFSNextOptimalSnapshotTime(l Logger, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||||
|
|
||||||
|
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
||||||
|
}
|
||||||
|
if len(fsvs) <= 0 {
|
||||||
|
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort versions by creation
|
||||||
|
sort.SliceStable(fsvs, func(i, j int) bool {
|
||||||
|
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
||||||
|
})
|
||||||
|
|
||||||
|
latest := fsvs[len(fsvs)-1]
|
||||||
|
l.WithField("creation", latest.Creation).Debug("found latest snapshot")
|
||||||
|
|
||||||
|
since := now.Sub(latest.Creation)
|
||||||
|
if since < 0 {
|
||||||
|
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
return latest.Creation.Add(interval), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIXME: properly abstract snapshotting:
|
||||||
|
// - split up things that trigger snapshotting from the mechanism
|
||||||
|
// - timer-based trigger (periodic)
|
||||||
|
// - call from control socket (manual)
|
||||||
|
// - mixed modes?
|
||||||
|
// - support a `zrepl snapshot JOBNAME` subcommand for config.SnapshottingManual
|
||||||
|
type PeriodicOrManual struct {
|
||||||
|
s *Snapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
|
if s.s != nil {
|
||||||
|
s.s.Run(ctx, wakeUpCommon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns nil if manual
|
||||||
|
func (s *PeriodicOrManual) Report() *Report {
|
||||||
|
if s.s != nil {
|
||||||
|
return s.s.Report()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||||
|
switch v := in.Ret.(type) {
|
||||||
|
case *config.SnapshottingPeriodic:
|
||||||
|
snapper, err := PeriodicFromConfig(g, fsf, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &PeriodicOrManual{snapper}, nil
|
||||||
|
case *config.SnapshottingManual:
|
||||||
|
return &PeriodicOrManual{}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown snapshotting type %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package snapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Report struct {
|
||||||
|
State State
|
||||||
|
// valid in state SyncUp and Waiting
|
||||||
|
SleepUntil time.Time
|
||||||
|
// valid in state Err
|
||||||
|
Error string
|
||||||
|
// valid in state Snapshotting
|
||||||
|
Progress []*ReportFilesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportFilesystem struct {
|
||||||
|
Path string
|
||||||
|
State SnapState
|
||||||
|
|
||||||
|
// Valid in SnapStarted and later
|
||||||
|
SnapName string
|
||||||
|
StartAt time.Time
|
||||||
|
Hooks string
|
||||||
|
HooksHadError bool
|
||||||
|
|
||||||
|
// Valid in SnapDone | SnapError
|
||||||
|
DoneAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func errOrEmptyString(e error) string {
|
||||||
|
if e != nil {
|
||||||
|
return e.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snapper) Report() *Report {
|
||||||
|
s.mtx.Lock()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
|
pReps := make([]*ReportFilesystem, 0, len(s.plan))
|
||||||
|
for fs, p := range s.plan {
|
||||||
|
var hooksStr string
|
||||||
|
var hooksHadError bool
|
||||||
|
if p.hookPlan != nil {
|
||||||
|
hr := p.hookPlan.Report()
|
||||||
|
// FIXME: technically this belongs into client
|
||||||
|
// but we can't serialize hooks.Step ATM
|
||||||
|
rightPad := func(str string, length int, pad string) string {
|
||||||
|
if len(str) > length {
|
||||||
|
return str[:length]
|
||||||
|
}
|
||||||
|
return str + strings.Repeat(pad, length-len(str))
|
||||||
|
}
|
||||||
|
hooksHadError = hr.HadError()
|
||||||
|
rows := make([][]string, len(hr))
|
||||||
|
const numCols = 4
|
||||||
|
lens := make([]int, numCols)
|
||||||
|
for i, e := range hr {
|
||||||
|
rows[i] = make([]string, numCols)
|
||||||
|
rows[i][0] = fmt.Sprintf("%d", i+1)
|
||||||
|
rows[i][1] = e.Status.String()
|
||||||
|
runTime := "..."
|
||||||
|
if e.Status != hooks.StepPending {
|
||||||
|
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
|
||||||
|
}
|
||||||
|
rows[i][2] = runTime
|
||||||
|
rows[i][3] = ""
|
||||||
|
if e.Report != nil {
|
||||||
|
rows[i][3] = e.Report.String()
|
||||||
|
}
|
||||||
|
for j, col := range lens {
|
||||||
|
if len(rows[i][j]) > col {
|
||||||
|
lens[j] = len(rows[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowsFlat := make([]string, len(hr))
|
||||||
|
for i, r := range rows {
|
||||||
|
colsPadded := make([]string, len(r))
|
||||||
|
for j, c := range r[:len(r)-1] {
|
||||||
|
colsPadded[j] = rightPad(c, lens[j], " ")
|
||||||
|
}
|
||||||
|
colsPadded[len(r)-1] = r[len(r)-1]
|
||||||
|
rowsFlat[i] = strings.Join(colsPadded, " ")
|
||||||
|
}
|
||||||
|
hooksStr = strings.Join(rowsFlat, "\n")
|
||||||
|
}
|
||||||
|
pReps = append(pReps, &ReportFilesystem{
|
||||||
|
Path: fs.ToString(),
|
||||||
|
State: p.state,
|
||||||
|
SnapName: p.name,
|
||||||
|
StartAt: p.startAt,
|
||||||
|
DoneAt: p.doneAt,
|
||||||
|
Hooks: hooksStr,
|
||||||
|
HooksHadError: hooksHadError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(pReps, func(i, j int) bool {
|
||||||
|
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
r := &Report{
|
||||||
|
State: s.state,
|
||||||
|
SleepUntil: s.sleepUntil,
|
||||||
|
Error: errOrEmptyString(s.err),
|
||||||
|
Progress: pReps,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger = logger.Logger
|
|
||||||
|
|
||||||
func getLogger(ctx context.Context) Logger {
|
|
||||||
return logging.GetLogger(ctx, logging.SubsysSnapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func errOrEmptyString(e error) string {
|
|
||||||
if e != nil {
|
|
||||||
return e.Error()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
+595
@@ -0,0 +1,595 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"$$hashKey": "object:3351",
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 1,
|
||||||
|
"id": 7,
|
||||||
|
"iteration": 1552746418826,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"content": "# zrepl Prometheus Metrics\n\nzrepl exposes Prometheus metrics and ships with this Grafana dashboard.\nThe exported metrics are suitable for health checks:\n\n* The log should generally be warning & error-free\n * The `Log Messages that require attention` graph visualizes log message counts indicating problems.\n* The number of goroutines should not grow unboundedly over time.\n * During replication, the number of goroutines can be way higher than during idle time.\n * If the goroutine count grows with each replication, there is clearly a goroutine leak. Please open a bug report.\n* The sys memory consumption should not grow unboundedly over time.\n * Note that the Go runtime pre-allocates some of its heap from the OS.\n * zrepl actually uses much less memory than allocated from the OS.\n * Since Go 1.11, Go pre-allocates more aggressively.\n* Monitor that some data is replicated, although that metric does not guarantee that replication was successful.\n\n**In general, note that the exported metrics are not stable unless declared otherwise.**",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 35,
|
||||||
|
"mode": "markdown",
|
||||||
|
"title": "Panel Title",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 9
|
||||||
|
},
|
||||||
|
"id": 15,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": true,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"$$hashKey": "object:3436",
|
||||||
|
"expr": "up{job='$prom_job_name'}",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "zrepl Instances Up",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": "5",
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 9
|
||||||
|
},
|
||||||
|
"id": 22,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "increase(zrepl_daemon_log_entries{job='$prom_job_name',level=~'warn|error'}[$__interval])",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Log Messages that require attention",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 18
|
||||||
|
},
|
||||||
|
"id": 33,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [
|
||||||
|
{
|
||||||
|
"alias": "/replicated bytes in last.*/",
|
||||||
|
"yaxis": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(zrepl_replication_bytes_replicated{job='$prom_job_name'}[$__interval])) by (zrepl_job)",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"interval": "",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "replication data rate zrepl_job={{zrepl_job}}",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "sum(increase(zrepl_replication_bytes_replicated{job='$prom_job_name'}[10m])) by (zrepl_job)",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"interval": "",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "replicated bytes in last 10min zrepl_job={{zrepl_job}}",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Replication Data Rate and Volume(integrates last 10min)",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "Bps",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "bytes",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 18
|
||||||
|
},
|
||||||
|
"id": 23,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(increase(zrepl_daemon_log_entries{job='$prom_job_name',zrepl_job=~\"^[^_].*\"}[$__interval])) by (instance,zrepl_job)",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Log Activity (without internal jobs)",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 27
|
||||||
|
},
|
||||||
|
"id": 17,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"$$hashKey": "object:3535",
|
||||||
|
"expr": "go_memstats_sys_bytes{job='$prom_job_name'}",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Memory Allocated by the Go runtime from the OS (should not grow unboundedly)",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "bytes",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": null,
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 27
|
||||||
|
},
|
||||||
|
"id": 19,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "go_goroutines{job='$prom_job_name'}",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "number of goroutines (should not grow unboundedly)",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "1m",
|
||||||
|
"schemaVersion": 16,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"allValue": null,
|
||||||
|
"current": {
|
||||||
|
"text": "zrepl",
|
||||||
|
"value": "zrepl"
|
||||||
|
},
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Prometheus Job Name",
|
||||||
|
"multi": false,
|
||||||
|
"name": "prom_job_name",
|
||||||
|
"options": [],
|
||||||
|
"query": "label_values(up, job)",
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"sort": 1,
|
||||||
|
"tagValuesQuery": "",
|
||||||
|
"tags": [],
|
||||||
|
"tagsQuery": "",
|
||||||
|
"type": "query",
|
||||||
|
"useTags": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-2d",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "zrepl 0.1",
|
||||||
|
"uid": "xTljn4qmk",
|
||||||
|
"version": 6
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user