Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a0104a44f | |||
| f0146d03d3 | |||
| 742e28d2a3 | |||
| 58f364dc04 | |||
| a5c9450b2d | |||
| b222574ff3 | |||
| 28e1c486f5 | |||
| 82370f3555 | |||
| 4f6c57fbba | |||
| 5a558e3b71 | |||
| b33f670b9d | |||
| dce98d50da | |||
| 10a14a8c50 | |||
| bcb5965617 | |||
| f772b3d39f | |||
| 27db8c0afe | |||
| 0e5c77d2be | |||
| 0280727985 | |||
| b4abebce00 | |||
| d89afe58d4 | |||
| c855546b9f | |||
| 7d6ee4c166 | |||
| 2fbd9d8f8c | |||
| 18e101a04e | |||
| e594421322 | |||
| 0d4bfda2fb | |||
| b83c026cdc | |||
| 46caf31075 | |||
| 2b9d696b49 | |||
| 70afff6e3b | |||
| 01bbda13e5 | |||
| c5a8f6635f | |||
| 6f441c55dc | |||
| 7b34d6cba5 | |||
| 70f9c6482f | |||
| aed6149c8c | |||
| 0834a184b8 | |||
| f61295f76c | |||
| 0920a40751 | |||
| e0b5bd75f8 | |||
| 96e188d7c4 | |||
| 1336c91865 | |||
| 9568e46f05 | |||
| 3187129672 | |||
| 44bd354eae | |||
| 94caf8b8db | |||
| 3ff1966cab | |||
| a3842155c5 | |||
| 02b3b4f80c | |||
| 0882290595 | |||
| b8d9f4ba92 | |||
| 1462c5caa5 | |||
| 58c08c855f | |||
| 9a4763ceee | |||
| f8200a6386 | |||
| e35320f8ee | |||
| 5b52e5e331 | |||
| 6ebd9f1037 | |||
| 99ab16d7be | |||
| e675b35cda | |||
| ddd7acec49 | |||
| e7aa08564b | |||
| 93ccdb8024 | |||
| 501645f918 | |||
| 4fd9d30c98 | |||
| e2b9c16959 | |||
| 4ba85c40cc | |||
| cca95f613b | |||
| 5b50a66c6c | |||
| dd508280f0 | |||
| 2927d0ca15 | |||
| d35e2400b2 | |||
| 47ed599db7 | |||
| f899f4cbe4 | |||
| b3231d2bed | |||
| c24c327151 | |||
| 5e17d7ba80 | |||
| 0261dbfe3d | |||
| 4301f741db | |||
| 7e743c74dc | |||
| ad0b055245 | |||
| 27db3e6f70 | |||
| 4994b7a9ea | |||
| 080f2c0616 | |||
| d469cc04b6 | |||
| e0a25d04ac | |||
| b0f2c79944 | |||
| d2bc40f78d | |||
| 9e54f11960 | |||
| 6eda1f743f | |||
| 6787decef1 | |||
| d56d45a2ab | |||
| fcf16a163a | |||
| dc39c819a3 | |||
| 1048b09487 | |||
| 3806e97404 | |||
| c396f9508a | |||
| b9933f6cb2 | |||
| 0ba4b5eda6 | |||
| 18d2c350de | |||
| f8f9fd11cd | |||
| b422e6f12e | |||
| f8d5082bdd | |||
| ffe677e55a |
+50
-5
@@ -6,8 +6,11 @@ workflows:
|
|||||||
- build-1.11
|
- build-1.11
|
||||||
- build-1.12
|
- build-1.12
|
||||||
- build-1.13
|
- build-1.13
|
||||||
|
- build-1.14
|
||||||
- build-latest
|
- build-latest
|
||||||
|
- test-build-in-docker
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
# build-latest serves as the template
|
# build-latest serves as the template
|
||||||
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
|
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
|
||||||
build-latest: &build-latest
|
build-latest: &build-latest
|
||||||
@@ -52,15 +55,15 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- "/usr/include/google/protobuf"
|
- "/usr/include/google/protobuf"
|
||||||
|
|
||||||
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev
|
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev gawk
|
||||||
- run: ./lazy.sh devsetup
|
- run: ./lazy.sh devsetup
|
||||||
|
|
||||||
- run: make
|
- run: make zrepl-bin
|
||||||
- run: make vet
|
- run: make vet
|
||||||
- run: make test
|
|
||||||
- run: make lint
|
- run: make lint
|
||||||
- run: make release
|
- run: make release
|
||||||
|
- run: make test-go
|
||||||
|
# cannot run test-platform because circle-ci runs in linux containers
|
||||||
|
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: ./artifacts/release
|
path: ./artifacts/release
|
||||||
@@ -89,11 +92,21 @@ jobs:
|
|||||||
REPO="zrepl/zrepl"
|
REPO="zrepl/zrepl"
|
||||||
COMMIT="${CIRCLE_SHA1}"
|
COMMIT="${CIRCLE_SHA1}"
|
||||||
JOB_NAME="${CIRCLE_JOB}"
|
JOB_NAME="${CIRCLE_JOB}"
|
||||||
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT?access_token=$GITHUB_COMMIT_STATUS_TOKEN" \
|
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":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
|
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
|
||||||
|
|
||||||
|
- run:
|
||||||
|
shell: /bin/bash -euo pipefail
|
||||||
|
command: |
|
||||||
|
# Trigger Debian Package Build
|
||||||
|
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
|
||||||
|
-H 'Accept: application/vnd.github.v3+json' \
|
||||||
|
-H "Authorization: token $ZREPL_DEBIAN_BINARYPACKAGIN_TRIGGER_BUILD_GITHUB_TOKEN" \
|
||||||
|
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$CIRCLE_SHA1"'", "go_version": "'"${CIRCLE_JOB##build-}"'" }}'
|
||||||
|
|
||||||
build-1.11:
|
build-1.11:
|
||||||
<<: *build-latest
|
<<: *build-latest
|
||||||
docker:
|
docker:
|
||||||
@@ -108,3 +121,35 @@ jobs:
|
|||||||
<<: *build-latest
|
<<: *build-latest
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.13
|
- image: circleci/golang:1.13
|
||||||
|
|
||||||
|
build-1.14:
|
||||||
|
<<: *build-latest
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.14
|
||||||
|
|
||||||
|
# this job tries to mimic the build-in-docker instructions
|
||||||
|
# given in docs/installation.rst
|
||||||
|
#
|
||||||
|
# However, CircleCI doesn't support volume mounts, so we have to copy
|
||||||
|
# the source into the build-container by modifying the Dockerfile
|
||||||
|
test-build-in-docker:
|
||||||
|
description: Check that build-in-docker works
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:latest
|
||||||
|
environment:
|
||||||
|
working_directory: /go/src/github.com/zrepl/zrepl
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- setup_remote_docker
|
||||||
|
- run:
|
||||||
|
name: (hacky) circleci doesn't allow volume mounts, so copy src to container
|
||||||
|
command: echo "ADD . /src" >> build.Dockerfile
|
||||||
|
- run:
|
||||||
|
name: (hacky) commit modified Dockerfile to avoid failing git clean check in Makefile
|
||||||
|
command: git -c user.name='circleci' -c user.email='circleci@localhost' commit -m 'CIRCLECI modified Dockerfile with zrepl src' --author 'autoauthor <circleci@localhost>' -- build.Dockerfile
|
||||||
|
- run:
|
||||||
|
name: build the build image (build deps)
|
||||||
|
command: docker build -t zrepl_build -f build.Dockerfile .
|
||||||
|
- run:
|
||||||
|
name: try compiling
|
||||||
|
command: docker run -it zrepl_build make release
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
|
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
|
||||||
.DEFAULT_GOAL := build
|
.PHONY: release bins-all release-noarch
|
||||||
|
.DEFAULT_GOAL := zrepl-bin
|
||||||
|
|
||||||
ARTIFACTDIR := artifacts
|
ARTIFACTDIR := artifacts
|
||||||
|
|
||||||
@@ -12,97 +13,73 @@ ifndef _ZREPL_VERSION
|
|||||||
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
GO := go
|
GO := go
|
||||||
|
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
||||||
|
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
|
||||||
|
GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"')
|
||||||
|
GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"')
|
||||||
|
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
||||||
|
GO_ENV_VARS := GO111MODULE=on
|
||||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||||
GO_MOD_READONLY := -mod=readonly
|
GO_MOD_READONLY := -mod=readonly
|
||||||
GO_BUILDFLAGS := $(GO_MOD_READONLY)
|
GO_EXTRA_BUILDFLAGS :=
|
||||||
GO_BUILD := GO111MODULE=on $(GO) build $(GO_BUILDFLAGS) -v -ldflags $(GO_LDFLAGS)
|
GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
||||||
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
|
GOLANGCI_LINT := golangci-lint
|
||||||
|
GOCOVMERGE := gocovmerge
|
||||||
|
|
||||||
# keep in sync with vet target
|
ifneq ($(GOARM),)
|
||||||
RELEASE_BINS := $(ARTIFACTDIR)/zrepl-freebsd-amd64
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-amd64
|
else
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-arm64
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-darwin-amd64
|
endif
|
||||||
|
|
||||||
RELEASE_NOARCH := $(ARTIFACTDIR)/zrepl-noarch.tar
|
.PHONY: printvars
|
||||||
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <($(GO) env) && echo "zrepl-$${GOOS}-$${GOARCH}"' )
|
printvars:
|
||||||
|
@echo GOOS=$(GOOS)
|
||||||
|
@echo GOARCH=$(GOARCH)
|
||||||
|
@echo GOARM=$(GOARM)
|
||||||
|
|
||||||
generate: #not part of the build, must do that manually
|
|
||||||
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
|
||||||
$(GO) generate $(GO_BUILDFLAGS) -x ./...
|
|
||||||
|
|
||||||
format:
|
##################### PRODUCING A RELEASE #############
|
||||||
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')
|
.PHONY: release wrapup-and-checksum check-git-clean sign clean
|
||||||
|
|
||||||
lint:
|
release: clean
|
||||||
golangci-lint run ./...
|
# no cross-platform support for target test
|
||||||
|
$(MAKE) test-go
|
||||||
|
$(MAKE) bins-all
|
||||||
|
$(MAKE) noarch
|
||||||
|
$(MAKE) wrapup-and-checksum
|
||||||
|
$(MAKE) check-git-clean
|
||||||
|
ifeq (SIGN, 1)
|
||||||
|
$(make) sign
|
||||||
|
endif
|
||||||
|
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
||||||
|
|
||||||
build:
|
# expects `release` target to have run before
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl"
|
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||||
|
wrapup-and-checksum:
|
||||||
test:
|
rm -f $(NOARCH_TARBALL)
|
||||||
$(GO) test $(GO_BUILDFLAGS) ./...
|
|
||||||
# TODO compile the tests for each supported platform
|
|
||||||
# but `go test -c ./...` is not supported
|
|
||||||
|
|
||||||
vet:
|
|
||||||
$(GO) vet $(GO_BUILDFLAGS) ./...
|
|
||||||
# for each supported platform to cover conditional compilation
|
|
||||||
# (keep in sync with RELEASE_BINS)
|
|
||||||
GOOS=freebsd GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
|
||||||
GOOS=linux GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
|
||||||
GOOS=linux GOARCH=arm64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
|
||||||
GOOS=darwin GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
|
||||||
|
|
||||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
|
||||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
|
||||||
$(ARTIFACTDIR)/zrepl_platformtest:
|
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl_platformtest" ./platformtest/harness
|
|
||||||
platformtest: $(ARTIFACTDIR)/zrepl_platformtest
|
|
||||||
"$(ARTIFACTDIR)/zrepl_platformtest" -poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" -imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR):
|
|
||||||
mkdir -p "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
|
||||||
mkdir -p "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/bash_completion: $(RELEASE_BINS)
|
|
||||||
artifacts/$(THIS_PLATFORM_RELEASE_BIN) bashcomp "$@"
|
|
||||||
|
|
||||||
.PHONY: $(ARTIFACTDIR)/go_version.txt
|
|
||||||
$(ARTIFACTDIR)/go_version.txt:
|
|
||||||
$(GO) version > $@
|
|
||||||
|
|
||||||
docs: $(ARTIFACTDIR)/docs
|
|
||||||
make -C docs \
|
|
||||||
html \
|
|
||||||
BUILDDIR=../artifacts/docs \
|
|
||||||
|
|
||||||
docs-clean:
|
|
||||||
make -C docs \
|
|
||||||
clean \
|
|
||||||
BUILDDIR=../artifacts/docs
|
|
||||||
|
|
||||||
.PHONY: $(RELEASE_BINS)
|
|
||||||
# TODO: two wildcards possible
|
|
||||||
$(RELEASE_BINS): $(ARTIFACTDIR)/zrepl-%: generate $(ARTIFACTDIR) vet test lint
|
|
||||||
STEM=$*; GOOS="$${STEM%%-*}"; GOARCH="$${STEM##*-}"; export GOOS GOARCH; \
|
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$$GOOS-$$GOARCH"
|
|
||||||
|
|
||||||
$(RELEASE_NOARCH): docs $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_version.txt
|
|
||||||
tar --mtime='1970-01-01' --sort=name \
|
tar --mtime='1970-01-01' --sort=name \
|
||||||
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
||||||
-acf $@ \
|
--transform 's#dist#zrepl-$(_ZREPL_VERSION)-noarch/dist#' \
|
||||||
|
--transform 's#config/samples#zrepl-$(_ZREPL_VERSION)-noarch/config#' \
|
||||||
|
-acf $(NOARCH_TARBALL) \
|
||||||
$(ARTIFACTDIR)/docs/html \
|
$(ARTIFACTDIR)/docs/html \
|
||||||
$(ARTIFACTDIR)/bash_completion \
|
$(ARTIFACTDIR)/bash_completion \
|
||||||
$(ARTIFACTDIR)/go_version.txt
|
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
||||||
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
release: $(RELEASE_BINS) $(RELEASE_NOARCH)
|
dist \
|
||||||
|
config/samples
|
||||||
rm -rf "$(ARTIFACTDIR)/release"
|
rm -rf "$(ARTIFACTDIR)/release"
|
||||||
mkdir -p "$(ARTIFACTDIR)/release"
|
mkdir -p "$(ARTIFACTDIR)/release"
|
||||||
cp $^ "$(ARTIFACTDIR)/release"
|
cp -l $(ARTIFACTDIR)/zrepl-* \
|
||||||
|
$(ARTIFACTDIR)/platformtest-* \
|
||||||
|
"$(ARTIFACTDIR)/release"
|
||||||
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
|
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
|
||||||
|
|
||||||
|
check-git-clean:
|
||||||
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
|
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
|
||||||
@if git describe --always --dirty 2>/dev/null | grep dirty >/dev/null; then \
|
@if git describe --always --dirty 2>/dev/null | grep dirty >/dev/null; then \
|
||||||
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
||||||
@@ -115,5 +92,138 @@ release: $(RELEASE_BINS) $(RELEASE_NOARCH)
|
|||||||
fi; \
|
fi; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
|
sign:
|
||||||
|
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
||||||
|
--armor \
|
||||||
|
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
|
||||||
|
|
||||||
clean: docs-clean
|
clean: docs-clean
|
||||||
rm -rf "$(ARTIFACTDIR)"
|
rm -rf "$(ARTIFACTDIR)"
|
||||||
|
|
||||||
|
##################### BINARIES #####################
|
||||||
|
.PHONY: bins-all lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
|
||||||
|
|
||||||
|
BINS_ALL_TARGETS := zrepl-bin test-platform-bin vet lint
|
||||||
|
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
|
||||||
|
bins-all:
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=386
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=darwin GOARCH=amd64
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=solaris GOARCH=amd64
|
||||||
|
ifeq ($(GO_SUPPORTS_ILLUMOS), illumos)
|
||||||
|
$(MAKE) $(BINS_ALL_TARGETS) GOOS=illumos GOARCH=amd64
|
||||||
|
else ifeq ($(GO_SUPPORTS_ILLUMOS), noillumos)
|
||||||
|
@echo "SKIPPING ILLUMOS BUILD BECAUSE GO VERSION DOESN'T SUPPORT IT"
|
||||||
|
else
|
||||||
|
@echo "CANNOT DETERMINE WHETHER GO VERSION SUPPORTS GOOS=illumos"; exit 1
|
||||||
|
endif
|
||||||
|
|
||||||
|
lint:
|
||||||
|
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
$(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:
|
||||||
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||||
|
|
||||||
|
generate-platform-test-list:
|
||||||
|
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
|
||||||
|
|
||||||
|
test-platform-bin:
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
|
-c -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||||
|
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
|
||||||
|
./platformtest/harness
|
||||||
|
|
||||||
|
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||||
|
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||||
|
ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
||||||
|
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
||||||
|
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
||||||
|
ZREPL_PLATFORMTEST_ARGS :=
|
||||||
|
test-platform: $(ARTIFACTDIR) # do not track dependency on test-platform-bin to allow build of platformtest outside of test VM
|
||||||
|
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||||
|
rm -f "$(ARTIFACTDIR)/platformtest.cover"
|
||||||
|
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||||
|
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
|
||||||
|
-test.coverprofile "$(ARTIFACTDIR)/platformtest.cover" \
|
||||||
|
-test.v \
|
||||||
|
__DEVEL--i-heard-you-like-tests \
|
||||||
|
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||||
|
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||||
|
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||||
|
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
|
||||||
|
$(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"
|
||||||
|
|
||||||
|
test-full:
|
||||||
|
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
||||||
|
$(MAKE) test-go COVER=1
|
||||||
|
$(MAKE) test-platform
|
||||||
|
$(MAKE) cover-html
|
||||||
|
|
||||||
|
##################### DEV TARGETS #####################
|
||||||
|
# not part of the build, must do that manually
|
||||||
|
.PHONY: generate format
|
||||||
|
|
||||||
|
generate: generate-platform-test-list
|
||||||
|
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
||||||
|
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
|
||||||
|
|
||||||
|
format:
|
||||||
|
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
|
||||||
|
|
||||||
|
##################### NOARCH #####################
|
||||||
|
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||||
|
|
||||||
|
|
||||||
|
$(ARTIFACTDIR):
|
||||||
|
mkdir -p "$@"
|
||||||
|
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||||
|
mkdir -p "$@"
|
||||||
|
|
||||||
|
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||||
|
# pass
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/bash_completion:
|
||||||
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@"
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/_zrepl.zsh_completion:
|
||||||
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/go_env.txt:
|
||||||
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
|
|
||||||
|
docs: $(ARTIFACTDIR)/docs
|
||||||
|
make -C docs \
|
||||||
|
html \
|
||||||
|
BUILDDIR=../artifacts/docs \
|
||||||
|
|
||||||
|
docs-clean:
|
||||||
|
make -C docs \
|
||||||
|
clean \
|
||||||
|
BUILDDIR=../artifacts/docs
|
||||||
|
|||||||
@@ -1,8 +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://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
[](https://www.patreon.com/zrepl)
|
||||||
[](https://liberapay.com/zrepl/donate)
|
[](https://liberapay.com/zrepl/donate)
|
||||||
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||||
[](https://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)
|
||||||
|
|
||||||
# zrepl
|
# zrepl
|
||||||
@@ -23,6 +24,7 @@ zrepl is a one-stop ZFS backup & replication solution.
|
|||||||
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.
|
||||||
@@ -37,7 +39,7 @@ Check out the *Coding Workflow* section below for details.
|
|||||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
||||||
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
||||||
* Use `make platformtest` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
* Use `sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||||
|
|
||||||
## Developer Documentation
|
## Developer Documentation
|
||||||
@@ -45,12 +47,17 @@ Check out the *Coding Workflow* section below for details.
|
|||||||
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
||||||
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
||||||
|
|
||||||
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
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`.
|
||||||
|
|
||||||
### Overall Architecture
|
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`.
|
||||||
|
|
||||||
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
|
For a full **code coverage** profile, run `make test-go COVER=1 && sudo make test-platform && make cover-merge`.
|
||||||
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
|
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.
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
@@ -62,14 +69,15 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
|||||||
│ └── samples
|
│ └── samples
|
||||||
├── daemon # the implementation of `zrepl daemon` subcommand
|
├── daemon # the implementation of `zrepl daemon` subcommand
|
||||||
│ ├── filters
|
│ ├── filters
|
||||||
|
│ ├── hooks # snapshot hooks
|
||||||
│ ├── job # job implementations
|
│ ├── job # job implementations
|
||||||
│ ├── logging # logging outlets + formatters
|
│ ├── logging # logging outlets + formatters
|
||||||
│ ├── nethelpers
|
│ ├── nethelpers
|
||||||
│ ├── prometheus
|
│ ├── prometheus
|
||||||
│ ├── pruner # pruner implementation
|
│ ├── pruner # pruner implementation
|
||||||
│ ├── snapper # snapshotter implementation
|
│ ├── snapper # snapshotter implementation
|
||||||
├── docs # sphinx-based documentation
|
|
||||||
├── dist # supplemental material for users & package maintainers
|
├── dist # supplemental material for users & package maintainers
|
||||||
|
├── docs # sphinx-based documentation
|
||||||
│ ├── **/*.rst # documentation in reStructuredText
|
│ ├── **/*.rst # documentation in reStructuredText
|
||||||
│ ├── sphinxconf
|
│ ├── sphinxconf
|
||||||
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
||||||
@@ -78,6 +86,7 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
|||||||
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
||||||
├── endpoint # implementation of replication endpoints (=> package replication)
|
├── endpoint # implementation of replication endpoints (=> package replication)
|
||||||
├── logger # our own logger package
|
├── logger # our own logger package
|
||||||
|
├── platformtest # test suite for our zfs abstractions (error classification, etc)
|
||||||
├── pruning # pruning rules (the logic, not the actual execution)
|
├── pruning # pruning rules (the logic, not the actual execution)
|
||||||
│ └── retentiongrid
|
│ └── retentiongrid
|
||||||
├── replication
|
├── replication
|
||||||
@@ -92,14 +101,13 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
|||||||
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
|
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
|
||||||
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
||||||
├── tlsconf # abstraction for Go TLS server + client config
|
├── tlsconf # abstraction for Go TLS server + client config
|
||||||
├── transport # transports implementation
|
├── transport # transport implementations
|
||||||
│ ├── fromconfig
|
│ ├── fromconfig
|
||||||
│ ├── local
|
│ ├── local
|
||||||
│ ├── ssh
|
│ ├── ssh
|
||||||
│ ├── tcp
|
│ ├── tcp
|
||||||
│ └── tls
|
│ └── tls
|
||||||
├── util
|
├── util
|
||||||
├── vendor # managed by dep
|
|
||||||
├── version # abstraction for versions (filled during build by Makefile)
|
├── version # abstraction for versions (filled during build by Makefile)
|
||||||
└── zfs # zfs(8) wrappers
|
└── zfs # zfs(8) wrappers
|
||||||
```
|
```
|
||||||
@@ -134,3 +142,15 @@ There will not be a big refactoring (an attempt was made, but it's destroying to
|
|||||||
|
|
||||||
However, new contributions & patches should fix naming without further notice in the commit message.
|
However, new contributions & patches should fix naming without further notice in the commit message.
|
||||||
|
|
||||||
|
### RPC debugging
|
||||||
|
|
||||||
|
Optionally, there are various RPC-related environment variables, that if set to something != `""` will produce additional debug output on stderr:
|
||||||
|
|
||||||
|
https://github.com/zrepl/zrepl/blob/master/rpc/rpc_debug.go#L11
|
||||||
|
|
||||||
|
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/dataconn_debug.go#L11
|
||||||
|
|
||||||
|
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/stream/stream_debug.go#L11
|
||||||
|
|
||||||
|
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/heartbeatconn/heartbeatconn_debug.go#L11
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -2,7 +2,8 @@ FROM golang:latest
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
unzip
|
unzip \
|
||||||
|
gawk
|
||||||
|
|
||||||
ADD build.installprotoc.bash ./
|
ADD build.installprotoc.bash ./
|
||||||
RUN bash build.installprotoc.bash
|
RUN bash build.installprotoc.bash
|
||||||
@@ -22,7 +23,7 @@ RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
|||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
||||||
ADD go.mod go.sum ./
|
ADD build/ /tmp/build
|
||||||
RUN /tmp/lazy.sh godep
|
RUN /tmp/lazy.sh godep
|
||||||
|
|
||||||
RUN chmod -R 0777 /go
|
RUN chmod -R 0777 /go
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
//+build windows
|
|
||||||
|
|
||||||
// This package cannot actually be built, and since the Travis
|
|
||||||
// tests run go test ./..., we have to avoid a build attempt.
|
|
||||||
// Windows is not supported atm, so this works ¯\_(ツ)_/¯
|
|
||||||
|
|
||||||
// This package is a pseudo-user of build-time dependencies for zrepl,
|
|
||||||
// mostly for various code-generation tools.
|
|
||||||
//
|
|
||||||
// The imports are necessary to satisfy go dep
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
_ "github.com/alvaroloes/enumer"
|
|
||||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("just a placeholder")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
module github.com/zrepl/zrepl/build
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
|
||||||
|
github.com/alvaroloes/enumer v1.1.1
|
||||||
|
github.com/fatih/color v1.7.0 // indirect
|
||||||
|
github.com/go-critic/go-critic v0.3.4 // indirect
|
||||||
|
github.com/gogo/protobuf v1.2.1 // indirect
|
||||||
|
github.com/golang/mock v1.2.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.2.0
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
|
||||||
|
github.com/golangci/golangci-lint v1.17.1
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
|
||||||
|
github.com/golangci/misspell v0.3.4 // indirect
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||||
|
github.com/google/go-cmp v0.3.0 // indirect
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/pkg/errors v0.8.1 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.4.2 // indirect
|
||||||
|
github.com/spf13/afero v1.2.2 // indirect
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
|
github.com/spf13/viper v1.3.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad // indirect
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect
|
||||||
|
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
// invalid dates in transitive dependencies (first validated in Go 1.13, didn't fail in earlier Go versions)
|
||||||
|
replace (
|
||||||
|
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 => github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540
|
||||||
|
github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead => github.com/go-critic/go-critic v0.3.5-0.20190210220443-ee9bf5809ead
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6
|
||||||
|
github.com/golangci/go-tools v0.0.0-20180109140146-35a9f45a5db0 => github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0
|
||||||
|
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547
|
||||||
|
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217
|
||||||
|
golang.org/x/tools v0.0.0-20190125232054-379209517ffe => golang.org/x/tools v0.0.0-20190205201329-379209517ffe
|
||||||
|
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34
|
||||||
|
)
|
||||||
+282
@@ -0,0 +1,282 @@
|
|||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 h1:pFks+oaqVWDFq0KsLQZFB2pQGB5Us0HfMSBENtieXOs=
|
||||||
|
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||||
|
github.com/alvaroloes/enumer v1.1.1 h1:v+1scNqEhSFAPb9tLwblQegeTXJhnw8hf9O0amTbOvQ=
|
||||||
|
github.com/alvaroloes/enumer v1.1.1/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc=
|
||||||
|
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
||||||
|
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
|
||||||
|
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||||
|
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||||
|
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
|
||||||
|
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||||
|
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||||
|
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
|
||||||
|
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
|
||||||
|
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
||||||
|
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||||
|
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
|
||||||
|
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||||
|
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
|
||||||
|
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
|
||||||
|
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
|
||||||
|
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
||||||
|
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
|
||||||
|
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
|
||||||
|
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
|
||||||
|
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8 h1:vVouagbdmqTVlCIAxpyYsNNTbkKZ3V66VpKOLU/s6W4=
|
||||||
|
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
|
||||||
|
github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg=
|
||||||
|
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
|
||||||
|
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||||
|
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
|
||||||
|
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||||
|
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||||
|
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
|
||||||
|
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||||
|
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||||
|
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||||
|
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w=
|
||||||
|
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||||
|
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
|
||||||
|
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 h1:MRhC9XbUjE6XDOInSJ8pwHuPagqsyO89QDU9IdVhe3o=
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||||
|
github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||||
|
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
|
||||||
|
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0=
|
||||||
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
|
||||||
|
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||||
|
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
|
||||||
|
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc=
|
||||||
|
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
|
||||||
|
github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
|
||||||
|
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI=
|
||||||
|
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4=
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||||
|
github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||||
|
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
|
||||||
|
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||||
|
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||||
|
github.com/golangci/misspell v0.3.4 h1:kAD5J0611Zo2VirUn9KFP15RjEqkmfEfnFxyIVxZCYg=
|
||||||
|
github.com/golangci/misspell v0.3.4/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||||
|
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
|
||||||
|
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM=
|
||||||
|
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||||
|
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
|
||||||
|
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
||||||
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
|
||||||
|
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||||
|
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
|
||||||
|
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
|
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||||
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
|
||||||
|
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||||
|
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
|
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
|
||||||
|
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
|
||||||
|
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
||||||
|
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||||
|
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
|
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
|
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||||
|
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
|
||||||
|
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||||
|
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
|
||||||
|
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
|
||||||
|
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||||
|
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||||
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
|
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||||
|
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
|
||||||
|
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||||
|
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM=
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||||
|
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||||
|
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0=
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
|
||||||
|
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8 h1:b0PLhFjEMqrIqsD4rS5sp0YxBYgdqKzX0KkkGGKLV8I=
|
||||||
|
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b h1:iEAPfYPbYbxG/2lNN4cMOHkmgKNsCuUwkxlDCK46UlU=
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
|
||||||
|
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
||||||
|
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
|
||||||
|
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g=
|
||||||
|
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
||||||
|
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||||
|
sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o=
|
||||||
|
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
|
||||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
|
||||||
_ "github.com/alvaroloes/enumer"
|
_ "github.com/alvaroloes/enumer"
|
||||||
_ "golang.org/x/tools/cmd/goimports"
|
_ "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/stringer"
|
||||||
)
|
)
|
||||||
+53
-21
@@ -1,11 +1,13 @@
|
|||||||
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"
|
||||||
)
|
)
|
||||||
@@ -19,29 +21,55 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "One-stop ZFS replication solution",
|
Short: "One-stop ZFS replication solution",
|
||||||
}
|
}
|
||||||
|
|
||||||
var bashcompCmd = &cobra.Command{
|
func init() {
|
||||||
Use: "bashcomp path/to/out/file",
|
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
||||||
Short: "generate bash completions",
|
}
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if len(args) != 1 {
|
var genCompletionCmd = &cobra.Command{
|
||||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
Use: "gencompletion",
|
||||||
err := cmd.Usage()
|
Short: "generate shell auto-completions",
|
||||||
if err != nil {
|
}
|
||||||
panic(err)
|
|
||||||
}
|
type completionCmdInfo struct {
|
||||||
os.Exit(1)
|
genFunc func(outpath string) error
|
||||||
}
|
help string
|
||||||
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
}
|
||||||
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
|
|
||||||
os.Exit(1)
|
var completionCmdMap = map[string]completionCmdInfo{
|
||||||
}
|
"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() {
|
||||||
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
for sh, info := range completionCmdMap {
|
||||||
rootCmd.AddCommand(bashcompCmd)
|
sh, info := sh, info
|
||||||
|
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 {
|
||||||
@@ -49,7 +77,7 @@ type Subcommand struct {
|
|||||||
Short string
|
Short string
|
||||||
Example string
|
Example string
|
||||||
NoRequireConfig bool
|
NoRequireConfig bool
|
||||||
Run func(subcommand *Subcommand, args []string) error
|
Run func(ctx context.Context, subcommand *Subcommand, args []string) error
|
||||||
SetupFlags func(f *pflag.FlagSet)
|
SetupFlags func(f *pflag.FlagSet)
|
||||||
SetupSubcommands func() []*Subcommand
|
SetupSubcommands func() []*Subcommand
|
||||||
|
|
||||||
@@ -70,7 +98,11 @@ 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()
|
||||||
err := s.Run(s, args)
|
ctx := context.Background()
|
||||||
|
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)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -29,7 +30,7 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
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]")
|
||||||
},
|
},
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
formatMap := map[string]func(interface{}){
|
formatMap := map[string]func(interface{}){
|
||||||
"": func(i interface{}) {},
|
"": func(i interface{}) {},
|
||||||
"pretty": func(i interface{}) {
|
"pretty": func(i interface{}) {
|
||||||
|
|||||||
+167
-3
@@ -4,9 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/kr/pretty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
"github.com/zrepl/zrepl/cli"
|
||||||
@@ -31,20 +35,26 @@ var migrations = []*cli.Subcommand{
|
|||||||
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
|
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&cli.Subcommand{
|
||||||
|
Use: "replication-cursor:v1-v2",
|
||||||
|
Run: doMigrateReplicationCursor,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&migrateReplicationCursorArgs.dryRun, "dry-run", false, "dry run")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var migratePlaceholder0_1Args struct {
|
var migratePlaceholder0_1Args struct {
|
||||||
dryRun bool
|
dryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
func doMigratePlaceholder0_1(ctx context.Context, 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")
|
||||||
@@ -88,7 +98,7 @@ func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
|||||||
}
|
}
|
||||||
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(fs, migratePlaceholder0_1Args.dryRun)
|
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(ctx, 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 {
|
||||||
@@ -102,3 +112,157 @@ func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var migrateReplicationCursorArgs struct {
|
||||||
|
dryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var bold = color.New(color.Bold)
|
||||||
|
var succ = color.New(color.FgGreen)
|
||||||
|
var fail = color.New(color.FgRed)
|
||||||
|
|
||||||
|
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
|
||||||
|
|
||||||
|
func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := sc.Config()
|
||||||
|
jobs, err := job.JobsFromConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
||||||
|
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
||||||
|
return fmt.Errorf("exiting migration after error")
|
||||||
|
}
|
||||||
|
|
||||||
|
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
|
||||||
|
for i, j := range cfg.Jobs {
|
||||||
|
if jobs[i].Name() != j.Name() {
|
||||||
|
panic("implementation error")
|
||||||
|
}
|
||||||
|
switch j.Ret.(type) {
|
||||||
|
case *config.PushJob:
|
||||||
|
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||||
|
case *config.SourceJob:
|
||||||
|
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||||
|
default:
|
||||||
|
fmt.Printf("ignoring job %q (%d/%d, type %T), not supposed to create v1 replication cursors\n", j.Name(), i, len(cfg.Jobs), j.Ret)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan all filesystems for v1 replication cursors
|
||||||
|
|
||||||
|
fss, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "list filesystems")
|
||||||
|
}
|
||||||
|
|
||||||
|
var hadError bool
|
||||||
|
for _, fs := range fss {
|
||||||
|
|
||||||
|
bold.Printf("INSPECT FILESYSTEM %q\n", fs.ToString())
|
||||||
|
|
||||||
|
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
||||||
|
if err == migrateReplicationCursorSkipSentinel {
|
||||||
|
bold.Printf("FILESYSTEM SKIPPED\n")
|
||||||
|
} else if err != nil {
|
||||||
|
hadError = true
|
||||||
|
fail.Printf("MIGRATION FAILED: %s\n", err)
|
||||||
|
} else {
|
||||||
|
succ.Printf("FILESYSTEM %q COMPLETE\n", fs.ToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hadError {
|
||||||
|
fail.Printf("\n\none or more filesystems could not be migrated, please inspect output and or re-run migration")
|
||||||
|
return errors.Errorf("")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, fs *zfs.DatasetPath) error {
|
||||||
|
|
||||||
|
var owningJob job.Job = nil
|
||||||
|
for _, job := range v1CursorJobs {
|
||||||
|
conf := job.SenderConfig()
|
||||||
|
if conf == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pass, err := conf.FSF.Filter(fs)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "filesystem filter error in job %q for fs %q", job.Name(), fs.ToString())
|
||||||
|
}
|
||||||
|
if !pass {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if owningJob != nil {
|
||||||
|
return errors.Errorf("jobs %q and %q both match %q\ncannot attribute replication cursor to either one", owningJob.Name(), job.Name(), fs)
|
||||||
|
}
|
||||||
|
owningJob = job
|
||||||
|
}
|
||||||
|
if owningJob == nil {
|
||||||
|
fmt.Printf("no job's Filesystems filter matches\n")
|
||||||
|
return migrateReplicationCursorSkipSentinel
|
||||||
|
}
|
||||||
|
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
||||||
|
|
||||||
|
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
|
||||||
|
Types: zfs.Bookmarks,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldCursor *zfs.FilesystemVersion
|
||||||
|
for i, fsv := range bookmarks {
|
||||||
|
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
||||||
|
if err != endpoint.ErrV1ReplicationCursor {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCursor != nil {
|
||||||
|
fmt.Printf("unexpected v1 replication cursor candidate: %q", fsv.ToAbsPath(fs))
|
||||||
|
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldCursor = &bookmarks[i]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCursor == nil {
|
||||||
|
bold.Printf("no v1 replication cursor found for filesystem %q\n", fs.ToString())
|
||||||
|
return migrateReplicationCursorSkipSentinel
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("found v1 replication cursor:\n%s\n", pretty.Sprint(oldCursor))
|
||||||
|
|
||||||
|
mostRecentNew, err := endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs.ToString(), owningJob.SenderConfig().JobID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "get most recent v2 replication cursor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mostRecentNew == nil {
|
||||||
|
return errors.Errorf("no v2 replication cursor found for job %q on filesystem %q", owningJob.SenderConfig().JobID, fs.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("most recent v2 replication cursor:\n%#v", oldCursor)
|
||||||
|
|
||||||
|
if !(mostRecentNew.CreateTXG >= oldCursor.CreateTXG) {
|
||||||
|
return errors.Errorf("v1 replication cursor createtxg is higher than v2 cursor's, skipping this filesystem")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("determined that v2 cursor is bookmark of same or newer version than v1 cursor\n")
|
||||||
|
fmt.Printf("destroying v1 cursor %q\n", oldCursor.ToAbsPath(fs))
|
||||||
|
|
||||||
|
if migrateReplicationCursorArgs.dryRun {
|
||||||
|
succ.Printf("DRY RUN\n")
|
||||||
|
} else {
|
||||||
|
if err := zfs.ZFSDestroyFilesystemVersion(ctx, fs, oldCursor); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+4
-61
@@ -1,67 +1,10 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import "github.com/zrepl/zrepl/cli"
|
||||||
"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 off | [on TCP_LISTEN_ADDRESS]",
|
Use: "pprof",
|
||||||
Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS",
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
return []*cli.Subcommand{PprofListenCmd, pprofActivityTraceCmd}
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
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"
|
||||||
@@ -11,7 +13,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(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
return runSignalCmd(subcommand.Config(), args)
|
return runSignalCmd(subcommand.Config(), args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-12
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// tcell is the termbox-compatbile library for abstracting away escape sequences, etc.
|
// tcell is the termbox-compatible library for abstracting away escape sequences, etc.
|
||||||
// as of tcell#252, the number of default distributed terminals is relatively limited
|
// as of tcell#252, the number of default distributed terminals is relatively limited
|
||||||
// additional terminal definitions can be included via side-effect import
|
// 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/blob/master/terminfo/base/base.go
|
||||||
@@ -81,7 +82,7 @@ type tui struct {
|
|||||||
indent int
|
indent int
|
||||||
|
|
||||||
lock sync.Mutex //For report and error
|
lock sync.Mutex //For report and error
|
||||||
report map[string]job.Status
|
report map[string]*job.Status
|
||||||
err error
|
err error
|
||||||
|
|
||||||
jobFilter string
|
jobFilter string
|
||||||
@@ -180,7 +181,7 @@ var StatusCmd = &cli.Subcommand{
|
|||||||
Run: runStatus,
|
Run: runStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runStatus(s *cli.Subcommand, args []string) error {
|
func runStatus(ctx context.Context, s *cli.Subcommand, args []string) error {
|
||||||
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -219,7 +220,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
defer termbox.Close()
|
defer termbox.Close()
|
||||||
|
|
||||||
update := func() {
|
update := func() {
|
||||||
m := make(map[string]job.Status)
|
var m daemon.Status
|
||||||
|
|
||||||
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
||||||
struct{}{},
|
struct{}{},
|
||||||
@@ -228,7 +229,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
|||||||
|
|
||||||
t.lock.Lock()
|
t.lock.Lock()
|
||||||
t.err = err2
|
t.err = err2
|
||||||
t.report = m
|
t.report = m.Jobs
|
||||||
t.lock.Unlock()
|
t.lock.Unlock()
|
||||||
t.draw()
|
t.draw()
|
||||||
}
|
}
|
||||||
@@ -264,7 +265,7 @@ loop:
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
|
func (t *tui) getReplicationProgressHistory(jobName string) *bytesProgressHistory {
|
||||||
p, ok := t.replicationProgress[jobName]
|
p, ok := t.replicationProgress[jobName]
|
||||||
if !ok {
|
if !ok {
|
||||||
p = &bytesProgressHistory{}
|
p = &bytesProgressHistory{}
|
||||||
@@ -329,7 +330,7 @@ func (t *tui) draw() {
|
|||||||
t.printf("Replication:")
|
t.printf("Replication:")
|
||||||
t.newline()
|
t.newline()
|
||||||
t.addIndent(1)
|
t.addIndent(1)
|
||||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgresHistory(k))
|
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
|
||||||
t.addIndent(-1)
|
t.addIndent(-1)
|
||||||
|
|
||||||
t.printf("Pruning Sender:")
|
t.printf("Pruning Sender:")
|
||||||
@@ -461,12 +462,23 @@ func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgress
|
|||||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
||||||
// Draw global progress bar
|
// Draw global progress bar
|
||||||
// Progress: [---------------]
|
// Progress: [---------------]
|
||||||
expected, replicated := latest.BytesSum()
|
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||||
rate, changeCount := history.Update(replicated)
|
rate, changeCount := history.Update(replicated)
|
||||||
|
eta := time.Duration(0)
|
||||||
|
if rate > 0 {
|
||||||
|
eta = time.Duration((expected-replicated)/rate) * time.Second
|
||||||
|
}
|
||||||
t.write("Progress: ")
|
t.write("Progress: ")
|
||||||
t.drawBar(50, replicated, expected, changeCount)
|
t.drawBar(50, replicated, expected, changeCount)
|
||||||
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
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()
|
t.newline()
|
||||||
|
if containsInvalidSizeEstimates {
|
||||||
|
t.write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||||
|
t.newline()
|
||||||
|
}
|
||||||
|
|
||||||
var maxFSLen int
|
var maxFSLen int
|
||||||
for _, fs := range latest.Filesystems {
|
for _, fs := range latest.Filesystems {
|
||||||
@@ -686,7 +698,7 @@ func rightPad(str string, length int, pad string) string {
|
|||||||
|
|
||||||
var arrowPositions = `>\|/`
|
var arrowPositions = `>\|/`
|
||||||
|
|
||||||
// changeCount = 0 indicates stall / no progresss
|
// changeCount = 0 indicates stall / no progress
|
||||||
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
||||||
var completedLength int
|
var completedLength int
|
||||||
if totalBytes > 0 {
|
if totalBytes > 0 {
|
||||||
@@ -707,11 +719,20 @@ func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
|||||||
|
|
||||||
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
||||||
|
|
||||||
expected, replicated := rep.BytesSum()
|
expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
|
||||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)",
|
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)),
|
strings.ToUpper(string(rep.State)),
|
||||||
rep.CurrentStep, len(rep.Steps),
|
rep.CurrentStep, len(rep.Steps),
|
||||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
ByteCountBinary(replicated), ByteCountBinary(expected),
|
||||||
|
sizeEstimationImpreciseNotice,
|
||||||
)
|
)
|
||||||
|
|
||||||
activeIndicator := " "
|
activeIndicator := " "
|
||||||
@@ -731,8 +752,17 @@ func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, m
|
|||||||
if nextStep.IsIncremental() {
|
if nextStep.IsIncremental() {
|
||||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
||||||
} else {
|
} else {
|
||||||
next = fmt.Sprintf("next: %s (full)", nextStep.Info.To)
|
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 {
|
} else {
|
||||||
next = "" // individual FSes may still be in planning state
|
next = "" // individual FSes may still be in planning state
|
||||||
}
|
}
|
||||||
@@ -755,3 +785,27 @@ func ByteCountBinary(b int64) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func humanizeDuration(duration time.Duration) string {
|
||||||
|
days := int64(duration.Hours() / 24)
|
||||||
|
hours := int64(math.Mod(duration.Hours(), 24))
|
||||||
|
minutes := int64(math.Mod(duration.Minutes(), 60))
|
||||||
|
seconds := int64(math.Mod(duration.Seconds(), 60))
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
force := false
|
||||||
|
chunks := []int64{days, hours, minutes, seconds}
|
||||||
|
for i, chunk := range chunks {
|
||||||
|
if force || chunk > 0 {
|
||||||
|
padding := 0
|
||||||
|
if force {
|
||||||
|
padding = 2
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
return runStdinserver(subcommand.Config(), args)
|
return runStdinserver(subcommand.Config(), args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-6
@@ -1,7 +1,10 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
@@ -15,7 +18,7 @@ import (
|
|||||||
var TestCmd = &cli.Subcommand{
|
var TestCmd = &cli.Subcommand{
|
||||||
Use: "test",
|
Use: "test",
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
return []*cli.Subcommand{testFilter, testPlaceholder}
|
return []*cli.Subcommand{testFilter, testPlaceholder, testDecodeResumeToken}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ var testFilter = &cli.Subcommand{
|
|||||||
Run: runTestFilterCmd,
|
Run: runTestFilterCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
func runTestFilterCmd(ctx context.Context, 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")
|
||||||
@@ -57,6 +60,8 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
@@ -70,7 +75,7 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
|
|||||||
if testFilterArgs.input != "" {
|
if testFilterArgs.input != "" {
|
||||||
fsnames = []string{testFilterArgs.input}
|
fsnames = []string{testFilterArgs.input}
|
||||||
} else {
|
} else {
|
||||||
out, err := zfs.ZFSList([]string{"name"})
|
out, err := zfs.ZFSList(ctx, []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)
|
||||||
}
|
}
|
||||||
@@ -131,13 +136,13 @@ var testPlaceholder = &cli.Subcommand{
|
|||||||
Run: runTestPlaceholder,
|
Run: runTestPlaceholder,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
var checkDPs []*zfs.DatasetPath
|
var checkDPs []*zfs.DatasetPath
|
||||||
|
|
||||||
// all actions first
|
// all actions first
|
||||||
if testPlaceholderArgs.all {
|
if testPlaceholderArgs.all {
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -161,7 +166,7 @@ func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
|||||||
|
|
||||||
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(dp)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, dp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "cannot get placeholder state")
|
return errors.Wrap(err, "cannot get placeholder state")
|
||||||
}
|
}
|
||||||
@@ -176,3 +181,32 @@ func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var testDecodeResumeTokenArgs struct {
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
var testDecodeResumeToken = &cli.Subcommand{
|
||||||
|
Use: "decoderesumetoken --token TOKEN",
|
||||||
|
Short: "decode resume token",
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.StringVar(&testDecodeResumeTokenArgs.token, "token", "", "the resume token obtained from the receive_resume_token property")
|
||||||
|
},
|
||||||
|
Run: runTestDecodeResumeTokenCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestDecodeResumeTokenCmd(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
if testDecodeResumeTokenArgs.token == "" {
|
||||||
|
return fmt.Errorf("token argument must be specified")
|
||||||
|
}
|
||||||
|
token, err := zfs.ParseResumeToken(ctx, testDecodeResumeTokenArgs.token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err := enc.Encode(&token); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -25,7 +26,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(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, 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()
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
variants = sort.StringSlice(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
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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, err := endpoint.ListAbstractionsStreamed(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return err // context clear by invocation of command
|
||||||
|
}
|
||||||
|
|
||||||
|
var line chainlock.L
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
// print results
|
||||||
|
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(abstractions); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-7
@@ -75,16 +75,42 @@ type SnapJob struct {
|
|||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendOptions struct {
|
||||||
|
Encrypted bool `yaml:"encrypted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Defaulter = (*SendOptions)(nil)
|
||||||
|
|
||||||
|
func (l *SendOptions) SetDefault() {
|
||||||
|
*l = SendOptions{Encrypted: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecvOptions struct {
|
||||||
|
// Note: we cannot enforce encrypted recv as the ZFS cli doesn't provide a mechanism for it
|
||||||
|
// Encrypted bool `yaml:"may_encrypted"`
|
||||||
|
|
||||||
|
// Future:
|
||||||
|
// Reencrypt bool `yaml:"reencrypt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ yaml.Defaulter = (*RecvOptions)(nil)
|
||||||
|
|
||||||
|
func (l *RecvOptions) SetDefault() {
|
||||||
|
*l = RecvOptions{}
|
||||||
|
}
|
||||||
|
|
||||||
type PushJob struct {
|
type PushJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
Send *SendOptions `yaml:"send,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PullJob struct {
|
type PullJob struct {
|
||||||
ActiveJob `yaml:",inline"`
|
ActiveJob `yaml:",inline"`
|
||||||
RootFS string `yaml:"root_fs"`
|
RootFS string `yaml:"root_fs"`
|
||||||
Interval PositiveDurationOrManual `yaml:"interval"`
|
Interval PositiveDurationOrManual `yaml:"interval"`
|
||||||
|
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PositiveDurationOrManual struct {
|
type PositiveDurationOrManual struct {
|
||||||
@@ -120,13 +146,15 @@ func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error
|
|||||||
|
|
||||||
type SinkJob struct {
|
type SinkJob struct {
|
||||||
PassiveJob `yaml:",inline"`
|
PassiveJob `yaml:",inline"`
|
||||||
RootFS string `yaml:"root_fs"`
|
RootFS string `yaml:"root_fs"`
|
||||||
|
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SourceJob struct {
|
type SourceJob struct {
|
||||||
PassiveJob `yaml:",inline"`
|
PassiveJob `yaml:",inline"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
|
Send *SendOptions `yaml:"send,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilesystemsFilter map[string]bool
|
type FilesystemsFilter map[string]bool
|
||||||
@@ -245,14 +273,16 @@ type ServeCommon struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TCPServe struct {
|
type TCPServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
Clients map[string]string `yaml:"clients"`
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
|
Clients map[string]string `yaml:"clients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLSServe struct {
|
type TLSServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
Ca string `yaml:"ca"`
|
Ca string `yaml:"ca"`
|
||||||
Cert string `yaml:"cert"`
|
Cert string `yaml:"cert"`
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
@@ -279,6 +309,11 @@ type PruneKeepNotReplicated struct {
|
|||||||
KeepSnapshotAtCursor bool `yaml:"keep_snapshot_at_cursor,optional,default=true"`
|
KeepSnapshotAtCursor bool `yaml:"keep_snapshot_at_cursor,optional,default=true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PruneKeepStepHolds struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
AdditionalJobIds []string `yaml:"additional_job_ids,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
type PruneKeepLastN struct {
|
type PruneKeepLastN struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Count int `yaml:"count"`
|
Count int `yaml:"count"`
|
||||||
@@ -331,8 +366,9 @@ type MonitoringEnum struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PrometheusMonitoring struct {
|
type PrometheusMonitoring struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyslogFacility syslog.Priority
|
type SyslogFacility syslog.Priority
|
||||||
@@ -449,6 +485,7 @@ func (t *ServeEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|||||||
|
|
||||||
func (t *PruningEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
func (t *PruningEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||||
|
"step_holds": &PruneKeepStepHolds{},
|
||||||
"not_replicated": &PruneKeepNotReplicated{},
|
"not_replicated": &PruneKeepNotReplicated{},
|
||||||
"last_n": &PruneKeepLastN{},
|
"last_n": &PruneKeepLastN{},
|
||||||
"grid": &PruneGrid{},
|
"grid": &PruneGrid{},
|
||||||
@@ -589,7 +626,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 parsePostitiveDuration(e string) (d time.Duration, err error) {
|
func parsePositiveDuration(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)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
clients: {
|
clients: {
|
||||||
"10.0.0.1":"foo"
|
"10.0.0.1":"foo"
|
||||||
}
|
}
|
||||||
root_fs: zoot/foo
|
root_fs: zroot/foo
|
||||||
`
|
`
|
||||||
_, err := ParseConfigBytes([]byte(jobdef))
|
_, err := ParseConfigBytes([]byte(jobdef))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package config
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSendOptions(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: foo
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: foo
|
||||||
|
client_identity: bar
|
||||||
|
filesystems: {"<": true}
|
||||||
|
%s
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`
|
||||||
|
encrypted_false := `
|
||||||
|
send:
|
||||||
|
encrypted: false
|
||||||
|
`
|
||||||
|
|
||||||
|
encrypted_true := `
|
||||||
|
send:
|
||||||
|
encrypted: true
|
||||||
|
`
|
||||||
|
|
||||||
|
encrypted_unspecified := `
|
||||||
|
send: {}
|
||||||
|
`
|
||||||
|
|
||||||
|
send_not_specified := `
|
||||||
|
`
|
||||||
|
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
var c *Config
|
||||||
|
|
||||||
|
t.Run("encrypted_false", func(t *testing.T) {
|
||||||
|
c = testValidConfig(t, fill(encrypted_false))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, false, encrypted)
|
||||||
|
})
|
||||||
|
t.Run("encrypted_true", func(t *testing.T) {
|
||||||
|
c = testValidConfig(t, fill(encrypted_true))
|
||||||
|
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
|
||||||
|
assert.Equal(t, true, encrypted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("encrypted_unspecified", func(t *testing.T) {
|
||||||
|
c, err := testConfig(t, fill(encrypted_unspecified))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("send_not_specified", func(t *testing.T) {
|
||||||
|
c, err := testConfig(t, fill(send_not_specified))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// template must be a template/text template with a single '{{ . }}' as placehodler for val
|
// template must be a template/text template with a single '{{ . }}' as placeholder for val
|
||||||
//nolint[:deadcode,unused]
|
//nolint[:deadcode,unused]
|
||||||
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
||||||
tmp, err := template.New("master").Parse(tmpl)
|
tmp, err := template.New("master").Parse(tmpl)
|
||||||
|
|||||||
@@ -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 := parsePostitiveDuration(comps[2])
|
duration, err := parsePositiveDuration(comps[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
port: 22
|
port: 22
|
||||||
identity_file: /etc/zrepl/ssh/identity
|
identity_file: /etc/zrepl/ssh/identity
|
||||||
options: # optional, default [], `-o` arguments passed to ssh
|
options: # optional, default [], `-o` arguments passed to ssh
|
||||||
- "Compression=on"
|
- "Compression=yes"
|
||||||
root_fs: "pool2/backup_servers"
|
root_fs: "pool2/backup_servers"
|
||||||
interval: 10m
|
interval: 10m
|
||||||
pruning:
|
pruning:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ jobs:
|
|||||||
address: "backup-server.foo.bar:8888"
|
address: "backup-server.foo.bar:8888"
|
||||||
snapshotting:
|
snapshotting:
|
||||||
type: manual
|
type: manual
|
||||||
|
send:
|
||||||
|
encrypted: false
|
||||||
pruning:
|
pruning:
|
||||||
keep_sender:
|
keep_sender:
|
||||||
- type: not_replicated
|
- type: not_replicated
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ jobs:
|
|||||||
type: tcp
|
type: tcp
|
||||||
listen: "0.0.0.0:8888"
|
listen: "0.0.0.0:8888"
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "client1"
|
"192.168.122.123" : "mysql01",
|
||||||
|
"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,
|
||||||
|
|||||||
+9
-3
@@ -15,10 +15,12 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/nethelpers"
|
"github.com/zrepl/zrepl/daemon/nethelpers"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"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 {
|
||||||
@@ -44,6 +46,8 @@ func (j *controlJob) Status() *job.Status { return &job.Status{Type: job.TypeInt
|
|||||||
|
|
||||||
func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||||
|
|
||||||
|
func (j *controlJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
var promControl struct {
|
var promControl struct {
|
||||||
requestBegin *prometheus.CounterVec
|
requestBegin *prometheus.CounterVec
|
||||||
requestFinished *prometheus.HistogramVec
|
requestFinished *prometheus.HistogramVec
|
||||||
@@ -61,7 +65,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 finih",
|
Help: "time it took a request to finish",
|
||||||
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)
|
||||||
@@ -114,7 +118,9 @@ 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) {
|
||||||
s := j.jobs.status()
|
jobs := j.jobs.status()
|
||||||
|
globalZFS := zfscmd.GetReport()
|
||||||
|
s := Status{Jobs: jobs, Global: GlobalStatus{ZFSCmds: globalZFS}}
|
||||||
return s, nil
|
return s, nil
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -247,7 +253,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 marhsal error")
|
j.log.WithError(producerErr).Error("control handler json marshal 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)
|
||||||
|
|||||||
+40
-16
@@ -12,6 +12,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/endpoint"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
@@ -20,11 +22,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(conf *config.Config) error {
|
func Run(ctx context.Context, 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)
|
||||||
@@ -38,6 +40,7 @@ func Run(conf *config.Config) error {
|
|||||||
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)
|
confJobs, err := job.JobsFromConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,14 +50,23 @@ func Run(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
|
||||||
@@ -76,11 +88,16 @@ func Run(conf *config.Config) error {
|
|||||||
return errors.Errorf("unknown monitoring job #%d (type %T)", i, v)
|
return errors.Errorf("unknown monitoring job #%d (type %T)", i, v)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "cannot build monitorin gjob #%d", i)
|
return errors.Wrapf(err, "cannot build monitoring job #%d", i)
|
||||||
}
|
}
|
||||||
jobs.start(ctx, job, true)
|
jobs.start(ctx, job, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// register global (=non job-local) metrics
|
||||||
|
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
|
||||||
@@ -94,6 +111,8 @@ func Run(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
|
||||||
}
|
}
|
||||||
@@ -116,18 +135,24 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
@@ -189,9 +214,8 @@ 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()
|
||||||
|
|
||||||
jobLog := job.GetLogger(ctx).
|
ctx = logging.WithInjectedField(ctx, logging.JobField, j.Name())
|
||||||
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))
|
||||||
@@ -206,7 +230,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 = job.WithLogger(ctx, jobLog)
|
ctx = zfscmd.WithJobID(ctx, j.Name())
|
||||||
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
|
||||||
@@ -215,8 +239,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()
|
||||||
jobLog.Info("starting job")
|
job.GetLogger(ctx).Info("starting job")
|
||||||
defer jobLog.Info("job exited")
|
defer job.GetLogger(ctx).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 excactly those paths that were not forbidden by the mapping.
|
// The new filter allows exactly those paths that were not forbidden by the mapping.
|
||||||
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
||||||
|
|
||||||
if m.filterMode {
|
if m.filterMode {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package filters
|
package filters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestDatasetMapFilter(t *testing.T) {
|
func TestDatasetMapFilter(t *testing.T) {
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -6,29 +6,17 @@ 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 {
|
||||||
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
return logging.GetLogger(ctx, logging.SubsysHooks)
|
||||||
return log
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/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"
|
||||||
)
|
)
|
||||||
@@ -69,6 +71,9 @@ 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"
|
||||||
|
|
||||||
@@ -418,9 +423,8 @@ jobs:
|
|||||||
|
|
||||||
cbReached = false
|
cbReached = false
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
if testing.Verbose() && !tt.SuppressOutput {
|
if testing.Verbose() && !tt.SuppressOutput {
|
||||||
ctx = hooks.WithLogger(ctx, log)
|
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
|
||||||
}
|
}
|
||||||
plan.Run(ctx, false)
|
plan.Run(ctx, false)
|
||||||
report := plan.Report()
|
report := plan.Report()
|
||||||
|
|||||||
+124
-47
@@ -2,17 +2,19 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"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"
|
||||||
@@ -28,7 +30,7 @@ import (
|
|||||||
|
|
||||||
type ActiveSide struct {
|
type ActiveSide struct {
|
||||||
mode activeMode
|
mode activeMode
|
||||||
name string
|
name endpoint.JobID
|
||||||
connecter transport.Connecter
|
connecter transport.Connecter
|
||||||
|
|
||||||
prunerFactory *pruner.PrunerFactory
|
prunerFactory *pruner.PrunerFactory
|
||||||
@@ -78,31 +80,33 @@ func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type activeMode interface {
|
type activeMode interface {
|
||||||
ConnectEndpoints(rpcLoggers rpc.Loggers, connecter transport.Connecter)
|
ConnectEndpoints(ctx context.Context, connecter transport.Connecter)
|
||||||
DisconnectEndpoints()
|
DisconnectEndpoints()
|
||||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||||
Type() Type
|
Type() Type
|
||||||
|
PlannerPolicy() logic.PlannerPolicy
|
||||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
||||||
SnapperReport() *snapper.Report
|
SnapperReport() *snapper.Report
|
||||||
ResetConnectBackoff()
|
ResetConnectBackoff()
|
||||||
}
|
}
|
||||||
|
|
||||||
type modePush struct {
|
type modePush struct {
|
||||||
setupMtx sync.Mutex
|
setupMtx sync.Mutex
|
||||||
sender *endpoint.Sender
|
sender *endpoint.Sender
|
||||||
receiver *rpc.Client
|
receiver *rpc.Client
|
||||||
fsfilter endpoint.FSFilter
|
senderConfig *endpoint.SenderConfig
|
||||||
snapper *snapper.PeriodicOrManual
|
plannerPolicy *logic.PlannerPolicy
|
||||||
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
func (m *modePush) ConnectEndpoints(ctx context.Context, 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.fsfilter)
|
m.sender = endpoint.NewSender(*m.senderConfig)
|
||||||
m.receiver = rpc.NewClient(connecter, loggers)
|
m.receiver = rpc.NewClient(connecter, rpc.GetLoggersOrPanic(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) DisconnectEndpoints() {
|
func (m *modePush) DisconnectEndpoints() {
|
||||||
@@ -121,6 +125,8 @@ func (m *modePush) SenderReceiver() (logic.Sender, logic.Receiver) {
|
|||||||
|
|
||||||
func (m *modePush) Type() Type { return TypePush }
|
func (m *modePush) Type() Type { return TypePush }
|
||||||
|
|
||||||
|
func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||||
|
|
||||||
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
m.snapper.Run(ctx, wakeUpCommon)
|
m.snapper.Run(ctx, wakeUpCommon)
|
||||||
}
|
}
|
||||||
@@ -137,13 +143,22 @@ func (m *modePush) ResetConnectBackoff() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error) {
|
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
|
||||||
m := &modePush{}
|
m := &modePush{}
|
||||||
|
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
|
FSF: fsf,
|
||||||
|
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||||
|
JobID: jobID,
|
||||||
|
}
|
||||||
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
|
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||||
}
|
}
|
||||||
m.fsfilter = fsf
|
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
@@ -153,21 +168,23 @@ func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modePull struct {
|
type modePull struct {
|
||||||
setupMtx sync.Mutex
|
setupMtx sync.Mutex
|
||||||
receiver *endpoint.Receiver
|
receiver *endpoint.Receiver
|
||||||
sender *rpc.Client
|
receiverConfig endpoint.ReceiverConfig
|
||||||
rootFS *zfs.DatasetPath
|
sender *rpc.Client
|
||||||
interval config.PositiveDurationOrManual
|
rootFS *zfs.DatasetPath
|
||||||
|
plannerPolicy *logic.PlannerPolicy
|
||||||
|
interval config.PositiveDurationOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
func (m *modePull) ConnectEndpoints(ctx context.Context, 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.rootFS, false)
|
m.receiver = endpoint.NewReceiver(m.receiverConfig)
|
||||||
m.sender = rpc.NewClient(connecter, loggers)
|
m.sender = rpc.NewClient(connecter, rpc.GetLoggersOrPanic(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePull) DisconnectEndpoints() {
|
func (m *modePull) DisconnectEndpoints() {
|
||||||
@@ -186,6 +203,8 @@ func (m *modePull) SenderReceiver() (logic.Sender, logic.Receiver) {
|
|||||||
|
|
||||||
func (*modePull) Type() Type { return TypePull }
|
func (*modePull) Type() Type { return TypePull }
|
||||||
|
|
||||||
|
func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||||
|
|
||||||
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||||
if m.interval.Manual {
|
if m.interval.Manual {
|
||||||
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
||||||
@@ -223,7 +242,7 @@ func (m *modePull) ResetConnectBackoff() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err error) {
|
func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.JobID) (m *modePull, err error) {
|
||||||
m = &modePull{}
|
m = &modePull{}
|
||||||
m.interval = in.Interval
|
m.interval = in.Interval
|
||||||
|
|
||||||
@@ -235,26 +254,56 @@ func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err
|
|||||||
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.plannerPolicy = &logic.PlannerPolicy{
|
||||||
|
EncryptedSend: logic.DontCare,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.receiverConfig = endpoint.ReceiverConfig{
|
||||||
|
JobID: jobID,
|
||||||
|
RootWithoutClientComponent: m.rootFS,
|
||||||
|
AppendClientIdentity: false, // !
|
||||||
|
UpdateLastReceivedHold: true,
|
||||||
|
}
|
||||||
|
if err := m.receiverConfig.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *ActiveSide, err error) {
|
func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
|
||||||
|
|
||||||
|
j = &ActiveSide{}
|
||||||
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := configJob.(type) {
|
||||||
|
case *config.PushJob:
|
||||||
|
j.mode, err = modePushFromConfig(g, v, j.name) // shadow
|
||||||
|
case *config.PullJob:
|
||||||
|
j.mode, err = modePullFromConfig(g, v, j.name) // shadow
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("implementation error: unknown job type %T", v))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err // no wrapping required
|
||||||
|
}
|
||||||
|
|
||||||
j = &ActiveSide{mode: mode}
|
|
||||||
j.name = in.Name
|
|
||||||
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
Name: "state_time",
|
Name: "state_time",
|
||||||
Help: "seconds spent during replication",
|
Help: "seconds spent during replication",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"state"})
|
}, []string{"state"})
|
||||||
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
|
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "replication",
|
Subsystem: "replication",
|
||||||
Name: "bytes_replicated",
|
Name: "bytes_replicated",
|
||||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
Help: "number of bytes replicated from sender to receiver per filesystem",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"filesystem"})
|
}, []string{"filesystem"})
|
||||||
|
|
||||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
||||||
@@ -267,7 +316,7 @@ func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *Act
|
|||||||
Subsystem: "pruning",
|
Subsystem: "pruning",
|
||||||
Name: "time",
|
Name: "time",
|
||||||
Help: "seconds spent in pruner",
|
Help: "seconds spent in pruner",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"prune_side"})
|
}, []string{"prune_side"})
|
||||||
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
|
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -283,7 +332,7 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|||||||
registerer.MustRegister(j.promBytesReplicated)
|
registerer.MustRegister(j.promBytesReplicated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) Name() string { return j.name }
|
func (j *ActiveSide) Name() string { return j.name.String() }
|
||||||
|
|
||||||
type ActiveSideStatus struct {
|
type ActiveSideStatus struct {
|
||||||
Replication *report.Report
|
Replication *report.Report
|
||||||
@@ -318,16 +367,40 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|||||||
return pull.rootFS.Copy(), true
|
return pull.rootFS.Copy(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||||
|
push, ok := j.mode.(*modePush)
|
||||||
|
if !ok {
|
||||||
|
_ = j.mode.(*modePull) // make sure we didn't introduce a new job type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return push.senderConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
go j.mode.RunPeriodic(ctx, periodicDone)
|
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
|
||||||
|
defer endTask()
|
||||||
|
go j.mode.RunPeriodic(periodicCtx, periodicDone)
|
||||||
|
|
||||||
invocationCount := 0
|
invocationCount := 0
|
||||||
outer:
|
outer:
|
||||||
@@ -343,17 +416,15 @@ outer:
|
|||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
invLog := log.WithField("invocation", invocationCount)
|
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
|
||||||
j.do(WithLogger(ctx, invLog))
|
j.do(invocationCtx)
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) do(ctx context.Context) {
|
func (j *ActiveSide) do(ctx context.Context) {
|
||||||
|
|
||||||
log := GetLogger(ctx)
|
j.mode.ConnectEndpoints(ctx, j.connecter)
|
||||||
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)
|
||||||
@@ -376,20 +447,22 @@ 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 = repCancel
|
tasks.replicationCancel = func() { repCancel(); endSpan() }
|
||||||
tasks.replicationReport, repWait = replication.Do(
|
tasks.replicationReport, repWait = replication.Do(
|
||||||
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver),
|
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||||
)
|
)
|
||||||
tasks.state = ActiveSideReplicating
|
tasks.state = ActiveSideReplicating
|
||||||
})
|
})
|
||||||
log.Info("start replication")
|
GetLogger(ctx).Info("start replication")
|
||||||
repWait(true) // wait blocking
|
repWait(true) // wait blocking
|
||||||
repCancel() // always cancel to free up context resources
|
repCancel() // always cancel to free up context resources
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -398,16 +471,18 @@ 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 = senderCancel
|
tasks.prunerSenderCancel = func() { senderCancel(); endSpan() }
|
||||||
tasks.state = ActiveSidePruneSender
|
tasks.state = ActiveSidePruneSender
|
||||||
})
|
})
|
||||||
log.Info("start pruning sender")
|
GetLogger(ctx).Info("start pruning sender")
|
||||||
tasks.prunerSender.Prune()
|
tasks.prunerSender.Prune()
|
||||||
log.Info("finished pruning sender")
|
GetLogger(ctx).Info("finished pruning sender")
|
||||||
senderCancel()
|
senderCancel()
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
select {
|
select {
|
||||||
@@ -415,16 +490,18 @@ 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 = receiverCancel
|
tasks.prunerReceiverCancel = func() { receiverCancel(); endSpan() }
|
||||||
tasks.state = ActiveSidePruneReceiver
|
tasks.state = ActiveSidePruneReceiver
|
||||||
})
|
})
|
||||||
log.Info("start pruning receiver")
|
GetLogger(ctx).Info("start pruning receiver")
|
||||||
tasks.prunerReceiver.Prune()
|
tasks.prunerReceiver.Prune()
|
||||||
log.Info("finished pruning receiver")
|
GetLogger(ctx).Info("finished pruning receiver")
|
||||||
receiverCancel()
|
receiverCancel()
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|
||||||
j.updateTasks(func(tasks *activeSideTasks) {
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -48,20 +48,12 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
|||||||
// FIXME prettify this
|
// FIXME prettify this
|
||||||
switch v := in.Ret.(type) {
|
switch v := in.Ret.(type) {
|
||||||
case *config.SinkJob:
|
case *config.SinkJob:
|
||||||
m, err := modeSinkFromConfig(c, v)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.SourceJob:
|
case *config.SourceJob:
|
||||||
m, err := modeSourceFromConfig(c, v)
|
j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -71,20 +63,12 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
|
|||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PushJob:
|
case *config.PushJob:
|
||||||
m, err := modePushFromConfig(c, v)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = activeSide(c, &v.ActiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
case *config.PullJob:
|
case *config.PullJob:
|
||||||
m, err := modePullFromConfig(c, v)
|
j, err = activeSide(c, &v.ActiveJob, v)
|
||||||
if err != nil {
|
|
||||||
return cannotBuildJob(err, v.Name)
|
|
||||||
}
|
|
||||||
j, err = activeSide(c, &v.ActiveJob, m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cannotBuildJob(err, v.Name)
|
return cannotBuildJob(err, v.Name)
|
||||||
}
|
}
|
||||||
@@ -104,6 +88,11 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
|||||||
sort.Slice(rfss, func(i, j int) bool {
|
sort.Slice(rfss, func(i, j int) bool {
|
||||||
return strings.Compare(rfss[i], rfss[j]) == -1
|
return strings.Compare(rfss[i], rfss[j]) == -1
|
||||||
})
|
})
|
||||||
|
// add tailing slash because of hierarchy-simulation
|
||||||
|
// rootfs/ is not root of rootfs2/
|
||||||
|
for i := 0; i < len(rfss); i++ {
|
||||||
|
rfss[i] += "/"
|
||||||
|
}
|
||||||
// idea:
|
// idea:
|
||||||
// no path in rfss must be prefix of another
|
// no path in rfss must be prefix of another
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
||||||
@@ -18,6 +22,7 @@ func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
|||||||
{false, []string{"a"}},
|
{false, []string{"a"}},
|
||||||
{false, []string{"some/path"}},
|
{false, []string{"some/path"}},
|
||||||
{false, []string{"zroot/sink1", "zroot/sink2", "zroot/sink3"}},
|
{false, []string{"zroot/sink1", "zroot/sink2", "zroot/sink3"}},
|
||||||
|
{false, []string{"zroot/foo", "zroot/foobar"}},
|
||||||
{true, []string{"zroot/b", "zroot/b"}},
|
{true, []string{"zroot/b", "zroot/b"}},
|
||||||
{true, []string{"zroot/foo", "zroot/foo/bar", "zroot/baz"}},
|
{true, []string{"zroot/foo", "zroot/foo/bar", "zroot/baz"}},
|
||||||
{false, []string{"a/x", "b/x"}},
|
{false, []string{"a/x", "b/x"}},
|
||||||
@@ -40,3 +45,66 @@ func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJobIDErrorHandling(t *testing.T) {
|
||||||
|
tmpl := `
|
||||||
|
jobs:
|
||||||
|
- name: %s
|
||||||
|
type: push
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: foo
|
||||||
|
client_identity: bar
|
||||||
|
filesystems: {"<": true}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
`
|
||||||
|
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||||
|
|
||||||
|
type Case struct {
|
||||||
|
jobName string
|
||||||
|
valid bool
|
||||||
|
}
|
||||||
|
cases := []Case{
|
||||||
|
{"validjobname", true},
|
||||||
|
{"valid with spaces", true},
|
||||||
|
{"invalid\twith\ttabs", false},
|
||||||
|
{"invalid#withdelimiter", false},
|
||||||
|
{"invalid@withdelimiter", false},
|
||||||
|
{"withnewline\\nmiddle", false},
|
||||||
|
{"withnewline\\n", false},
|
||||||
|
{"withslash/", false},
|
||||||
|
{"withslash/inthemiddle", false},
|
||||||
|
{"/", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cases {
|
||||||
|
t.Run(cases[i].jobName, func(t *testing.T) {
|
||||||
|
c := cases[i]
|
||||||
|
|
||||||
|
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
|
||||||
|
require.NoError(t, err, "not expecting yaml-config to know about job ids")
|
||||||
|
require.NotNil(t, conf)
|
||||||
|
jobs, err := JobsFromConfig(conf)
|
||||||
|
|
||||||
|
if c.valid {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.Len(t, jobs, 1)
|
||||||
|
assert.Equal(t, c.jobName, jobs[0].Name())
|
||||||
|
} else {
|
||||||
|
t.Logf("error: %s", err)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, jobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
+4
-14
@@ -7,27 +7,16 @@ 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/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
||||||
if l, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
return logging.GetLogger(ctx, logging.SubsysJob)
|
||||||
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 {
|
||||||
@@ -38,6 +27,7 @@ type Job interface {
|
|||||||
// Jobs that return a subtree of the dataset hierarchy
|
// Jobs that return a subtree of the dataset hierarchy
|
||||||
// must return the root of that subtree as rfs and ok = true
|
// must return the root of that subtree as rfs and ok = true
|
||||||
OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
||||||
|
SenderConfig() *endpoint.SenderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type Type string
|
type Type string
|
||||||
|
|||||||
+69
-22
@@ -6,6 +6,7 @@ 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/filters"
|
||||||
@@ -20,7 +21,7 @@ import (
|
|||||||
|
|
||||||
type PassiveSide struct {
|
type PassiveSide struct {
|
||||||
mode passiveMode
|
mode passiveMode
|
||||||
name string
|
name endpoint.JobID
|
||||||
listen transport.AuthenticatedListenerFactory
|
listen transport.AuthenticatedListenerFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,43 +33,56 @@ type passiveMode interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modeSink struct {
|
type modeSink struct {
|
||||||
rootDataset *zfs.DatasetPath
|
receiverConfig endpoint.ReceiverConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSink) Type() Type { return TypeSink }
|
func (m *modeSink) Type() Type { return TypeSink }
|
||||||
|
|
||||||
func (m *modeSink) Handler() rpc.Handler {
|
func (m *modeSink) Handler() rpc.Handler {
|
||||||
return endpoint.NewReceiver(m.rootDataset, true)
|
return endpoint.NewReceiver(m.receiverConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
||||||
func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
||||||
|
|
||||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob) (m *modeSink, err error) {
|
func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
|
||||||
m = &modeSink{}
|
m = &modeSink{}
|
||||||
m.rootDataset, err = zfs.NewDatasetPath(in.RootFS)
|
|
||||||
|
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
||||||
}
|
}
|
||||||
if m.rootDataset.Length() <= 0 {
|
|
||||||
return nil, errors.New("root dataset must not be empty") // duplicates error check of receiver
|
m.receiverConfig = endpoint.ReceiverConfig{
|
||||||
|
JobID: jobID,
|
||||||
|
RootWithoutClientComponent: rootDataset,
|
||||||
|
AppendClientIdentity: true, // !
|
||||||
|
UpdateLastReceivedHold: true,
|
||||||
}
|
}
|
||||||
|
if err := m.receiverConfig.Validate(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type modeSource struct {
|
type modeSource struct {
|
||||||
fsfilter zfs.DatasetFilter
|
senderConfig *endpoint.SenderConfig
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper *snapper.PeriodicOrManual
|
||||||
}
|
}
|
||||||
|
|
||||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource, err error) {
|
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
||||||
// FIXME exact dedup of modePush
|
// FIXME exact dedup of modePush
|
||||||
m = &modeSource{}
|
m = &modeSource{}
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
|
}
|
||||||
|
m.senderConfig = &endpoint.SenderConfig{
|
||||||
|
FSF: fsf,
|
||||||
|
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
|
||||||
|
JobID: jobID,
|
||||||
}
|
}
|
||||||
m.fsfilter = fsf
|
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
return nil, errors.Wrap(err, "cannot build snapper")
|
||||||
@@ -80,7 +94,7 @@ func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource
|
|||||||
func (m *modeSource) Type() Type { return TypeSource }
|
func (m *modeSource) Type() Type { return TypeSource }
|
||||||
|
|
||||||
func (m *modeSource) Handler() rpc.Handler {
|
func (m *modeSource) Handler() rpc.Handler {
|
||||||
return endpoint.NewSender(m.fsfilter)
|
return endpoint.NewSender(*m.senderConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modeSource) RunPeriodic(ctx context.Context) {
|
func (m *modeSource) RunPeriodic(ctx context.Context) {
|
||||||
@@ -91,9 +105,25 @@ func (m *modeSource) SnapperReport() *snapper.Report {
|
|||||||
return m.snapper.Report()
|
return m.snapper.Report()
|
||||||
}
|
}
|
||||||
|
|
||||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) {
|
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}) (s *PassiveSide, err error) {
|
||||||
|
|
||||||
|
s = &PassiveSide{}
|
||||||
|
|
||||||
|
s.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := configJob.(type) {
|
||||||
|
case *config.SinkJob:
|
||||||
|
s.mode, err = modeSinkFromConfig(g, v, s.name) // shadow
|
||||||
|
case *config.SourceJob:
|
||||||
|
s.mode, err = modeSourceFromConfig(g, v, s.name) // shadow
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err // no wrapping necessary
|
||||||
|
}
|
||||||
|
|
||||||
s = &PassiveSide{mode: mode, name: in.Name}
|
|
||||||
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); 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")
|
||||||
}
|
}
|
||||||
@@ -101,7 +131,7 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passive
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *PassiveSide) Name() string { return j.name }
|
func (j *PassiveSide) Name() string { return j.name.String() }
|
||||||
|
|
||||||
type PassiveStatus struct {
|
type PassiveStatus struct {
|
||||||
Snapper *snapper.Report
|
Snapper *snapper.Report
|
||||||
@@ -120,18 +150,29 @@ func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
|
|||||||
_ = j.mode.(*modeSource) // make sure we didn't introduce a new job type
|
_ = j.mode.(*modeSource) // make sure we didn't introduce a new job type
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return sink.rootDataset.Copy(), true
|
return sink.receiverConfig.RootWithoutClientComponent.Copy(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PassiveSide) SenderConfig() *endpoint.SenderConfig {
|
||||||
|
source, ok := j.mode.(*modeSource)
|
||||||
|
if !ok {
|
||||||
|
_ = j.mode.(*modeSink) // make sure we didn't introduce a new job type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return source.senderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||||
|
|
||||||
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, cancel := context.WithCancel(ctx) // shadowing
|
ctx, endTask := trace.WithTask(ctx, "periodic") // shadowing
|
||||||
|
defer endTask()
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go j.mode.RunPeriodic(ctx)
|
go j.mode.RunPeriodic(ctx)
|
||||||
}
|
}
|
||||||
@@ -141,8 +182,14 @@ 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) context.Context {
|
ctxInterceptor := func(handlerCtx context.Context, info rpc.HandlerContextInterceptorData, handler func(ctx context.Context)) {
|
||||||
return logging.WithSubsystemLoggers(handlerCtx, log)
|
// the handlerCtx is clean => need to inherit logging and tracing config from job context
|
||||||
|
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
|
||||||
|
|||||||
+28
-15
@@ -7,11 +7,11 @@ 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/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"
|
||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SnapJob struct {
|
type SnapJob struct {
|
||||||
name string
|
name endpoint.JobID
|
||||||
fsfilter zfs.DatasetFilter
|
fsfilter zfs.DatasetFilter
|
||||||
snapper *snapper.PeriodicOrManual
|
snapper *snapper.PeriodicOrManual
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ type SnapJob struct {
|
|||||||
pruner *pruner.Pruner
|
pruner *pruner.Pruner
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *SnapJob) Name() string { return j.name }
|
func (j *SnapJob) Name() string { return j.name.String() }
|
||||||
|
|
||||||
func (j *SnapJob) Type() Type { return TypeSnap }
|
func (j *SnapJob) Type() Type { return TypeSnap }
|
||||||
|
|
||||||
@@ -39,20 +39,23 @@ func snapJobFromConfig(g *config.Global, in *config.SnapJob) (j *SnapJob, err er
|
|||||||
j = &SnapJob{}
|
j = &SnapJob{}
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
}
|
}
|
||||||
j.fsfilter = fsf
|
j.fsfilter = fsf
|
||||||
|
|
||||||
if j.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
if j.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")
|
||||||
}
|
}
|
||||||
j.name = in.Name
|
j.name, err = endpoint.MakeJobID(in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid job name")
|
||||||
|
}
|
||||||
j.promPruneSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
j.promPruneSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
Namespace: "zrepl",
|
Namespace: "zrepl",
|
||||||
Subsystem: "pruning",
|
Subsystem: "pruning",
|
||||||
Name: "time",
|
Name: "time",
|
||||||
Help: "seconds spent in pruner",
|
Help: "seconds spent in pruner",
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
|
||||||
}, []string{"prune_side"})
|
}, []string{"prune_side"})
|
||||||
j.prunerFactory, err = pruner.NewLocalPrunerFactory(in.Pruning, j.promPruneSecs)
|
j.prunerFactory, err = pruner.NewLocalPrunerFactory(in.Pruning, j.promPruneSecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -84,16 +87,21 @@ func (j *SnapJob) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
go j.snapper.Run(ctx, periodicDone)
|
periodicCtx, endTask := trace.WithTask(ctx, "snapshotting")
|
||||||
|
defer endTask()
|
||||||
|
go j.snapper.Run(periodicCtx, periodicDone)
|
||||||
|
|
||||||
invocationCount := 0
|
invocationCount := 0
|
||||||
outer:
|
outer:
|
||||||
@@ -108,8 +116,10 @@ outer:
|
|||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
invLog := log.WithField("invocation", invocationCount)
|
|
||||||
j.doPrune(WithLogger(ctx, invLog))
|
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
|
||||||
|
j.doPrune(invocationCtx)
|
||||||
|
endSpan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +143,6 @@ type alwaysUpToDateReplicationCursorHistory struct {
|
|||||||
var _ pruner.History = (*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) {
|
||||||
if req.GetGet() == nil {
|
|
||||||
return nil, fmt.Errorf("unsupported ReplicationCursor request: SnapJob only supports GETting a (faked) cursor")
|
|
||||||
}
|
|
||||||
fsvReq := &pdu.ListFilesystemVersionsReq{
|
fsvReq := &pdu.ListFilesystemVersionsReq{
|
||||||
Filesystem: req.GetFilesystem(),
|
Filesystem: req.GetFilesystem(),
|
||||||
}
|
}
|
||||||
@@ -160,9 +167,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(j.fsfilter)
|
JobID: j.name,
|
||||||
|
FSF: j.fsfilter,
|
||||||
|
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
|
||||||
|
Encrypt: &zfs.NilBool{B: true},
|
||||||
|
})
|
||||||
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
|
||||||
log.Info("start pruning")
|
log.Info("start pruning")
|
||||||
j.pruner.Prune()
|
j.pruner.Prune()
|
||||||
|
|||||||
@@ -9,19 +9,10 @@ import (
|
|||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
"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) {
|
||||||
@@ -69,8 +60,10 @@ 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"
|
||||||
SubsyEndpoint Subsystem = "endpoint"
|
SubsysEndpoint Subsystem = "endpoint"
|
||||||
SubsysPruning Subsystem = "pruning"
|
SubsysPruning Subsystem = "pruning"
|
||||||
SubsysSnapshot Subsystem = "snapshot"
|
SubsysSnapshot Subsystem = "snapshot"
|
||||||
SubsysHooks Subsystem = "hook"
|
SubsysHooks Subsystem = "hook"
|
||||||
@@ -79,29 +72,104 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
|
var AllSubsystems = []Subsystem{
|
||||||
ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
SubsysMeta,
|
||||||
ctx = driver.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
|
SubsysJob,
|
||||||
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsyEndpoint))
|
SubsysReplication,
|
||||||
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
|
SubsysEndpoint,
|
||||||
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
SubsysPruning,
|
||||||
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
|
SubsysSnapshot,
|
||||||
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
SubsysHooks,
|
||||||
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
SubsysTransport,
|
||||||
ctx = rpc.WithLoggers(ctx,
|
SubsysTransportMux,
|
||||||
rpc.Loggers{
|
SubsysRPC,
|
||||||
General: log.WithField(SubsysField, SubsysRPC),
|
SubsysRPCControl,
|
||||||
Control: log.WithField(SubsysField, SubsysRPCControl),
|
SubsysRPCData,
|
||||||
Data: log.WithField(SubsysField, SubsysRPCData),
|
SubsysZFSCmd,
|
||||||
},
|
SubsysTraceData,
|
||||||
)
|
SubsysPlatformtest,
|
||||||
return ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogSubsystem(log logger.Logger, subsys Subsystem) logger.Logger {
|
type injectedField struct {
|
||||||
return log.ReplaceField(SubsysField, subsys)
|
field string
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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,6 +22,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
JobField string = "job"
|
JobField string = "job"
|
||||||
SubsysField string = "subsystem"
|
SubsysField string = "subsystem"
|
||||||
|
SpanField string = "span"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetadataFlags int64
|
type MetadataFlags int64
|
||||||
@@ -85,7 +86,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}
|
prefixFields := []string{JobField, SubsysField, SpanField}
|
||||||
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]
|
||||||
@@ -174,8 +175,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, 2)
|
prefixed := make(map[string]bool, 3)
|
||||||
prefix := []string{JobField, SubsysField}
|
prefix := []string{JobField, SubsysField, SpanField}
|
||||||
for _, pf := range prefix {
|
for _, pf := range prefix {
|
||||||
v, ok := e.Fields[pf]
|
v, ok := e.Fields[pf]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -211,7 +212,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 unsuuported value type Go type")
|
return errors.Wrap(err, "cannot encode unsupported 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 previos is in io.Copy()
|
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previous is in io.Copy()
|
||||||
|
|
||||||
o := &TCPOutlet{
|
o := &TCPOutlet{
|
||||||
formatter: formatter,
|
formatter: formatter,
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
// 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"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 {
|
||||||
|
id string
|
||||||
|
annotation string
|
||||||
|
parentTask *traceNode
|
||||||
|
|
||||||
|
mtx chainlock.L
|
||||||
|
|
||||||
|
activeChildTasks int32 // only for task nodes, insignificant for span nodes
|
||||||
|
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 }
|
||||||
|
|
||||||
|
// 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 parentTask != nil {
|
||||||
|
this.parentTask.activeChildTasks++
|
||||||
|
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 {
|
||||||
|
panic(errors.Wrapf(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks", this.activeChildTasks))
|
||||||
|
}
|
||||||
|
|
||||||
|
// support idempotent task ends
|
||||||
|
if !this.endedAt.IsZero() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
this.endedAt = time.Now()
|
||||||
|
|
||||||
|
if this.parentTask != nil {
|
||||||
|
this.parentTask.activeChildTasks--
|
||||||
|
if this.parentTask.activeChildTasks < 0 {
|
||||||
|
panic("impl error: parent task with negative activeChildTasks count")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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{},
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
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()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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
|
||||||
|
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)
|
||||||
|
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 ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const debugEnabled = false
|
||||||
|
|
||||||
|
func debug(format string, args ...interface{}) {
|
||||||
|
if !debugEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
)
|
||||||
|
|
||||||
|
var genIdPRNG = rand.New(rand.NewSource(1))
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
genIdPRNG.Seed(time.Now().UnixNano())
|
||||||
|
genIdPRNG.Seed(int64(os.Getpid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
var genIdNumBytes = envconst.Int("ZREPL_TRACE_ID_NUM_BYTES", 3)
|
||||||
|
|
||||||
|
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 := genIdPRNG.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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
+4
-2
@@ -1,6 +1,8 @@
|
|||||||
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"
|
||||||
)
|
)
|
||||||
@@ -10,7 +12,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(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
return Run(subcommand.Config())
|
return Run(ctx, subcommand.Config())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ 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 {
|
||||||
@@ -64,6 +68,8 @@ 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 {
|
||||||
|
|||||||
+17
-7
@@ -10,20 +10,24 @@ 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/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
||||||
|
"github.com/zrepl/zrepl/util/tcpsock"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type prometheusJob struct {
|
type prometheusJob struct {
|
||||||
listen string
|
listen string
|
||||||
|
freeBind bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
||||||
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &prometheusJob{in.Listen}, nil
|
return &prometheusJob{in.Listen, in.ListenFreeBind}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var prom struct {
|
var prom struct {
|
||||||
@@ -46,6 +50,8 @@ func (j *prometheusJob) Status() *job.Status { return &job.Status{Type: job.Type
|
|||||||
|
|
||||||
func (j *prometheusJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
func (j *prometheusJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
|
||||||
|
|
||||||
|
func (j *prometheusJob) SenderConfig() *endpoint.SenderConfig { return nil }
|
||||||
|
|
||||||
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
|
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
|
||||||
|
|
||||||
func (j *prometheusJob) Run(ctx context.Context) {
|
func (j *prometheusJob) Run(ctx context.Context) {
|
||||||
@@ -60,9 +66,10 @@ func (j *prometheusJob) Run(ctx context.Context) {
|
|||||||
|
|
||||||
log := job.GetLogger(ctx)
|
log := job.GetLogger(ctx)
|
||||||
|
|
||||||
l, err := net.Listen("tcp", j.listen)
|
l, err := tcpsock.Listen(j.listen, j.freeBind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("cannot listen")
|
log.WithError(err).Error("cannot listen")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@@ -80,16 +87,19 @@ 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(jobName string) prometheusJobOutlet {
|
func newPrometheusLogOutlet() prometheusJobOutlet {
|
||||||
return prometheusJobOutlet{jobName}
|
return prometheusJobOutlet{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
|
func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
|
||||||
prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc()
|
jobFieldVal, ok := entry.Fields[logging.JobField].(string)
|
||||||
|
if !ok {
|
||||||
|
jobFieldVal = "_nojobid"
|
||||||
|
}
|
||||||
|
prom.taskLogEntries.WithLabelValues(jobFieldVal, entry.Level.String()).Inc()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Code generated by "enumer -type=FSState -json"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package pruner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _FSStateName = "FSStateInitializedFSStatePlanningFSStatePlanErrFSStateExecutingFSStateExecuteErrFSStateExecuteSuccess"
|
||||||
|
|
||||||
|
var _FSStateIndex = [...]uint8{0, 18, 33, 47, 63, 80, 101}
|
||||||
|
|
||||||
|
func (i FSState) String() string {
|
||||||
|
if i < 0 || i >= FSState(len(_FSStateIndex)-1) {
|
||||||
|
return fmt.Sprintf("FSState(%d)", i)
|
||||||
|
}
|
||||||
|
return _FSStateName[_FSStateIndex[i]:_FSStateIndex[i+1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _FSStateValues = []FSState{0, 1, 2, 3, 4, 5}
|
||||||
|
|
||||||
|
var _FSStateNameToValueMap = map[string]FSState{
|
||||||
|
_FSStateName[0:18]: 0,
|
||||||
|
_FSStateName[18:33]: 1,
|
||||||
|
_FSStateName[33:47]: 2,
|
||||||
|
_FSStateName[47:63]: 3,
|
||||||
|
_FSStateName[63:80]: 4,
|
||||||
|
_FSStateName[80:101]: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSStateString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func FSStateString(s string) (FSState, error) {
|
||||||
|
if val, ok := _FSStateNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to FSState values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSStateValues returns all values of the enum
|
||||||
|
func FSStateValues() []FSState {
|
||||||
|
return _FSStateValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAFSState returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i FSState) IsAFSState() bool {
|
||||||
|
for _, v := range _FSStateValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for FSState
|
||||||
|
func (i FSState) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(i.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface for FSState
|
||||||
|
func (i *FSState) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
return fmt.Errorf("FSState should be a string, got %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
*i, err = FSStateString(s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
package pruner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/pruning"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pruner struct {
|
||||||
|
fsfilter endpoint.FSFilter
|
||||||
|
jid endpoint.JobID
|
||||||
|
side Side
|
||||||
|
keepRules []pruning.KeepRule
|
||||||
|
|
||||||
|
// all channels consumed by the run loop
|
||||||
|
reportReqs chan reportRequest
|
||||||
|
stopReqs chan stopRequest
|
||||||
|
done chan struct{}
|
||||||
|
fsListRes chan fsListRes
|
||||||
|
|
||||||
|
state State
|
||||||
|
|
||||||
|
listFilesystemsError error // only in state StateListFilesystemsError
|
||||||
|
fsPruners []*FSPruner // only in state StateFanOutFilesystems
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate enumer -type=State -json
|
||||||
|
type State int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateInitialized State = iota
|
||||||
|
StateListFilesystems
|
||||||
|
StateListFilesystemsError
|
||||||
|
StateFanOutFilesystems
|
||||||
|
StateDone
|
||||||
|
)
|
||||||
|
|
||||||
|
type Report struct {
|
||||||
|
State State
|
||||||
|
ListFilesystemsError error // only valid in StateListFilesystemsError
|
||||||
|
Filesystems []*FSReport // valid from StateFanOutFilesystems
|
||||||
|
}
|
||||||
|
|
||||||
|
type reportRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
reply chan *Report
|
||||||
|
}
|
||||||
|
|
||||||
|
type runRequest struct {
|
||||||
|
complete chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type stopRequest struct {
|
||||||
|
complete chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fsListRes struct {
|
||||||
|
filesystems []*zfs.DatasetPath
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Side interface {
|
||||||
|
// may return both nil, indicating there is no replication position
|
||||||
|
GetReplicationPosition(ctx context.Context, fs string) (*zfs.FilesystemVersion, error)
|
||||||
|
isSide() Side
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPruner(fsfilter endpoint.FSFilter, jid endpoint.JobID, side Side, keepRules []pruning.KeepRule) *Pruner {
|
||||||
|
return &Pruner{
|
||||||
|
fsfilter,
|
||||||
|
jid,
|
||||||
|
side,
|
||||||
|
keepRules,
|
||||||
|
make(chan reportRequest),
|
||||||
|
make(chan stopRequest),
|
||||||
|
make(chan struct{}),
|
||||||
|
make(chan fsListRes),
|
||||||
|
StateInitialized,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pruner) Run(ctx context.Context) *Report {
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if p.state != StateInitialized {
|
||||||
|
panic("Run can onl[y be called once")
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fss, err := zfs.ZFSListMapping(ctx, p.fsfilter)
|
||||||
|
p.fsListRes <- fsListRes{fss, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case res := <-p.fsListRes:
|
||||||
|
if res.err != nil {
|
||||||
|
p.state = StateListFilesystemsError
|
||||||
|
p.listFilesystemsError = res.err
|
||||||
|
close(p.done)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
p.state = StateFanOutFilesystems
|
||||||
|
|
||||||
|
p.fsPruners = make([]*FSPruner, len(res.filesystems))
|
||||||
|
_, add, end := trace.WithTaskGroup(ctx, "pruner-fan-out-fs")
|
||||||
|
for i, fs := range res.filesystems {
|
||||||
|
p.fsPruners[i] = NewFSPruner(p.jid, p.side, p.keepRules, fs)
|
||||||
|
add(func(ctx context.Context) {
|
||||||
|
p.fsPruners[i].Run(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
end()
|
||||||
|
close(p.done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
case req := <-p.stopReqs:
|
||||||
|
cancel()
|
||||||
|
go func() {
|
||||||
|
<-p.done
|
||||||
|
close(req.complete)
|
||||||
|
}()
|
||||||
|
case req := <-p.reportReqs:
|
||||||
|
req.reply <- p.report(req.ctx)
|
||||||
|
case <-p.done:
|
||||||
|
p.state = StateDone
|
||||||
|
return p.report(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pruner) Report(ctx context.Context) *Report {
|
||||||
|
req := reportRequest{
|
||||||
|
ctx: ctx,
|
||||||
|
reply: make(chan *Report, 1),
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case p.reportReqs <- req:
|
||||||
|
return <-req.reply
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case <-p.done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pruner) report(ctx context.Context) *Report {
|
||||||
|
fsreports := make([]*FSReport, len(p.fsPruners))
|
||||||
|
for i := range fsreports {
|
||||||
|
fsreports[i] = p.fsPruners[i].report()
|
||||||
|
}
|
||||||
|
return &Report{
|
||||||
|
State: p.state,
|
||||||
|
ListFilesystemsError: p.listFilesystemsError,
|
||||||
|
Filesystems: fsreports,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements pruning.Snapshot
|
||||||
|
type snapshot struct {
|
||||||
|
replicated bool
|
||||||
|
stepHolds []pruning.StepHold
|
||||||
|
zfs.FilesystemVersion
|
||||||
|
|
||||||
|
state SnapState
|
||||||
|
destroyOp *zfs.DestroySnapOp
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate enumer -type=SnapState -json
|
||||||
|
type SnapState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SnapStateInitialized SnapState = iota
|
||||||
|
SnapStateKeeping
|
||||||
|
SnapStateDeletePending
|
||||||
|
SnapStateDeleteAttempted
|
||||||
|
)
|
||||||
|
|
||||||
|
// implements pruning.StepHold
|
||||||
|
type stepHold struct {
|
||||||
|
endpoint.Abstraction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s snapshot) Replicated() bool { return s.replicated }
|
||||||
|
func (s snapshot) StepHolds() []pruning.StepHold { return s.stepHolds }
|
||||||
|
|
||||||
|
func (s stepHold) GetJobID() endpoint.JobID { return *s.Abstraction.GetJobID() }
|
||||||
|
|
||||||
|
type FSPruner struct {
|
||||||
|
jid endpoint.JobID
|
||||||
|
side Side
|
||||||
|
keepRules []pruning.KeepRule
|
||||||
|
fsp *zfs.DatasetPath
|
||||||
|
|
||||||
|
state FSState
|
||||||
|
|
||||||
|
// all channels consumed by the run loop
|
||||||
|
planned chan fsPlanRes
|
||||||
|
executed chan fsExecuteRes
|
||||||
|
done chan struct{}
|
||||||
|
reportReqs chan fsReportReq
|
||||||
|
|
||||||
|
keepList []*snapshot // valid in FSStateExecuting and forward
|
||||||
|
destroyList []*snapshot // valid in FSStateExecuting and forward, field .destroyOp is invalid until FSStateExecuting is left
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type fsPlanRes struct {
|
||||||
|
keepList []*snapshot
|
||||||
|
destroyList []*snapshot
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type fsExecuteRes struct {
|
||||||
|
completedDestroyOps []*zfs.DestroySnapOp // same len() as FSPruner.destroyList
|
||||||
|
}
|
||||||
|
|
||||||
|
type fsReportReq struct {
|
||||||
|
res chan *FSReport
|
||||||
|
}
|
||||||
|
|
||||||
|
type FSReport struct {
|
||||||
|
State FSState
|
||||||
|
KeepList []*SnapReport
|
||||||
|
Destroy []*SnapReport
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapReport struct {
|
||||||
|
State SnapState
|
||||||
|
Name string
|
||||||
|
Replicated bool
|
||||||
|
StepHoldCount int
|
||||||
|
DestroyError error
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate enumer -type=FSState -json
|
||||||
|
type FSState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FSStateInitialized FSState = iota
|
||||||
|
FSStatePlanning
|
||||||
|
FSStatePlanErr
|
||||||
|
FSStateExecuting
|
||||||
|
FSStateExecuteErr
|
||||||
|
FSStateExecuteSuccess
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s FSState) IsTerminal() bool {
|
||||||
|
return s == FSStatePlanErr || s == FSStateExecuteErr || s == FSStateExecuteSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFSPruner(jid endpoint.JobID, side Side, keepRules []pruning.KeepRule, fsp *zfs.DatasetPath) *FSPruner {
|
||||||
|
return &FSPruner{
|
||||||
|
jid, side, keepRules, fsp,
|
||||||
|
FSStateInitialized,
|
||||||
|
make(chan fsPlanRes),
|
||||||
|
make(chan fsExecuteRes),
|
||||||
|
make(chan struct{}),
|
||||||
|
make(chan fsReportReq),
|
||||||
|
nil, nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FSPruner) Run(ctx context.Context) *FSReport {
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
}()
|
||||||
|
|
||||||
|
p.state = FSStatePlanning
|
||||||
|
|
||||||
|
go func() { p.planned <- p.plan(ctx) }()
|
||||||
|
|
||||||
|
out:
|
||||||
|
for !p.state.IsTerminal() {
|
||||||
|
select {
|
||||||
|
case res := <-p.planned:
|
||||||
|
|
||||||
|
if res.err != nil {
|
||||||
|
p.state = FSStatePlanErr
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.state = FSStateExecuting
|
||||||
|
p.keepList = res.keepList
|
||||||
|
p.destroyList = res.destroyList
|
||||||
|
|
||||||
|
go func() { p.executed <- p.execute(ctx, p.destroyList) }()
|
||||||
|
|
||||||
|
case res := <-p.executed:
|
||||||
|
|
||||||
|
if len(res.completedDestroyOps) != len(p.destroyList) {
|
||||||
|
panic("impl error: completedDestroyOps is a vector corresponding to entries in p.destroyList")
|
||||||
|
}
|
||||||
|
|
||||||
|
var erronous []*zfs.DestroySnapOp
|
||||||
|
for i, op := range res.completedDestroyOps {
|
||||||
|
if *op.ErrOut != nil {
|
||||||
|
erronous = append(erronous, op)
|
||||||
|
}
|
||||||
|
p.destroyList[i].destroyOp = op
|
||||||
|
p.destroyList[i].state = SnapStateDeleteAttempted
|
||||||
|
}
|
||||||
|
if len(erronous) > 0 {
|
||||||
|
p.state = FSStateExecuteErr
|
||||||
|
} else {
|
||||||
|
p.state = FSStateExecuteSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
close(p.done)
|
||||||
|
|
||||||
|
case <-p.reportReqs:
|
||||||
|
panic("unimp")
|
||||||
|
case <-p.done:
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO render last FS report
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FSPruner) plan(ctx context.Context) fsPlanRes {
|
||||||
|
fs := p.fsp.ToString()
|
||||||
|
vs, err := zfs.ZFSListFilesystemVersions(ctx, p.fsp, zfs.ListFilesystemVersionsOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fsPlanRes{err: errors.Wrap(err, "list filesystem versions")}
|
||||||
|
}
|
||||||
|
|
||||||
|
allJobsStepHolds, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||||
|
FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
|
||||||
|
FS: &fs,
|
||||||
|
},
|
||||||
|
What: endpoint.AbstractionTypeSet{
|
||||||
|
endpoint.AbstractionStepHold: true,
|
||||||
|
},
|
||||||
|
Concurrency: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fsPlanRes{err: errors.Wrap(err, "list abstractions")}
|
||||||
|
}
|
||||||
|
if len(absErrs) > 0 {
|
||||||
|
logging.GetLogger(ctx, logging.SubsysPruning).WithError(endpoint.ListAbstractionsErrors(absErrs)).
|
||||||
|
Error("error listing some step holds, prune attempt might fail with 'dataset is busy' errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
repPos, err := p.side.GetReplicationPosition(ctx, p.fsp.ToString())
|
||||||
|
if err != nil {
|
||||||
|
return fsPlanRes{err: errors.Wrap(err, "get replication position")}
|
||||||
|
}
|
||||||
|
|
||||||
|
vsAsSnaps := make([]pruning.Snapshot, len(vs))
|
||||||
|
for i := range vs {
|
||||||
|
var repPosCreateTxgOrZero uint64
|
||||||
|
if repPos != nil {
|
||||||
|
repPosCreateTxgOrZero = repPos.GetCreateTXG()
|
||||||
|
}
|
||||||
|
s := &snapshot{
|
||||||
|
state: SnapStateInitialized,
|
||||||
|
FilesystemVersion: vs[i],
|
||||||
|
replicated: vs[i].GetCreateTXG() <= repPosCreateTxgOrZero,
|
||||||
|
}
|
||||||
|
for _, h := range allJobsStepHolds {
|
||||||
|
if zfs.FilesystemVersionEqualIdentity(vs[i], h.GetFilesystemVersion()) {
|
||||||
|
s.stepHolds = append(s.stepHolds, stepHold{h})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vsAsSnaps[i] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
downcastToSnapshots := func(l []pruning.Snapshot) (r []*snapshot) {
|
||||||
|
r = make([]*snapshot, len(l))
|
||||||
|
for i, e := range l {
|
||||||
|
r[i] = e.(*snapshot)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
pruningResult := pruning.PruneSnapshots(vsAsSnaps, p.keepRules)
|
||||||
|
remove, keep := downcastToSnapshots(pruningResult.Remove), downcastToSnapshots(pruningResult.Keep)
|
||||||
|
if len(remove)+len(keep) != len(vsAsSnaps) {
|
||||||
|
for _, s := range vsAsSnaps {
|
||||||
|
r, _ := json.MarshalIndent(s.(*snapshot).report(), "", " ")
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", string(r))
|
||||||
|
}
|
||||||
|
panic("indecisive")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range remove {
|
||||||
|
s.state = SnapStateDeletePending
|
||||||
|
}
|
||||||
|
for _, s := range keep {
|
||||||
|
s.state = SnapStateKeeping
|
||||||
|
}
|
||||||
|
|
||||||
|
return fsPlanRes{keepList: keep, destroyList: remove, err: nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FSPruner) execute(ctx context.Context, destroyList []*snapshot) fsExecuteRes {
|
||||||
|
ops := make([]*zfs.DestroySnapOp, len(destroyList))
|
||||||
|
for i, fsv := range p.destroyList {
|
||||||
|
ops[i] = &zfs.DestroySnapOp{
|
||||||
|
Filesystem: p.fsp.ToString(),
|
||||||
|
Name: fsv.GetName(),
|
||||||
|
ErrOut: new(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zfs.ZFSDestroyFilesystemVersions(ctx, ops)
|
||||||
|
|
||||||
|
return fsExecuteRes{completedDestroyOps: ops}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FSPruner) report() *FSReport {
|
||||||
|
return &FSReport{
|
||||||
|
State: p.state,
|
||||||
|
KeepList: p.reportRenderSnapReports(p.keepList),
|
||||||
|
Destroy: p.reportRenderSnapReports(p.destroyList),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FSPruner) reportRenderSnapReports(l []*snapshot) (r []*SnapReport) {
|
||||||
|
r = make([]*SnapReport, len(l))
|
||||||
|
for i := range l {
|
||||||
|
r[i] = l[i].report()
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *snapshot) report() *SnapReport {
|
||||||
|
var snapErr error
|
||||||
|
if s.state == SnapStateDeleteAttempted {
|
||||||
|
if *s.destroyOp.ErrOut != nil {
|
||||||
|
snapErr = (*s.destroyOp.ErrOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &SnapReport{
|
||||||
|
State: s.state,
|
||||||
|
Name: s.Name,
|
||||||
|
Replicated: s.Replicated(),
|
||||||
|
StepHoldCount: len(s.stepHolds),
|
||||||
|
DestroyError: snapErr,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package pruner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SideSender struct {
|
||||||
|
jobID endpoint.JobID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSideSender(jid endpoint.JobID) *SideSender {
|
||||||
|
return &SideSender{jid}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SideSender) isSide() Side { return nil }
|
||||||
|
|
||||||
|
var _ Side = (*SideSender)(nil)
|
||||||
|
|
||||||
|
func (s *SideSender) GetReplicationPosition(ctx context.Context, fs string) (*zfs.FilesystemVersion, error) {
|
||||||
|
if fs == "" {
|
||||||
|
panic("must not pass zero value for fs")
|
||||||
|
}
|
||||||
|
return endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs, s.jobID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// Code generated by "enumer -type=SnapState -json"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package pruner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _SnapStateName = "SnapStateInitializedSnapStateKeepingSnapStateDeletePendingSnapStateDeleteAttempted"
|
||||||
|
|
||||||
|
var _SnapStateIndex = [...]uint8{0, 20, 36, 58, 82}
|
||||||
|
|
||||||
|
func (i SnapState) String() string {
|
||||||
|
if i < 0 || i >= SnapState(len(_SnapStateIndex)-1) {
|
||||||
|
return fmt.Sprintf("SnapState(%d)", i)
|
||||||
|
}
|
||||||
|
return _SnapStateName[_SnapStateIndex[i]:_SnapStateIndex[i+1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _SnapStateValues = []SnapState{0, 1, 2, 3}
|
||||||
|
|
||||||
|
var _SnapStateNameToValueMap = map[string]SnapState{
|
||||||
|
_SnapStateName[0:20]: 0,
|
||||||
|
_SnapStateName[20:36]: 1,
|
||||||
|
_SnapStateName[36:58]: 2,
|
||||||
|
_SnapStateName[58:82]: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SnapStateString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func SnapStateString(s string) (SnapState, error) {
|
||||||
|
if val, ok := _SnapStateNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to SnapState values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SnapStateValues returns all values of the enum
|
||||||
|
func SnapStateValues() []SnapState {
|
||||||
|
return _SnapStateValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsASnapState returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i SnapState) IsASnapState() bool {
|
||||||
|
for _, v := range _SnapStateValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for SnapState
|
||||||
|
func (i SnapState) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(i.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface for SnapState
|
||||||
|
func (i *SnapState) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
return fmt.Errorf("SnapState should be a string, got %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
*i, err = SnapStateString(s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Code generated by "enumer -type=State -json"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//
|
||||||
|
package pruner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _StateName = "StateInitializedStateListFilesystemsStateListFilesystemsErrorStateFanOutFilesystemsStateDone"
|
||||||
|
|
||||||
|
var _StateIndex = [...]uint8{0, 16, 36, 61, 83, 92}
|
||||||
|
|
||||||
|
func (i State) String() string {
|
||||||
|
if i < 0 || i >= State(len(_StateIndex)-1) {
|
||||||
|
return fmt.Sprintf("State(%d)", i)
|
||||||
|
}
|
||||||
|
return _StateName[_StateIndex[i]:_StateIndex[i+1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _StateValues = []State{0, 1, 2, 3, 4}
|
||||||
|
|
||||||
|
var _StateNameToValueMap = map[string]State{
|
||||||
|
_StateName[0:16]: 0,
|
||||||
|
_StateName[16:36]: 1,
|
||||||
|
_StateName[36:61]: 2,
|
||||||
|
_StateName[61:83]: 3,
|
||||||
|
_StateName[83:92]: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func StateString(s string) (State, error) {
|
||||||
|
if val, ok := _StateNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to State values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateValues returns all values of the enum
|
||||||
|
func StateValues() []State {
|
||||||
|
return _StateValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAState returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i State) IsAState() bool {
|
||||||
|
for _, v := range _StateValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for State
|
||||||
|
func (i State) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(i.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface for State
|
||||||
|
func (i *State) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
return fmt.Errorf("State should be a string, got %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
*i, err = StateString(s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
+64
-48
@@ -12,22 +12,22 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
type History interface {
|
type Endpoint interface {
|
||||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
|
||||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
|
|
||||||
type Target interface {
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
|
||||||
|
type Target interface {
|
||||||
|
Endpoint
|
||||||
DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error)
|
DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,27 +35,24 @@ type Logger = logger.Logger
|
|||||||
|
|
||||||
type contextKey int
|
type contextKey int
|
||||||
|
|
||||||
const contextKeyLogger contextKey = 0
|
const (
|
||||||
|
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 {
|
||||||
if l, ok := ctx.Value(contextKeyLogger).(Logger); ok {
|
pruneSide := ctx.Value(contextKeyPruneSide).(string)
|
||||||
return l
|
return logging.GetLogger(ctx, logging.SubsysPruning).WithField("prune_side", pruneSide)
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
target Target
|
target Target
|
||||||
receiver History
|
sender, receiver Endpoint
|
||||||
rules []pruning.KeepRule
|
rules []pruning.KeepRule
|
||||||
retryWait time.Duration
|
retryWait time.Duration
|
||||||
considerSnapAtCursorReplicated bool
|
considerSnapAtCursorReplicated bool
|
||||||
promPruneSecs prometheus.Observer
|
convertAnyStepHoldToStepBookmark bool
|
||||||
|
promPruneSecs prometheus.Observer
|
||||||
}
|
}
|
||||||
|
|
||||||
type Pruner struct {
|
type Pruner struct {
|
||||||
@@ -73,11 +70,12 @@ type Pruner struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PrunerFactory struct {
|
type PrunerFactory struct {
|
||||||
senderRules []pruning.KeepRule
|
senderRules []pruning.KeepRule
|
||||||
receiverRules []pruning.KeepRule
|
receiverRules []pruning.KeepRule
|
||||||
retryWait time.Duration
|
retryWait time.Duration
|
||||||
considerSnapAtCursorReplicated bool
|
considerSnapAtCursorReplicated bool
|
||||||
promPruneSecs *prometheus.HistogramVec
|
convertAnyStepHoldToStepBookmark bool
|
||||||
|
promPruneSecs *prometheus.HistogramVec
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalPrunerFactory struct {
|
type LocalPrunerFactory struct {
|
||||||
@@ -125,25 +123,35 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
|
|||||||
}
|
}
|
||||||
considerSnapAtCursorReplicated = considerSnapAtCursorReplicated || !knr.KeepSnapshotAtCursor
|
considerSnapAtCursorReplicated = considerSnapAtCursorReplicated || !knr.KeepSnapshotAtCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convertAnyStepHoldToStepBookmark := false
|
||||||
|
for _, r := range in.KeepSender {
|
||||||
|
_, ok := r.Ret.(*config.PruneKeepStepHolds)
|
||||||
|
convertAnyStepHoldToStepBookmark = convertAnyStepHoldToStepBookmark || ok
|
||||||
|
}
|
||||||
|
|
||||||
f := &PrunerFactory{
|
f := &PrunerFactory{
|
||||||
senderRules: keepRulesSender,
|
senderRules: keepRulesSender,
|
||||||
receiverRules: keepRulesReceiver,
|
receiverRules: keepRulesReceiver,
|
||||||
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
|
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
|
||||||
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
|
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
|
||||||
promPruneSecs: promPruneSecs,
|
convertAnyStepHoldToStepBookmark: convertAnyStepHoldToStepBookmark,
|
||||||
|
promPruneSecs: promPruneSecs,
|
||||||
}
|
}
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, sender Target, receiver Endpoint) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
WithLogger(ctx, GetLogger(ctx).WithField("prune_side", "sender")),
|
context.WithValue(ctx, contextKeyPruneSide, "sender"),
|
||||||
target,
|
sender,
|
||||||
|
sender,
|
||||||
receiver,
|
receiver,
|
||||||
f.senderRules,
|
f.senderRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
f.considerSnapAtCursorReplicated,
|
f.considerSnapAtCursorReplicated,
|
||||||
|
f.convertAnyStepHoldToStepBookmark,
|
||||||
f.promPruneSecs.WithLabelValues("sender"),
|
f.promPruneSecs.WithLabelValues("sender"),
|
||||||
},
|
},
|
||||||
state: Plan,
|
state: Plan,
|
||||||
@@ -151,15 +159,17 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, re
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, receiver Target, sender Endpoint) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
WithLogger(ctx, GetLogger(ctx).WithField("prune_side", "receiver")),
|
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
|
||||||
target,
|
receiver,
|
||||||
|
sender,
|
||||||
receiver,
|
receiver,
|
||||||
f.receiverRules,
|
f.receiverRules,
|
||||||
f.retryWait,
|
f.retryWait,
|
||||||
false, // senseless here anyways
|
false, // senseless here anyways
|
||||||
|
false, // senseless here anyways
|
||||||
f.promPruneSecs.WithLabelValues("receiver"),
|
f.promPruneSecs.WithLabelValues("receiver"),
|
||||||
},
|
},
|
||||||
state: Plan,
|
state: Plan,
|
||||||
@@ -167,15 +177,17 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
|
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target) *Pruner {
|
||||||
p := &Pruner{
|
p := &Pruner{
|
||||||
args: args{
|
args: args{
|
||||||
ctx,
|
context.WithValue(ctx, contextKeyPruneSide, "local"),
|
||||||
|
target,
|
||||||
|
target,
|
||||||
target,
|
target,
|
||||||
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
|
||||||
|
false, // convertAnyStepHoldToStepBookmark is not relevant for local pruning
|
||||||
f.promPruneSecs.WithLabelValues("local"),
|
f.promPruneSecs.WithLabelValues("local"),
|
||||||
},
|
},
|
||||||
state: Plan,
|
state: Plan,
|
||||||
@@ -344,11 +356,13 @@ func (s snapshot) Replicated() bool { return s.replicated }
|
|||||||
|
|
||||||
func (s snapshot) Date() time.Time { return s.date }
|
func (s snapshot) Date() time.Time { return s.date }
|
||||||
|
|
||||||
|
func (s snapshot) CreateTXG() uint64 { return s.fsv.GetCreateTXG() }
|
||||||
|
|
||||||
func doOneAttempt(a *args, u updater) {
|
func doOneAttempt(a *args, u updater) {
|
||||||
|
|
||||||
ctx, target, receiver := a.ctx, a.target, a.receiver
|
ctx, sender, receiver, target := a.ctx, a.sender, a.receiver, a.target
|
||||||
|
|
||||||
sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
sfssres, err := sender.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u(func(p *Pruner) {
|
u(func(p *Pruner) {
|
||||||
p.state = PlanErr
|
p.state = PlanErr
|
||||||
@@ -410,17 +424,19 @@ tfss_loop:
|
|||||||
|
|
||||||
pfs.snaps = make([]pruning.Snapshot, 0, len(tfsvs))
|
pfs.snaps = make([]pruning.Snapshot, 0, len(tfsvs))
|
||||||
|
|
||||||
|
receiver.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{
|
||||||
|
Filesystem: tfs.Path,
|
||||||
|
})
|
||||||
|
|
||||||
rcReq := &pdu.ReplicationCursorReq{
|
rcReq := &pdu.ReplicationCursorReq{
|
||||||
Filesystem: tfs.Path,
|
Filesystem: tfs.Path,
|
||||||
Op: &pdu.ReplicationCursorReq_Get{
|
|
||||||
Get: &pdu.ReplicationCursorReq_GetOp{},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
rc, err := receiver.ReplicationCursor(ctx, rcReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
|
||||||
continue tfss_loop
|
continue tfss_loop
|
||||||
}
|
}
|
||||||
|
|
||||||
if rc.GetNotexist() {
|
if rc.GetNotexist() {
|
||||||
err := errors.New("replication cursor bookmark does not exist (one successful replication is required before pruning works)")
|
err := errors.New("replication cursor bookmark does not exist (one successful replication is required before pruning works)")
|
||||||
pfsPlanErrAndLog(err, "")
|
pfsPlanErrAndLog(err, "")
|
||||||
|
|||||||
+104
-70
@@ -8,11 +8,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"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/util/envconst"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,7 +47,6 @@ type snapProgress struct {
|
|||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
log Logger
|
|
||||||
prefix string
|
prefix string
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
fsf *filters.DatasetMapFilter
|
fsf *filters.DatasetMapFilter
|
||||||
@@ -101,23 +103,10 @@ func (s State) sf() state {
|
|||||||
type updater func(u func(*Snapper)) State
|
type updater func(u func(*Snapper)) State
|
||||||
type state func(a args, u updater) state
|
type state func(a args, u updater) state
|
||||||
|
|
||||||
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 {
|
func getLogger(ctx context.Context) Logger {
|
||||||
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
return logging.GetLogger(ctx, logging.SubsysSnapshot)
|
||||||
return log
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
||||||
@@ -145,13 +134,12 @@ func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *con
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
getLogger(ctx).Debug("start")
|
getLogger(ctx).Debug("start")
|
||||||
defer getLogger(ctx).Debug("stop")
|
defer getLogger(ctx).Debug("stop")
|
||||||
|
|
||||||
s.args.snapshotsTaken = snapshotsTaken
|
s.args.snapshotsTaken = snapshotsTaken
|
||||||
s.args.ctx = ctx
|
s.args.ctx = ctx
|
||||||
s.args.log = getLogger(ctx)
|
|
||||||
s.args.dryRun = false // for future expansion
|
s.args.dryRun = false // for future expansion
|
||||||
|
|
||||||
u := func(u func(*Snapper)) State {
|
u := func(u func(*Snapper)) State {
|
||||||
@@ -189,7 +177,7 @@ func onErr(err error, u updater) state {
|
|||||||
case Snapshotting:
|
case Snapshotting:
|
||||||
s.state = ErrorWait
|
s.state = ErrorWait
|
||||||
}
|
}
|
||||||
s.args.log.WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
||||||
}).sf()
|
}).sf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +196,7 @@ func syncUp(a args, u updater) state {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return onErr(err, u)
|
return onErr(err, u)
|
||||||
}
|
}
|
||||||
syncPoint, err := findSyncPoint(a.log, fss, a.prefix, a.interval)
|
syncPoint, err := findSyncPoint(a.ctx, fss, a.prefix, a.interval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return onErr(err, u)
|
return onErr(err, u)
|
||||||
}
|
}
|
||||||
@@ -265,18 +253,18 @@ func snapshot(a args, u updater) state {
|
|||||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||||
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
||||||
|
|
||||||
l := a.log.
|
ctx := logging.WithInjectedField(a.ctx, "fs", fs.ToString())
|
||||||
WithField("fs", fs.ToString()).
|
ctx = logging.WithInjectedField(ctx, "snap", snapname)
|
||||||
WithField("snap", snapname)
|
|
||||||
|
|
||||||
hookEnvExtra := hooks.Env{
|
hookEnvExtra := hooks.Env{
|
||||||
hooks.EnvFS: fs.ToString(),
|
hooks.EnvFS: fs.ToString(),
|
||||||
hooks.EnvSnapshot: snapname,
|
hooks.EnvSnapshot: snapname,
|
||||||
}
|
}
|
||||||
|
|
||||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
|
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
|
||||||
|
l := getLogger(ctx)
|
||||||
l.Debug("create snapshot")
|
l.Debug("create snapshot")
|
||||||
err = zfs.ZFSSnapshot(fs, snapname, false) // TODO propagagte context to ZFSSnapshot
|
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot create snapshot")
|
l.WithError(err).Error("cannot create snapshot")
|
||||||
}
|
}
|
||||||
@@ -289,7 +277,7 @@ func snapshot(a args, u updater) state {
|
|||||||
{
|
{
|
||||||
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("unexpected filter error")
|
getLogger(ctx).WithError(err).Error("unexpected filter error")
|
||||||
fsHadErr = true
|
fsHadErr = true
|
||||||
goto updateFSState
|
goto updateFSState
|
||||||
}
|
}
|
||||||
@@ -302,7 +290,7 @@ func snapshot(a args, u updater) state {
|
|||||||
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
||||||
if planErr != nil {
|
if planErr != nil {
|
||||||
fsHadErr = true
|
fsHadErr = true
|
||||||
l.WithError(planErr).Error("cannot create job hook plan")
|
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
|
||||||
goto updateFSState
|
goto updateFSState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,15 +301,14 @@ func snapshot(a args, u updater) state {
|
|||||||
progress.state = SnapStarted
|
progress.state = SnapStarted
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
l := hooks.GetLogger(a.ctx).WithField("fs", fs.ToString()).WithField("snap", snapname)
|
getLogger(ctx).WithField("report", plan.Report().String()).Debug("begin run job plan")
|
||||||
l.WithField("report", plan.Report().String()).Debug("begin run job plan")
|
plan.Run(ctx, a.dryRun)
|
||||||
plan.Run(hooks.WithLogger(a.ctx, l), a.dryRun)
|
|
||||||
planReport = plan.Report()
|
planReport = plan.Report()
|
||||||
fsHadErr = planReport.HadError() // not just fatal errors
|
fsHadErr = planReport.HadError() // not just fatal errors
|
||||||
if fsHadErr {
|
if fsHadErr {
|
||||||
l.WithField("report", planReport.String()).Error("end run job plan with error")
|
getLogger(ctx).WithField("report", planReport.String()).Error("end run job plan with error")
|
||||||
} else {
|
} else {
|
||||||
l.WithField("report", planReport.String()).Info("end run job plan successful")
|
getLogger(ctx).WithField("report", planReport.String()).Info("end run job plan successful")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +328,7 @@ func snapshot(a args, u updater) state {
|
|||||||
case a.snapshotsTaken <- struct{}{}:
|
case a.snapshotsTaken <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
if a.snapshotsTaken != nil {
|
if a.snapshotsTaken != nil {
|
||||||
a.log.Warn("callback channel is full, discarding snapshot update event")
|
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +341,7 @@ func snapshot(a args, u updater) state {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.log.WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
getLogger(a.ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +362,7 @@ func wait(a args, u updater) state {
|
|||||||
lastTick := snapper.lastInvocation
|
lastTick := snapper.lastInvocation
|
||||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
snapper.sleepUntil = lastTick.Add(a.interval)
|
||||||
sleepUntil = snapper.sleepUntil
|
sleepUntil = snapper.sleepUntil
|
||||||
log := a.log.WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
||||||
logFunc := log.Debug
|
logFunc := log.Debug
|
||||||
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
||||||
logFunc = log.Error
|
logFunc = log.Error
|
||||||
@@ -400,9 +387,19 @@ func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.Dat
|
|||||||
return zfs.ZFSListMapping(ctx, mf)
|
return zfs.ZFSListMapping(ctx, mf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
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 {
|
type snapTime struct {
|
||||||
ds *zfs.DatasetPath
|
ds *zfs.DatasetPath
|
||||||
|
prio int // lower is higher
|
||||||
time time.Time
|
time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,55 +408,92 @@ func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval t
|
|||||||
}
|
}
|
||||||
|
|
||||||
snaptimes := make([]snapTime, 0, len(fss))
|
snaptimes := make([]snapTime, 0, len(fss))
|
||||||
|
hardErrs := 0
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
log.Debug("examine filesystem state")
|
getLogger(ctx).Debug("examine filesystem state to find sync point")
|
||||||
for _, d := range fss {
|
for _, d := range fss {
|
||||||
|
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
|
||||||
l := log.WithField("fs", d.ToString())
|
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
|
||||||
|
if err == findSyncPointFSNoFilesystemVersionsErr {
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
snaptimes = append(snaptimes, snapTime{
|
||||||
if err != nil {
|
ds: d,
|
||||||
l.WithError(err).Error("cannot list filesystem versions")
|
prio: prioNoVersions,
|
||||||
continue
|
time: now,
|
||||||
}
|
})
|
||||||
if len(fsvs) <= 0 {
|
} else if err != nil {
|
||||||
l.WithField("prefix", prefix).Debug("no filesystem versions with prefix")
|
hardErrs++
|
||||||
continue
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort versions by creation
|
if hardErrs == len(fss) {
|
||||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
||||||
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 {
|
|
||||||
l.WithField("snapshot", latest.Name).
|
|
||||||
WithField("creation", latest.Creation).
|
|
||||||
Error("snapshot is from the future")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
next := now
|
|
||||||
if since < interval {
|
|
||||||
next = latest.Creation.Add(interval)
|
|
||||||
}
|
|
||||||
snaptimes = append(snaptimes, snapTime{d, next})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(snaptimes) == 0 {
|
if len(snaptimes) == 0 {
|
||||||
snaptimes = append(snaptimes, snapTime{nil, now})
|
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 {
|
sort.Slice(snaptimes, func(i, j int) bool {
|
||||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+7
-17
@@ -4,8 +4,9 @@ Documentation=https://zrepl.github.io
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
ExecStartPre=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml configcheck
|
||||||
ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
|
ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
|
||||||
RuntimeDirectory=zrepl
|
RuntimeDirectory=zrepl zrepl/stdinserver
|
||||||
RuntimeDirectoryMode=0700
|
RuntimeDirectoryMode=0700
|
||||||
|
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
@@ -20,24 +21,13 @@ RestrictNamespaces=true
|
|||||||
RestrictRealtime=yes
|
RestrictRealtime=yes
|
||||||
SystemCallArchitectures=native
|
SystemCallArchitectures=native
|
||||||
|
|
||||||
# BEGIN ProtectHome
|
|
||||||
# DEBIAN STRETCH
|
|
||||||
ProtectHome=read-only
|
ProtectHome=read-only
|
||||||
# FEDORA 28 / 29
|
# ProtectHome=tmpfs totally possible, not by default though because of Debian stretch
|
||||||
# ProtectHome=tmpfs
|
|
||||||
# END ProtectHome
|
# SystemCallFilter
|
||||||
|
# ~@privileged doesn't work with Ubuntu 18.04 ssh
|
||||||
|
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources
|
||||||
|
|
||||||
# BEGIN SystemCallFilter
|
|
||||||
## BEGIN DEBIAN STRETCH
|
|
||||||
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @privileged @raw-io @debug @clock @resources
|
|
||||||
## END DEBIAN STRETCH
|
|
||||||
## BEGIN FEDORA 28/29
|
|
||||||
## Syscall blacklist (should be fairly stable)
|
|
||||||
#SystemCallFilter=~ @mount @aio @cpu-emulation @keyring @memlock @module @obsolete @privileged @raw-io @reboot @setuid @swap @sync @timer @debug @clock @chown @resources
|
|
||||||
## Syscall whitelist (not sure how stable)
|
|
||||||
#SystemCallFilter=@default @file-system @process @basic-io @ipc @network-io @signal @io-event brk mprotect sched_getaffinity ioctl getrandom
|
|
||||||
## END END FEDORA 28/29
|
|
||||||
# END SystemCallFilter
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
+51
-1
@@ -27,6 +27,56 @@ We use the following annotations for classifying changes:
|
|||||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||||
* |docs| Change to the documentation.
|
* |docs| Change to the documentation.
|
||||||
|
|
||||||
|
0.3
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a big one! Headlining features:
|
||||||
|
|
||||||
|
* **Resumable Send & Recv Support**
|
||||||
|
No knobs required, automatically used where supported.
|
||||||
|
* **Hold-Protected Send & Recv**
|
||||||
|
Automatic ZFS holds to ensure that we can always resume a replication step.
|
||||||
|
* **Encrypted Send & Recv Support** for OpenZFS native encryption.
|
||||||
|
:ref:`Configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||||
|
* **Receive-side hold on last received dataset**
|
||||||
|
The counterpart to the replication cursor bookmark on the send-side.
|
||||||
|
Ensures that incremental replication will always be possible between a sender and receiver.
|
||||||
|
|
||||||
|
Actual changelog:
|
||||||
|
|
||||||
|
* |break_config| **more restrictive job names than in prior zrepl versions**
|
||||||
|
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names.
|
||||||
|
|
||||||
|
* |break| |mig| replication cursor representation changed
|
||||||
|
|
||||||
|
* zrepl now manages the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||||
|
In the future, this will permit multiple sending jobs to send from the same filesystems.
|
||||||
|
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
||||||
|
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
||||||
|
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
|
||||||
|
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
||||||
|
|
||||||
|
* |bugfix| missing logger context vars in control connection handlers
|
||||||
|
* |bugfix| improved error messages on ``zfs send`` errors
|
||||||
|
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||||
|
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
||||||
|
that will not be snapshotted until the sync-up phase is over
|
||||||
|
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
|
||||||
|
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||||
|
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
||||||
|
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
|
||||||
|
|
||||||
|
0.2.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* |feature| Illumos (and Solaris) compatibility and binary builds (thanks, `MNX.io <https://mnx.io>`_ )
|
||||||
|
* |feature| 32bit binaries for Linux and FreeBSD (untested, though)
|
||||||
|
* |bugfix| better error messages in ``ssh+stdinserver`` transport
|
||||||
|
* |bugfix| systemd + ``ssh+stdinserver``: automatically create ``/var/run/zrepl/stdinserver``
|
||||||
|
* |bugfix| crash if Prometheus listening socket cannot be opened
|
||||||
|
|
||||||
|
* [MAINTAINER NOTICE] ``Makefile`` refactoring, see :commit:`080f2c0`
|
||||||
|
|
||||||
0.2
|
0.2
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -57,7 +107,7 @@ We use the following annotations for classifying changes:
|
|||||||
| You can support maintenance and feature development through one of the following services:
|
| You can support maintenance and feature development through one of the following services:
|
||||||
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||||
| Note that PayPal processing fees are relatively high for small donations.
|
| Note that PayPal processing fees are relatively high for small donations.
|
||||||
| For SEPA wire transfer and **commerical support**, please `contact Christian directly <https://cschwarz.com>`_.
|
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
|
|
||||||
0.1.1
|
0.1.1
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Configuration
|
|||||||
configuration/jobs
|
configuration/jobs
|
||||||
configuration/transports
|
configuration/transports
|
||||||
configuration/filter_syntax
|
configuration/filter_syntax
|
||||||
|
configuration/sendrecvoptions
|
||||||
configuration/snapshotting
|
configuration/snapshotting
|
||||||
configuration/prune
|
configuration/prune
|
||||||
configuration/logging
|
configuration/logging
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Filter Syntax
|
Filter Syntax
|
||||||
=============
|
=============
|
||||||
|
|
||||||
For :ref:`source jobs <job-source>` and :ref:`push jobs <job-push>`, a filesystem filter must be defined (field ``filesystems``).
|
For :ref:`source<job-source>`, :ref:`push<job-push>` and :ref:`snap<job-snap>` jobs, a filesystem filter must be defined (field ``filesystems``).
|
||||||
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
||||||
|
|
||||||
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ Job Type ``push``
|
|||||||
- |connect-transport|
|
- |connect-transport|
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
@@ -101,6 +103,8 @@ Job Type ``source``
|
|||||||
- |serve-transport|
|
- |serve-transport|
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
|
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ The latter is particularly useful in combination with log aggregation services.
|
|||||||
.. WARNING::
|
.. WARNING::
|
||||||
|
|
||||||
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
|
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
|
||||||
Note that TCP buffering in the kernel must first run full becfore messages are dropped.
|
Note that TCP buffering in the kernel must first run full before messages are dropped.
|
||||||
|
|
||||||
Make sure to always configure a ``stdout`` outlet as the special error outlet to be informed about problems
|
Make sure to always configure a ``stdout`` outlet as the special error outlet to be informed about problems
|
||||||
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
|
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ Prometheus & Grafana
|
|||||||
|
|
||||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
||||||
The Prometheues monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
|
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
||||||
|
|
||||||
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
||||||
see :repomasterlink:`dist/grafana`.
|
see :repomasterlink:`dist/grafana`.
|
||||||
@@ -30,6 +31,7 @@ The dashboard also contains some advice on which metrics are important to monito
|
|||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9091'
|
listen: ':9091'
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ Config File Structure
|
|||||||
type: push
|
type: push
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
zrepl is confgured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
zrepl is configured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
||||||
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
||||||
The ``jobs`` section is a list of jobs which we are goind to explain now.
|
The ``jobs`` section is a list of jobs which we are going to explain now.
|
||||||
|
|
||||||
.. _job-overview:
|
.. _job-overview:
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ Jobs are identified by their ``name``, both in log files and the ``zrepl status`
|
|||||||
|
|
||||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
||||||
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
||||||
The passive side responds to requests from the active side after checking its persmissions.
|
The passive side responds to requests from the active side after checking its permissions.
|
||||||
|
|
||||||
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
|
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
|
||||||
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
||||||
@@ -90,6 +90,8 @@ It uses the client identity for access control:
|
|||||||
.. TIP::
|
.. TIP::
|
||||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
||||||
|
|
||||||
|
.. _overview-how-replication-works:
|
||||||
|
|
||||||
How Replication Works
|
How Replication Works
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
@@ -107,9 +109,8 @@ The following steps take place during replication and can be monitored using the
|
|||||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
* Per filesystem, compute a diff between sender and receiver snapshots
|
||||||
* Build a list of replication steps
|
* Build a list of replication steps
|
||||||
|
|
||||||
* If possible, use incremental sends (``zfs send -i``)
|
* If possible, use incremental and resumable sends (``zfs send -i``)
|
||||||
* Otherwise, use full send of most recent snapshot on sender
|
* Otherwise, use full send of most recent snapshot on sender
|
||||||
* Give up on filesystems that cannot be replicated without data loss
|
|
||||||
|
|
||||||
* Retry on errors that are likely temporary (i.e. network failures).
|
* Retry on errors that are likely temporary (i.e. network failures).
|
||||||
* Give up on filesystems where a permanent error was received over RPC.
|
* Give up on filesystems where a permanent error was received over RPC.
|
||||||
@@ -119,22 +120,28 @@ The following steps take place during replication and can be monitored using the
|
|||||||
* Perform replication steps in the following order:
|
* Perform replication steps in the following order:
|
||||||
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
||||||
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||||
* After a successful replication step, update the replication cursor bookmark (see below).
|
* Acquire send-side *step-holds* on the step's `from` and `to` snapshots.
|
||||||
|
* Perform the replication step.
|
||||||
|
* Move the **replication cursor** bookmark on the sending side (see below).
|
||||||
|
* Move the **last-received-hold** on the receiving side (see below).
|
||||||
|
* Release the send-side step-holds.
|
||||||
|
|
||||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||||
|
|
||||||
.. _replication-cursor-bookmark:
|
.. _replication-cursor-and-last-received-hold:
|
||||||
|
|
||||||
The **replication cursor bookmark** ``#zrepl_replication_cursor`` is kept per filesystem on the sending side of a replication setup:
|
**Replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally:
|
||||||
It is a bookmark of the most recent successfully replicated snapshot to the receiving side.
|
the replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||||
It is is used by the :ref:`not_replicated <prune-keep-not-replicated>` keep rule to identify all snapshots that have not yet been replicated to the receiving side.
|
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||||
Regardless of whether that keep rule is used, the bookmark ensures that replication can always continue incrementally.
|
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||||
Note that there is only one cursor bookmark per filesystem, which prohibits multiple jobs to replicate the same filesystem (:ref:`see below<jobs-multiple-jobs>`).
|
The last-received-hold tag has the format ``#zrepl_last_received_J_<JOBNAME>``.
|
||||||
|
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||||
|
The ``zrepl holds list`` provides a listing of all bookmarks and holds managed by zrepl.
|
||||||
|
|
||||||
.. _replication-placeholder-property:
|
.. _replication-placeholder-property:
|
||||||
|
|
||||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
||||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
|
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
|
||||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||||
@@ -144,9 +151,13 @@ The ``zrepl test placeholder`` command can be used to check whether a filesystem
|
|||||||
.. ATTENTION::
|
.. ATTENTION::
|
||||||
|
|
||||||
Currently, zrepl does not replicate filesystem properties.
|
Currently, zrepl does not replicate filesystem properties.
|
||||||
Whe receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||||
This is temporary and being worked on :issue:`24`.
|
This is temporary and being worked on :issue:`24`.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||||
|
|
||||||
|
|
||||||
.. _jobs-multiple-jobs:
|
.. _jobs-multiple-jobs:
|
||||||
|
|
||||||
@@ -170,8 +181,8 @@ No Overlapping
|
|||||||
|
|
||||||
Jobs run independently of each other.
|
Jobs run independently of each other.
|
||||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
|
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B assumed the snapshot to still be present.
|
||||||
More subtle race conditions can occur with the :ref:`replication cursor bookmark <replication-cursor-bookmark>`, which currently only exists once per filesystem.
|
However, the next replication attempt will re-examine the situation from scratch and should work.
|
||||||
|
|
||||||
N push jobs to 1 sink
|
N push jobs to 1 sink
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -187,5 +198,5 @@ Multiple pull jobs pulling from the same source have potential for race conditio
|
|||||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||||
|
|
||||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
||||||
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
Thus, it is not possible to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Pruning Policies
|
|||||||
================
|
================
|
||||||
|
|
||||||
In zrepl, *pruning* means *destroying snapshots*.
|
In zrepl, *pruning* means *destroying snapshots*.
|
||||||
Pruning must happen on both sides of a replication or the systems would inevitable run out of disk space at some point.
|
Pruning must happen on both sides of a replication or the systems would inevitably run out of disk space at some point.
|
||||||
|
|
||||||
Typically, the requirements to temporal resolution and maximum retention time differ per side.
|
Typically, the requirements to temporal resolution and maximum retention time differ per side.
|
||||||
For example, when using zrepl to back up a busy database server, you will want high temporal resolution (snapshots every 10 min) for the last 24h in case of administrative disasters, but cannot afford to store them for much longer because you might have high turnover volume in the database.
|
For example, when using zrepl to back up a busy database server, you will want high temporal resolution (snapshots every 10 min) for the last 24h in case of administrative disasters, but cannot afford to store them for much longer because you might have high turnover volume in the database.
|
||||||
@@ -66,7 +66,7 @@ Policy ``not_replicated``
|
|||||||
|
|
||||||
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||||
It only makes sense to specify this rule on a sender (source or push job).
|
It only makes sense to specify this rule on a sender (source or push job).
|
||||||
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-bookmark>` on the sending side.
|
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` on the sending side.
|
||||||
|
|
||||||
.. _prune-keep-retention-grid:
|
.. _prune-keep-retention-grid:
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ Policy ``regex``
|
|||||||
negate: true
|
negate: true
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
|
|
||||||
``regex`` keeps all snapshots whose names are matched by the regular expressionin ``regex``.
|
``regex`` keeps all snapshots whose names are matched by the regular expression in ``regex``.
|
||||||
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
|
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
|
||||||
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
|
||||||
|
Send & Recv Options
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. _job-send-options:
|
||||||
|
|
||||||
|
Send Options
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
filesystems: ...
|
||||||
|
send:
|
||||||
|
encrypted: true
|
||||||
|
...
|
||||||
|
|
||||||
|
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
|
||||||
|
|
||||||
|
``encryption`` option
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The ``encryption`` variable controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
||||||
|
More specifically, if ``encryption=true``, zrepl
|
||||||
|
|
||||||
|
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
|
||||||
|
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
|
||||||
|
* expects the receiving side to support OpenZFS native encryption (recv will fail otherwise)
|
||||||
|
|
||||||
|
Filesystems matched by ``filesystems`` that are not encrypted are not sent and will cause error log messages.
|
||||||
|
|
||||||
|
If ``encryption=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
|
||||||
|
|
||||||
|
.. _job-recv-options:
|
||||||
|
|
||||||
|
Recv Options
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section.
|
||||||
|
However, there are currently no variables to configure there.
|
||||||
|
|
||||||
|
|
||||||
@@ -9,8 +9,15 @@ The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snaps
|
|||||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||||
|
|
||||||
|
When a job is started, the snapshotter attempts to get the snapshotting rhythms of the matched ``filesystems`` in sync because snapshotting all filesystems at the same time results in a more consistent backup.
|
||||||
|
To find that sync point, the most recent snapshot, made by the snapshotter, in any of the matched ``filesystems`` is used.
|
||||||
|
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||||
|
|
||||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||||
|
|
||||||
|
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||||
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -113,14 +120,11 @@ Most hook types take additional parameters, please refer to the respective subse
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
The hook type ``command`` is the only currently supported hook type.
|
``command`` hooks take a ``path`` to an executable script or binary to be executed before and after the snapshot.
|
||||||
Future versions of zrepl may support other hook types.
|
``path`` must be absolute (e.g. ``/etc/zrepl/hooks/zrepl-notify.sh``).
|
||||||
The ``path`` to the hook executables must be absolute (e.g. ``/etc/zrepl/hooks/zrepl-notify.sh``).
|
|
||||||
No arguments may be specified; create a wrapper script if zrepl must call an executable that requires arguments.
|
No arguments may be specified; create a wrapper script if zrepl must call an executable that requires arguments.
|
||||||
zrepl will call the hook both before and after the snapshot, but with different values of the ``ZREPL_HOOKTYPE`` environment variable; see below.
|
|
||||||
The process standard output is logged at level INFO. Standard error is logged at level WARN.
|
The process standard output is logged at level INFO. Standard error is logged at level WARN.
|
||||||
|
The following environment variables are set:
|
||||||
zrepl sets a number of environment variables for the hook processes:
|
|
||||||
|
|
||||||
* ``ZREPL_HOOKTYPE``: either "pre_snapshot" or "post_snapshot"
|
* ``ZREPL_HOOKTYPE``: either "pre_snapshot" or "post_snapshot"
|
||||||
* ``ZREPL_FS``: the ZFS filesystem name being snapshotted
|
* ``ZREPL_FS``: the ZFS filesystem name being snapshotted
|
||||||
|
|||||||
@@ -50,12 +50,25 @@ Serve
|
|||||||
serve:
|
serve:
|
||||||
type: tcp
|
type: tcp
|
||||||
listen: ":8888"
|
listen: ":8888"
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "mysql01"
|
"192.168.122.123" : "mysql01",
|
||||||
"192.168.122.123" : "mx01"
|
"192.168.122.42" : "mx01",
|
||||||
|
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||||
|
|
||||||
|
# CIDR masks require a '*' in the client identity string
|
||||||
|
# that is expanded to the client's IP address
|
||||||
|
|
||||||
|
"10.23.42.0/24": "cluster-*"
|
||||||
|
"fde4:8dba:82e1::/64": "san-*"
|
||||||
}
|
}
|
||||||
...
|
...
|
||||||
|
|
||||||
|
.. _listen-freebind-explanation:
|
||||||
|
|
||||||
|
``listen_freebind`` controls whether the socket is allowed to bind to non-local or unconfigured IP addresses (Linux ``IP_FREEBIND`` , FreeBSD ``IP_BINDANY``).
|
||||||
|
Enable this option if you want to ``listen`` on a specific IP address that might not yet be configured when the zrepl daemon starts.
|
||||||
|
|
||||||
Connect
|
Connect
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
@@ -101,6 +114,7 @@ Serve
|
|||||||
serve:
|
serve:
|
||||||
type: tls
|
type: tls
|
||||||
listen: ":8888"
|
listen: ":8888"
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
ca: /etc/zrepl/ca.crt
|
ca: /etc/zrepl/ca.crt
|
||||||
cert: /etc/zrepl/prod.fullchain
|
cert: /etc/zrepl/prod.fullchain
|
||||||
key: /etc/zrepl/prod.key
|
key: /etc/zrepl/prod.key
|
||||||
@@ -110,6 +124,7 @@ Serve
|
|||||||
|
|
||||||
The ``ca`` field specified the certificate authority used to validate client certificates.
|
The ``ca`` field specified the certificate authority used to validate client certificates.
|
||||||
The ``client_cns`` list specifies a list of accepted client common names (which are also the client identities for this transport).
|
The ``client_cns`` list specifies a list of accepted client common names (which are also the client identities for this transport).
|
||||||
|
The ``listen_freebind`` field is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
|
|
||||||
Connect
|
Connect
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@@ -287,9 +302,9 @@ Connect
|
|||||||
user: root
|
user: root
|
||||||
port: 22
|
port: 22
|
||||||
identity_file: /etc/zrepl/ssh/identity
|
identity_file: /etc/zrepl/ssh/identity
|
||||||
options: # optional, default [], `-o` arguments passed to ssh
|
# options: # optional, default [], `-o` arguments passed to ssh
|
||||||
- "Compression=on"
|
# - "Compression=yes"
|
||||||
dial_timeout: # optional, default 10s, max time.Duration until initial handshake is completed
|
# dial_timeout: 10s # optional, default 10s, max time.Duration until initial handshake is completed
|
||||||
|
|
||||||
The connecting zrepl daemon
|
The connecting zrepl daemon
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
||||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
||||||
|
.. |send-options| replace:: :ref:`send options<job-send-options>`, e.g. for encrypted sends
|
||||||
|
.. |recv-options| replace:: :ref:`recv options<job-recv-options>`
|
||||||
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
||||||
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
||||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
.. _implementation_toc:
|
|
||||||
|
|
||||||
Implementation Overview
|
|
||||||
=======================
|
|
||||||
|
|
||||||
.. WARNING::
|
|
||||||
|
|
||||||
Incomplete and possibly outdated.
|
|
||||||
Check out the :ref:`talks about zrepl <pr-talks>` at various conferences for up-to-date material.
|
|
||||||
Alternatively, have a `look at the source code <http://github.com/zrepl/zrepl>`_ ;)
|
|
||||||
|
|
||||||
The following design aspects may convince you that ``zrepl`` is superior to a hacked-together shell script solution.
|
|
||||||
Also check out the :ref:`talks about zrepl <pr-talks>` at various conferences.
|
|
||||||
|
|
||||||
Testability & Performance
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
zrepl is written in Go, a real programming language with type safety,
|
|
||||||
reasonable performance, testing infrastructure and an (opinionated) idea of
|
|
||||||
software engineering.
|
|
||||||
|
|
||||||
* key parts & algorithms of zrepl are covered by unit tests (work in progress)
|
|
||||||
* zrepl is noticably faster than comparable shell scripts
|
|
||||||
|
|
||||||
|
|
||||||
RPC protocol
|
|
||||||
------------
|
|
||||||
|
|
||||||
While it is tempting to just issue a few ``ssh remote 'zfs send ...' | zfs recv``, this has a number of drawbacks:
|
|
||||||
|
|
||||||
* The snapshot streams need to be compatible.
|
|
||||||
* Communication is still unidirectional. Thus, you will most likely
|
|
||||||
|
|
||||||
* either not take advantage of advanced replication features such as *compressed send & recv*
|
|
||||||
* or issue additional ``ssh`` commands in advance to figure out what features are supported on the other side.
|
|
||||||
|
|
||||||
* Advanced logic in shell scripts is ugly to read, poorly testable and a pain to maintain.
|
|
||||||
|
|
||||||
zrepl takes a different approach:
|
|
||||||
|
|
||||||
* Define an RPC protocol.
|
|
||||||
* Establish an encrypted, authenticated, bidirectional communication channel.
|
|
||||||
* Run daemons on both sides of the setup and let them talk to each other.
|
|
||||||
|
|
||||||
This has several obvious benefits:
|
|
||||||
|
|
||||||
* No blank root shell access is given to the other side.
|
|
||||||
* An *authenticated* peer *requests* filesystem lists, snapshot streams, etc.
|
|
||||||
* The server decides which filesystems it exposes to which peers.
|
|
||||||
* The :ref:`transport mechanism <transport>` is decoupled from the remaining logic, which allows us to painlessly offer multiple transport mechanisms.
|
|
||||||
|
|
||||||
Protocol Implementation
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
zrepl uses a custom RPC protol because, at the time of writing, existing solutions like gRPC do not provide efficient means to transport large amounts of data, whose size is unknown at send time (= zfs send streams).
|
|
||||||
The package used is `github.com/problame/go-streamrpc <https://github.com/problame/go-streamrpc/tree/master>`_.
|
|
||||||
|
|
||||||
Logging & Transparency
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
zrepl comes with :ref:`rich, structured and configurable logging <logging>`, allowing administators to understand what the software is actually doing.
|
|
||||||
+5
-3
@@ -57,9 +57,11 @@ Main Features
|
|||||||
* Advanced replication features
|
* Advanced replication features
|
||||||
|
|
||||||
* [x] Automatic retries for temporary network errors
|
* [x] Automatic retries for temporary network errors
|
||||||
* [ ] Resumable send & receive
|
* [x] Automatic resumable send & receive
|
||||||
|
* [x] Automatic ZFS holds during send & receive
|
||||||
|
* [x] Automatic bookmark \& hold management for guaranteed incremental send & recv
|
||||||
|
* [x] Encrypted raw send & receive to untrusted receivers (OpenZFS native encryption)
|
||||||
* [ ] Compressed send & receive
|
* [ ] Compressed send & receive
|
||||||
* [ ] Raw encrypted send & receive
|
|
||||||
|
|
||||||
* **Automatic snapshot management**
|
* **Automatic snapshot management**
|
||||||
|
|
||||||
@@ -83,6 +85,7 @@ Main Features
|
|||||||
* **Maintainable implementation in Go**
|
* **Maintainable implementation in Go**
|
||||||
|
|
||||||
* [x] Cross platform
|
* [x] Cross platform
|
||||||
|
* [x] Dynamic feature checking
|
||||||
* [x] Type safe & testable code
|
* [x] Type safe & testable code
|
||||||
|
|
||||||
|
|
||||||
@@ -126,7 +129,6 @@ Table of Contents
|
|||||||
installation
|
installation
|
||||||
configuration
|
configuration
|
||||||
usage
|
usage
|
||||||
implementation
|
|
||||||
pr
|
pr
|
||||||
changelog
|
changelog
|
||||||
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||||
|
|||||||
+10
-86
@@ -1,95 +1,19 @@
|
|||||||
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
|
||||||
|
|
||||||
.. _installation:
|
.. _installation_toc:
|
||||||
|
|
||||||
|
************
|
||||||
Installation
|
Installation
|
||||||
============
|
************
|
||||||
|
|
||||||
.. TIP::
|
.. TIP::
|
||||||
|
|
||||||
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
||||||
|
|
||||||
User Privileges
|
.. toctree::
|
||||||
---------------
|
|
||||||
|
|
||||||
It is possible to run zrepl as an unprivileged user in combination with
|
|
||||||
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
|
||||||
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
|
||||||
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
|
|
||||||
|
|
||||||
Packages
|
|
||||||
--------
|
|
||||||
|
|
||||||
zrepl source releases are signed & tagged by the author in the git repository.
|
|
||||||
Your OS vendor may provide binary packages of zrepl through the package manager.
|
|
||||||
Additionally, `binary releases`_ are provided on GitHub.
|
|
||||||
The following list may be incomplete, feel free to submit a PR with an update:
|
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - OS / Distro
|
|
||||||
- Install Command
|
|
||||||
- Link
|
|
||||||
* - FreeBSD
|
|
||||||
- ``pkg install zrepl``
|
|
||||||
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
|
||||||
* - MacOS
|
|
||||||
- ``brew install zrepl``
|
|
||||||
- Available on `homebrew <https://brew.sh>`_
|
|
||||||
* - Arch Linux
|
|
||||||
- ``yay install zrepl-bin``
|
|
||||||
- Available on `AUR <https://aur.archlinux.org/packages/zrepl-bin>`_
|
|
||||||
* - Others
|
|
||||||
-
|
|
||||||
- Use `binary releases`_ or build from source.
|
|
||||||
|
|
||||||
Compile From Source
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
|
||||||
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
|
||||||
Python and pip3 should probably be installed via your distro's package manager.
|
|
||||||
|
|
||||||
Alternatively, you can use the Docker build process:
|
|
||||||
it is used to produce the official zrepl `binary releases`_
|
|
||||||
and serves as a reference for build dependencies and procedure:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
git clone https://github.com/zrepl/zrepl.git
|
|
||||||
cd zrepl
|
|
||||||
sudo docker build -t zrepl_build -f build.Dockerfile .
|
|
||||||
sudo docker run -it --rm \
|
|
||||||
-v "${PWD}:/src" \
|
|
||||||
--user "$(id -u):$(id -g)" \
|
|
||||||
zrepl_build make release
|
|
||||||
|
|
||||||
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
python3 -m venv3
|
|
||||||
source venv3/bin/activate
|
|
||||||
./lazy.sh devsetup
|
|
||||||
make release
|
|
||||||
|
|
||||||
The Python venv is used for the documentation build dependencies.
|
|
||||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
|
||||||
Either way, all build results are located in the ``artifacts/`` directory.
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
It is your job to install the apropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
|
||||||
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
|
||||||
|
|
||||||
What next?
|
|
||||||
----------
|
|
||||||
|
|
||||||
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
|
||||||
|
|
||||||
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
|
||||||
|
|
||||||
|
installation/user-privileges
|
||||||
|
installation/packages
|
||||||
|
installation/apt-repos
|
||||||
|
installation/compile-from-source
|
||||||
|
installation/freebsd-jail-with-iocage
|
||||||
|
installation/what-next
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
.. _installation-apt-repos:
|
||||||
|
|
||||||
|
Debian / Ubuntu APT repositories
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
||||||
|
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
||||||
|
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
||||||
|
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
|
||||||
|
|
||||||
|
The following snippet configure the repository for your Debian or Ubuntu release:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
apt update && apt install curl gnupg lsb-release; \
|
||||||
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
|
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
||||||
|
echo "Using Distro and Codename: $CODENAME"; \
|
||||||
|
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
||||||
|
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
||||||
|
apt update
|
||||||
|
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
||||||
|
This includes breaking changes between zrepl versions.
|
||||||
|
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||||
|
|
||||||
|
.. _installation-compile-from-source:
|
||||||
|
|
||||||
|
Compile From Source
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||||
|
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
||||||
|
Python and pip3 should probably be installed via your distro's package manager.
|
||||||
|
|
||||||
|
Alternatively, you can use the Docker build process:
|
||||||
|
it is used to produce the official zrepl `binary releases`_
|
||||||
|
and serves as a reference for build dependencies and procedure:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git clone https://github.com/zrepl/zrepl.git && \
|
||||||
|
cd zrepl && \
|
||||||
|
sudo docker build -t zrepl_build -f build.Dockerfile . && \
|
||||||
|
sudo docker run -it --rm \
|
||||||
|
-v "${PWD}:/src" \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
zrepl_build make release
|
||||||
|
|
||||||
|
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
python3 -m venv3
|
||||||
|
source venv3/bin/activate
|
||||||
|
./lazy.sh devsetup
|
||||||
|
make release
|
||||||
|
|
||||||
|
The Python venv is used for the documentation build dependencies.
|
||||||
|
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
||||||
|
Either way, all build results are located in the ``artifacts/`` directory.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||||
|
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _installation-freebsd-jail-with-iocage:
|
||||||
|
|
||||||
|
FreeBSD Jail With iocage
|
||||||
|
========================
|
||||||
|
|
||||||
|
|
||||||
|
This tutorial shows how zrepl can be installed on FreeBSD, or FreeNAS in a jail using iocage.
|
||||||
|
While this tutorial focuses on using iocage, much of the setup would be similar
|
||||||
|
using a different jail manager.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
From a security perspective, just keep in mind that ``zfs send``/``recv`` was never designed with
|
||||||
|
jails in mind, an attacker could probably crash the receive-side kernel or worse induce stateful
|
||||||
|
damage to the receive-side pool if they were able to get access to the jail.
|
||||||
|
|
||||||
|
The jail doesn't provide security benefits, but only management ones.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
A dataset that will be delegated to the jail needs to be created if one does not already exist.
|
||||||
|
For the tutorial ``tank/zrepl`` will be used.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
zfs create -o mountpoint=none tank/zrepl
|
||||||
|
|
||||||
|
The only software requirements on the host system are ``iocage``, which can be installed
|
||||||
|
from ports or packages.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pkg install py37-iocage
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
By default ``iocage`` will "activate" on first use which will set up some defaults such as
|
||||||
|
which pool will be used. To activate ``iocage`` manually the ``iocage activate`` command can be used.
|
||||||
|
|
||||||
|
Jail Creation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
There are two options for jail creation using FreeBSD.
|
||||||
|
|
||||||
|
1. Manually set up the jail from scratch
|
||||||
|
2. Create the jail using the ``zrepl`` plugin. On FreeNAS this is possible from the user interface using the community index.
|
||||||
|
|
||||||
|
Manual Jail
|
||||||
|
###########
|
||||||
|
|
||||||
|
Create a jail, using the same release as the host, called ``zrepl`` that will be automatically started at boot.
|
||||||
|
The jail will have ``tank/zrepl`` delegated into it.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage create --release "$(freebsd-version -k | cut -d '-' -f '1,2')" --name zrepl \
|
||||||
|
boot=on nat=1 \
|
||||||
|
jail_zfs=on \
|
||||||
|
jail_zfs_dataset=zrepl \
|
||||||
|
jail_zfs_mountpoint='none'
|
||||||
|
|
||||||
|
Enter the jail:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage console zrepl
|
||||||
|
|
||||||
|
Install ``zrepl``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pkg update && pkg upgrade
|
||||||
|
pkg install zrepl
|
||||||
|
|
||||||
|
Create the log file ``/var/log/zrepl.log``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
touch /var/log/zrepl.log && service newsyslog restart
|
||||||
|
|
||||||
|
Tell syslogd to redirect facility local0 to the ``zrepl.log`` file:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
service syslogd reload
|
||||||
|
|
||||||
|
Enable the zrepl daemon to start automatically at boot:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sysrc zrepl_enable="YES"
|
||||||
|
|
||||||
|
|
||||||
|
Plugin
|
||||||
|
######
|
||||||
|
|
||||||
|
When using the plugin, ``zrepl`` will be installed for you in a jail using the following ``iocage`` properties.
|
||||||
|
|
||||||
|
* ``nat=1``
|
||||||
|
* ``jail_zfs=on``
|
||||||
|
* ``jail_zfs_mountpoint=none``
|
||||||
|
|
||||||
|
Additionally the delegated dataset should be specified upon creation, and optionally start on boot can be set.
|
||||||
|
This can also be done from the FreeNAS webui.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
fetch https://raw.githubusercontent.com/ix-plugin-hub/iocage-plugin-index/master/zrepl.json -o /tmp/zrepl.json
|
||||||
|
iocage fetch -P /tmp/zrepl.json --name zrepl jail_zfs_dataset=zrepl boot=on
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Now ``zrepl`` can be configured.
|
||||||
|
|
||||||
|
Enter the jail.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage console zrepl
|
||||||
|
|
||||||
|
Modify the ``/usr/local/etc/zrepl/zrepl.yml`` configuration file.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`tutorial` for examples of a ``sink`` job.
|
||||||
|
|
||||||
|
Now ``zrepl`` can be started.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
service zrepl start
|
||||||
|
|
||||||
|
Summary
|
||||||
|
-------
|
||||||
|
|
||||||
|
Congratulations, you have a working jail!
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.. _installation-packages:
|
||||||
|
|
||||||
|
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||||
|
|
||||||
|
Packages
|
||||||
|
--------
|
||||||
|
|
||||||
|
zrepl source releases are signed & tagged by the author in the git repository.
|
||||||
|
Your OS vendor may provide binary packages of zrepl through the package manager.
|
||||||
|
Additionally, `binary releases`_ are provided on GitHub.
|
||||||
|
The following list may be incomplete, feel free to submit a PR with an update:
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - OS / Distro
|
||||||
|
- Install Command
|
||||||
|
- Link
|
||||||
|
* - FreeBSD
|
||||||
|
- ``pkg install zrepl``
|
||||||
|
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
||||||
|
|
||||||
|
:ref:`installation-freebsd-jail-with-iocage`
|
||||||
|
* - FreeNAS
|
||||||
|
-
|
||||||
|
- :ref:`installation-freebsd-jail-with-iocage`
|
||||||
|
* - MacOS
|
||||||
|
- ``brew install zrepl``
|
||||||
|
- Available on `homebrew <https://brew.sh>`_
|
||||||
|
* - Arch Linux
|
||||||
|
- ``yay install zrepl``
|
||||||
|
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
||||||
|
* - Fedora
|
||||||
|
- ``dnf install zrepl``
|
||||||
|
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||||
|
* - CentOS/RHEL
|
||||||
|
- ``yum install zrepl``
|
||||||
|
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||||
|
* - Debian + Ubuntu
|
||||||
|
- ``apt install zrepl``
|
||||||
|
- APT repository config :ref:`see below <installation-apt-repos>`
|
||||||
|
* - OmniOS
|
||||||
|
- ``pkg install zrepl``
|
||||||
|
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
||||||
|
* - Void Linux
|
||||||
|
- ``xbps-install zrepl``
|
||||||
|
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
|
||||||
|
* - Others
|
||||||
|
-
|
||||||
|
- Use `binary releases`_ or build from source.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.. _installation-user-privileges:
|
||||||
|
|
||||||
|
User Privileges
|
||||||
|
---------------
|
||||||
|
|
||||||
|
It is possible to run zrepl as an unprivileged user in combination with
|
||||||
|
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
||||||
|
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`installation-freebsd-jail-with-iocage` for FreeBSD jail setup instructions.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.. _installation-what-next:
|
||||||
|
|
||||||
|
What next?
|
||||||
|
----------
|
||||||
|
|
||||||
|
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||||
|
|
||||||
|
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
||||||
@@ -56,7 +56,6 @@ sphinx-versioning build \
|
|||||||
--show-banner \
|
--show-banner \
|
||||||
--whitelist-branches '^master$' \
|
--whitelist-branches '^master$' \
|
||||||
--whitelist-tags '(v)?\d+\.[1-9][0-9]*.\d+$' \
|
--whitelist-tags '(v)?\d+\.[1-9][0-9]*.\d+$' \
|
||||||
--whitelist-tags '(v)?0.2.0-rc.*$' \
|
|
||||||
docs ./public_git \
|
docs ./public_git \
|
||||||
-- -c sphinxconf # older conf.py throw errors because they used
|
-- -c sphinxconf # older conf.py throw errors because they used
|
||||||
# version = subprocess.show_output(["git", "describe"])
|
# version = subprocess.show_output(["git", "describe"])
|
||||||
|
|||||||
+31
-9
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||||
You can support maintenance and feature development through one of the services listed above.
|
You can support maintenance and feature development through one of the services listed above.
|
||||||
For SEPA wire transfer and **commerical support**, please `contact Christian directly <https://cschwarz.com>`_.
|
For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
**Thanks for your support!**
|
**Thanks for your support!**
|
||||||
|
|
||||||
@@ -18,12 +18,34 @@ For SEPA wire transfer and **commerical support**, please `contact Christian dir
|
|||||||
Supporters
|
Supporters
|
||||||
==========
|
==========
|
||||||
|
|
||||||
We would like to thank the following people / organizations for supporting zrepl through monetary and other means:
|
We would like to thank the following people and organizations for supporting zrepl through monetary and other means:
|
||||||
|
|
||||||
* `Marshall Clyburn <https://github.com/mdclyburn>`_
|
.. |supporter-std| raw:: html
|
||||||
* `Ross Williams <https://github.com/overhacked>`_
|
|
||||||
* Mike T.
|
<div class="fa fa-star-o" style="width: 1em;"></div>
|
||||||
* `Justin Scholz <https://github.com/JMoVS>`_
|
|
||||||
* `InsanePrawn <https://github.com/InsanePrawn>`_
|
.. |supporter-gold| raw:: html
|
||||||
* `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
|
||||||
* `Anton Schirg <https://github.com/antonxy>`_
|
<div class="fa fa-star" style="width: 1em;"></div>
|
||||||
|
|
||||||
|
.. |supporter-code| raw:: html
|
||||||
|
|
||||||
|
<div class="fa fa-code" style="width: 1em;"></div>
|
||||||
|
|
||||||
|
* |supporter-std| Henning Kessler
|
||||||
|
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
|
||||||
|
* |supporter-std| `DrLuke <https://github.com/drluke>`_
|
||||||
|
* |supporter-gold| `Mateusz Kwiatkowski (runhyve.app) <https://runhyve.app>`_
|
||||||
|
* |supporter-std| `Gaelan D'costa <https://www.robot-disco.net>`_
|
||||||
|
* |supporter-std| `Tenzin Lhakhang <https://confluence.tenzin.io>`_
|
||||||
|
* |supporter-std| `Lapo Luchini <https://github.com/lapo-luchini>`_
|
||||||
|
* |supporter-std| `F. Schmid <https://github.com/tdschmidl>`_
|
||||||
|
* |supporter-gold| `MNX.io <https://mnx.io>`_
|
||||||
|
* |supporter-std| `Marshall Clyburn <https://github.com/mdclyburn>`_
|
||||||
|
* |supporter-code| `Ross Williams <https://github.com/overhacked>`_
|
||||||
|
* |supporter-std| Mike T.
|
||||||
|
* |supporter-code| `Justin Scholz <https://github.com/JMoVS>`_
|
||||||
|
* |supporter-code| `InsanePrawn <https://github.com/InsanePrawn>`_
|
||||||
|
* |supporter-code| `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
||||||
|
* |supporter-code| `Janis Streib <https://github.com/janisstreib>`_
|
||||||
|
* |supporter-code| `Anton Schirg <https://github.com/antonxy>`_
|
||||||
|
|||||||
+5
-8
@@ -47,14 +47,14 @@ We can model this situation as two jobs:
|
|||||||
Install zrepl
|
Install zrepl
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
|
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here.
|
||||||
|
|
||||||
Generate TLS Certificates
|
Generate TLS Certificates
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
We use the `TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
We use the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
||||||
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
|
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
|
||||||
Again, for convenience, We generate the key pairs on our local machine and distribute them using ssh:
|
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
:emphasize-lines: 6,13
|
:emphasize-lines: 6,13
|
||||||
@@ -119,7 +119,7 @@ We define a **push job** named ``prod_to_backups`` in ``/etc/zrepl/zrepl.yml`` o
|
|||||||
Configure server ``backups``
|
Configure server ``backups``
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` : ::
|
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``backups`` : ::
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- name: sink
|
- name: sink
|
||||||
@@ -138,7 +138,7 @@ We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`
|
|||||||
Apply Configuration Changes
|
Apply Configuration Changes
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
We use ``zrepl configcheck`` before to catch any configuration errors: no output indicates that everything is fine.
|
We use ``zrepl configcheck`` to catch any configuration errors: no output indicates that everything is fine.
|
||||||
If that is the case, restart the zrepl daemon on **both** ``prod`` and ``backups`` using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
If that is the case, restart the zrepl daemon on **both** ``prod`` and ``backups`` using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||||
|
|
||||||
|
|
||||||
@@ -175,6 +175,3 @@ Congratulations, you have a working push backup. Where to go next?
|
|||||||
|
|
||||||
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
||||||
* Configure :ref:`logging <logging>` \& :ref:`monitoring <monitoring>`.
|
* Configure :ref:`logging <logging>` \& :ref:`monitoring <monitoring>`.
|
||||||
* Learn about :ref:`implementation details <implementation_toc>` of zrepl.
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -38,6 +38,8 @@ CLI Overview
|
|||||||
* - ``zrepl migrate``
|
* - ``zrepl migrate``
|
||||||
- | perform on-disk state / ZFS property migrations
|
- | perform on-disk state / ZFS property migrations
|
||||||
| (see :ref:`changelog <changelog>` for details)
|
| (see :ref:`changelog <changelog>` for details)
|
||||||
|
* - ``zrepl zfs-abstractions``
|
||||||
|
- list and remove zrepl's abstractions on top of ZFS, e.g. holds and step bookmarks (see :ref:`overview <replication-cursor-and-last-received-hold>` )
|
||||||
|
|
||||||
.. _usage-zrepl-daemon:
|
.. _usage-zrepl-daemon:
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ zrepl daemon
|
|||||||
All actual work zrepl does is performed by a daemon process.
|
All actual work zrepl does is performed by a daemon process.
|
||||||
The daemon supports structured :ref:`logging <logging>` and provides :ref:`monitoring endpoints <monitoring>`.
|
The daemon supports structured :ref:`logging <logging>` and provides :ref:`monitoring endpoints <monitoring>`.
|
||||||
|
|
||||||
When installating from a package, the package maintainer should have provided an init script / systemd.service file.
|
When installing from a package, the package maintainer should have provided an init script / systemd.service file.
|
||||||
You should thus be able to start zrepl daemon using your init system.
|
You should thus be able to start zrepl daemon using your init system.
|
||||||
|
|
||||||
Alternatively, or for running zrepl in the foreground, simply execute ``zrepl daemon``.
|
Alternatively, or for running zrepl in the foreground, simply execute ``zrepl daemon``.
|
||||||
@@ -73,6 +75,6 @@ The daemon exits as soon as all jobs have reported shut down.
|
|||||||
Systemd Unit File
|
Systemd Unit File
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
A systemd service defintion template is available in :repomasterlink:`dist/systemd`.
|
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
|
||||||
Note that some of the options only work on recent versions of systemd.
|
Note that some of the options only work on recent versions of systemd.
|
||||||
Any help & improvements are very welcome, see :issue:`145`.
|
Any help & improvements are very welcome, see :issue:`145`.
|
||||||
+3
-10
@@ -3,25 +3,18 @@ package endpoint
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey int
|
type contextKey int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
contextKeyLogger contextKey = iota
|
ClientIdentityKey contextKey = iota
|
||||||
ClientIdentityKey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Logger = logger.Logger
|
type Logger = logger.Logger
|
||||||
|
|
||||||
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 {
|
||||||
if l, ok := ctx.Value(contextKeyLogger).(Logger); ok {
|
return logging.GetLogger(ctx, logging.SubsysEndpoint)
|
||||||
return l
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+500
-65
@@ -2,26 +2,57 @@
|
|||||||
package endpoint
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
|
"github.com/zrepl/zrepl/util/chainedio"
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/util/semaphore"
|
"github.com/zrepl/zrepl/util/semaphore"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SenderConfig struct {
|
||||||
|
FSF zfs.DatasetFilter
|
||||||
|
Encrypt *zfs.NilBool
|
||||||
|
JobID JobID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SenderConfig) Validate() error {
|
||||||
|
c.JobID.MustValidate()
|
||||||
|
if err := c.Encrypt.Validate(); err != nil {
|
||||||
|
return errors.Wrap(err, "`Encrypt` field invalid")
|
||||||
|
}
|
||||||
|
if _, err := StepHoldTag(c.JobID); err != nil {
|
||||||
|
return fmt.Errorf("JobID cannot be used for hold tag: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Sender implements replication.ReplicationEndpoint for a sending side
|
// Sender implements replication.ReplicationEndpoint for a sending side
|
||||||
type Sender struct {
|
type Sender struct {
|
||||||
FSFilter zfs.DatasetFilter
|
FSFilter zfs.DatasetFilter
|
||||||
|
encrypt *zfs.NilBool
|
||||||
|
jobId JobID
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSender(fsf zfs.DatasetFilter) *Sender {
|
func NewSender(conf SenderConfig) *Sender {
|
||||||
return &Sender{FSFilter: fsf}
|
if err := conf.Validate(); err != nil {
|
||||||
|
panic("invalid config" + err.Error())
|
||||||
|
}
|
||||||
|
return &Sender{
|
||||||
|
FSFilter: conf.FSF,
|
||||||
|
encrypt: conf.Encrypt,
|
||||||
|
jobId: conf.JobID,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) filterCheckFS(fs string) (*zfs.DatasetPath, error) {
|
func (s *Sender) filterCheckFS(fs string) (*zfs.DatasetPath, error) {
|
||||||
@@ -43,16 +74,23 @@ func (s *Sender) filterCheckFS(fs string) (*zfs.DatasetPath, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
fss, err := zfs.ZFSListMapping(ctx, s.FSFilter)
|
fss, err := zfs.ZFSListMapping(ctx, s.FSFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rfss := make([]*pdu.Filesystem, len(fss))
|
rfss := make([]*pdu.Filesystem, len(fss))
|
||||||
for i := range fss {
|
for i := range fss {
|
||||||
|
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, fss[i].ToString())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot get filesystem encryption status")
|
||||||
|
}
|
||||||
rfss[i] = &pdu.Filesystem{
|
rfss[i] = &pdu.Filesystem{
|
||||||
Path: fss[i].ToString(),
|
Path: fss[i].ToString(),
|
||||||
// FIXME: not supporting ResumeToken yet
|
// ResumeToken does not make sense from Sender
|
||||||
IsPlaceholder: false, // sender FSs are never placeholders
|
IsPlaceholder: false, // sender FSs are never placeholders
|
||||||
|
IsEncrypted: encEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res := &pdu.ListFilesystemRes{Filesystems: rfss}
|
res := &pdu.ListFilesystemRes{Filesystems: rfss}
|
||||||
@@ -60,11 +98,13 @@ func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error) {
|
func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
lp, err := s.filterCheckFS(r.GetFilesystem())
|
lp, err := s.filterCheckFS(r.GetFilesystem())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
|
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -72,18 +112,77 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
|||||||
for i := range fsvs {
|
for i := range fsvs {
|
||||||
rfsvs[i] = pdu.FilesystemVersionFromZFS(&fsvs[i])
|
rfsvs[i] = pdu.FilesystemVersionFromZFS(&fsvs[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendAbstractions := sendAbstractionsCacheSingleton.GetByFS(ctx, lp.ToString())
|
||||||
|
rSabsInfo := make([]*pdu.SendAbstraction, len(sendAbstractions))
|
||||||
|
for i := range rSabsInfo {
|
||||||
|
rSabsInfo[i] = SendAbstractionToPDU(sendAbstractions[i])
|
||||||
|
}
|
||||||
|
|
||||||
res := &pdu.ListFilesystemVersionsRes{Versions: rfsvs}
|
res := &pdu.ListFilesystemVersionsRes{Versions: rfsvs}
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxConcurrentZFSSendSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10))
|
var maxConcurrentZFSSend = envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10)
|
||||||
|
var maxConcurrentZFSSendSemaphore = semaphore.New(maxConcurrentZFSSend)
|
||||||
|
|
||||||
|
func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion {
|
||||||
|
if fsv == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &zfs.ZFSSendArgVersion{RelName: fsv.GetRelName(), GUID: fsv.Guid}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs string, fsv *pdu.FilesystemVersion) (v zfs.FilesystemVersion, err error) {
|
||||||
|
sendArgs := uncheckedSendArgsFromPDU(fsv)
|
||||||
|
if sendArgs == nil {
|
||||||
|
return v, errors.New("must not be nil")
|
||||||
|
}
|
||||||
|
version, err := sendArgs.ValidateExistsAndGetVersion(ctx, fs)
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
|
||||||
_, err := s.filterCheckFS(r.Filesystem)
|
_, err := s.filterCheckFS(r.Filesystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
switch r.Encrypted {
|
||||||
|
case pdu.Tri_DontCare:
|
||||||
|
// use s.encrypt setting
|
||||||
|
// ok, fallthrough outer
|
||||||
|
case pdu.Tri_False:
|
||||||
|
if s.encrypt.B {
|
||||||
|
return nil, nil, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
|
||||||
|
}
|
||||||
|
// fallthrough outer
|
||||||
|
case pdu.Tri_True:
|
||||||
|
if !s.encrypt.B {
|
||||||
|
return nil, nil, errors.New("only unencrypted sends allowed, but encrypted send requested")
|
||||||
|
}
|
||||||
|
// fallthrough outer
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
|
||||||
|
FS: r.Filesystem,
|
||||||
|
From: uncheckedSendArgsFromPDU(r.GetFrom()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||||
|
To: uncheckedSendArgsFromPDU(r.GetTo()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
|
||||||
|
Encrypted: s.encrypt,
|
||||||
|
ResumeToken: r.ResumeToken, // nil or not nil, depending on decoding success
|
||||||
|
}
|
||||||
|
|
||||||
|
sendArgs, err := sendArgsUnvalidated.Validate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "validate send arguments")
|
||||||
|
}
|
||||||
|
|
||||||
getLogger(ctx).Debug("acquire concurrent send semaphore")
|
getLogger(ctx).Debug("acquire concurrent send semaphore")
|
||||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||||
@@ -95,29 +194,180 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
|
|||||||
}
|
}
|
||||||
defer guard.Release()
|
defer guard.Release()
|
||||||
|
|
||||||
si, err := zfs.ZFSSendDry(r.Filesystem, r.From, r.To, "")
|
si, err := zfs.ZFSSendDry(ctx, sendArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, errors.Wrap(err, "zfs send dry failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From now on, assume that sendArgs has been validated by ZFSSendDry
|
||||||
|
// (because validation involves shelling out, it's actually a little expensive)
|
||||||
|
|
||||||
var expSize int64 = 0 // protocol says 0 means no estimate
|
var expSize int64 = 0 // protocol says 0 means no estimate
|
||||||
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
||||||
expSize = si.SizeEstimate
|
expSize = si.SizeEstimate
|
||||||
}
|
}
|
||||||
res := &pdu.SendRes{ExpectedSize: expSize}
|
res := &pdu.SendRes{
|
||||||
|
ExpectedSize: expSize,
|
||||||
|
UsedResumeToken: r.ResumeToken != "",
|
||||||
|
}
|
||||||
|
|
||||||
if r.DryRun {
|
if r.DryRun {
|
||||||
return res, nil, nil
|
return res, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
streamCopier, err := zfs.ZFSSend(ctx, r.Filesystem, r.From, r.To, "")
|
// create a replication cursor for `From` (usually an idempotent no-op because SendCompleted already created it before)
|
||||||
if err != nil {
|
var fromReplicationCursor Abstraction
|
||||||
return nil, nil, err
|
if sendArgs.From != nil {
|
||||||
|
// For all but the first replication, this should always be a no-op because SendCompleted already moved the cursor
|
||||||
|
fromReplicationCursor, err = CreateReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
|
||||||
|
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||||
|
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
|
||||||
|
// fallthrough
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "cannot set replication cursor to `from` version before starting send")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return res, streamCopier, nil
|
|
||||||
|
var fromHold, toHold Abstraction
|
||||||
|
// make sure `From` doesn't go away in order to make this step resumable
|
||||||
|
if sendArgs.From != nil {
|
||||||
|
fromHold, err = HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
|
||||||
|
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||||
|
getLogger(ctx).Debug("not creating step bookmark because ZFS does not support it")
|
||||||
|
// fallthrough
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "cannot hold `from` version %q before starting send", *sendArgs.FromVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// make sure `To` doesn't go away in order to make this step resumable
|
||||||
|
toHold, err = HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, s.jobId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.ToVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup the mess that _this function_ might have created in prior failed attempts:
|
||||||
|
//
|
||||||
|
// In summary, we delete every endpoint ZFS abstraction created on this filesystem for this job id,
|
||||||
|
// except for the ones we just created above.
|
||||||
|
//
|
||||||
|
// This is the most robust approach to avoid leaking (= forgetting to clean up) endpoint ZFS abstractions,
|
||||||
|
// all under the assumption that there will only ever be one send for a (jobId,fs) combination at any given time.
|
||||||
|
//
|
||||||
|
// Note that the SendCompleted rpc can't be relied upon for this purpose:
|
||||||
|
// - it might be lost due to network errors,
|
||||||
|
// - or never be sent by a potentially malicious or buggy client,
|
||||||
|
// - or never be send because the replication step failed at some point
|
||||||
|
// (potentially leaving a resumable state on the receiver, which is the case where we really do not want to blow away the step holds too soon.)
|
||||||
|
//
|
||||||
|
// Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep,
|
||||||
|
// will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup.
|
||||||
|
func() {
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
|
||||||
|
defer endSpan()
|
||||||
|
|
||||||
|
liveAbs := []Abstraction{fromHold, toHold, fromReplicationCursor}
|
||||||
|
keep := func(a Abstraction) (keep bool) {
|
||||||
|
keep = false
|
||||||
|
for _, k := range liveAbs {
|
||||||
|
keep = keep || AbstractionEquals(a, k)
|
||||||
|
}
|
||||||
|
return keep
|
||||||
|
}
|
||||||
|
check := func(obsoleteAbs []Abstraction) {
|
||||||
|
// last line of defense: check that we don't destroy the incremental `from` and `to`
|
||||||
|
// if we did that, we might be about to blow away the last common filesystem version between sender and receiver
|
||||||
|
mustLiveVersions := []zfs.FilesystemVersion{sendArgs.ToVersion}
|
||||||
|
if sendArgs.FromVersion != nil {
|
||||||
|
mustLiveVersions = append(mustLiveVersions, *sendArgs.FromVersion)
|
||||||
|
}
|
||||||
|
for _, staleVersion := range obsoleteAbs {
|
||||||
|
for _, mustLiveVersion := range mustLiveVersions {
|
||||||
|
if zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion()) {
|
||||||
|
panic(fmt.Sprintf("impl error: %q would be destroyed because it is considered stale but it is part of of sendArgs=%s", mustLiveVersion.String(), pretty.Sprint(sendArgs)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, keep, check)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if fromHold != nil {
|
||||||
|
sendAbstractionsCacheSingleton.Put(fromHold)
|
||||||
|
}
|
||||||
|
sendAbstractionsCacheSingleton.Put(toHold)
|
||||||
|
if fromReplicationCursor != nil {
|
||||||
|
sendAbstractionsCacheSingleton.Put(fromReplicationCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||||
|
if err != nil {
|
||||||
|
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
||||||
|
return nil, nil, errors.Wrap(err, "zfs send failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, sendStream, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
|
orig := r.GetOriginalReq() // may be nil, always use proto getters
|
||||||
|
fsp, err := p.filterCheckFS(orig.GetFilesystem())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fs := fsp.ToString()
|
||||||
|
|
||||||
|
var from *zfs.FilesystemVersion
|
||||||
|
if orig.GetFrom() != nil {
|
||||||
|
f, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, orig.GetFrom()) // no shadow
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "validate `from` exists")
|
||||||
|
}
|
||||||
|
from = &f
|
||||||
|
}
|
||||||
|
to, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, orig.GetTo())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "validate `to` exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := func(ctx context.Context) Logger {
|
||||||
|
log := getLogger(ctx).WithField("to_guid", to.Guid).
|
||||||
|
WithField("fs", fs).
|
||||||
|
WithField("to", to.RelName)
|
||||||
|
if from != nil {
|
||||||
|
log = log.WithField("from", from.RelName).WithField("from_guid", from.Guid)
|
||||||
|
}
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, p.jobId)
|
||||||
|
if err != nil {
|
||||||
|
if err == zfs.ErrBookmarkCloningNotSupported {
|
||||||
|
log(ctx).Debug("not setting replication cursor, bookmark cloning not supported")
|
||||||
|
} else {
|
||||||
|
msg := "cannot move replication cursor, keeping hold on `to` until successful"
|
||||||
|
log(ctx).WithError(err).Error(msg)
|
||||||
|
err = errors.Wrap(err, msg)
|
||||||
|
// it is correct to not destroy from and to step holds if we can't move the cursor!
|
||||||
|
return &pdu.SendCompletedRes{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(ctx).WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor")
|
||||||
|
}
|
||||||
|
|
||||||
|
keep := func(a Abstraction) bool {
|
||||||
|
return AbstractionEquals(a, toReplicationCursor)
|
||||||
|
}
|
||||||
|
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, keep, nil)
|
||||||
|
|
||||||
|
return &pdu.SendCompletedRes{}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Sender) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
func (p *Sender) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
dp, err := p.filterCheckFS(req.Filesystem)
|
dp, err := p.filterCheckFS(req.Filesystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -126,6 +376,8 @@ func (p *Sender) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshots
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Sender) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
func (p *Sender) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
res := pdu.PingRes{
|
res := pdu.PingRes{
|
||||||
Echo: req.GetMessage(),
|
Echo: req.GetMessage(),
|
||||||
}
|
}
|
||||||
@@ -133,41 +385,36 @@ func (p *Sender) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Sender) PingDataconn(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
func (p *Sender) PingDataconn(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
return p.Ping(ctx, req)
|
return p.Ping(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Sender) WaitForConnectivity(ctx context.Context) error {
|
func (p *Sender) WaitForConnectivity(ctx context.Context) error {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Sender) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
func (p *Sender) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
dp, err := p.filterCheckFS(req.Filesystem)
|
dp, err := p.filterCheckFS(req.Filesystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch op := req.Op.(type) {
|
cursor, err := GetMostRecentReplicationCursorOfJob(ctx, dp.ToString(), p.jobId)
|
||||||
case *pdu.ReplicationCursorReq_Get:
|
if err != nil {
|
||||||
cursor, err := zfs.ZFSGetReplicationCursor(dp)
|
return nil, err
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if cursor == nil {
|
|
||||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Notexist{Notexist: true}}, nil
|
|
||||||
}
|
|
||||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil
|
|
||||||
case *pdu.ReplicationCursorReq_Set:
|
|
||||||
guid, err := zfs.ZFSSetReplicationCursor(dp, op.Set.Snapshot)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: guid}}, nil
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("unknown op %T", op)
|
|
||||||
}
|
}
|
||||||
|
if cursor == nil {
|
||||||
|
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Notexist{Notexist: true}}, nil
|
||||||
|
}
|
||||||
|
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, _ io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||||
return nil, fmt.Errorf("sender does not implement Receive()")
|
return nil, fmt.Errorf("sender does not implement Receive()")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,22 +430,42 @@ type FSMap interface { // FIXME unused
|
|||||||
AsFilter() FSFilter
|
AsFilter() FSFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReceiverConfig struct {
|
||||||
|
JobID JobID
|
||||||
|
|
||||||
|
RootWithoutClientComponent *zfs.DatasetPath // TODO use
|
||||||
|
AppendClientIdentity bool
|
||||||
|
|
||||||
|
UpdateLastReceivedHold bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ReceiverConfig) copyIn() {
|
||||||
|
c.RootWithoutClientComponent = c.RootWithoutClientComponent.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ReceiverConfig) Validate() error {
|
||||||
|
c.JobID.MustValidate()
|
||||||
|
if c.RootWithoutClientComponent.Length() <= 0 {
|
||||||
|
return errors.New("RootWithoutClientComponent must not be an empty dataset path")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Receiver implements replication.ReplicationEndpoint for a receiving side
|
// Receiver implements replication.ReplicationEndpoint for a receiving side
|
||||||
type Receiver struct {
|
type Receiver struct {
|
||||||
rootWithoutClientComponent *zfs.DatasetPath
|
conf ReceiverConfig // validated
|
||||||
appendClientIdentity bool
|
|
||||||
|
|
||||||
recvParentCreationMtx *chainlock.L
|
recvParentCreationMtx *chainlock.L
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReceiver(rootDataset *zfs.DatasetPath, appendClientIdentity bool) *Receiver {
|
func NewReceiver(config ReceiverConfig) *Receiver {
|
||||||
if rootDataset.Length() <= 0 {
|
config.copyIn()
|
||||||
panic(fmt.Sprintf("root dataset must not be an empty path: %v", rootDataset))
|
if err := config.Validate(); err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
return &Receiver{
|
return &Receiver{
|
||||||
rootWithoutClientComponent: rootDataset.Copy(),
|
conf: config,
|
||||||
appendClientIdentity: appendClientIdentity,
|
recvParentCreationMtx: chainlock.New(),
|
||||||
recvParentCreationMtx: chainlock.New(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +488,8 @@ func clientRoot(rootFS *zfs.DatasetPath, clientIdentity string) (*zfs.DatasetPat
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
||||||
if !s.appendClientIdentity {
|
if !s.conf.AppendClientIdentity {
|
||||||
return s.rootWithoutClientComponent.Copy()
|
return s.conf.RootWithoutClientComponent.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIdentity, ok := ctx.Value(ClientIdentityKey).(string)
|
clientIdentity, ok := ctx.Value(ClientIdentityKey).(string)
|
||||||
@@ -230,7 +497,7 @@ func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
|||||||
panic(fmt.Sprintf("ClientIdentityKey context value must be set"))
|
panic(fmt.Sprintf("ClientIdentityKey context value must be set"))
|
||||||
}
|
}
|
||||||
|
|
||||||
clientRoot, err := clientRoot(s.rootWithoutClientComponent, clientIdentity)
|
clientRoot, err := clientRoot(s.conf.RootWithoutClientComponent, clientIdentity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("ClientIdentityContextKey must have been validated before invoking Receiver: %s", err))
|
panic(fmt.Sprintf("ClientIdentityContextKey must have been validated before invoking Receiver: %s", err))
|
||||||
}
|
}
|
||||||
@@ -262,6 +529,15 @@ func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
|
// first make sure that root_fs is imported
|
||||||
|
if rphs, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, s.conf.RootWithoutClientComponent); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot determine whether root_fs exists")
|
||||||
|
} else if !rphs.FSExists {
|
||||||
|
return nil, errors.New("root_fs does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
root := s.clientRootFromCtx(ctx)
|
||||||
filtered, err := zfs.ZFSListMapping(ctx, subroot{root})
|
filtered, err := zfs.ZFSListMapping(ctx, subroot{root})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -271,7 +547,7 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
|||||||
fss := make([]*pdu.Filesystem, 0, len(filtered))
|
fss := make([]*pdu.Filesystem, 0, len(filtered))
|
||||||
for _, a := range filtered {
|
for _, a := range filtered {
|
||||||
l := getLogger(ctx).WithField("fs", a)
|
l := getLogger(ctx).WithField("fs", a)
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(a)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("error getting placeholder state")
|
l.WithError(err).Error("error getting placeholder state")
|
||||||
return nil, errors.Wrapf(err, "cannot get placeholder state for fs %q", a)
|
return nil, errors.Wrapf(err, "cannot get placeholder state for fs %q", a)
|
||||||
@@ -282,8 +558,27 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
|||||||
err := errors.Errorf("inconsistent placeholder state: filesystem %q must exist in this context", a.ToString())
|
err := errors.Errorf("inconsistent placeholder state: filesystem %q must exist in this context", a.ToString())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
token, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, a)
|
||||||
|
if err != nil {
|
||||||
|
l.WithError(err).Error("cannot get receive resume token")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, a.ToString())
|
||||||
|
if err != nil {
|
||||||
|
l.WithError(err).Error("cannot get encryption enabled status")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
l.WithField("receive_resume_token", token).Debug("receive resume token")
|
||||||
|
|
||||||
a.TrimPrefix(root)
|
a.TrimPrefix(root)
|
||||||
fss = append(fss, &pdu.Filesystem{Path: a.ToString(), IsPlaceholder: ph.IsPlaceholder})
|
|
||||||
|
fs := &pdu.Filesystem{
|
||||||
|
Path: a.ToString(),
|
||||||
|
IsPlaceholder: ph.IsPlaceholder,
|
||||||
|
ResumeToken: token,
|
||||||
|
IsEncrypted: encEnabled,
|
||||||
|
}
|
||||||
|
fss = append(fss, fs)
|
||||||
}
|
}
|
||||||
if len(fss) == 0 {
|
if len(fss) == 0 {
|
||||||
getLogger(ctx).Debug("no filesystems found")
|
getLogger(ctx).Debug("no filesystems found")
|
||||||
@@ -293,13 +588,16 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error) {
|
func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
root := s.clientRootFromCtx(ctx)
|
||||||
lp, err := subroot{root}.MapToLocal(req.GetFilesystem())
|
lp, err := subroot{root}.MapToLocal(req.GetFilesystem())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// TODO share following code with sender
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
|
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -313,6 +611,8 @@ func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
func (s *Receiver) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
res := pdu.PingRes{
|
res := pdu.PingRes{
|
||||||
Echo: req.GetMessage(),
|
Echo: req.GetMessage(),
|
||||||
}
|
}
|
||||||
@@ -320,31 +620,45 @@ func (s *Receiver) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) PingDataconn(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
func (s *Receiver) PingDataconn(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
return s.Ping(ctx, req)
|
return s.Ping(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) WaitForConnectivity(ctx context.Context) error {
|
func (s *Receiver) WaitForConnectivity(ctx context.Context) error {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) ReplicationCursor(context.Context, *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
func (s *Receiver) ReplicationCursor(ctx context.Context, _ *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver")
|
return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10))
|
var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10))
|
||||||
|
|
||||||
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
getLogger(ctx).Debug("incoming Receive")
|
getLogger(ctx).Debug("incoming Receive")
|
||||||
defer receive.Close()
|
defer receive.Close()
|
||||||
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
root := s.clientRootFromCtx(ctx)
|
||||||
lp, err := subroot{root}.MapToLocal(req.Filesystem)
|
lp, err := subroot{root}.MapToLocal(req.Filesystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "`Filesystem` invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
to := uncheckedSendArgsFromPDU(req.GetTo())
|
||||||
|
if to == nil {
|
||||||
|
return nil, errors.New("`To` must not be nil")
|
||||||
|
}
|
||||||
|
if !to.IsSnapshot() {
|
||||||
|
return nil, errors.New("`To` must be a snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
// create placeholder parent filesystems as appropriate
|
// create placeholder parent filesystems as appropriate
|
||||||
@@ -355,9 +669,9 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
// ZFS dataset hierarchy subtrees.
|
// ZFS dataset hierarchy subtrees.
|
||||||
var visitErr error
|
var visitErr error
|
||||||
func() {
|
func() {
|
||||||
getLogger(ctx).Debug("begin aquire recvParentCreationMtx")
|
getLogger(ctx).Debug("begin acquire recvParentCreationMtx")
|
||||||
defer s.recvParentCreationMtx.Lock().Unlock()
|
defer s.recvParentCreationMtx.Lock().Unlock()
|
||||||
getLogger(ctx).Debug("end aquire recvParentCreationMtx")
|
getLogger(ctx).Debug("end acquire recvParentCreationMtx")
|
||||||
defer getLogger(ctx).Debug("release recvParentCreationMtx")
|
defer getLogger(ctx).Debug("release recvParentCreationMtx")
|
||||||
|
|
||||||
f := zfs.NewDatasetPathForest()
|
f := zfs.NewDatasetPathForest()
|
||||||
@@ -367,7 +681,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
if v.Path.Equal(lp) {
|
if v.Path.Equal(lp) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(v.Path)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
|
||||||
getLogger(ctx).
|
getLogger(ctx).
|
||||||
WithField("fs", v.Path.ToString()).
|
WithField("fs", v.Path.ToString()).
|
||||||
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
||||||
@@ -380,18 +694,18 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ph.FSExists {
|
if !ph.FSExists {
|
||||||
if s.rootWithoutClientComponent.HasPrefix(v.Path) {
|
if s.conf.RootWithoutClientComponent.HasPrefix(v.Path) {
|
||||||
if v.Path.Length() == 1 {
|
if v.Path.Length() == 1 {
|
||||||
visitErr = fmt.Errorf("pool %q not imported", v.Path.ToString())
|
visitErr = fmt.Errorf("pool %q not imported", v.Path.ToString())
|
||||||
} else {
|
} else {
|
||||||
visitErr = fmt.Errorf("root_fs %q does not exist", s.rootWithoutClientComponent.ToString())
|
visitErr = fmt.Errorf("root_fs %q does not exist", s.conf.RootWithoutClientComponent.ToString())
|
||||||
}
|
}
|
||||||
getLogger(ctx).WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
getLogger(ctx).WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||||
l.Debug("create placeholder filesystem")
|
l.Debug("create placeholder filesystem")
|
||||||
err := zfs.ZFSCreatePlaceholderFilesystem(v.Path)
|
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
l.WithError(err).Error("cannot create placeholder filesystem")
|
||||||
visitErr = err
|
visitErr = err
|
||||||
@@ -408,21 +722,41 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
return nil, visitErr
|
return nil, visitErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log := getLogger(ctx).WithField("proto_fs", req.GetFilesystem()).WithField("local_fs", lp.ToString())
|
||||||
|
|
||||||
// determine whether we need to rollback the filesystem / change its placeholder state
|
// determine whether we need to rollback the filesystem / change its placeholder state
|
||||||
var clearPlaceholderProperty bool
|
var clearPlaceholderProperty bool
|
||||||
var recvOpts zfs.RecvOptions
|
var recvOpts zfs.RecvOptions
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(lp)
|
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, lp)
|
||||||
if err == nil && ph.FSExists && ph.IsPlaceholder {
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot get placeholder state")
|
||||||
|
}
|
||||||
|
log.WithField("placeholder_state", fmt.Sprintf("%#v", ph)).Debug("placeholder state")
|
||||||
|
if ph.FSExists && ph.IsPlaceholder {
|
||||||
recvOpts.RollbackAndForceRecv = true
|
recvOpts.RollbackAndForceRecv = true
|
||||||
clearPlaceholderProperty = true
|
clearPlaceholderProperty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if clearPlaceholderProperty {
|
if clearPlaceholderProperty {
|
||||||
if err := zfs.ZFSSetPlaceholder(lp, false); err != nil {
|
log.Info("clearing placeholder property")
|
||||||
|
if err := zfs.ZFSSetPlaceholder(ctx, lp, false); err != nil {
|
||||||
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
|
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogger(ctx).Debug("acquire concurrent recv semaphore")
|
if req.ClearResumeToken && ph.FSExists {
|
||||||
|
log.Info("clearing resume token")
|
||||||
|
if err := zfs.ZFSRecvClearResumeToken(ctx, lp.ToString()); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot clear resume token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recvOpts.SavePartialRecvState, err = zfs.ResumeRecvSupported(ctx, lp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("acquire concurrent recv semaphore")
|
||||||
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
// TODO use try-acquire and fail with resource-exhaustion rpc status
|
||||||
// => would require handling on the client-side
|
// => would require handling on the client-side
|
||||||
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
|
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
|
||||||
@@ -432,19 +766,114 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
|||||||
}
|
}
|
||||||
defer guard.Release()
|
defer guard.Release()
|
||||||
|
|
||||||
getLogger(ctx).WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
|
var peek bytes.Buffer
|
||||||
|
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
||||||
|
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
||||||
|
if _, err := io.Copy(&peek, io.LimitReader(receive, MaxPeek)); err != nil {
|
||||||
|
log.WithError(err).Error("cannot read peek-buffer from send stream")
|
||||||
|
}
|
||||||
|
var peekCopy bytes.Buffer
|
||||||
|
if n, err := peekCopy.Write(peek.Bytes()); err != nil || n != peek.Len() {
|
||||||
|
panic(peek.Len())
|
||||||
|
}
|
||||||
|
|
||||||
if err := zfs.ZFSRecv(ctx, lp.ToString(), receive, recvOpts); err != nil {
|
log.WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
|
||||||
getLogger(ctx).
|
|
||||||
|
snapFullPath := to.FullPath(lp.ToString())
|
||||||
|
if err := zfs.ZFSRecv(ctx, lp.ToString(), to, chainedio.NewChainedReader(&peek, receive), recvOpts); err != nil {
|
||||||
|
|
||||||
|
// best-effort rollback of placeholder state if the recv didn't start
|
||||||
|
_, resumableStatePresent := err.(*zfs.RecvFailedWithResumeTokenErr)
|
||||||
|
disablePlaceholderRestoration := envconst.Bool("ZREPL_ENDPOINT_DISABLE_PLACEHOLDER_RESTORATION", false)
|
||||||
|
placeholderRestored := !ph.IsPlaceholder
|
||||||
|
if !disablePlaceholderRestoration && !resumableStatePresent && recvOpts.RollbackAndForceRecv && ph.FSExists && ph.IsPlaceholder && clearPlaceholderProperty {
|
||||||
|
log.Info("restoring placeholder property")
|
||||||
|
if phErr := zfs.ZFSSetPlaceholder(ctx, lp, true); phErr != nil {
|
||||||
|
log.WithError(phErr).Error("cannot restore placeholder property after failed receive, subsequent replications will likely fail with a different error")
|
||||||
|
// fallthrough
|
||||||
|
} else {
|
||||||
|
placeholderRestored = true
|
||||||
|
}
|
||||||
|
// fallthrough
|
||||||
|
}
|
||||||
|
|
||||||
|
// deal with failing initial encrypted send & recv
|
||||||
|
if _, ok := err.(*zfs.RecvDestroyOrOverwriteEncryptedErr); ok && ph.IsPlaceholder && placeholderRestored {
|
||||||
|
msg := `cannot automatically replace placeholder filesystem with incoming send stream - please see receive-side log for details`
|
||||||
|
err := errors.New(msg)
|
||||||
|
log.Error(msg)
|
||||||
|
|
||||||
|
log.Error(`zrepl creates placeholder filesystems on the receiving side of a replication to match the sending side's dataset hierarchy`)
|
||||||
|
log.Error(`zrepl uses zfs receive -F to replace those placeholders with incoming full sends`)
|
||||||
|
log.Error(`OpenZFS native encryption prohibits zfs receive -F for encrypted filesystems`)
|
||||||
|
log.Error(`the current zrepl placeholder filesystem concept is thus incompatible with OpenZFS native encryption`)
|
||||||
|
|
||||||
|
tempStartFullRecvFS := lp.Copy().ToString() + ".zrepl.initial-recv"
|
||||||
|
tempStartFullRecvFSDP, dpErr := zfs.NewDatasetPath(tempStartFullRecvFS)
|
||||||
|
if dpErr != nil {
|
||||||
|
log.WithError(dpErr).Error("cannot determine temporary filesystem name for initial encrypted recv workaround")
|
||||||
|
return nil, err // yes, err, not dpErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log := log.WithField("temp_recv_fs", tempStartFullRecvFS)
|
||||||
|
log.Error(`as a workaround, zrepl will now attempt to re-receive the beginning of the stream into a temporary filesystem temp_recv_fs`)
|
||||||
|
log.Error(`if that step succeeds: shut down zrepl and use 'zfs rename' to swap temp_recv_fs with local_fs, then restart zrepl`)
|
||||||
|
log.Error(`replication will then resume using resumable send+recv`)
|
||||||
|
|
||||||
|
tempPH, phErr := zfs.ZFSGetFilesystemPlaceholderState(ctx, tempStartFullRecvFSDP)
|
||||||
|
if phErr != nil {
|
||||||
|
log.WithError(phErr).Error("cannot determine placeholder state of temp_recv_fs")
|
||||||
|
return nil, err // yes, err, not dpErr
|
||||||
|
}
|
||||||
|
if tempPH.FSExists {
|
||||||
|
log.Error("temp_recv_fs already exists, assuming a (partial) initial recv to that filesystem has already been done")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recvOpts.RollbackAndForceRecv = false
|
||||||
|
recvOpts.SavePartialRecvState = true
|
||||||
|
rerecvErr := zfs.ZFSRecv(ctx, tempStartFullRecvFS, to, chainedio.NewChainedReader(&peekCopy), recvOpts)
|
||||||
|
if _, isResumable := rerecvErr.(*zfs.RecvFailedWithResumeTokenErr); rerecvErr == nil || isResumable {
|
||||||
|
log.Error("completed re-receive into temporary filesystem temp_recv_fs, now shut down zrepl and use zfs rename to swap temp_recv_fs with local_fs")
|
||||||
|
} else {
|
||||||
|
log.WithError(rerecvErr).Error("failed to receive the beginning of the stream into temporary filesystem temp_recv_fs")
|
||||||
|
log.Error("we advise you to collect the error log and current configuration, open an issue on GitHub, and revert to your previous configuration in the meantime")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error(`if you would like to see improvements to this situation, please open an issue on GitHub`)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.
|
||||||
WithError(err).
|
WithError(err).
|
||||||
WithField("opts", recvOpts).
|
WithField("opts", fmt.Sprintf("%#v", recvOpts)).
|
||||||
Error("zfs receive failed")
|
Error("zfs receive failed")
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate that we actually received what the sender claimed
|
||||||
|
toRecvd, err := to.ValidateExistsAndGetVersion(ctx, lp.ToString())
|
||||||
|
if err != nil {
|
||||||
|
msg := "receive request's `To` version does not match what we received in the stream"
|
||||||
|
log.WithError(err).WithField("snap", snapFullPath).Error(msg)
|
||||||
|
log.Error("aborting recv request, but keeping received snapshot for inspection")
|
||||||
|
return nil, errors.Wrap(err, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.conf.UpdateLastReceivedHold {
|
||||||
|
log.Debug("move last-received-hold")
|
||||||
|
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot move last-received-hold")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &pdu.ReceiveRes{}, nil
|
return &pdu.ReceiveRes{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
root := s.clientRootFromCtx(ctx)
|
||||||
lp, err := subroot{root}.MapToLocal(req.Filesystem)
|
lp, err := subroot{root}.MapToLocal(req.Filesystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -453,6 +882,12 @@ func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapsho
|
|||||||
return doDestroySnapshots(ctx, lp, req.Snapshots)
|
return doDestroySnapshots(ctx, lp, req.Snapshots)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Receiver) SendCompleted(ctx context.Context, _ *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
|
||||||
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
|
return &pdu.SendCompletedRes{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.FilesystemVersion) (*pdu.DestroySnapshotsRes, error) {
|
func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.FilesystemVersion) (*pdu.DestroySnapshotsRes, error) {
|
||||||
reqs := make([]*zfs.DestroySnapOp, len(snaps))
|
reqs := make([]*zfs.DestroySnapOp, len(snaps))
|
||||||
ress := make([]*pdu.DestroySnapshotRes, len(snaps))
|
ress := make([]*pdu.DestroySnapshotRes, len(snaps))
|
||||||
@@ -471,7 +906,7 @@ func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.F
|
|||||||
ErrOut: &errs[i],
|
ErrOut: &errs[i],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
zfs.ZFSDestroyFilesystemVersions(reqs)
|
zfs.ZFSDestroyFilesystemVersions(ctx, reqs)
|
||||||
for i := range reqs {
|
for i := range reqs {
|
||||||
if errs[i] != nil {
|
if errs[i] != nil {
|
||||||
if de, ok := errs[i].(*zfs.DestroySnapshotsError); ok && len(de.Reason) == 1 {
|
if de, ok := errs[i].(*zfs.DestroySnapshotsError); ok && len(de.Reason) == 1 {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user