Compare commits
43 Commits
v0.1.0
..
v0.2.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c95c21727 | |||
| a6b578b648 | |||
| 8af824df41 | |||
| 58ab25919e | |||
| 215848f476 | |||
| f9c7766073 | |||
| f976212ec9 | |||
| 8c88e168c1 | |||
| a78c854404 | |||
| 8a5af2f80e | |||
| f7aa26d418 | |||
| b5ff1a9926 | |||
| 729c83ee72 | |||
| 00434f4ac9 | |||
| 2cd9173bfb | |||
| 7ba3ae077f | |||
| a6497b2c6e | |||
| 07956c2299 | |||
| 77d3a1ad4d | |||
| 2e0ff9a582 | |||
| a65d8f1f4c | |||
| 424234c2d1 | |||
| 3bfe0c16d0 | |||
| e5f944c2f8 | |||
| d81a1818d6 | |||
| 921b34235e | |||
| a0cf9cff2a | |||
| 3105609d39 | |||
| 254a292362 | |||
| 95e16f01c1 | |||
| df21b5d1b4 | |||
| c4be60c29f | |||
| ff18181064 | |||
| b167de3096 | |||
| 9dd187e29d | |||
| 413bd0f43b | |||
| 3c3606d516 | |||
| b5dc04e62c | |||
| 234a327a03 | |||
| 90b32c7377 | |||
| 0fc6a63564 | |||
| 5138681c13 | |||
| d6304f4f1d |
+26
-18
@@ -5,6 +5,7 @@ workflows:
|
||||
jobs:
|
||||
- build-1.11
|
||||
- build-1.12
|
||||
- build-1.13
|
||||
- build-latest
|
||||
jobs:
|
||||
# build-latest serves as the template
|
||||
@@ -27,11 +28,12 @@ jobs:
|
||||
command: |
|
||||
# used by pip (for docs)
|
||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
|
||||
# we use modules
|
||||
echo 'export GO111MODULE=on' >> $BASH_ENV
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- source
|
||||
- vendor
|
||||
- protobuf
|
||||
|
||||
- checkout
|
||||
@@ -50,22 +52,9 @@ jobs:
|
||||
paths:
|
||||
- "/usr/include/google/protobuf"
|
||||
|
||||
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
|
||||
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev
|
||||
- run: ./lazy.sh devsetup
|
||||
|
||||
- run:
|
||||
name: Install minio client, prepare minio
|
||||
command: |
|
||||
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
|
||||
chmod +x ${GOPATH}/bin/mc
|
||||
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||
|
||||
- run: make vendordeps
|
||||
- save_cache:
|
||||
key: vendor
|
||||
paths:
|
||||
- "./vendor"
|
||||
|
||||
- run: make
|
||||
- run: make vet
|
||||
- run: make test
|
||||
@@ -78,11 +67,25 @@ jobs:
|
||||
when: always
|
||||
|
||||
- run:
|
||||
shell: /bin/bash -euo pipefail
|
||||
shell: /bin/bash -eo pipefail
|
||||
when: always
|
||||
command: |
|
||||
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
||||
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
|
||||
exit 0
|
||||
fi
|
||||
set -u # from now on
|
||||
|
||||
# Download and install minio
|
||||
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
|
||||
chmod +x ${GOPATH}/bin/mc
|
||||
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||
|
||||
# Upload artifacts
|
||||
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
|
||||
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
|
||||
|
||||
# Push Artifact Link to GitHub
|
||||
REPO="zrepl/zrepl"
|
||||
COMMIT="${CIRCLE_SHA1}"
|
||||
JOB_NAME="${CIRCLE_JOB}"
|
||||
@@ -91,12 +94,17 @@ jobs:
|
||||
-X POST \
|
||||
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
|
||||
|
||||
|
||||
build-1.11:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
- image: circleci/golang:1.11
|
||||
|
||||
build-1.12:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
|
||||
build-1.13:
|
||||
<<: *build-latest
|
||||
docker:
|
||||
- image: circleci/golang:1.13
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
patreon: zrepl
|
||||
liberapay: zrepl
|
||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
ko_fi: problame
|
||||
Generated
-1038
File diff suppressed because it is too large
Load Diff
-67
@@ -1,67 +0,0 @@
|
||||
ignored = [
|
||||
"github.com/inconshreveable/mousetrap",
|
||||
]
|
||||
|
||||
required = [
|
||||
"golang.org/x/tools/cmd/stringer",
|
||||
"github.com/alvaroloes/enumer",
|
||||
"github.com/golangci/golangci-lint/cmd/golangci-lint",
|
||||
"golang.org/x/tools/cmd/goimports",
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/copier"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/pretty"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/cobra"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.1.4"
|
||||
|
||||
[[constraint]]
|
||||
branch = "v2"
|
||||
name = "github.com/zrepl/yaml-config"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-logfmt/logfmt"
|
||||
version = "*"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/problame/go-netssh"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/color"
|
||||
version = "1.7.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gdamore/tcell"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "google.golang.org/grpc"
|
||||
version = "1"
|
||||
|
||||
[[constraint]]
|
||||
version = "1.1.0"
|
||||
name = "github.com/google/uuid"
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: generate build test vet cover release docs docs-clean clean vendordeps format lint
|
||||
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
|
||||
.DEFAULT_GOAL := build
|
||||
|
||||
ARTIFACTDIR := artifacts
|
||||
@@ -12,23 +12,24 @@ ifndef _ZREPL_VERSION
|
||||
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
||||
endif
|
||||
endif
|
||||
GO := go
|
||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||
GO_MOD_READONLY := -mod=readonly
|
||||
GO_BUILDFLAGS := $(GO_MOD_READONLY)
|
||||
GO_BUILD := GO111MODULE=on $(GO) build $(GO_BUILDFLAGS) -v -ldflags $(GO_LDFLAGS)
|
||||
|
||||
GO_BUILD := go build -ldflags $(GO_LDFLAGS)
|
||||
|
||||
# keep in sync with vet target
|
||||
RELEASE_BINS := $(ARTIFACTDIR)/zrepl-freebsd-amd64
|
||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-amd64
|
||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-arm64
|
||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-darwin-amd64
|
||||
|
||||
RELEASE_NOARCH := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <(go env) && echo "zrepl-$${GOOS}-$${GOARCH}"' )
|
||||
|
||||
vendordeps:
|
||||
dep ensure -v -vendor-only
|
||||
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <($(GO) env) && echo "zrepl-$${GOOS}-$${GOARCH}"' )
|
||||
|
||||
generate: #not part of the build, must do that manually
|
||||
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
||||
go generate -x ./...
|
||||
$(GO) generate $(GO_BUILDFLAGS) -x ./...
|
||||
|
||||
format:
|
||||
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
|
||||
@@ -37,17 +38,28 @@ lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
build:
|
||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl"
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
$(GO) test $(GO_BUILDFLAGS) ./...
|
||||
# TODO compile the tests for each supported platform
|
||||
# but `go test -c ./...` is not supported
|
||||
|
||||
vet:
|
||||
$(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
# for each supported platform to cover conditional compilation
|
||||
GOOS=linux go vet ./...
|
||||
GOOS=darwin go vet ./...
|
||||
GOOS=freebsd go vet ./...
|
||||
# (keep in sync with RELEASE_BINS)
|
||||
GOOS=freebsd GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
GOOS=linux GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
GOOS=linux GOARCH=arm64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
GOOS=darwin GOARCH=amd64 $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||
|
||||
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||
$(ARTIFACTDIR)/zrepl_platformtest:
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl_platformtest" ./platformtest/harness
|
||||
platformtest: $(ARTIFACTDIR)/zrepl_platformtest
|
||||
"$(ARTIFACTDIR)/zrepl_platformtest" -poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" -imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)"
|
||||
|
||||
$(ARTIFACTDIR):
|
||||
mkdir -p "$@"
|
||||
@@ -58,8 +70,9 @@ $(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||
$(ARTIFACTDIR)/bash_completion: $(RELEASE_BINS)
|
||||
artifacts/$(THIS_PLATFORM_RELEASE_BIN) bashcomp "$@"
|
||||
|
||||
.PHONY: $(ARTIFACTDIR)/go_version.txt
|
||||
$(ARTIFACTDIR)/go_version.txt:
|
||||
go version > $@
|
||||
$(GO) version > $@
|
||||
|
||||
docs: $(ARTIFACTDIR)/docs
|
||||
make -C docs \
|
||||
@@ -71,12 +84,11 @@ docs-clean:
|
||||
clean \
|
||||
BUILDDIR=../artifacts/docs
|
||||
|
||||
|
||||
.PHONY: $(RELEASE_BINS)
|
||||
# TODO: two wildcards possible
|
||||
$(RELEASE_BINS): $(ARTIFACTDIR)/zrepl-%-amd64: generate $(ARTIFACTDIR) vet test lint
|
||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
||||
GOOS=$* GOARCH=amd64 $(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$*-amd64"
|
||||
$(RELEASE_BINS): $(ARTIFACTDIR)/zrepl-%: generate $(ARTIFACTDIR) vet test lint
|
||||
STEM=$*; GOOS="$${STEM%%-*}"; GOARCH="$${STEM##*-}"; export GOOS GOARCH; \
|
||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$$GOOS-$$GOARCH"
|
||||
|
||||
$(RELEASE_NOARCH): docs $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_version.txt
|
||||
tar --mtime='1970-01-01' --sort=name \
|
||||
@@ -96,6 +108,9 @@ release: $(RELEASE_BINS) $(RELEASE_NOARCH)
|
||||
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
||||
if [ "$(ZREPL_VERSION)" = "" ]; then \
|
||||
echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
|
||||
git status; \
|
||||
echo "git diff:"; \
|
||||
git diff | cat; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[](https://golang.org/)
|
||||
[](https://zrepl.github.io)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||
[](https://liberapay.com/zrepl/donate)
|
||||
[](https://liberapay.com/zrepl/donate)
|
||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||
|
||||
# zrepl
|
||||
@@ -37,11 +37,12 @@ Check out the *Coding Workflow* section below for details.
|
||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
||||
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
||||
* Use `make platformtest` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||
|
||||
## Developer Documentation
|
||||
|
||||
zrepl is written in [Go](https://golang.org) and uses [`dep`](https://github.com/golang/dep) to manage dependencies.
|
||||
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
||||
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
||||
|
||||
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
||||
|
||||
+10
-12
@@ -4,9 +4,8 @@ RUN apt-get update && apt-get install -y \
|
||||
python3-pip \
|
||||
unzip
|
||||
|
||||
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
||||
RUN echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
||||
RUN unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
||||
ADD build.installprotoc.bash ./
|
||||
RUN bash build.installprotoc.bash
|
||||
|
||||
ADD lazy.sh /tmp/lazy.sh
|
||||
ADD docs/requirements.txt /tmp/requirements.txt
|
||||
@@ -14,18 +13,17 @@ ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
|
||||
RUN /tmp/lazy.sh docdep
|
||||
|
||||
# prepare volume mount of git checkout to /zrepl
|
||||
RUN mkdir -p /go/src/github.com/zrepl/zrepl
|
||||
RUN mkdir -p /src/github.com/zrepl/zrepl
|
||||
RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
||||
WORKDIR /go/src/github.com/zrepl/zrepl
|
||||
|
||||
ADD Gopkg.toml Gopkg.lock ./
|
||||
# $GOPATH is /go
|
||||
# Go 1.12 doesn't use modules within GOPATH, but 1.13 and later do
|
||||
# => store source outside of GOPATH
|
||||
WORKDIR /src
|
||||
|
||||
# godep will install the Go dependencies to vendor in order to then build and install
|
||||
# build dependencies like stringer to $GOPATH/bin.
|
||||
# However, since users volume-mount their Git checkout into /go/src/github.com/zrepl/zrepl
|
||||
# the vendor directory will be empty at build time, allowing them to experiment with
|
||||
# new checkouts, etc.
|
||||
# Thus, we only use the vendored deps for building dependencies.
|
||||
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
|
||||
ADD go.mod go.sum ./
|
||||
RUN /tmp/lazy.sh godep
|
||||
|
||||
RUN chmod -R 0777 /go
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
MACH=$(uname -m)
|
||||
MACH="${MACH/aarch64/aarch_64}"
|
||||
|
||||
VERSION=3.6.1
|
||||
FILENAME=protoc-"$VERSION"-linux-"$MACH".zip
|
||||
|
||||
if [ -e "$FILENAME" ]; then
|
||||
echo "$FILENAME" already exists 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wget https://github.com/protocolbuffers/protobuf/releases/download/v"$VERSION"/"$FILENAME"
|
||||
|
||||
stat "$FILENAME"
|
||||
|
||||
sha256sum -c --ignore-missing <<EOF
|
||||
6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip
|
||||
af8e5aaaf39ddec62ec8dd2be1b8d9602c6da66564883a16393ade5f71170922 protoc-3.6.1-linux-aarch_64.zip
|
||||
EOF
|
||||
|
||||
unzip -d /usr "$FILENAME"
|
||||
+136
-11
@@ -11,7 +11,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// tcell is the termbox-compatbile library for abstracting away escape sequences, etc.
|
||||
// as of tcell#252, the number of default distributed terminals is relatively limited
|
||||
// additional terminal definitions can be included via side-effect import
|
||||
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
|
||||
// See https://github.com/gdamore/tcell/issues/252#issuecomment-533836078
|
||||
"github.com/gdamore/tcell/termbox"
|
||||
_ "github.com/gdamore/tcell/terminfo/s/screen" // tmux on FreeBSD 11 & 12 without ncurses
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/zrepl/yaml-config"
|
||||
@@ -20,6 +27,7 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon"
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
)
|
||||
|
||||
@@ -76,6 +84,8 @@ type tui struct {
|
||||
report map[string]job.Status
|
||||
err error
|
||||
|
||||
jobFilter string
|
||||
|
||||
replicationProgress map[string]*bytesProgressHistory // by job name
|
||||
}
|
||||
|
||||
@@ -157,6 +167,7 @@ func (t *tui) addIndent(indent int) {
|
||||
|
||||
var statusFlags struct {
|
||||
Raw bool
|
||||
Job string
|
||||
}
|
||||
|
||||
var StatusCmd = &cli.Subcommand{
|
||||
@@ -164,6 +175,7 @@ var StatusCmd = &cli.Subcommand{
|
||||
Short: "show job activity or dump as JSON for monitoring",
|
||||
SetupFlags: func(f *pflag.FlagSet) {
|
||||
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
||||
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
|
||||
},
|
||||
Run: runStatus,
|
||||
}
|
||||
@@ -198,6 +210,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
|
||||
t.lock.Lock()
|
||||
t.err = errors.New("Got no report yet")
|
||||
t.lock.Unlock()
|
||||
t.jobFilter = statusFlags.Job
|
||||
|
||||
err = termbox.Init()
|
||||
if err != nil {
|
||||
@@ -273,19 +286,29 @@ func (t *tui) draw() {
|
||||
t.write(t.err.Error())
|
||||
} else {
|
||||
//Iterate over map in alphabetical order
|
||||
keys := make([]string, len(t.report))
|
||||
i := 0
|
||||
keys := make([]string, 0, len(t.report))
|
||||
for k := range t.report {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := t.report[k]
|
||||
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
||||
continue
|
||||
}
|
||||
if t.jobFilter != "" && k != t.jobFilter {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
if len(keys) == 0 {
|
||||
t.setIndent(0)
|
||||
t.printf("no jobs to display")
|
||||
t.newline()
|
||||
termbox.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
v := t.report[k]
|
||||
|
||||
t.setIndent(0)
|
||||
|
||||
t.printf("Job: %s", k)
|
||||
@@ -320,6 +343,15 @@ func (t *tui) draw() {
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(activeStatus.PruningReceiver)
|
||||
t.addIndent(-1)
|
||||
|
||||
if v.Type == job.TypePush {
|
||||
t.printf("Snapshotting:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(activeStatus.Snapshotting)
|
||||
t.addIndent(-1)
|
||||
}
|
||||
|
||||
} else if v.Type == job.TypeSnap {
|
||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
||||
if !ok || snapStatus == nil {
|
||||
@@ -332,6 +364,19 @@ func (t *tui) draw() {
|
||||
t.addIndent(1)
|
||||
t.renderPrunerReport(snapStatus.Pruning)
|
||||
t.addIndent(-1)
|
||||
t.printf("Snapshotting:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(snapStatus.Snapshotting)
|
||||
t.addIndent(-1)
|
||||
} else if v.Type == job.TypeSource {
|
||||
|
||||
st := v.JobSpecific.(*job.PassiveStatus)
|
||||
t.printf("Snapshotting:\n")
|
||||
t.addIndent(1)
|
||||
t.renderSnapperReport(st.Snapper)
|
||||
t.addIndent(-1)
|
||||
|
||||
} else {
|
||||
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
||||
t.newline()
|
||||
@@ -384,7 +429,7 @@ func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgress
|
||||
} else {
|
||||
t.printf("Attempt #%d", len(rep.Attempts))
|
||||
if len(rep.Attempts) > 1 {
|
||||
t.printf(". Previous attempts failed with the follwing statuses:")
|
||||
t.printf(". Previous attempts failed with the following statuses:")
|
||||
t.newline()
|
||||
t.addIndent(1)
|
||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
||||
@@ -545,6 +590,86 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
|
||||
|
||||
}
|
||||
|
||||
func (t *tui) renderSnapperReport(r *snapper.Report) {
|
||||
if r == nil {
|
||||
t.printf("<snapshot type does not have a report>\n")
|
||||
return
|
||||
}
|
||||
|
||||
t.printf("Status: %s", r.State)
|
||||
t.newline()
|
||||
|
||||
if r.Error != "" {
|
||||
t.printf("Error: %s\n", r.Error)
|
||||
}
|
||||
if !r.SleepUntil.IsZero() {
|
||||
t.printf("Sleep until: %s\n", r.SleepUntil)
|
||||
}
|
||||
|
||||
sort.Slice(r.Progress, func(i, j int) bool {
|
||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||
})
|
||||
|
||||
t.addIndent(1)
|
||||
defer t.addIndent(-1)
|
||||
dur := func(d time.Duration) string {
|
||||
return d.Round(100 * time.Millisecond).String()
|
||||
}
|
||||
|
||||
type row struct {
|
||||
path, state, duration, remainder, hookReport string
|
||||
}
|
||||
var widths struct {
|
||||
path, state, duration int
|
||||
}
|
||||
rows := make([]*row, len(r.Progress))
|
||||
for i, fs := range r.Progress {
|
||||
r := &row{
|
||||
path: fs.Path,
|
||||
state: fs.State.String(),
|
||||
}
|
||||
if fs.HooksHadError {
|
||||
r.hookReport = fs.Hooks // FIXME render here, not in daemon
|
||||
}
|
||||
switch fs.State {
|
||||
case snapper.SnapPending:
|
||||
r.duration = "..."
|
||||
r.remainder = ""
|
||||
case snapper.SnapStarted:
|
||||
r.duration = dur(time.Since(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
case snapper.SnapDone:
|
||||
fallthrough
|
||||
case snapper.SnapError:
|
||||
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
|
||||
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
|
||||
}
|
||||
rows[i] = r
|
||||
if len(r.path) > widths.path {
|
||||
widths.path = len(r.path)
|
||||
}
|
||||
if len(r.state) > widths.state {
|
||||
widths.state = len(r.state)
|
||||
}
|
||||
if len(r.duration) > widths.duration {
|
||||
widths.duration = len(r.duration)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
path := rightPad(r.path, widths.path, " ")
|
||||
state := rightPad(r.state, widths.state, " ")
|
||||
duration := rightPad(r.duration, widths.duration, " ")
|
||||
t.printf("%s %s %s", path, state, duration)
|
||||
t.printfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||
if r.hookReport != "" {
|
||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||
}
|
||||
t.newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func times(str string, n int) (out string) {
|
||||
for i := 0; i < n; i++ {
|
||||
out += str
|
||||
@@ -556,7 +681,7 @@ func rightPad(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + times(pad, length-len(str))
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
|
||||
var arrowPositions = `>\|/`
|
||||
|
||||
+51
-8
@@ -139,6 +139,7 @@ type SnapshottingPeriodic struct {
|
||||
Type string `yaml:"type"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Interval time.Duration `yaml:"interval,positive"`
|
||||
Hooks HookList `yaml:"hooks,optional"`
|
||||
}
|
||||
|
||||
type SnapshottingManual struct {
|
||||
@@ -202,13 +203,13 @@ type ConnectCommon struct {
|
||||
|
||||
type TCPConnect struct {
|
||||
ConnectCommon `yaml:",inline"`
|
||||
Address string `yaml:"address"`
|
||||
Address string `yaml:"address,hostport"`
|
||||
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
||||
}
|
||||
|
||||
type TLSConnect struct {
|
||||
ConnectCommon `yaml:",inline"`
|
||||
Address string `yaml:"address"`
|
||||
Address string `yaml:"address,hostport"`
|
||||
Ca string `yaml:"ca"`
|
||||
Cert string `yaml:"cert"`
|
||||
Key string `yaml:"key"`
|
||||
@@ -230,8 +231,9 @@ type SSHStdinserverConnect struct {
|
||||
|
||||
type LocalConnect struct {
|
||||
ConnectCommon `yaml:",inline"`
|
||||
ListenerName string `yaml:"listener_name"`
|
||||
ClientIdentity string `yaml:"client_identity"`
|
||||
ListenerName string `yaml:"listener_name"`
|
||||
ClientIdentity string `yaml:"client_identity"`
|
||||
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=2s"`
|
||||
}
|
||||
|
||||
type ServeEnum struct {
|
||||
@@ -244,13 +246,13 @@ type ServeCommon struct {
|
||||
|
||||
type TCPServe struct {
|
||||
ServeCommon `yaml:",inline"`
|
||||
Listen string `yaml:"listen"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
Clients map[string]string `yaml:"clients"`
|
||||
}
|
||||
|
||||
type TLSServe struct {
|
||||
ServeCommon `yaml:",inline"`
|
||||
Listen string `yaml:"listen"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
Ca string `yaml:"ca"`
|
||||
Cert string `yaml:"cert"`
|
||||
Key string `yaml:"key"`
|
||||
@@ -312,7 +314,7 @@ type SyslogLoggingOutlet struct {
|
||||
|
||||
type TCPLoggingOutlet struct {
|
||||
LoggingOutletCommon `yaml:",inline"`
|
||||
Address string `yaml:"address"`
|
||||
Address string `yaml:"address,hostport"`
|
||||
Net string `yaml:"net,default=tcp"`
|
||||
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
||||
TLS *TCPLoggingOutletTLS `yaml:"tls,optional"`
|
||||
@@ -330,7 +332,7 @@ type MonitoringEnum struct {
|
||||
|
||||
type PrometheusMonitoring struct {
|
||||
Type string `yaml:"type"`
|
||||
Listen string `yaml:"listen"`
|
||||
Listen string `yaml:"listen,hostport"`
|
||||
}
|
||||
|
||||
type SyslogFacility syslog.Priority
|
||||
@@ -361,6 +363,38 @@ type JobDebugSettings struct {
|
||||
RPCLog bool `yaml:"rpc_log,optional,default=false"`
|
||||
}
|
||||
|
||||
type HookList []HookEnum
|
||||
|
||||
type HookEnum struct {
|
||||
Ret interface{}
|
||||
}
|
||||
|
||||
type HookCommand struct {
|
||||
Path string `yaml:"path"`
|
||||
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems,optional,default={'<': true}"`
|
||||
HookSettingsCommon `yaml:",inline"`
|
||||
}
|
||||
|
||||
type HookPostgresCheckpoint struct {
|
||||
HookSettingsCommon `yaml:",inline"`
|
||||
DSN string `yaml:"dsn"`
|
||||
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"` // required, user should not CHECKPOINT for every FS
|
||||
}
|
||||
|
||||
type HookMySQLLockTables struct {
|
||||
HookSettingsCommon `yaml:",inline"`
|
||||
DSN string `yaml:"dsn"`
|
||||
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
|
||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||
}
|
||||
|
||||
type HookSettingsCommon struct {
|
||||
Type string `yaml:"type"`
|
||||
ErrIsFatal bool `yaml:"err_is_fatal,optional,default=false"`
|
||||
}
|
||||
|
||||
func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}) (interface{}, error) {
|
||||
var in struct {
|
||||
Type string
|
||||
@@ -501,6 +535,15 @@ func (t *SyslogFacility) UnmarshalYAML(u func(interface{}, bool) error) (err err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *HookEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||
"command": &HookCommand{},
|
||||
"postgres-checkpoint": &HookPostgresCheckpoint{},
|
||||
"mysql-lock-tables": &HookMySQLLockTables{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var ConfigFileDefaultLocations = []string{
|
||||
"/etc/zrepl/zrepl.yml",
|
||||
"/usr/local/etc/zrepl/zrepl.yml",
|
||||
|
||||
@@ -38,6 +38,29 @@ jobs:
|
||||
interval: 10m
|
||||
`
|
||||
|
||||
hooks := `
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
hooks:
|
||||
- type: command
|
||||
path: /tmp/path/to/command
|
||||
- type: command
|
||||
path: /tmp/path/to/command
|
||||
filesystems: { "zroot<": true, "<": false }
|
||||
- type: postgres-checkpoint
|
||||
dsn: "host=localhost port=5432 user=postgres sslmode=disable"
|
||||
filesystems: {
|
||||
"tank/postgres/data11": true
|
||||
}
|
||||
- type: mysql-lock-tables
|
||||
dsn: "root@tcp(localhost)/"
|
||||
filesystems: {
|
||||
"tank/mysql": true
|
||||
}
|
||||
`
|
||||
|
||||
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||
var c *Config
|
||||
|
||||
@@ -55,4 +78,13 @@ jobs:
|
||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||
})
|
||||
|
||||
t.Run("hooks", func(t *testing.T) {
|
||||
c = testValidConfig(t, fillSnapshotting(hooks))
|
||||
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
|
||||
assert.Equal(t, hs[0].Ret.(*HookCommand).Filesystems["<"], true)
|
||||
assert.Equal(t, hs[1].Ret.(*HookCommand).Filesystems["zroot<"], true)
|
||||
assert.Equal(t, hs[2].Ret.(*HookPostgresCheckpoint).Filesystems["tank/postgres/data11"], true)
|
||||
assert.Equal(t, hs[3].Ret.(*HookMySQLLockTables).Filesystems["tank/mysql"], true)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -65,3 +69,20 @@ func testConfig(t *testing.T, input string) (*Config, error) {
|
||||
t.Helper()
|
||||
return ParseConfigBytes([]byte(input))
|
||||
}
|
||||
|
||||
func trimSpaceEachLineAndPad(s, pad string) string {
|
||||
var out strings.Builder
|
||||
scan := bufio.NewScanner(strings.NewReader(s))
|
||||
for scan.Scan() {
|
||||
fmt.Fprintf(&out, "%s%s\n", pad, bytes.TrimSpace(scan.Bytes()))
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func TestTrimSpaceEachLineAndPad(t *testing.T) {
|
||||
foo := `
|
||||
foo
|
||||
bar baz
|
||||
`
|
||||
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTransportConnect(t *testing.T) {
|
||||
tmpl := `
|
||||
jobs:
|
||||
- name: foo
|
||||
type: push
|
||||
connect:
|
||||
%s
|
||||
filesystems: {"<": true}
|
||||
snapshotting:
|
||||
type: manual
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 10
|
||||
`
|
||||
|
||||
mconf := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||
|
||||
type test struct {
|
||||
Name string
|
||||
ExpectError bool
|
||||
Connect string
|
||||
}
|
||||
|
||||
testTable := []test{
|
||||
{
|
||||
Name: "tcp_with_address_and_port",
|
||||
ExpectError: false,
|
||||
Connect: `
|
||||
type: tcp
|
||||
address: 10.0.0.23:42
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "tls_with_host_and_port",
|
||||
ExpectError: false,
|
||||
Connect: `
|
||||
type: tls
|
||||
address: "server1.foo.bar:8888"
|
||||
ca: /etc/zrepl/ca.crt
|
||||
cert: /etc/zrepl/backupserver.fullchain
|
||||
key: /etc/zrepl/backupserver.key
|
||||
server_cn: "server1"
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "tcp_without_port",
|
||||
ExpectError: true,
|
||||
Connect: `
|
||||
type: tcp
|
||||
address: 10.0.0.23
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "tls_without_port",
|
||||
ExpectError: true,
|
||||
Connect: `
|
||||
type: tls
|
||||
address: 10.0.0.23
|
||||
ca: /etc/zrepl/ca.crt
|
||||
cert: /etc/zrepl/backupserver.fullchain
|
||||
key: /etc/zrepl/backupserver.key
|
||||
server_cn: "server1"
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testTable {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
require.NotEmpty(t, tc.Connect)
|
||||
connect := trimSpaceEachLineAndPad(tc.Connect, " ")
|
||||
conf := mconf(connect)
|
||||
config, err := testConfig(t, conf)
|
||||
if tc.ExpectError && err == nil {
|
||||
t.Errorf("expected test failure, but got valid config %v", config)
|
||||
return
|
||||
}
|
||||
if !tc.ExpectError && err != nil {
|
||||
t.Errorf("not expecint test failure but got error: %s", err)
|
||||
return
|
||||
}
|
||||
t.Logf("error=%v", err)
|
||||
t.Logf("config=%v", config)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
[ "$ZREPL_DRYRUN" = "true" ] && DRYRUN="echo DRYRUN: "
|
||||
|
||||
pre_snapshot() {
|
||||
$DRYRUN date
|
||||
}
|
||||
|
||||
post_snapshot() {
|
||||
$DRYRUN date
|
||||
}
|
||||
|
||||
case "$ZREPL_HOOKTYPE" in
|
||||
pre_snapshot|post_snapshot)
|
||||
"$ZREPL_HOOKTYPE"
|
||||
;;
|
||||
*)
|
||||
printf 'Unrecognized hook type: %s\n' "$ZREPL_HOOKTYPE"
|
||||
exit 255
|
||||
;;
|
||||
esac
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/zrepl/zrepl/daemon/job"
|
||||
"github.com/zrepl/zrepl/daemon/nethelpers"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/version"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -86,6 +87,12 @@ func (j *controlJob) Run(ctx context.Context) {
|
||||
}
|
||||
|
||||
pprofServer := NewPProfServer(ctx)
|
||||
if listen := envconst.String("ZREPL_DAEMON_AUTOSTART_PPROF_SERVER", ""); listen != "" {
|
||||
pprofServer.Control(PprofServerControlMsg{
|
||||
Run: true,
|
||||
HttpListenAddress: listen,
|
||||
})
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(ControlJobEndpointPProf,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDatasetMapFilter(t *testing.T) {
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
filter map[string]string
|
||||
checkPass map[string]bool // each entry is checked to match the filter's `pass` return value
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
"default_no_match",
|
||||
map[string]string{},
|
||||
map[string]bool{
|
||||
"": false,
|
||||
"foo": false,
|
||||
"zroot": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"more_specific_path_has_precedence",
|
||||
map[string]string{
|
||||
"tank<": "ok",
|
||||
"tank/tmp<": "!",
|
||||
"tank/home/x<": "!",
|
||||
"tank/home/x/1": "ok",
|
||||
},
|
||||
map[string]bool{
|
||||
"zroot": false,
|
||||
"tank": true,
|
||||
"tank/tmp": false,
|
||||
"tank/tmp/foo": false,
|
||||
"tank/home/x": false,
|
||||
"tank/home/y": true,
|
||||
"tank/home/x/1": true,
|
||||
"tank/home/x/2": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"precedence_of_specific_over_subtree_wildcard_on_same_path",
|
||||
map[string]string{
|
||||
"tank/home/bob": "ok",
|
||||
"tank/home/bob<": "!",
|
||||
},
|
||||
map[string]bool{
|
||||
"tank/home/bob": true,
|
||||
"tank/home/bob/downloads": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for tc := range tcs {
|
||||
t.Run(tcs[tc].name, func(t *testing.T) {
|
||||
c := tcs[tc]
|
||||
f := NewDatasetMapFilter(len(c.filter), true)
|
||||
for p, a := range c.filter {
|
||||
err := f.Add(p, a)
|
||||
if err != nil {
|
||||
t.Fatalf("incorrect filter spec: %s", err)
|
||||
}
|
||||
}
|
||||
for p, checkPass := range c.checkPass {
|
||||
zp, err := zfs.NewDatasetPath(p)
|
||||
if err != nil {
|
||||
t.Fatalf("incorrect path spec: %s", err)
|
||||
}
|
||||
pass, err := f.Filter(zp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected filter error: %s", err)
|
||||
}
|
||||
ok := pass == checkPass
|
||||
failstr := "OK"
|
||||
if !ok {
|
||||
failstr = "FAIL"
|
||||
t.Fail()
|
||||
}
|
||||
t.Logf("%-40q %5v (exp=%v act=%v)", p, failstr, checkPass, pass)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Code generated by "stringer -type=Edge"; DO NOT EDIT.
|
||||
|
||||
package hooks
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[Pre-1]
|
||||
_ = x[Callback-2]
|
||||
_ = x[Post-4]
|
||||
}
|
||||
|
||||
const (
|
||||
_Edge_name_0 = "PreCallback"
|
||||
_Edge_name_1 = "Post"
|
||||
)
|
||||
|
||||
var (
|
||||
_Edge_index_0 = [...]uint8{0, 3, 11}
|
||||
)
|
||||
|
||||
func (i Edge) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 2:
|
||||
i -= 1
|
||||
return _Edge_name_0[_Edge_index_0[i]:_Edge_index_0[i+1]]
|
||||
case i == 4:
|
||||
return _Edge_name_1
|
||||
default:
|
||||
return "Edge(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type List []Hook
|
||||
|
||||
func HookFromConfig(in config.HookEnum) (Hook, error) {
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.HookCommand:
|
||||
return NewCommandHook(v)
|
||||
case *config.HookPostgresCheckpoint:
|
||||
return PgChkptHookFromConfig(v)
|
||||
case *config.HookMySQLLockTables:
|
||||
return MyLockTablesFromConfig(v)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown hook type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func ListFromConfig(in *config.HookList) (r *List, err error) {
|
||||
hl := make(List, len(*in))
|
||||
|
||||
for i, h := range *in {
|
||||
hl[i], err = HookFromConfig(h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create hook #%d: %s", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &hl, nil
|
||||
}
|
||||
|
||||
func (l List) CopyFilteredForFilesystem(fs *zfs.DatasetPath) (ret List, err error) {
|
||||
ret = make(List, 0, len(l))
|
||||
|
||||
for _, h := range l {
|
||||
var passFilesystem bool
|
||||
if passFilesystem, err = h.Filesystems().Filter(fs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if passFilesystem {
|
||||
ret = append(ret, h)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Package hooks implements pre- and post snapshot hooks.
|
||||
//
|
||||
// Plan is a generic executor for ExpectStepReports before and after an activity specified in a callback.
|
||||
// It provides a reporting facility that can be polled while the plan is executing to gather progress information.
|
||||
//
|
||||
// This package also provides all supported hook type implementations and abstractions around them.
|
||||
//
|
||||
// Use For Other Kinds Of ExpectStepReports
|
||||
//
|
||||
// This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication:
|
||||
//
|
||||
// The Hook interface requires a hook to provide a Filesystems() filter, which doesn't make sense for
|
||||
// all kinds of activities.
|
||||
//
|
||||
// The hook implementations should move out of this package.
|
||||
// However, there is a lot of tight coupling which to untangle isn't worth it ATM.
|
||||
//
|
||||
// How This Package Is Used By Package Snapper
|
||||
//
|
||||
// Deserialize a config.List using ListFromConfig().
|
||||
// Then it MUST filter the list to only contain hooks for a particular filesystem using
|
||||
// hooksList.CopyFilteredForFilesystem(fs).
|
||||
//
|
||||
// Then create a CallbackHook using NewCallbackHookForFilesystem().
|
||||
//
|
||||
// Pass all of the above to NewPlan() which provides a Report() and Run() method:
|
||||
//
|
||||
// Plan.Run(ctx context.Context,dryRun bool) executes the plan and take a context as argument that should contain a logger added using hooks.WithLogger()).
|
||||
// The value of dryRun is passed through to the hooks' Run() method.
|
||||
// Command hooks make it available in the environment variable ZREPL_DRYRUN.
|
||||
//
|
||||
// Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status").
|
||||
//
|
||||
package hooks
|
||||
@@ -0,0 +1,292 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// Re-export type here so that
|
||||
// every file in package hooks doesn't
|
||||
// have to import github.com/zrepl/zrepl/zfs
|
||||
type Filter zfs.DatasetFilter
|
||||
|
||||
type Hook interface {
|
||||
Filesystems() Filter
|
||||
|
||||
// If true and the Pre edge invocation of Run fails, Post edge will not run and other Pre edges will not run.
|
||||
ErrIsFatal() bool
|
||||
|
||||
// Run is invoked by HookPlan for a Pre edge.
|
||||
// If HookReport.HadError() == false, the Post edge will be invoked, too.
|
||||
Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport
|
||||
|
||||
String() string
|
||||
}
|
||||
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
PhaseSnapshot = Phase("snapshot")
|
||||
PhaseTesting = Phase("testing")
|
||||
)
|
||||
|
||||
func (p Phase) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
//go:generate stringer -type=Edge
|
||||
type Edge uint
|
||||
|
||||
const (
|
||||
Pre = Edge(1 << iota)
|
||||
Callback
|
||||
Post
|
||||
)
|
||||
|
||||
func (e Edge) StringForPhase(phase Phase) string {
|
||||
return fmt.Sprintf("%s_%s", e.String(), phase.String())
|
||||
}
|
||||
|
||||
//go:generate enumer -type=StepStatus -trimprefix=Step
|
||||
type StepStatus int
|
||||
|
||||
const (
|
||||
StepPending StepStatus = 1 << iota
|
||||
StepExec
|
||||
StepOk
|
||||
StepErr
|
||||
StepSkippedDueToFatalErr
|
||||
StepSkippedDueToPreErr
|
||||
)
|
||||
|
||||
type HookReport interface {
|
||||
String() string
|
||||
HadError() bool
|
||||
Error() string
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
Hook Hook
|
||||
Edge Edge
|
||||
Status StepStatus
|
||||
Begin, End time.Time
|
||||
// Report may be nil
|
||||
// FIXME cannot serialize this for client status, but contains interesting info (like what error happened)
|
||||
Report HookReport
|
||||
state map[interface{}]interface{}
|
||||
}
|
||||
|
||||
func (s Step) String() (out string) {
|
||||
fatal := "~"
|
||||
if s.Hook.ErrIsFatal() && s.Edge == Pre {
|
||||
fatal = "!"
|
||||
}
|
||||
runTime := "..."
|
||||
if s.Status != StepPending {
|
||||
t := s.End.Sub(s.Begin)
|
||||
runTime = t.Round(time.Millisecond).String()
|
||||
}
|
||||
return fmt.Sprintf("[%s] [%5s] %s [%s] %s", s.Status, runTime, fatal, s.Edge, s.Hook)
|
||||
}
|
||||
|
||||
type Plan struct {
|
||||
mtx sync.RWMutex
|
||||
|
||||
steps []*Step
|
||||
pre []*Step // protected by mtx
|
||||
cb *Step
|
||||
post []*Step // not reversed, i.e. entry at index i corresponds to pre-edge in pre[i]
|
||||
|
||||
phase Phase
|
||||
env Env
|
||||
}
|
||||
|
||||
func NewPlan(hooks *List, phase Phase, cb *CallbackHook, extra Env) (*Plan, error) {
|
||||
|
||||
var pre, post []*Step
|
||||
// TODO sanity check unique name of hook?
|
||||
for _, hook := range *hooks {
|
||||
state := make(map[interface{}]interface{})
|
||||
preE := &Step{
|
||||
Hook: hook,
|
||||
Edge: Pre,
|
||||
Status: StepPending,
|
||||
state: state,
|
||||
}
|
||||
pre = append(pre, preE)
|
||||
postE := &Step{
|
||||
Hook: hook,
|
||||
Edge: Post,
|
||||
Status: StepPending,
|
||||
state: state,
|
||||
}
|
||||
post = append(post, postE)
|
||||
}
|
||||
|
||||
cbE := &Step{
|
||||
Hook: cb,
|
||||
Edge: Callback,
|
||||
Status: StepPending,
|
||||
}
|
||||
|
||||
steps := make([]*Step, 0, len(pre)+len(post)+1)
|
||||
steps = append(steps, pre...)
|
||||
steps = append(steps, cbE)
|
||||
for i := len(post) - 1; i >= 0; i-- {
|
||||
steps = append(steps, post[i])
|
||||
}
|
||||
|
||||
plan := &Plan{
|
||||
phase: phase,
|
||||
env: extra,
|
||||
steps: steps,
|
||||
pre: pre,
|
||||
post: post,
|
||||
cb: cbE,
|
||||
}
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
type PlanReport []Step
|
||||
|
||||
func (p *Plan) Report() PlanReport {
|
||||
p.mtx.RLock()
|
||||
defer p.mtx.RUnlock()
|
||||
rep := make([]Step, len(p.steps))
|
||||
for i := range rep {
|
||||
rep[i] = *p.steps[i]
|
||||
}
|
||||
return rep
|
||||
}
|
||||
|
||||
func (r PlanReport) HadError() bool {
|
||||
for _, e := range r {
|
||||
if e.Status == StepErr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r PlanReport) HadFatalError() bool {
|
||||
for _, e := range r {
|
||||
if e.Status == StepSkippedDueToFatalErr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r PlanReport) String() string {
|
||||
stepStrings := make([]string, len(r))
|
||||
for i, e := range r {
|
||||
stepStrings[i] = fmt.Sprintf("%02d %s", i+1, e)
|
||||
}
|
||||
return strings.Join(stepStrings, "\n")
|
||||
}
|
||||
|
||||
func (p *Plan) Run(ctx context.Context, dryRun bool) {
|
||||
p.mtx.RLock()
|
||||
defer p.mtx.RUnlock()
|
||||
w := func(f func()) {
|
||||
p.mtx.RUnlock()
|
||||
defer p.mtx.RLock()
|
||||
p.mtx.Lock()
|
||||
defer p.mtx.Unlock()
|
||||
f()
|
||||
}
|
||||
runHook := func(s *Step, ctx context.Context, edge Edge) HookReport {
|
||||
w(func() { s.Status = StepExec })
|
||||
begin := time.Now()
|
||||
r := s.Hook.Run(ctx, edge, p.phase, dryRun, p.env, s.state)
|
||||
end := time.Now()
|
||||
w(func() {
|
||||
s.Report = r
|
||||
s.Status = StepOk
|
||||
if r.HadError() {
|
||||
s.Status = StepErr
|
||||
}
|
||||
s.Begin, s.End = begin, end
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
l := getLogger(ctx)
|
||||
|
||||
// it's a stack, execute until we reach the end of the list (last item in)
|
||||
// or fail inbetween
|
||||
l.Info("run pre-edges in configuration order")
|
||||
next := 0
|
||||
for ; next < len(p.pre); next++ {
|
||||
e := p.pre[next]
|
||||
l := l.WithField("hook", e.Hook)
|
||||
r := runHook(e, ctx, Pre)
|
||||
if r.HadError() {
|
||||
l.WithError(r).Error("hook invocation failed for pre-edge")
|
||||
if e.Hook.ErrIsFatal() {
|
||||
l.Error("the hook run was aborted due to a fatal error in this hook")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hadFatalErr := next != len(p.pre)
|
||||
if hadFatalErr {
|
||||
l.Error("fatal error in a pre-snapshot hook invocation")
|
||||
l.Error("no snapshot will be taken")
|
||||
l.Error("only running post-edges for successful pre-edges")
|
||||
w(func() {
|
||||
p.post[next].Status = StepSkippedDueToFatalErr
|
||||
for i := next + 1; i < len(p.pre); i++ {
|
||||
p.pre[i].Status = StepSkippedDueToFatalErr
|
||||
p.post[i].Status = StepSkippedDueToFatalErr
|
||||
}
|
||||
p.cb.Status = StepSkippedDueToFatalErr
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
l.Info("running callback")
|
||||
cbR := runHook(p.cb, ctx, Callback)
|
||||
if cbR.HadError() {
|
||||
l.WithError(cbR).Error("callback failed")
|
||||
}
|
||||
|
||||
l.Info("run post-edges for successful pre-edges in reverse configuration order")
|
||||
|
||||
// the constructor produces pre and post entries
|
||||
// post is NOT reversed
|
||||
next-- // now at index of last executed pre-edge
|
||||
for ; next >= 0; next-- {
|
||||
e := p.post[next]
|
||||
l := l.WithField("hook", e.Hook)
|
||||
|
||||
if p.pre[next].Status != StepOk {
|
||||
if p.pre[next].Status != StepErr {
|
||||
panic(fmt.Sprintf("expecting a pre-edge hook report to be either Ok or Err, got %s", p.pre[next].Status))
|
||||
}
|
||||
l.Info("skip post-edge because pre-edge failed")
|
||||
w(func() {
|
||||
e.Status = StepSkippedDueToPreErr
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
report := runHook(e, ctx, Post)
|
||||
|
||||
if report.HadError() {
|
||||
l.WithError(report).Error("hook invocation failed for post-edge")
|
||||
l.Error("subsequent post-edges run regardless of this post-edge failure")
|
||||
}
|
||||
|
||||
// ErrIsFatal is only relevant for Pre
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
contextKeyLog contextKey = 0
|
||||
)
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
||||
return context.WithValue(ctx, contextKeyLog, log)
|
||||
}
|
||||
|
||||
func GetLogger(ctx context.Context) Logger { return getLogger(ctx) }
|
||||
|
||||
func getLogger(ctx context.Context) Logger {
|
||||
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
||||
return log
|
||||
}
|
||||
return logger.NewNullLogger()
|
||||
}
|
||||
|
||||
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
|
||||
|
||||
type logWriter struct {
|
||||
/*
|
||||
Mutex prevents:
|
||||
concurrent writes to buf, scanner in Write([]byte)
|
||||
data race on scanner vs Write([]byte)
|
||||
and concurrent write to buf (call to buf.Reset())
|
||||
in Close()
|
||||
|
||||
(Also, Close() should generally block until any Write() call completes.)
|
||||
*/
|
||||
mtx *sync.Mutex
|
||||
buf bytes.Buffer
|
||||
scanner *bufio.Scanner
|
||||
logger Logger
|
||||
level logger.Level
|
||||
field string
|
||||
}
|
||||
|
||||
func NewLogWriter(mtx *sync.Mutex, logger Logger, level logger.Level, field string) *logWriter {
|
||||
w := new(logWriter)
|
||||
w.mtx = mtx
|
||||
w.scanner = bufio.NewScanner(&w.buf)
|
||||
w.logger = logger
|
||||
w.level = level
|
||||
w.field = field
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *logWriter) log(line string) {
|
||||
w.logger.WithField(w.field, line).Log(w.level, "hook output")
|
||||
}
|
||||
|
||||
func (w *logWriter) logUnreadBytes() error {
|
||||
for w.scanner.Scan() {
|
||||
w.log(w.scanner.Text())
|
||||
}
|
||||
if w.buf.Cap() > envconst.Int("ZREPL_MAX_HOOK_LOG_SIZE", MAX_HOOK_LOG_SIZE_DEFAULT) {
|
||||
w.buf.Reset()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *logWriter) Write(in []byte) (int, error) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
|
||||
n, bufErr := w.buf.Write(in)
|
||||
if bufErr != nil {
|
||||
return n, bufErr
|
||||
}
|
||||
|
||||
err := w.logUnreadBytes()
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
// Always reset the scanner for the next Write
|
||||
w.scanner = bufio.NewScanner(&w.buf)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *logWriter) Close() (err error) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
|
||||
return w.logUnreadBytes()
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type HookJobCallback func(ctx context.Context) error
|
||||
|
||||
type CallbackHook struct {
|
||||
cb HookJobCallback
|
||||
filter Filter
|
||||
displayString string
|
||||
}
|
||||
|
||||
func NewCallbackHookForFilesystem(displayString string, fs *zfs.DatasetPath, cb HookJobCallback) *CallbackHook {
|
||||
filter, _ := filters.DatasetMapFilterFromConfig(map[string]bool{fs.ToString(): true})
|
||||
return NewCallbackHook(displayString, cb, filter)
|
||||
}
|
||||
|
||||
func NewCallbackHook(displayString string, cb HookJobCallback, filter Filter) *CallbackHook {
|
||||
return &CallbackHook{
|
||||
cb: cb,
|
||||
filter: filter,
|
||||
displayString: displayString,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CallbackHook) Filesystems() Filter {
|
||||
return h.filter
|
||||
}
|
||||
|
||||
func (h *CallbackHook) ErrIsFatal() bool {
|
||||
return false // callback is by definition
|
||||
}
|
||||
|
||||
func (h *CallbackHook) String() string {
|
||||
return h.displayString
|
||||
}
|
||||
|
||||
type CallbackHookReport struct {
|
||||
Name string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (r *CallbackHookReport) String() string {
|
||||
if r.HadError() {
|
||||
return r.Error()
|
||||
}
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *CallbackHookReport) HadError() bool { return r.Err != nil }
|
||||
|
||||
func (r *CallbackHookReport) Error() string {
|
||||
return fmt.Sprintf("%s error: %s", r.Name, r.Err)
|
||||
}
|
||||
|
||||
func (h *CallbackHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||
err := h.cb(ctx)
|
||||
return &CallbackHookReport{h.displayString, err}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/util/circlog"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
)
|
||||
|
||||
type HookEnvVar string
|
||||
|
||||
const (
|
||||
EnvType HookEnvVar = "ZREPL_HOOKTYPE"
|
||||
EnvDryRun HookEnvVar = "ZREPL_DRYRUN"
|
||||
EnvFS HookEnvVar = "ZREPL_FS"
|
||||
EnvSnapshot HookEnvVar = "ZREPL_SNAPNAME"
|
||||
EnvTimeout HookEnvVar = "ZREPL_TIMEOUT"
|
||||
)
|
||||
|
||||
type Env map[HookEnvVar]string
|
||||
|
||||
func NewHookEnv(edge Edge, phase Phase, dryRun bool, timeout time.Duration, extra Env) Env {
|
||||
r := Env{
|
||||
EnvTimeout: fmt.Sprintf("%.f", math.Floor(timeout.Seconds())),
|
||||
}
|
||||
|
||||
edgeString := edge.StringForPhase(phase)
|
||||
r[EnvType] = strings.ToLower(edgeString)
|
||||
|
||||
var dryRunString string
|
||||
if dryRun {
|
||||
dryRunString = "true"
|
||||
} else {
|
||||
dryRunString = ""
|
||||
}
|
||||
r[EnvDryRun] = dryRunString
|
||||
|
||||
for k, v := range extra {
|
||||
r[k] = v
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type CommandHook struct {
|
||||
edge Edge
|
||||
filter Filter
|
||||
errIsFatal bool
|
||||
command string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type CommandHookReport struct {
|
||||
Command string
|
||||
Args []string // currently always empty
|
||||
Env Env
|
||||
Err error
|
||||
CapturedStdoutStderrCombined []byte
|
||||
}
|
||||
|
||||
func (r *CommandHookReport) String() string {
|
||||
// Reproduces a POSIX shell-compatible command line
|
||||
var cmdLine strings.Builder
|
||||
sep := ""
|
||||
|
||||
// Make sure environment variables are always
|
||||
// printed in the same order
|
||||
var hookEnvKeys []HookEnvVar
|
||||
for k := range r.Env {
|
||||
hookEnvKeys = append(hookEnvKeys, k)
|
||||
}
|
||||
sort.Slice(hookEnvKeys, func(i, j int) bool { return string(hookEnvKeys[i]) < string(hookEnvKeys[j]) })
|
||||
|
||||
for _, k := range hookEnvKeys {
|
||||
cmdLine.WriteString(fmt.Sprintf("%s%s='%s'", sep, k, r.Env[k]))
|
||||
sep = " "
|
||||
}
|
||||
|
||||
cmdLine.WriteString(fmt.Sprintf("%s%s", sep, r.Command))
|
||||
for _, a := range r.Args {
|
||||
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
|
||||
}
|
||||
func (r *CommandHookReport) Error() string {
|
||||
if r.Err == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s FAILED with error: %s", r.String(), r.Err)
|
||||
}
|
||||
|
||||
func (r *CommandHookReport) HadError() bool {
|
||||
return r.Err != nil
|
||||
}
|
||||
|
||||
func NewCommandHook(in *config.HookCommand) (r *CommandHook, err error) {
|
||||
r = &CommandHook{
|
||||
errIsFatal: in.ErrIsFatal,
|
||||
command: in.Path,
|
||||
timeout: in.Timeout,
|
||||
}
|
||||
|
||||
r.filter, err = filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse filesystem filter: %s", err)
|
||||
}
|
||||
|
||||
r.edge = Pre | Post
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (h *CommandHook) Filesystems() Filter {
|
||||
return h.filter
|
||||
}
|
||||
|
||||
func (h *CommandHook) ErrIsFatal() bool {
|
||||
return h.errIsFatal
|
||||
}
|
||||
|
||||
func (h *CommandHook) String() string {
|
||||
return h.command
|
||||
}
|
||||
|
||||
func (h *CommandHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||
l := getLogger(ctx).WithField("command", h.command)
|
||||
|
||||
cmdCtx, cancel := context.WithTimeout(ctx, h.timeout)
|
||||
defer cancel()
|
||||
|
||||
cmdExec := exec.CommandContext(cmdCtx, h.command)
|
||||
|
||||
hookEnv := NewHookEnv(edge, phase, dryRun, h.timeout, extra)
|
||||
cmdEnv := os.Environ()
|
||||
for k, v := range hookEnv {
|
||||
cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
cmdExec.Env = cmdEnv
|
||||
|
||||
var scanMutex sync.Mutex
|
||||
combinedOutput, err := circlog.NewCircularLog(envconst.Int("ZREPL_MAX_HOOK_LOG_SIZE", MAX_HOOK_LOG_SIZE_DEFAULT))
|
||||
if err != nil {
|
||||
return &CommandHookReport{Err: err}
|
||||
}
|
||||
logErrWriter := NewLogWriter(&scanMutex, l, logger.Warn, "stderr")
|
||||
logOutWriter := NewLogWriter(&scanMutex, l, logger.Info, "stdout")
|
||||
defer logErrWriter.Close()
|
||||
defer logOutWriter.Close()
|
||||
|
||||
cmdExec.Stderr = io.MultiWriter(logErrWriter, combinedOutput)
|
||||
cmdExec.Stdout = io.MultiWriter(logOutWriter, combinedOutput)
|
||||
|
||||
report := &CommandHookReport{
|
||||
Command: h.command,
|
||||
Env: hookEnv,
|
||||
// no report.Args
|
||||
}
|
||||
|
||||
err = cmdExec.Start()
|
||||
if err != nil {
|
||||
report.Err = err
|
||||
return report
|
||||
}
|
||||
|
||||
err = cmdExec.Wait()
|
||||
combinedOutputBytes := combinedOutput.Bytes()
|
||||
report.CapturedStdoutStderrCombined = make([]byte, len(combinedOutputBytes))
|
||||
copy(report.CapturedStdoutStderrCombined, combinedOutputBytes)
|
||||
if err != nil {
|
||||
if cmdCtx.Err() == context.DeadlineExceeded {
|
||||
report.Err = fmt.Errorf("timed out after %s: %s", h.timeout, err)
|
||||
return report
|
||||
}
|
||||
report.Err = err
|
||||
return report
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
sqldriver "database/sql/driver"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
|
||||
//
|
||||
// Making Backups Using a File System Snapshot:
|
||||
//
|
||||
// If you are using a Veritas file system, you can make a backup like this:
|
||||
//
|
||||
// From a client program, execute FLUSH TABLES WITH READ LOCK.
|
||||
// From another shell, execute mount vxfs snapshot.
|
||||
// From the first client, execute UNLOCK TABLES.
|
||||
// Copy files from the snapshot.
|
||||
// Unmount the snapshot.
|
||||
//
|
||||
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
||||
type MySQLLockTables struct {
|
||||
errIsFatal bool
|
||||
connector sqldriver.Connector
|
||||
filesystems Filter
|
||||
}
|
||||
|
||||
type myLockTablesStateKey int
|
||||
|
||||
const (
|
||||
myLockTablesConnection myLockTablesStateKey = 1 + iota
|
||||
)
|
||||
|
||||
func MyLockTablesFromConfig(in *config.HookMySQLLockTables) (*MySQLLockTables, error) {
|
||||
conf, err := mysql.ParseDSN(in.DSN)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "`dsn` invalid")
|
||||
}
|
||||
cn, err := mysql.NewConnector(conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "`connect` invalid")
|
||||
}
|
||||
|
||||
filesystems, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "`filesystems` invalid")
|
||||
}
|
||||
|
||||
return &MySQLLockTables{
|
||||
in.ErrIsFatal,
|
||||
cn,
|
||||
filesystems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *MySQLLockTables) ErrIsFatal() bool { return h.errIsFatal }
|
||||
func (h *MySQLLockTables) Filesystems() Filter { return h.filesystems }
|
||||
func (h *MySQLLockTables) String() string { return "MySQL FLUSH TABLES WITH READ LOCK" }
|
||||
|
||||
type MyLockTablesReport struct {
|
||||
What string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (r *MyLockTablesReport) HadError() bool { return r.Err != nil }
|
||||
func (r *MyLockTablesReport) Error() string { return r.String() }
|
||||
func (r *MyLockTablesReport) String() string {
|
||||
var s strings.Builder
|
||||
s.WriteString(r.What)
|
||||
if r.Err != nil {
|
||||
fmt.Fprintf(&s, ": %s", r.Err)
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (h *MySQLLockTables) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||
fs, ok := extra[EnvFS]
|
||||
if !ok {
|
||||
panic(extra)
|
||||
}
|
||||
dp, err := zfs.NewDatasetPath(fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if pass, err := h.filesystems.Filter(dp); err != nil {
|
||||
return &MyLockTablesReport{What: "filesystem filter", Err: err}
|
||||
} else if !pass {
|
||||
getLogger(ctx).Debug("filesystem does not match filter, skipping")
|
||||
return &MyLockTablesReport{What: "filesystem filter skipped this filesystem", Err: nil}
|
||||
}
|
||||
|
||||
switch edge {
|
||||
case Pre:
|
||||
err := h.doRunPre(ctx, dp, dryRun, state)
|
||||
return &MyLockTablesReport{"FLUSH TABLES WITH READ LOCK", err}
|
||||
case Post:
|
||||
err := h.doRunPost(ctx, dp, dryRun, state)
|
||||
return &MyLockTablesReport{"UNLOCK TABLES", err}
|
||||
}
|
||||
return &MyLockTablesReport{What: "skipped this edge", Err: nil}
|
||||
}
|
||||
|
||||
func (h *MySQLLockTables) doRunPre(ctx context.Context, fs *zfs.DatasetPath, dry bool, state map[interface{}]interface{}) (err error) {
|
||||
db := sql.OpenDB(h.connector)
|
||||
defer func(err *error) {
|
||||
if *err != nil {
|
||||
db.Close()
|
||||
}
|
||||
}(&err)
|
||||
|
||||
getLogger(ctx).Debug("do FLUSH TABLES WITH READ LOCK")
|
||||
_, err = db.ExecContext(ctx, "FLUSH TABLES WITH READ LOCK")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
state[myLockTablesConnection] = db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MySQLLockTables) doRunPost(ctx context.Context, fs *zfs.DatasetPath, dry bool, state map[interface{}]interface{}) error {
|
||||
|
||||
db := state[myLockTablesConnection].(*sql.DB)
|
||||
defer db.Close()
|
||||
|
||||
getLogger(ctx).Debug("do UNLOCK TABLES")
|
||||
_, err := db.ExecContext(ctx, "UNLOCK TABLES")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type PgChkptHook struct {
|
||||
errIsFatal bool
|
||||
connector *pq.Connector
|
||||
filesystems Filter
|
||||
}
|
||||
|
||||
func PgChkptHookFromConfig(in *config.HookPostgresCheckpoint) (*PgChkptHook, error) {
|
||||
filesystems, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "`filesystems` invalid")
|
||||
}
|
||||
cn, err := pq.NewConnector(in.DSN)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "`dsn` invalid")
|
||||
}
|
||||
|
||||
return &PgChkptHook{
|
||||
in.ErrIsFatal,
|
||||
cn,
|
||||
filesystems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *PgChkptHook) ErrIsFatal() bool { return h.errIsFatal }
|
||||
func (h *PgChkptHook) Filesystems() Filter { return h.filesystems }
|
||||
func (h *PgChkptHook) String() string { return "postgres checkpoint" }
|
||||
|
||||
type PgChkptHookReport struct{ Err error }
|
||||
|
||||
func (r *PgChkptHookReport) HadError() bool { return r.Err != nil }
|
||||
func (r *PgChkptHookReport) Error() string { return r.Err.Error() }
|
||||
func (r *PgChkptHookReport) String() string {
|
||||
if r.Err != nil {
|
||||
return fmt.Sprintf("postgres CHECKPOINT failed: %s", r.Err)
|
||||
} else {
|
||||
return "postgres CHECKPOINT completed"
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PgChkptHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
|
||||
if edge != Pre {
|
||||
return &PgChkptHookReport{nil}
|
||||
}
|
||||
fs, ok := extra[EnvFS]
|
||||
if !ok {
|
||||
panic(extra)
|
||||
}
|
||||
dp, err := zfs.NewDatasetPath(fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = h.doRunPre(ctx, dp, dryRun)
|
||||
return &PgChkptHookReport{err}
|
||||
}
|
||||
|
||||
func (h *PgChkptHook) doRunPre(ctx context.Context, fs *zfs.DatasetPath, dry bool) error {
|
||||
|
||||
if pass, err := h.filesystems.Filter(fs); err != nil || !pass {
|
||||
getLogger(ctx).Debug("filesystem does not match filter, skipping")
|
||||
return err
|
||||
}
|
||||
|
||||
db := sql.OpenDB(h.connector)
|
||||
defer db.Close()
|
||||
dl, ok := ctx.Deadline()
|
||||
if ok {
|
||||
timeout := uint64(math.Floor(time.Until(dl).Seconds() * 1000)) // TODO go1.13 milliseconds
|
||||
getLogger(ctx).WithField("statement_timeout", timeout).Debug("setting statement timeout for CHECKPOINT")
|
||||
_, err := db.ExecContext(ctx, "SET statement_timeout TO ?", timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if dry {
|
||||
getLogger(ctx).Debug("dry-run - use ping instead of CHECKPOINT")
|
||||
return db.PingContext(ctx)
|
||||
}
|
||||
getLogger(ctx).Info("execute CHECKPOINT command")
|
||||
_, err := db.ExecContext(ctx, "CHECKPOINT")
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package hooks_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type comparisonAssertionFunc func(require.TestingT, interface{}, interface{}, ...interface{})
|
||||
type valueAssertionFunc func(require.TestingT, interface{}, ...interface{})
|
||||
|
||||
type expectStep struct {
|
||||
ExpectedEdge hooks.Edge
|
||||
ExpectStatus hooks.StepStatus
|
||||
OutputTest valueAssertionFunc
|
||||
ErrorTest valueAssertionFunc
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
Name string
|
||||
Config []string
|
||||
IsSlow bool
|
||||
SuppressOutput bool
|
||||
|
||||
ExpectCallbackSkipped bool
|
||||
ExpectHadFatalErr bool
|
||||
ExpectHadError bool
|
||||
ExpectStepReports []expectStep
|
||||
}
|
||||
|
||||
func curryLeft(f comparisonAssertionFunc, expected interface{}) valueAssertionFunc {
|
||||
return curry(f, expected, false)
|
||||
}
|
||||
|
||||
func curryRight(f comparisonAssertionFunc, expected interface{}) valueAssertionFunc {
|
||||
return curry(f, expected, true)
|
||||
}
|
||||
|
||||
func curry(f comparisonAssertionFunc, expected interface{}, right bool) (ret valueAssertionFunc) {
|
||||
ret = func(t require.TestingT, s interface{}, v ...interface{}) {
|
||||
var x interface{}
|
||||
var y interface{}
|
||||
if right {
|
||||
x = s
|
||||
y = expected
|
||||
} else {
|
||||
x = expected
|
||||
y = s
|
||||
}
|
||||
|
||||
if len(v) > 0 {
|
||||
f(t, x, y, v)
|
||||
} else {
|
||||
f(t, x, y)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestHooks(t *testing.T) {
|
||||
testFSName := "testpool/testdataset"
|
||||
testSnapshotName := "testsnap"
|
||||
|
||||
tmpl, err := template.New("TestHooks").Parse(`
|
||||
jobs:
|
||||
- name: TestHooks
|
||||
type: snap
|
||||
filesystems: {"<": true}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
interval: 1m
|
||||
prefix: zrepl_snapjob_
|
||||
hooks:
|
||||
{{- template "List" . }}
|
||||
pruning:
|
||||
keep:
|
||||
- type: last_n
|
||||
count: 10
|
||||
`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
regexpTest := func(s string) valueAssertionFunc {
|
||||
return curryLeft(require.Regexp, regexp.MustCompile(s))
|
||||
}
|
||||
|
||||
containsTest := func(s string) valueAssertionFunc {
|
||||
return curryRight(require.Contains, s)
|
||||
}
|
||||
|
||||
testTable := []testCase{
|
||||
testCase{
|
||||
Name: "no_hooks",
|
||||
ExpectStepReports: []expectStep{
|
||||
{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
},
|
||||
},
|
||||
testCase{
|
||||
Name: "timeout",
|
||||
IsSlow: true,
|
||||
ExpectHadError: true,
|
||||
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-timeout.sh, timeout: 2s}`},
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s ZREPL_TIMEOUT=2", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest(`timed out after 2(.\d+)?s`),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||
},
|
||||
},
|
||||
},
|
||||
testCase{
|
||||
Name: "check_env",
|
||||
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`},
|
||||
ExpectHadError: false,
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
testCase{
|
||||
Name: "nonfatal_pre_error_continues",
|
||||
ExpectCallbackSkipped: false,
|
||||
ExpectHadError: true,
|
||||
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-error.sh}`},
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepSkippedDueToPreErr, // post-edge is not executed for failing pre-edge
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
testCase{
|
||||
Name: "pre_error_fatal_skips_subsequent_pre_edges_and_callback_and_its_post_edge_and_post_edges",
|
||||
ExpectCallbackSkipped: true,
|
||||
ExpectHadFatalErr: true,
|
||||
ExpectHadError: true,
|
||||
Config: []string{
|
||||
`{type: command, path: {{.WorkDir}}/test/test-error.sh, err_is_fatal: true}`,
|
||||
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||
},
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepSkippedDueToFatalErr},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||
},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepSkippedDueToFatalErr,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
testCase{
|
||||
Name: "post_error_fails_are_ignored_even_if_fatal",
|
||||
ExpectHadFatalErr: false, // only occurs during Post, so it's not a fatal error
|
||||
ExpectHadError: true,
|
||||
Config: []string{
|
||||
`{type: command, path: {{.WorkDir}}/test/test-post-error.sh, err_is_fatal: true}`,
|
||||
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||
},
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
// No-action run of test-post-error.sh
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: require.Empty,
|
||||
},
|
||||
expectStep{
|
||||
// Pre run of test-report-env.sh
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||
},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
testCase{
|
||||
Name: "cleanup_check_env",
|
||||
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`},
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
testCase{
|
||||
Name: "pre_error_cancels_post",
|
||||
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-pre-error-post-ok.sh}`},
|
||||
ExpectHadError: true,
|
||||
ExpectHadFatalErr: false,
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
testCase{
|
||||
Name: "pre_error_does_not_cancel_other_posts_but_itself",
|
||||
Config: []string{
|
||||
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
|
||||
`{type: command, path: {{.WorkDir}}/test/test-pre-error-post-ok.sh}`,
|
||||
},
|
||||
ExpectHadError: true,
|
||||
ExpectHadFatalErr: false,
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepSkippedDueToPreErr,
|
||||
},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
testCase{
|
||||
Name: "exceed_buffer_limit",
|
||||
SuppressOutput: true,
|
||||
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-large-stdout.sh}`},
|
||||
ExpectHadError: false,
|
||||
ExpectHadFatalErr: false,
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: func(t require.TestingT, s interface{}, v ...interface{}) {
|
||||
require.Len(t, s, 1<<20)
|
||||
},
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
// No-action run of above hook
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
OutputTest: require.Empty,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
Following not intended to test functionality of
|
||||
filter package. Only to demonstrate that hook
|
||||
filters are being applied. The following should
|
||||
result in NO hooks running. If it does run, a
|
||||
fatal hooks.RunReport will be returned.
|
||||
*/
|
||||
testCase{
|
||||
Name: "exclude_all_filesystems",
|
||||
Config: []string{
|
||||
`{type: command, path: {{.WorkDir}}/test/test-error.sh, err_is_fatal: true, filesystems: {"<": false}}`,
|
||||
},
|
||||
ExpectStepReports: []expectStep{
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Callback,
|
||||
ExpectStatus: hooks.StepOk,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
parseHookConfig := func(t *testing.T, in string) *config.Config {
|
||||
t.Helper()
|
||||
conf, err := config.ParseConfigBytes([]byte(in))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, conf)
|
||||
return conf
|
||||
}
|
||||
|
||||
fillHooks := func(tt *testCase) string {
|
||||
// make hook path absolute
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic("os.Getwd() failed")
|
||||
}
|
||||
var hooksTmpl string = "\n"
|
||||
for _, l := range tt.Config {
|
||||
hooksTmpl += fmt.Sprintf(" - %s\n", l)
|
||||
}
|
||||
tmpl.New("List").Parse(hooksTmpl)
|
||||
|
||||
var outBytes bytes.Buffer
|
||||
data := struct {
|
||||
WorkDir string
|
||||
}{
|
||||
WorkDir: cwd,
|
||||
}
|
||||
if err := tmpl.Execute(&outBytes, data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return outBytes.String()
|
||||
}
|
||||
|
||||
var c *config.Config
|
||||
fs, err := zfs.NewDatasetPath(testFSName)
|
||||
require.NoError(t, err)
|
||||
|
||||
log := logger.NewTestLogger(t)
|
||||
|
||||
var cbReached bool
|
||||
cb := hooks.NewCallbackHookForFilesystem("testcallback", fs, func(_ context.Context) error {
|
||||
cbReached = true
|
||||
return nil
|
||||
})
|
||||
|
||||
hookEnvExtra := hooks.Env{
|
||||
hooks.EnvFS: fs.ToString(),
|
||||
hooks.EnvSnapshot: testSnapshotName,
|
||||
}
|
||||
|
||||
for _, tt := range testTable {
|
||||
if testing.Short() && tt.IsSlow {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
c = parseHookConfig(t, fillHooks(&tt))
|
||||
snp := c.Jobs[0].Ret.(*config.SnapJob).Snapshotting.Ret.(*config.SnapshottingPeriodic)
|
||||
hookList, err := hooks.ListFromConfig(&snp.Hooks)
|
||||
require.NoError(t, err)
|
||||
|
||||
filteredHooks, err := hookList.CopyFilteredForFilesystem(fs)
|
||||
require.NoError(t, err)
|
||||
plan, err := hooks.NewPlan(&filteredHooks, hooks.PhaseTesting, cb, hookEnvExtra)
|
||||
require.NoError(t, err)
|
||||
t.Logf("REPORT PRE EXECUTION:\n%s", plan.Report())
|
||||
|
||||
cbReached = false
|
||||
|
||||
ctx := context.Background()
|
||||
if testing.Verbose() && !tt.SuppressOutput {
|
||||
ctx = hooks.WithLogger(ctx, log)
|
||||
}
|
||||
plan.Run(ctx, false)
|
||||
report := plan.Report()
|
||||
|
||||
t.Logf("REPORT POST EXECUTION:\n%s", report)
|
||||
|
||||
/*
|
||||
* TEST ASSERTIONS
|
||||
*/
|
||||
|
||||
t.Logf("len(runReports)=%v", len(report))
|
||||
t.Logf("len(tt.ExpectStepReports)=%v", len(tt.ExpectStepReports))
|
||||
require.Equal(t, len(tt.ExpectStepReports), len(report), "ExpectStepReports must be same length as expected number of hook runs, excluding possible Callback")
|
||||
|
||||
// Check if callback ran, when required
|
||||
if tt.ExpectCallbackSkipped {
|
||||
require.False(t, cbReached, "callback ran but should not have run")
|
||||
} else {
|
||||
require.True(t, cbReached, "callback should have run but did not")
|
||||
}
|
||||
|
||||
// Check if a fatal run error occurred and was expected
|
||||
require.Equal(t, tt.ExpectHadFatalErr, report.HadFatalError(), "non-matching HadFatalError")
|
||||
require.Equal(t, tt.ExpectHadError, report.HadError(), "non-matching HadError")
|
||||
|
||||
if tt.ExpectHadFatalErr {
|
||||
require.True(t, tt.ExpectHadError, "ExpectHadFatalErr implies ExpectHadError")
|
||||
}
|
||||
if !tt.ExpectHadError {
|
||||
require.False(t, tt.ExpectHadFatalErr, "!ExpectHadError implies !ExpectHadFatalErr")
|
||||
}
|
||||
|
||||
// Iterate through each expected hook run
|
||||
for i, hook := range tt.ExpectStepReports {
|
||||
t.Logf("expecting report conforming to %v", hook)
|
||||
|
||||
exp, act := hook.ExpectStatus, report[i].Status
|
||||
require.Equal(t, exp, act, "%s != %s", exp, act)
|
||||
|
||||
// Check for required ExpectedEdge
|
||||
require.NotZero(t, hook.ExpectedEdge, "each hook must have an ExpectedEdge")
|
||||
require.Equal(t, hook.ExpectedEdge, report[i].Edge,
|
||||
"incorrect edge: expected %q, actual %q", hook.ExpectedEdge.String(), report[i].Edge.String(),
|
||||
)
|
||||
|
||||
// Check for expected output
|
||||
if hook.OutputTest != nil {
|
||||
require.IsType(t, (*hooks.CommandHookReport)(nil), report[i].Report)
|
||||
chr := report[i].Report.(*hooks.CommandHookReport)
|
||||
hook.OutputTest(t, string(chr.CapturedStdoutStderrCombined))
|
||||
}
|
||||
|
||||
// Check for expected errors
|
||||
if hook.ErrorTest != nil {
|
||||
hook.ErrorTest(t, string(report[i].Report.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Code generated by "enumer -type=StepStatus -trimprefix=Step"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
_StepStatusName_0 = "PendingExec"
|
||||
_StepStatusName_1 = "Ok"
|
||||
_StepStatusName_2 = "Err"
|
||||
_StepStatusName_3 = "SkippedDueToFatalErr"
|
||||
_StepStatusName_4 = "SkippedDueToPreErr"
|
||||
)
|
||||
|
||||
var (
|
||||
_StepStatusIndex_0 = [...]uint8{0, 7, 11}
|
||||
_StepStatusIndex_1 = [...]uint8{0, 2}
|
||||
_StepStatusIndex_2 = [...]uint8{0, 3}
|
||||
_StepStatusIndex_3 = [...]uint8{0, 20}
|
||||
_StepStatusIndex_4 = [...]uint8{0, 18}
|
||||
)
|
||||
|
||||
func (i StepStatus) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 2:
|
||||
i -= 1
|
||||
return _StepStatusName_0[_StepStatusIndex_0[i]:_StepStatusIndex_0[i+1]]
|
||||
case i == 4:
|
||||
return _StepStatusName_1
|
||||
case i == 8:
|
||||
return _StepStatusName_2
|
||||
case i == 16:
|
||||
return _StepStatusName_3
|
||||
case i == 32:
|
||||
return _StepStatusName_4
|
||||
default:
|
||||
return fmt.Sprintf("StepStatus(%d)", i)
|
||||
}
|
||||
}
|
||||
|
||||
var _StepStatusValues = []StepStatus{1, 2, 4, 8, 16, 32}
|
||||
|
||||
var _StepStatusNameToValueMap = map[string]StepStatus{
|
||||
_StepStatusName_0[0:7]: 1,
|
||||
_StepStatusName_0[7:11]: 2,
|
||||
_StepStatusName_1[0:2]: 4,
|
||||
_StepStatusName_2[0:3]: 8,
|
||||
_StepStatusName_3[0:20]: 16,
|
||||
_StepStatusName_4[0:18]: 32,
|
||||
}
|
||||
|
||||
// StepStatusString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func StepStatusString(s string) (StepStatus, error) {
|
||||
if val, ok := _StepStatusNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to StepStatus values", s)
|
||||
}
|
||||
|
||||
// StepStatusValues returns all values of the enum
|
||||
func StepStatusValues() []StepStatus {
|
||||
return _StepStatusValues
|
||||
}
|
||||
|
||||
// IsAStepStatus returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i StepStatus) IsAStepStatus() bool {
|
||||
for _, v := range _StepStatusValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh -eu
|
||||
|
||||
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||
|
||||
exit 1
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh -eu
|
||||
|
||||
# Exceed default hook log size of 1<<20 bytes by double
|
||||
# The Go test should fail if buffer size exceeds 1MB
|
||||
|
||||
pre_testing() {
|
||||
ln=0
|
||||
while :; do printf '%06d: 012345678901234567890123456789012345678901234567890123456789012345678901\n' "$ln"; ln="$(($ln + 1))"; done \
|
||||
| head -c$(( 2 << 20 ))
|
||||
}
|
||||
|
||||
case "$ZREPL_HOOKTYPE" in
|
||||
pre_testing)
|
||||
"$ZREPL_HOOKTYPE";;
|
||||
*)
|
||||
# Not handled by this script
|
||||
exit 0;;
|
||||
esac
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh -eu
|
||||
|
||||
post_testing() {
|
||||
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "$ZREPL_HOOKTYPE" in
|
||||
post_testing)
|
||||
"$ZREPL_HOOKTYPE";;
|
||||
*)
|
||||
# Not handled by this script
|
||||
exit 0;;
|
||||
esac
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh -eu
|
||||
|
||||
if [ "$ZREPL_HOOKTYPE" = "pre_testing" ]; then
|
||||
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||
exit 1
|
||||
elif [ "$ZREPL_HOOKTYPE" = "post_testing" ]; then
|
||||
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
|
||||
else
|
||||
printf "Unknown hook type: %s" "$ZREPL_HOOKTYPE"
|
||||
exit 255
|
||||
fi
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh -eu
|
||||
|
||||
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME ${ZREPL_DRYRUN:+(dry run)}"
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh -eu
|
||||
|
||||
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME ZREPL_TIMEOUT=$ZREPL_TIMEOUT"
|
||||
|
||||
exec sleep $(($ZREPL_TIMEOUT + 1))
|
||||
@@ -83,6 +83,7 @@ type activeMode interface {
|
||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||
Type() Type
|
||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
||||
SnapperReport() *snapper.Report
|
||||
ResetConnectBackoff()
|
||||
}
|
||||
|
||||
@@ -124,6 +125,10 @@ func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
||||
m.snapper.Run(ctx, wakeUpCommon)
|
||||
}
|
||||
|
||||
func (m *modePush) SnapperReport() *snapper.Report {
|
||||
return m.snapper.Report()
|
||||
}
|
||||
|
||||
func (m *modePush) ResetConnectBackoff() {
|
||||
m.setupMtx.Lock()
|
||||
defer m.setupMtx.Unlock()
|
||||
@@ -206,6 +211,10 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modePull) SnapperReport() *snapper.Report {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *modePull) ResetConnectBackoff() {
|
||||
m.setupMtx.Lock()
|
||||
defer m.setupMtx.Unlock()
|
||||
@@ -279,6 +288,7 @@ func (j *ActiveSide) Name() string { return j.name }
|
||||
type ActiveSideStatus struct {
|
||||
Replication *report.Report
|
||||
PruningSender, PruningReceiver *pruner.Report
|
||||
Snapshotting *snapper.Report
|
||||
}
|
||||
|
||||
func (j *ActiveSide) Status() *Status {
|
||||
@@ -295,6 +305,7 @@ func (j *ActiveSide) Status() *Status {
|
||||
if tasks.prunerReceiver != nil {
|
||||
s.PruningReceiver = tasks.prunerReceiver.Report()
|
||||
}
|
||||
s.Snapshotting = j.mode.SnapperReport()
|
||||
return &Status{Type: t, JobSpecific: s}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Code generated by "enumer -type=ActiveSideState"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package job
|
||||
|
||||
import (
|
||||
|
||||
+14
-3
@@ -27,6 +27,7 @@ type PassiveSide struct {
|
||||
type passiveMode interface {
|
||||
Handler() rpc.Handler
|
||||
RunPeriodic(ctx context.Context)
|
||||
SnapperReport() *snapper.Report // may be nil
|
||||
Type() Type
|
||||
}
|
||||
|
||||
@@ -40,7 +41,8 @@ func (m *modeSink) Handler() rpc.Handler {
|
||||
return endpoint.NewReceiver(m.rootDataset, true)
|
||||
}
|
||||
|
||||
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
||||
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
||||
func (m *modeSink) SnapperReport() *snapper.Report { return nil }
|
||||
|
||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob) (m *modeSink, err error) {
|
||||
m = &modeSink{}
|
||||
@@ -85,6 +87,10 @@ func (m *modeSource) RunPeriodic(ctx context.Context) {
|
||||
m.snapper.Run(ctx, nil)
|
||||
}
|
||||
|
||||
func (m *modeSource) SnapperReport() *snapper.Report {
|
||||
return m.snapper.Report()
|
||||
}
|
||||
|
||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) {
|
||||
|
||||
s = &PassiveSide{mode: mode, name: in.Name}
|
||||
@@ -97,10 +103,15 @@ func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passive
|
||||
|
||||
func (j *PassiveSide) Name() string { return j.name }
|
||||
|
||||
type PassiveStatus struct{}
|
||||
type PassiveStatus struct {
|
||||
Snapper *snapper.Report
|
||||
}
|
||||
|
||||
func (s *PassiveSide) Status() *Status {
|
||||
return &Status{Type: s.mode.Type()} // FIXME PassiveStatus
|
||||
st := &PassiveStatus{
|
||||
Snapper: s.mode.SnapperReport(),
|
||||
}
|
||||
return &Status{Type: s.mode.Type(), JobSpecific: st}
|
||||
}
|
||||
|
||||
func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
||||
|
||||
@@ -66,7 +66,8 @@ func (j *SnapJob) RegisterMetrics(registerer prometheus.Registerer) {
|
||||
}
|
||||
|
||||
type SnapJobStatus struct {
|
||||
Pruning *pruner.Report
|
||||
Pruning *pruner.Report
|
||||
Snapshotting *snapper.Report // may be nil
|
||||
}
|
||||
|
||||
func (j *SnapJob) Status() *Status {
|
||||
@@ -75,6 +76,7 @@ func (j *SnapJob) Status() *Status {
|
||||
if j.pruner != nil {
|
||||
s.Pruning = j.pruner.Report()
|
||||
}
|
||||
s.Snapshotting = j.snapper.Report()
|
||||
return &Status{Type: t, JobSpecific: s}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/daemon/pruner"
|
||||
"github.com/zrepl/zrepl/daemon/snapper"
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
@@ -37,7 +38,7 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
|
||||
var syslogOutlets, stdoutOutlets int
|
||||
for lei, le := range in {
|
||||
|
||||
outlet, minLevel, err := parseOutlet(le)
|
||||
outlet, minLevel, err := ParseOutlet(le)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
||||
}
|
||||
@@ -72,6 +73,7 @@ const (
|
||||
SubsyEndpoint Subsystem = "endpoint"
|
||||
SubsysPruning Subsystem = "pruning"
|
||||
SubsysSnapshot Subsystem = "snapshot"
|
||||
SubsysHooks Subsystem = "hook"
|
||||
SubsysTransport Subsystem = "transport"
|
||||
SubsysTransportMux Subsystem = "transportmux"
|
||||
SubsysRPC Subsystem = "rpc"
|
||||
@@ -85,6 +87,7 @@ func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Contex
|
||||
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsyEndpoint))
|
||||
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
|
||||
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
|
||||
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
|
||||
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
|
||||
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
|
||||
ctx = rpc.WithLoggers(ctx,
|
||||
@@ -123,7 +126,7 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
||||
|
||||
}
|
||||
|
||||
func parseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
|
||||
func ParseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
|
||||
|
||||
parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) {
|
||||
if common.Level == "" || common.Format == "" {
|
||||
|
||||
@@ -88,7 +88,7 @@ func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
|
||||
prefixFields := []string{JobField, SubsysField}
|
||||
prefixed := make(map[string]bool, len(prefixFields)+2)
|
||||
for _, field := range prefixFields {
|
||||
val, ok := e.Fields[field].(string)
|
||||
val, ok := e.Fields[field]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Code generated by "enumer -type=State"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package pruner
|
||||
|
||||
import (
|
||||
|
||||
+101
-25
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/filters"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -25,18 +26,20 @@ const (
|
||||
SnapError
|
||||
)
|
||||
|
||||
// All fields protected by Snapper.mtx
|
||||
type snapProgress struct {
|
||||
state SnapState
|
||||
|
||||
// SnapStarted, SnapDone, SnapError
|
||||
name string
|
||||
startAt time.Time
|
||||
name string
|
||||
startAt time.Time
|
||||
hookPlan *hooks.Plan
|
||||
|
||||
// SnapDone
|
||||
doneAt time.Time
|
||||
|
||||
// SnapErr
|
||||
err error
|
||||
// SnapErr TODO disambiguate state
|
||||
runResults hooks.PlanReport
|
||||
}
|
||||
|
||||
type args struct {
|
||||
@@ -46,6 +49,8 @@ type args struct {
|
||||
interval time.Duration
|
||||
fsf *filters.DatasetMapFilter
|
||||
snapshotsTaken chan<- struct{}
|
||||
hooks *hooks.List
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
type Snapper struct {
|
||||
@@ -58,7 +63,7 @@ type Snapper struct {
|
||||
lastInvocation time.Time
|
||||
|
||||
// valid for state Snapshotting
|
||||
plan map[*zfs.DatasetPath]snapProgress
|
||||
plan map[*zfs.DatasetPath]*snapProgress
|
||||
|
||||
// valid for state SyncUp and Waiting
|
||||
sleepUntil time.Time
|
||||
@@ -123,10 +128,16 @@ func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *con
|
||||
return nil, errors.New("interval must be positive")
|
||||
}
|
||||
|
||||
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "hook config error")
|
||||
}
|
||||
|
||||
args := args{
|
||||
prefix: in.Prefix,
|
||||
interval: in.Interval,
|
||||
fsf: fsf,
|
||||
hooks: hookList,
|
||||
// ctx and log is set in Run()
|
||||
}
|
||||
|
||||
@@ -141,6 +152,7 @@ func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||
s.args.snapshotsTaken = snapshotsTaken
|
||||
s.args.ctx = ctx
|
||||
s.args.log = getLogger(ctx)
|
||||
s.args.dryRun = false // for future expansion
|
||||
|
||||
u := func(u func(*Snapper)) State {
|
||||
s.mtx.Lock()
|
||||
@@ -224,24 +236,30 @@ func plan(a args, u updater) state {
|
||||
return onErr(err, u)
|
||||
}
|
||||
|
||||
plan := make(map[*zfs.DatasetPath]snapProgress, len(fss))
|
||||
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
||||
for _, fs := range fss {
|
||||
plan[fs] = snapProgress{state: SnapPending}
|
||||
plan[fs] = &snapProgress{state: SnapPending}
|
||||
}
|
||||
return u(func(s *Snapper) {
|
||||
s.state = Snapshotting
|
||||
s.plan = plan
|
||||
s.err = nil
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func snapshot(a args, u updater) state {
|
||||
|
||||
var plan map[*zfs.DatasetPath]snapProgress
|
||||
var plan map[*zfs.DatasetPath]*snapProgress
|
||||
u(func(snapper *Snapper) {
|
||||
plan = snapper.plan
|
||||
})
|
||||
|
||||
hadErr := false
|
||||
hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
|
||||
for _, h := range *a.hooks {
|
||||
hookMatchCount[h] = 0
|
||||
}
|
||||
|
||||
anyFsHadErr := false
|
||||
// TODO channel programs -> allow a little jitter?
|
||||
for fs, progress := range plan {
|
||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||
@@ -251,27 +269,71 @@ func snapshot(a args, u updater) state {
|
||||
WithField("fs", fs.ToString()).
|
||||
WithField("snap", snapname)
|
||||
|
||||
hookEnvExtra := hooks.Env{
|
||||
hooks.EnvFS: fs.ToString(),
|
||||
hooks.EnvSnapshot: snapname,
|
||||
}
|
||||
|
||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
|
||||
l.Debug("create snapshot")
|
||||
err = zfs.ZFSSnapshot(fs, snapname, false) // TODO propagagte context to ZFSSnapshot
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create snapshot")
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
fsHadErr := false
|
||||
var planReport hooks.PlanReport
|
||||
var plan *hooks.Plan
|
||||
{
|
||||
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("unexpected filter error")
|
||||
fsHadErr = true
|
||||
goto updateFSState
|
||||
}
|
||||
// account for running hooks
|
||||
for _, h := range filteredHooks {
|
||||
hookMatchCount[h] = hookMatchCount[h] + 1
|
||||
}
|
||||
|
||||
var planErr error
|
||||
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
||||
if planErr != nil {
|
||||
fsHadErr = true
|
||||
l.WithError(planErr).Error("cannot create job hook plan")
|
||||
goto updateFSState
|
||||
}
|
||||
}
|
||||
u(func(snapper *Snapper) {
|
||||
progress.name = snapname
|
||||
progress.startAt = time.Now()
|
||||
progress.hookPlan = plan
|
||||
progress.state = SnapStarted
|
||||
})
|
||||
|
||||
l.Debug("create snapshot")
|
||||
err := zfs.ZFSSnapshot(fs, snapname, false)
|
||||
if err != nil {
|
||||
hadErr = true
|
||||
l.WithError(err).Error("cannot create snapshot")
|
||||
}
|
||||
doneAt := time.Now()
|
||||
|
||||
u(func(snapper *Snapper) {
|
||||
progress.doneAt = doneAt
|
||||
progress.state = SnapDone
|
||||
if err != nil {
|
||||
progress.state = SnapError
|
||||
progress.err = err
|
||||
{
|
||||
l := hooks.GetLogger(a.ctx).WithField("fs", fs.ToString()).WithField("snap", snapname)
|
||||
l.WithField("report", plan.Report().String()).Debug("begin run job plan")
|
||||
plan.Run(hooks.WithLogger(a.ctx, l), a.dryRun)
|
||||
planReport = plan.Report()
|
||||
fsHadErr = planReport.HadError() // not just fatal errors
|
||||
if fsHadErr {
|
||||
l.WithField("report", planReport.String()).Error("end run job plan with error")
|
||||
} else {
|
||||
l.WithField("report", planReport.String()).Info("end run job plan successful")
|
||||
}
|
||||
}
|
||||
|
||||
updateFSState:
|
||||
anyFsHadErr = anyFsHadErr || fsHadErr
|
||||
u(func(snapper *Snapper) {
|
||||
progress.doneAt = time.Now()
|
||||
progress.state = SnapDone
|
||||
if fsHadErr {
|
||||
progress.state = SnapError
|
||||
}
|
||||
progress.runResults = planReport
|
||||
})
|
||||
}
|
||||
|
||||
@@ -283,12 +345,26 @@ func snapshot(a args, u updater) state {
|
||||
}
|
||||
}
|
||||
|
||||
for h, mc := range hookMatchCount {
|
||||
if mc == 0 {
|
||||
hookIdx := -1
|
||||
for idx, ah := range *a.hooks {
|
||||
if ah == h {
|
||||
hookIdx = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
a.log.WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
||||
}
|
||||
}
|
||||
|
||||
return u(func(snapper *Snapper) {
|
||||
if hadErr {
|
||||
if anyFsHadErr {
|
||||
snapper.state = ErrorWait
|
||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
||||
} else {
|
||||
snapper.state = Waiting
|
||||
snapper.err = nil
|
||||
}
|
||||
}).sf()
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns nil if manual
|
||||
func (s *PeriodicOrManual) Report() *Report {
|
||||
if s.s != nil {
|
||||
return s.s.Report()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.SnapshottingPeriodic:
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
State State
|
||||
// valid in state SyncUp and Waiting
|
||||
SleepUntil time.Time
|
||||
// valid in state Err
|
||||
Error string
|
||||
// valid in state Snapshotting
|
||||
Progress []*ReportFilesystem
|
||||
}
|
||||
|
||||
type ReportFilesystem struct {
|
||||
Path string
|
||||
State SnapState
|
||||
|
||||
// Valid in SnapStarted and later
|
||||
SnapName string
|
||||
StartAt time.Time
|
||||
Hooks string
|
||||
HooksHadError bool
|
||||
|
||||
// Valid in SnapDone | SnapError
|
||||
DoneAt time.Time
|
||||
}
|
||||
|
||||
func errOrEmptyString(e error) string {
|
||||
if e != nil {
|
||||
return e.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Snapper) Report() *Report {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
|
||||
pReps := make([]*ReportFilesystem, 0, len(s.plan))
|
||||
for fs, p := range s.plan {
|
||||
var hooksStr string
|
||||
var hooksHadError bool
|
||||
if p.hookPlan != nil {
|
||||
hr := p.hookPlan.Report()
|
||||
// FIXME: technically this belongs into client
|
||||
// but we can't serialize hooks.Step ATM
|
||||
rightPad := func(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
hooksHadError = hr.HadError()
|
||||
rows := make([][]string, len(hr))
|
||||
const numCols = 4
|
||||
lens := make([]int, numCols)
|
||||
for i, e := range hr {
|
||||
rows[i] = make([]string, numCols)
|
||||
rows[i][0] = fmt.Sprintf("%d", i+1)
|
||||
rows[i][1] = e.Status.String()
|
||||
runTime := "..."
|
||||
if e.Status != hooks.StepPending {
|
||||
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
|
||||
}
|
||||
rows[i][2] = runTime
|
||||
rows[i][3] = ""
|
||||
if e.Report != nil {
|
||||
rows[i][3] = e.Report.String()
|
||||
}
|
||||
for j, col := range lens {
|
||||
if len(rows[i][j]) > col {
|
||||
lens[j] = len(rows[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
rowsFlat := make([]string, len(hr))
|
||||
for i, r := range rows {
|
||||
colsPadded := make([]string, len(r))
|
||||
for j, c := range r[:len(r)-1] {
|
||||
colsPadded[j] = rightPad(c, lens[j], " ")
|
||||
}
|
||||
colsPadded[len(r)-1] = r[len(r)-1]
|
||||
rowsFlat[i] = strings.Join(colsPadded, " ")
|
||||
}
|
||||
hooksStr = strings.Join(rowsFlat, "\n")
|
||||
}
|
||||
pReps = append(pReps, &ReportFilesystem{
|
||||
Path: fs.ToString(),
|
||||
State: p.state,
|
||||
SnapName: p.name,
|
||||
StartAt: p.startAt,
|
||||
DoneAt: p.doneAt,
|
||||
Hooks: hooksStr,
|
||||
HooksHadError: hooksHadError,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(pReps, func(i, j int) bool {
|
||||
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
|
||||
})
|
||||
|
||||
r := &Report{
|
||||
State: s.state,
|
||||
SleepUntil: s.sleepUntil,
|
||||
Error: errOrEmptyString(s.err),
|
||||
Progress: pReps,
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -4,6 +4,16 @@ package snapper
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[SnapPending-1]
|
||||
_ = x[SnapStarted-2]
|
||||
_ = x[SnapDone-4]
|
||||
_ = x[SnapError-8]
|
||||
}
|
||||
|
||||
const (
|
||||
_SnapState_name_0 = "SnapPendingSnapStarted"
|
||||
_SnapState_name_1 = "SnapDone"
|
||||
|
||||
@@ -4,6 +4,19 @@ package snapper
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[SyncUp-1]
|
||||
_ = x[SyncUpErrWait-2]
|
||||
_ = x[Planning-4]
|
||||
_ = x[Snapshotting-8]
|
||||
_ = x[Waiting-16]
|
||||
_ = x[ErrorWait-32]
|
||||
_ = x[Stopped-64]
|
||||
}
|
||||
|
||||
const (
|
||||
_State_name_0 = "SyncUpSyncUpErrWait"
|
||||
_State_name_1 = "Planning"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. include:: global.rst.inc
|
||||
|
||||
.. |break_config| replace:: **[CONFIG]**
|
||||
.. |break| replace:: **[BREAK]**
|
||||
.. |bugfix| replace:: [BUG]
|
||||
@@ -25,6 +27,46 @@ We use the following annotations for classifying changes:
|
||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||
* |docs| Change to the documentation.
|
||||
|
||||
0.2
|
||||
---
|
||||
|
||||
* |feature| :ref:`Pre- and Post-Snapshot Hooks <job-snapshotting-hooks>`
|
||||
with built-in support for MySQL and Postgres checkpointing
|
||||
as well as custom scripts (thanks, `@overhacked <https://github.com/overhacked>`_!)
|
||||
* |feature| Use ``zfs destroy pool/fs@snap1,snap2,...`` CLI feature if available
|
||||
* |feature| Linux ARM64 Docker build support & binary builds
|
||||
* |feature| ``zrepl status`` now displays snapshotting reports
|
||||
* |feature| ``zrepl status --job <JOBNAME>`` filter flag
|
||||
* |bugfix| i386 build
|
||||
* |bugfix| early validation of host:port tuples in config
|
||||
* |bugfix| ``zrepl status`` now supports ``TERM=screen`` (tmux on FreeBSD / FreeNAS)
|
||||
* |bugfix| ignore *connection reset by peer* errors when shutting down connections
|
||||
* |bugfix| correct error messages when receive-side pool or ``root_fs`` dataset is not imported
|
||||
* |bugfix| fail fast for misconfigured local transport
|
||||
* |bugfix| race condition in replication report generation would crash the daemon when running ``zrepl status``
|
||||
* |bugfix| rpc goroutine leak in ``push`` mode if zfs recv fails on the ``sink`` side
|
||||
* [MAINTAINER NOTICE] Go modules for dependency management both inside and outside of GOPATH
|
||||
(``lazy.sh`` and ``Makefile`` force ``GO111MODULE=on``)
|
||||
* [MAINTAINER NOTICE] ``make platformtest`` target to check zrepl's ZFS abstractions (screen scraping, etc.).
|
||||
These tests only work on a system with ZFS installed, and must be run as root because they create a file-backed pool for each test case.
|
||||
The pool name ``zreplplatformtest`` is reserved for this use case.
|
||||
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
|
||||
|
||||
.. NOTE::
|
||||
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
| You can support maintenance and feature development through one of the following services:
|
||||
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
| For commerical support, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
|
||||
0.1.1
|
||||
-----
|
||||
|
||||
* |bugfix| :issue:`162` :commit:`d6304f4` : fix I/O timeout errors on variable receive rate
|
||||
|
||||
* A significant reduction or sudden stall of the receive rate (e.g. recv pool has other I/O to do)
|
||||
would cause a ``writev I/O timeout`` error after approximately ten seconds.
|
||||
|
||||
0.1
|
||||
---
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ Configuration
|
||||
|
||||
.. toctree::
|
||||
|
||||
configuration/preface
|
||||
configuration/overview
|
||||
configuration/jobs
|
||||
configuration/transports
|
||||
configuration/filter_syntax
|
||||
configuration/snapshotting
|
||||
configuration/prune
|
||||
configuration/logging
|
||||
configuration/monitoring
|
||||
|
||||
+2
-215
@@ -1,222 +1,9 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
||||
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
||||
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||
|
||||
.. _job:
|
||||
|
||||
Job Types & Replication
|
||||
=======================
|
||||
|
||||
.. _job-overview:
|
||||
|
||||
Overview & Terminology
|
||||
----------------------
|
||||
|
||||
A *job* is the unit of activity tracked by the zrepl daemon.
|
||||
Every job has a unique ``name``, a ``type`` and type-dependent fields which are documented on this page.
|
||||
|
||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
||||
The active side executes the replication logic whereas the passive side responds to requests after checking the active side's permissions.
|
||||
For communication, the active side connects to the passive side using a :ref:`transport <transport>` and starts issuing remote procedure calls (RPCs).
|
||||
|
||||
The following table shows how different job types can be combined to achieve both push and pull mode setups:
|
||||
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Setup name | active side | passive side | use case |
|
||||
+=======================+==============+==================================+====================================================================================+
|
||||
| Push mode | ``push`` | ``sink`` | * Laptop backup |
|
||||
| | | | * NAS behind NAT to offsite |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
||||
| | | | * Remote server to NAS behind NAT |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool |
|
||||
| | | with :ref:`local transport <transport-local>` | |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
||||
| | | | | required |
|
||||
| | | | * Workaround for :ref:`source-side pruning <prune-workaround-source-side-pruning>` |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
|
||||
How the Active Side Works
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
|
||||
|
||||
* Wakeup because of finished snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
||||
* Connect to the corresponding passive side using a :ref:`transport <transport>` and instantiate an RPC client.
|
||||
* Replicate data from the sending to the receiving side.
|
||||
* Prune on sender & receiver.
|
||||
|
||||
.. TIP::
|
||||
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
||||
|
||||
How the Passive Side Works
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
||||
using the transport listener type specified in the ``serve`` field of the job configuration.
|
||||
Each transport listener provides a client's identity to the passive side job.
|
||||
It uses the client identity for access control:
|
||||
|
||||
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
||||
* The ``source`` job has a whitelist of client identities that are allowed pull access.
|
||||
|
||||
.. TIP::
|
||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
||||
|
||||
How Replication Works
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
One of the major design goals of the replication module is to avoid any duplication of the nontrivial logic.
|
||||
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
||||
|
||||
The following steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
||||
|
||||
* Plan the replication:
|
||||
|
||||
* Compare sender and receiver filesystem snapshots
|
||||
* Build the **replication plan**
|
||||
|
||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
||||
* Build a list of replication steps
|
||||
|
||||
* If possible, use incremental sends (``zfs send -i``)
|
||||
* Otherwise, use full send of most recent snapshot on sender
|
||||
* Give up on filesystems that cannot be replicated without data loss
|
||||
|
||||
* Retry on errors that are likely temporary (i.e. network failures).
|
||||
* Give up on filesystems where a permanent error was received over RPC.
|
||||
|
||||
* Execute the plan
|
||||
|
||||
* Perform replication steps in the following order:
|
||||
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
||||
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||
* After a successful replication step, update the replication cursor bookmark (see below).
|
||||
|
||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||
|
||||
.. _replication-cursor-bookmark:
|
||||
|
||||
The **replication cursor bookmark** ``#zrepl_replication_cursor`` is kept per filesystem on the sending side of a replication setup:
|
||||
It is a bookmark of the most recent successfully replicated snapshot to the receiving side.
|
||||
It is is used by the :ref:`not_replicated <prune-keep-not-replicated>` keep rule to identify all snapshots that have not yet been replicated to the receiving side.
|
||||
Regardless of whether that keep rule is used, the bookmark ensures that replication can always continue incrementally.
|
||||
Note that there is only one cursor bookmark per filesystem, which prohibits multiple jobs to replicate the same filesystem (:ref:`see below<jobs-multiple-jobs>`).
|
||||
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
|
||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
Currently, zrepl does not replicate filesystem properties.
|
||||
Whe receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||
This is temporary and being worked on :issue:`24`.
|
||||
|
||||
|
||||
.. _job-snapshotting-spec:
|
||||
|
||||
Taking Snaphots
|
||||
---------------
|
||||
|
||||
The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
|
||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||
|
||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
...
|
||||
|
||||
There is also a ``manual`` snapshotting type, which covers the following use cases:
|
||||
|
||||
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
||||
* Run scripts before and after taking snapshots (like locking database tables).
|
||||
We are working on better integration for this use case: see :issue:`74`.
|
||||
* Handling snapshotting through a separate ``snap`` job.
|
||||
|
||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: manual
|
||||
...
|
||||
|
||||
.. _jobs-multiple-jobs:
|
||||
|
||||
Multiple Jobs & More than 2 Machines
|
||||
------------------------------------
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss:
|
||||
|
||||
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine.
|
||||
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
|
||||
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
|
||||
|
||||
Explanations & exceptions to above rules are detailed below.
|
||||
|
||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||
|
||||
No Overlapping
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Jobs run independently of each other.
|
||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
|
||||
More subtle race conditions can occur with the :ref:`replication cursor bookmark <replication-cursor-bookmark>`, which currently only exists once per filesystem.
|
||||
|
||||
N push jobs to 1 sink
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
||||
It is thus safe to push to one sink job with different client identities.
|
||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
||||
|
||||
N pull jobs from 1 source
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||
|
||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
||||
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
Job Types in Detail
|
||||
===================
|
||||
|
||||
.. _job-push:
|
||||
|
||||
|
||||
@@ -6,16 +6,16 @@ Miscellaneous
|
||||
Runtime Directories & UNIX Sockets
|
||||
----------------------------------
|
||||
|
||||
zrepl daemon creates various UNIX sockets to allow communicating with it:
|
||||
The zrepl daemon needs to open various UNIX sockets in a runtime directory:
|
||||
|
||||
* the :ref:`transport-ssh+stdinserver` transport connects to a socket named after ``client_identity`` parameter
|
||||
* the ``control`` CLI subcommand connects to a defined control socket
|
||||
* a ``control`` socket that the CLI commands use to interact with the daemon
|
||||
* the :ref:`transport-ssh+stdinserver` listener opens one socket per configured client, named after ``client_identity`` parameter
|
||||
|
||||
There is no further authentication on these sockets.
|
||||
Therefore we have to make sure they can only be created and accessed by ``zrepl daemon``.
|
||||
In fact, ``zrepl daemon`` will not bind a socket to a path in a directory that is world-accessible.
|
||||
There is no authentication on these sockets except the UNIX permissions.
|
||||
The zrepl daemon will refuse to bind any of the above sockets in a directory that is world-accessible.
|
||||
|
||||
The directories can be configured in the main configuration file, the defaults are provided below:
|
||||
The following sections of the ``global`` config shows the default paths.
|
||||
The shell script below shows how the default runtime directory can be created.
|
||||
|
||||
::
|
||||
|
||||
@@ -27,6 +27,12 @@ The directories can be configured in the main configuration file, the defaults a
|
||||
sockdir: /var/run/zrepl/stdinserver
|
||||
|
||||
|
||||
::
|
||||
|
||||
mkdir -p /var/run/zrepl/stdinserver
|
||||
chmod -R 0700 /var/run/zrepl
|
||||
|
||||
|
||||
Durations & Intervals
|
||||
---------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
|
||||
Overview & Terminology
|
||||
======================
|
||||
|
||||
All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup.
|
||||
The following paths are considered:
|
||||
|
||||
* If set, the location specified via the global ``--config`` flag
|
||||
* ``/etc/zrepl/zrepl.yml``
|
||||
* ``/usr/local/etc/zrepl/zrepl.yml``
|
||||
|
||||
The ``zrepl configcheck`` subcommand can be used to validate the configuration.
|
||||
The command will output nothing and exit with zero status code if the configuration is valid.
|
||||
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
||||
Full example configs such as in the :ref:`tutorial` or the :sampleconf:`/` directory might also be helpful.
|
||||
However, copy-pasting examples is no substitute for reading documentation!
|
||||
|
||||
Config File Structure
|
||||
---------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
global: ...
|
||||
jobs:
|
||||
- name: backup
|
||||
type: push
|
||||
- ...
|
||||
|
||||
zrepl is confgured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
||||
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
||||
The ``jobs`` section is a list of jobs which we are goind to explain now.
|
||||
|
||||
.. _job-overview:
|
||||
|
||||
Jobs \& How They Work Together
|
||||
------------------------------
|
||||
|
||||
A *job* is the unit of activity tracked by the zrepl daemon.
|
||||
The ``type`` of a job determines its role in a replication setup and in snapshot management.
|
||||
Jobs are identified by their ``name``, both in log files and the ``zrepl status`` command.
|
||||
|
||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
||||
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
||||
The passive side responds to requests from the active side after checking its persmissions.
|
||||
|
||||
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
|
||||
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
||||
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Setup name | active side | passive side | use case |
|
||||
+=======================+==============+==================================+====================================================================================+
|
||||
| Push mode | ``push`` | ``sink`` | * Laptop backup |
|
||||
| | (snap) | | * NAS behind NAT to offsite |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
||||
| | | (snap) | * Remote server to NAS behind NAT |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool |
|
||||
| | | with :ref:`local transport <transport-local>` | |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
||||
| | (snap) | | | required |
|
||||
| | | | * Workaround for :ref:`source-side pruning <prune-workaround-source-side-pruning>` |
|
||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||
|
||||
How the Active Side Works
|
||||
-------------------------
|
||||
|
||||
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
|
||||
|
||||
* Wakeup because of finished snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
||||
* Connect to the corresponding passive side using a :ref:`transport <transport>` and instantiate an RPC client.
|
||||
* Replicate data from the sending to the receiving side (see below).
|
||||
* Prune on sender & receiver.
|
||||
|
||||
.. TIP::
|
||||
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
||||
|
||||
How the Passive Side Works
|
||||
--------------------------
|
||||
|
||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
||||
using the transport listener type specified in the ``serve`` field of the job configuration.
|
||||
Each transport listener provides a client's identity to the passive side job.
|
||||
It uses the client identity for access control:
|
||||
|
||||
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
||||
* The ``source`` job has a whitelist of client identities that are allowed pull access.
|
||||
|
||||
.. TIP::
|
||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
||||
|
||||
How Replication Works
|
||||
---------------------
|
||||
|
||||
One of the major design goals of the replication module is to avoid any duplication of the nontrivial logic.
|
||||
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
||||
|
||||
The following steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
||||
|
||||
* Plan the replication:
|
||||
|
||||
* Compare sender and receiver filesystem snapshots
|
||||
* Build the **replication plan**
|
||||
|
||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
||||
* Build a list of replication steps
|
||||
|
||||
* If possible, use incremental sends (``zfs send -i``)
|
||||
* Otherwise, use full send of most recent snapshot on sender
|
||||
* Give up on filesystems that cannot be replicated without data loss
|
||||
|
||||
* Retry on errors that are likely temporary (i.e. network failures).
|
||||
* Give up on filesystems where a permanent error was received over RPC.
|
||||
|
||||
* Execute the plan
|
||||
|
||||
* Perform replication steps in the following order:
|
||||
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
||||
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||
* After a successful replication step, update the replication cursor bookmark (see below).
|
||||
|
||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||
|
||||
.. _replication-cursor-bookmark:
|
||||
|
||||
The **replication cursor bookmark** ``#zrepl_replication_cursor`` is kept per filesystem on the sending side of a replication setup:
|
||||
It is a bookmark of the most recent successfully replicated snapshot to the receiving side.
|
||||
It is is used by the :ref:`not_replicated <prune-keep-not-replicated>` keep rule to identify all snapshots that have not yet been replicated to the receiving side.
|
||||
Regardless of whether that keep rule is used, the bookmark ensures that replication can always continue incrementally.
|
||||
Note that there is only one cursor bookmark per filesystem, which prohibits multiple jobs to replicate the same filesystem (:ref:`see below<jobs-multiple-jobs>`).
|
||||
|
||||
.. _replication-placeholder-property:
|
||||
|
||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
|
||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
Currently, zrepl does not replicate filesystem properties.
|
||||
Whe receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||
This is temporary and being worked on :issue:`24`.
|
||||
|
||||
|
||||
.. _jobs-multiple-jobs:
|
||||
|
||||
Multiple Jobs & More than 2 Machines
|
||||
------------------------------------
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss:
|
||||
|
||||
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine.
|
||||
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
|
||||
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
|
||||
|
||||
Explanations & exceptions to above rules are detailed below.
|
||||
|
||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||
|
||||
No Overlapping
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Jobs run independently of each other.
|
||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
|
||||
More subtle race conditions can occur with the :ref:`replication cursor bookmark <replication-cursor-bookmark>`, which currently only exists once per filesystem.
|
||||
|
||||
N push jobs to 1 sink
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
||||
It is thus safe to push to one sink job with different client identities.
|
||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
||||
|
||||
N pull jobs from 1 source
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
||||
|
||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
||||
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
.. _configuration_preface:
|
||||
|
||||
=======
|
||||
Preface
|
||||
=======
|
||||
|
||||
-----------------------
|
||||
Configuration File Path
|
||||
-----------------------
|
||||
|
||||
zrepl searches for its main configuration file in the following locations (in that order):
|
||||
|
||||
* If set, the location specified via the global ``--config`` flag
|
||||
* ``/etc/zrepl/zrepl.yml``
|
||||
* ``/usr/local/etc/zrepl/zrepl.yml``
|
||||
|
||||
The examples in the :ref:`tutorial` or the :sampleconf:`/` directory should provide a good starting point.
|
||||
|
||||
-------------------
|
||||
Runtime Directories
|
||||
-------------------
|
||||
|
||||
zrepl requires runtime directories for various UNIX sockets --- they are documented in the :ref:`config file<conf-runtime-directories>`.
|
||||
Your package maintainer / init script should take care of creating them.
|
||||
Alternatively, for default settings, the following should to the trick.
|
||||
|
||||
::
|
||||
|
||||
mkdir -p /var/run/zrepl/stdinserver
|
||||
chmod -R 0700 /var/run/zrepl
|
||||
|
||||
|
||||
----------
|
||||
Validating
|
||||
----------
|
||||
|
||||
The config can be validated using the ``zrepl configcheck`` subcommand.
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.. include:: ../global.rst.inc
|
||||
|
||||
.. _job-snapshotting-spec:
|
||||
|
||||
Taking Snaphots
|
||||
===============
|
||||
|
||||
The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
|
||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||
|
||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
hooks: ...
|
||||
...
|
||||
|
||||
There is also a ``manual`` snapshotting type, which covers the following use cases:
|
||||
|
||||
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
||||
* Handling snapshotting through a separate ``snap`` job.
|
||||
|
||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: manual
|
||||
...
|
||||
|
||||
.. _job-snapshotting-hooks:
|
||||
|
||||
Pre- and Post-Snapshot Hooks
|
||||
----------------------------
|
||||
|
||||
Jobs with `periodic snapshots <job-snapshotting-spec_>`_ can run hooks before and/or after taking the snapshot specified in ``snapshotting.hooks``:
|
||||
Hooks are called per filesystem before and after the snapshot is taken (pre- and post-edge).
|
||||
Pre-edge invocations are in configuration order, post-edge invocations in reverse order, i.e. like a stack.
|
||||
If a pre-snapshot invocation fails, ``err_is_fatal=true`` cuts off subsequent hooks, does not take a snapshot, and only invokes post-edges corresponding to previous successful pre-edges.
|
||||
``err_is_fatal=false`` logs the failed pre-edge invocation but does not affect subsequent hooks nor snapshotting itself.
|
||||
Post-edges are only invoked for hooks whose pre-edges ran without error.
|
||||
Note that hook failures for one filesystem never affect other filesystems.
|
||||
|
||||
The optional ``timeout`` parameter specifies a period after which zrepl will kill the hook process and report an error.
|
||||
The default is 30 seconds and may be specified in any units understood by `time.ParseDuration <https://golang.org/pkg/time/#ParseDuration>`_.
|
||||
|
||||
The optional ``filesystems`` filter which limits the filesystems the hook runs for. This uses the same |filter-spec| as jobs.
|
||||
|
||||
Most hook types take additional parameters, please refer to the respective subsections below.
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 10 70
|
||||
:header-rows: 1
|
||||
|
||||
* - Hook ``type``
|
||||
- Details
|
||||
- Description
|
||||
* - ``command``
|
||||
- :ref:`Details <job-hook-type-command>`
|
||||
- Arbitrary pre- and post snapshot scripts.
|
||||
* - ``postgres-checkpoint``
|
||||
- :ref:`Details <job-hook-type-postgres-checkpoint>`
|
||||
- Execute Postgres ``CHECKPOINT`` SQL command before snapshot.
|
||||
* - ``mysql-lock-tables``
|
||||
- :ref:`Details <job-hook-type-mysql-lock-tables>`
|
||||
- Flush and read-Lock MySQL tables while taking the snapshot.
|
||||
|
||||
.. _job-hook-type-command:
|
||||
|
||||
``command`` Hooks
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
hooks:
|
||||
- type: command
|
||||
path: /etc/zrepl/hooks/zrepl-notify.sh
|
||||
timeout: 30s
|
||||
err_is_fatal: false
|
||||
- type: command
|
||||
path: /etc/zrepl/hooks/special-snapshot.sh
|
||||
filesystems: {
|
||||
"tank/special": true
|
||||
}
|
||||
...
|
||||
|
||||
|
||||
The hook type ``command`` is the only currently supported hook type.
|
||||
Future versions of zrepl may support other hook types.
|
||||
The ``path`` to the hook executables must be absolute (e.g. ``/etc/zrepl/hooks/zrepl-notify.sh``).
|
||||
No arguments may be specified; create a wrapper script if zrepl must call an executable that requires arguments.
|
||||
zrepl will call the hook both before and after the snapshot, but with different values of the ``ZREPL_HOOKTYPE`` environment variable; see below.
|
||||
The process standard output is logged at level INFO. Standard error is logged at level WARN.
|
||||
|
||||
zrepl sets a number of environment variables for the hook processes:
|
||||
|
||||
* ``ZREPL_HOOKTYPE``: either "pre_snapshot" or "post_snapshot"
|
||||
* ``ZREPL_FS``: the ZFS filesystem name being snapshotted
|
||||
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
|
||||
* ``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
|
||||
|
||||
An empty template hook can be found in :sampleconf:`hooks/template.sh`.
|
||||
|
||||
.. _job-hook-type-postgres-checkpoint:
|
||||
|
||||
``postgres-checkpoint`` Hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Connects to a Postgres server and executes the ``CHECKPOINT`` statement pre-snapshot.
|
||||
Checkpointing applies the WAL contents to all data files and syncs the data files to disk.
|
||||
This is not required for a consistent database backup: it merely forward-pays the "cost" of WAL replay to the time of snapshotting instead of at restore.
|
||||
However, the Postgres manual recommends against checkpointing during normal operation.
|
||||
Further, the operation requires Postgres superuser privileges.
|
||||
zrepl users must decide on their own whether this hook is useful for them (it likely isn't).
|
||||
|
||||
.. ATTENTION::
|
||||
Note that WALs and Postgres data directory (with all database data files) must be on the same filesystem to guarantee a correct point-in-time backup with the ZFS snapshot.
|
||||
|
||||
DSN syntax documented here: `<https://godoc.org/github.com/lib/pq>`_
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE USER zrepl_checkpoint PASSWORD yourpasswordhere;
|
||||
ALTER ROLE zrepl_checkpoint SUPERUSER;
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- type: postgres-checkpoint
|
||||
dsn: "host=localhost port=5432 user=postgres password=yourpasswordhere sslmode=disable"
|
||||
filesystems: {
|
||||
"p1/postgres/data11": true
|
||||
}
|
||||
|
||||
.. _job-hook-type-mysql-lock-tables:
|
||||
|
||||
``mysql-lock-tables`` Hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Connects to MySQL and executes
|
||||
|
||||
* pre-snapshot ``FLUSH TABLES WITH READ LOCK`` to lock all tables in all databases in the MySQL server we connect to (`docs <https://dev.mysql.com/doc/refman/8.0/en/flush.html#flush-tables-with-read-lock>`_)
|
||||
* post-snapshot ``UNLOCK TABLES`` reverse above operation.
|
||||
|
||||
Above procedure is documented in the `MySQL manual <https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html>`_
|
||||
as a means to produce a consistent backup of a MySQL DBMS installation (i.e., all databases).
|
||||
|
||||
`DSN syntax <https://github.com/go-sql-driver/mysql#dsn-data-source-name>`_: ``[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]``
|
||||
|
||||
.. ATTENTION::
|
||||
All MySQL databases must be on the same ZFS filesystem to guarantee a consistent point-in-time backup with the ZFS snapshot.
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE USER zrepl_lock_tables IDENTIFIED BY 'yourpasswordhere';
|
||||
GRANT RELOAD ON *.* TO zrepl_lock_tables;
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- type: mysql-lock-tables
|
||||
dsn: "zrepl_lock_tables:yourpasswordhere@tcp(localhost)/"
|
||||
filesystems: {
|
||||
"tank/mysql": true
|
||||
}
|
||||
@@ -345,5 +345,6 @@ The ``client_identity`` is used by the sink as documented above.
|
||||
type: local
|
||||
listener_name: localsink
|
||||
client_identity: local_backup
|
||||
dial_timeout: 2s # optional, 0 for no timeout
|
||||
...
|
||||
|
||||
|
||||
@@ -2,3 +2,25 @@
|
||||
|
||||
..
|
||||
.. |macroname| replace:: macro expansion
|
||||
|
||||
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
||||
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
||||
.. |Language: Go| image:: https://img.shields.io/badge/language-Go-6ad7e5.svg
|
||||
:target: https://golang.org/
|
||||
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
||||
:target: https://zrepl.github.io
|
||||
.. |Donate via PayPal| image:: https://img.shields.io/badge/donate-paypal-yellow.svg
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/receives/zrepl.svg?logo=liberapay
|
||||
:target: https://liberapay.com/zrepl/donate
|
||||
.. |Donate via Patreon| image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow
|
||||
:target: https://www.patreon.com/zrepl
|
||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||
|
||||
|
||||
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
||||
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
||||
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||
+7
-15
@@ -5,21 +5,8 @@
|
||||
|
||||
.. include:: global.rst.inc
|
||||
|
||||
|GitHub license| |Language: Go| |User Docs| |Donate via PayPal| |Donate
|
||||
via Liberapay| |Twitter|
|
||||
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
|
||||
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
||||
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
||||
.. |Language: Go| image:: https://img.shields.io/badge/language-Go-6ad7e5.svg
|
||||
:target: https://golang.org/
|
||||
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
||||
:target: https://zrepl.github.io
|
||||
.. |Donate via PayPal| image:: https://img.shields.io/badge/donate-paypal-yellow.svg
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||
.. |Donate via Liberapay| image:: https://img.shields.io/badge/donate-liberapay-yellow.svg
|
||||
:target: https://liberapay.com/zrepl/donate
|
||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||
|
||||
zrepl - ZFS replication
|
||||
-----------------------
|
||||
@@ -76,7 +63,8 @@ Main Features
|
||||
|
||||
* **Automatic snapshot management**
|
||||
|
||||
* [x] Periodic filesystem snapshots
|
||||
* [x] Periodic :ref:`filesystem snapshots <job-snapshotting-spec>`
|
||||
* [x] Support for :ref:`pre- and post-snapshot hooks <job-snapshotting-hooks>` with builtins for MySQL & Postgres
|
||||
* [x] Flexible :ref:`pruning rule system <prune>`
|
||||
|
||||
* [x] Age-based fading (grandfathering scheme)
|
||||
@@ -109,6 +97,7 @@ Contributing
|
||||
|
||||
We are happy about any help we can get!
|
||||
|
||||
* :ref:`Financial Support <supporters>`
|
||||
* Explore the codebase
|
||||
|
||||
* These docs live in the ``docs/`` subdirectory
|
||||
@@ -124,6 +113,8 @@ We are happy about any help we can get!
|
||||
Make sure to read the `Developer Documentation section <https://github.com/zrepl/zrepl>`_ and open new issues or pull requests there.
|
||||
|
||||
|
||||
|
||||
|
||||
Table of Contents
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -139,4 +130,5 @@ Table of Contents
|
||||
pr
|
||||
changelog
|
||||
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||
supporters
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ The following list may be incomplete, feel free to submit a PR with an update:
|
||||
* - FreeBSD
|
||||
- ``pkg install zrepl``
|
||||
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
||||
* - MacOS
|
||||
- ``brew install zrepl``
|
||||
- Available on `homebrew <https://brew.sh>`_
|
||||
* - Others
|
||||
-
|
||||
- Use `binary releases`_ or build from source.
|
||||
@@ -41,7 +44,7 @@ The following list may be incomplete, feel free to submit a PR with an update:
|
||||
Compile From Source
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Producing a release requires **Go 1.9** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
||||
Python and pip3 should probably be installed via your distro's package manager.
|
||||
|
||||
@@ -55,9 +58,9 @@ and serves as a reference for build dependencies and procedure:
|
||||
cd zrepl
|
||||
sudo docker build -t zrepl_build -f build.Dockerfile .
|
||||
sudo docker run -it --rm \
|
||||
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
|
||||
-v "${PWD}:/src" \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
zrepl_build make vendordeps release
|
||||
zrepl_build make release
|
||||
|
||||
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
||||
|
||||
@@ -69,7 +72,7 @@ Alternatively, you can install build dependencies on your local system and then
|
||||
python3 -m venv3
|
||||
source venv3/bin/activate
|
||||
./lazy.sh devsetup
|
||||
make vendordeps release
|
||||
make release
|
||||
|
||||
The Python venv is used for the documentation build dependencies.
|
||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
||||
@@ -83,8 +86,7 @@ Either way, all build results are located in the ``artifacts/`` directory.
|
||||
What next?
|
||||
----------
|
||||
|
||||
Read the :ref:`configuration chapter<configuration_toc>` and optionally create the :ref:`runtime directories <configuration_preface>`.
|
||||
Afterwards, continue with the :ref:`usage chapter<usage>`.
|
||||
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||
|
||||
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
||||
|
||||
|
||||
+27
-17
@@ -1,20 +1,30 @@
|
||||
alabaster==0.7.12
|
||||
Babel==2.6.0
|
||||
certifi==2018.10.15
|
||||
attrs==19.1.0
|
||||
Babel==2.7.0
|
||||
certifi==2019.6.16
|
||||
chardet==3.0.4
|
||||
docutils==0.14
|
||||
idna==2.7
|
||||
Click==7.0
|
||||
colorclass==2.2.0
|
||||
docutils==0.15.2
|
||||
idna==2.8
|
||||
imagesize==1.1.0
|
||||
Jinja2==2.10
|
||||
MarkupSafe==1.1.0
|
||||
packaging==18.0
|
||||
Pygments==2.2.0
|
||||
pyparsing==2.3.0
|
||||
pytz==2018.7
|
||||
requests==2.20.1
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
Sphinx==1.8.2
|
||||
sphinx-rtd-theme==0.4.2
|
||||
sphinxcontrib-websupport==1.1.0
|
||||
urllib3==1.24.1
|
||||
Jinja2==2.10.1
|
||||
MarkupSafe==1.1.1
|
||||
packaging==19.1
|
||||
Pygments==2.4.2
|
||||
pyparsing==2.4.2
|
||||
pytz==2019.2
|
||||
requests==2.22.0
|
||||
six==1.12.0
|
||||
snowballstemmer==1.9.1
|
||||
Sphinx==1.8.5
|
||||
sphinx-rtd-theme==0.4.3
|
||||
sphinxcontrib-applehelp==1.0.1
|
||||
sphinxcontrib-devhelp==1.0.1
|
||||
sphinxcontrib-htmlhelp==1.0.2
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==1.0.2
|
||||
sphinxcontrib-serializinghtml==1.1.3
|
||||
-e git://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
|
||||
sphinxcontrib-websupport==1.1.2
|
||||
urllib3==1.25.3
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
.. include:: global.rst.inc
|
||||
|
||||
.. _supporters:
|
||||
|
||||
|Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
|
||||
|
||||
zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||
You can support maintenance and feature development through one of the services listed above.
|
||||
For commerical support, please `contact Christian directly <https://cschwarz.com>`_.
|
||||
|
||||
**Thanks for your support!**
|
||||
|
||||
.. NOTE ::
|
||||
|
||||
PayPal takes a relatively high fixed processing fee plus percentage of the donation.
|
||||
Larger less-frequent donations make more sense there.
|
||||
|
||||
Supporters
|
||||
==========
|
||||
|
||||
We would like to thank the following people / organizations for supporting zrepl through monetary and other means:
|
||||
|
||||
* `Ross Williams <https://github.com/overhacked>`_
|
||||
* Mike T.
|
||||
* `Justin Scholz <https://github.com/JMoVS>`_
|
||||
* `InsanePrawn <https://github.com/InsanePrawn>`_
|
||||
* `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
||||
* `Anton Schirg <https://github.com/antonxy>`_
|
||||
+31
-18
@@ -380,6 +380,15 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
|
||||
}
|
||||
|
||||
if !ph.FSExists {
|
||||
if s.rootWithoutClientComponent.HasPrefix(v.Path) {
|
||||
if v.Path.Length() == 1 {
|
||||
visitErr = fmt.Errorf("pool %q not imported", v.Path.ToString())
|
||||
} else {
|
||||
visitErr = fmt.Errorf("root_fs %q does not exist", s.rootWithoutClientComponent.ToString())
|
||||
}
|
||||
getLogger(ctx).WithError(visitErr).Error("placeholders are only created automatically below root_fs")
|
||||
return false
|
||||
}
|
||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
||||
l.Debug("create placeholder filesystem")
|
||||
err := zfs.ZFSCreatePlaceholderFilesystem(v.Path)
|
||||
@@ -445,30 +454,34 @@ func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapsho
|
||||
}
|
||||
|
||||
func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.FilesystemVersion) (*pdu.DestroySnapshotsRes, error) {
|
||||
fsvs := make([]*zfs.FilesystemVersion, len(snaps))
|
||||
reqs := make([]*zfs.DestroySnapOp, len(snaps))
|
||||
ress := make([]*pdu.DestroySnapshotRes, len(snaps))
|
||||
errs := make([]error, len(snaps))
|
||||
for i, fsv := range snaps {
|
||||
if fsv.Type != pdu.FilesystemVersion_Snapshot {
|
||||
return nil, fmt.Errorf("version %q is not a snapshot", fsv.Name)
|
||||
}
|
||||
var err error
|
||||
fsvs[i], err = fsv.ZFSFilesystemVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
ress[i] = &pdu.DestroySnapshotRes{
|
||||
Snapshot: fsv,
|
||||
// Error set after batch operation
|
||||
}
|
||||
reqs[i] = &zfs.DestroySnapOp{
|
||||
Filesystem: lp.ToString(),
|
||||
Name: fsv.Name,
|
||||
ErrOut: &errs[i],
|
||||
}
|
||||
}
|
||||
res := &pdu.DestroySnapshotsRes{
|
||||
Results: make([]*pdu.DestroySnapshotRes, len(fsvs)),
|
||||
}
|
||||
for i, fsv := range fsvs {
|
||||
err := zfs.ZFSDestroyFilesystemVersion(lp, fsv)
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
res.Results[i] = &pdu.DestroySnapshotRes{
|
||||
Snapshot: pdu.FilesystemVersionFromZFS(fsv),
|
||||
Error: errMsg,
|
||||
zfs.ZFSDestroyFilesystemVersions(reqs)
|
||||
for i := range reqs {
|
||||
if errs[i] != nil {
|
||||
if de, ok := errs[i].(*zfs.DestroySnapshotsError); ok && len(de.Reason) == 1 {
|
||||
ress[i].Error = de.Reason[0]
|
||||
} else {
|
||||
ress[i].Error = errs[i].Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
return &pdu.DestroySnapshotsRes{
|
||||
Results: ress,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
module github.com/zrepl/zrepl
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
|
||||
github.com/alvaroloes/enumer v1.1.1
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff // indirect
|
||||
github.com/gdamore/tcell v1.2.0
|
||||
github.com/go-critic/go-critic v0.3.4 // indirect
|
||||
github.com/go-logfmt/logfmt v0.3.0
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
|
||||
github.com/gogo/protobuf v1.2.1 // indirect
|
||||
github.com/golang/mock v1.2.0 // indirect
|
||||
github.com/golang/protobuf v1.2.0
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
|
||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
|
||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
|
||||
github.com/golangci/golangci-lint v1.17.1
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
|
||||
github.com/golangci/misspell v0.3.4 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
|
||||
github.com/kr/pretty v0.1.0
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/mattn/go-isatty v0.0.3
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/montanaflynn/stats v0.5.0
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pkg/profile v1.2.1
|
||||
github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5 // indirect
|
||||
github.com/prometheus/common v0.0.0-20180413074202-d0f7cd64bda4 // indirect
|
||||
github.com/prometheus/procfs v0.0.0-20180408092902-8b1c2da0d56d // indirect
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.4.0 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cobra v0.0.2
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.3.2 // indirect
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/theckman/goconstraint v1.11.0 // indirect
|
||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
|
||||
github.com/yudai/pp v2.0.1+incompatible // indirect
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 // indirect
|
||||
google.golang.org/grpc v1.17.0
|
||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect
|
||||
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect
|
||||
)
|
||||
|
||||
// invalid dates in transitive dependencies (first validated in Go 1.13, didn't fail in earlier Go versions)
|
||||
replace (
|
||||
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 => github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540
|
||||
github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead => github.com/go-critic/go-critic v0.3.5-0.20190210220443-ee9bf5809ead
|
||||
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6
|
||||
github.com/golangci/go-tools v0.0.0-20180109140146-35a9f45a5db0 => github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0
|
||||
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196
|
||||
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547
|
||||
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc
|
||||
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217
|
||||
golang.org/x/tools v0.0.0-20190125232054-379209517ffe => golang.org/x/tools v0.0.0-20190205201329-379209517ffe
|
||||
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34
|
||||
)
|
||||
@@ -0,0 +1,358 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 h1:pFks+oaqVWDFq0KsLQZFB2pQGB5Us0HfMSBENtieXOs=
|
||||
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/alvaroloes/enumer v1.1.1 h1:v+1scNqEhSFAPb9tLwblQegeTXJhnw8hf9O0amTbOvQ=
|
||||
github.com/alvaroloes/enumer v1.1.1/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff h1:zk1wwii7uXmI0znwU+lqg+wFL9G5+vm5I+9rv2let60=
|
||||
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff/go.mod h1:yUhRXHewUVJ1k89wHKP68xfzk7kwXUx/DV1nx4EBMbw=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
|
||||
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc=
|
||||
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
|
||||
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
||||
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
|
||||
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||
github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4 h1:0suja/iKSDbEIYLbrS/8C7iArJiWpgCNcR+zwAHu7Ig=
|
||||
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
|
||||
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
|
||||
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
|
||||
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
|
||||
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
||||
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
|
||||
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
|
||||
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
|
||||
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
|
||||
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
||||
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
|
||||
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
|
||||
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
|
||||
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8 h1:vVouagbdmqTVlCIAxpyYsNNTbkKZ3V66VpKOLU/s6W4=
|
||||
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
|
||||
github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg=
|
||||
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
|
||||
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
|
||||
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
|
||||
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w=
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
|
||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 h1:MRhC9XbUjE6XDOInSJ8pwHuPagqsyO89QDU9IdVhe3o=
|
||||
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||
github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
|
||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
|
||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
|
||||
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc=
|
||||
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
|
||||
github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
|
||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI=
|
||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4=
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||
github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||
github.com/golangci/misspell v0.3.4 h1:kAD5J0611Zo2VirUn9KFP15RjEqkmfEfnFxyIVxZCYg=
|
||||
github.com/golangci/misspell v0.3.4/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
|
||||
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM=
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8 h1:+dKzeuiDYbD/Cfi/sEkkNrs6z4/WZ8ZIR8dAbUpjXbU=
|
||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.0 h1:YNOwxxSJzSUARoD9KRZLzM9Y858MNGCOACTvCW9TSAc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk=
|
||||
github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
|
||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
||||
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284 h1:GX8YFBfSL6GlKymQZOsQllPbxVamfEYpis7oaJLXYZA=
|
||||
github.com/problame/go-netssh v0.0.0-20190110232351-09d6bc45d284/go.mod h1:XMR9fjP5/wMyFK0Ifk9ZHDFem4Rukm+ChvoYr2nxrgw=
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b h1:ymnAeobXUW0sfmt5n8euMzW4vgdQvZNTagdYMcpK63g=
|
||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5 h1:cLL6NowurKLMfCeQy4tIeph12XNQWgANCNvdyrOYKV4=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180413074202-d0f7cd64bda4 h1:rPmL0pCWefiqV7RjMYuBUrLbMJmNVjRKz82caELPTUQ=
|
||||
github.com/prometheus/common v0.0.0-20180413074202-d0f7cd64bda4/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180408092902-8b1c2da0d56d h1:RCcsxyRr6+/pLg6wr0cUjPovhEhSNOtPh0SOz6u3hGU=
|
||||
github.com/prometheus/procfs v0.0.0-20180408092902-8b1c2da0d56d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.4.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE=
|
||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
|
||||
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
|
||||
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0=
|
||||
github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g=
|
||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM=
|
||||
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M=
|
||||
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190903123849-809f4603401a h1:l37btErUqI1yNmt5mf8kzglbeQfo32+TRacnxthiuck=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190903123849-809f4603401a/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f h1:3MuiGfgMHCSwKUcsuI7ODbi50j+evTB7SsoOBMNC5Fk=
|
||||
github.com/zrepl/yaml-config v0.0.0-20190928121844-af7ca3f8448f/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8 h1:b0PLhFjEMqrIqsD4rS5sp0YxBYgdqKzX0KkkGGKLV8I=
|
||||
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b h1:iEAPfYPbYbxG/2lNN4cMOHkmgKNsCuUwkxlDCK46UlU=
|
||||
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 h1:yvw+zsSmSM02Z5H3ZdEV7B7Ql7eFrjQTnmByJvK+3J8=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g=
|
||||
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o=
|
||||
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
|
||||
if tty -s; then
|
||||
bold=$(tput bold)
|
||||
normal=$(tput sgr0)
|
||||
@@ -26,21 +27,15 @@ fi
|
||||
CHECKOUTPATH="${GOPATH}/src/github.com/zrepl/zrepl"
|
||||
|
||||
godep() {
|
||||
step "Install go dep using 'go get' to \$GOPATH/bin"
|
||||
go get -u github.com/golang/dep/cmd/dep
|
||||
if ! type dep ; then
|
||||
echo "Unable to install go dep" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
step "Fetching dependencies using 'dep ensure'"
|
||||
dep ensure -v -vendor-only
|
||||
step "go install build dependencies fetched using dep"
|
||||
# these will be in the vendor directory
|
||||
go build -o "$GOPATH/bin/stringer" ./vendor/golang.org/x/tools/cmd/stringer
|
||||
go build -o "$GOPATH/bin/protoc-gen-go" ./vendor/github.com/golang/protobuf/protoc-gen-go
|
||||
go build -o "$GOPATH/bin/enumer" ./vendor/github.com/alvaroloes/enumer
|
||||
go build -o "$GOPATH/bin/goimports" ./vendor/golang.org/x/tools/cmd/goimports
|
||||
go build -o "$GOPATH/bin/golangci-lint" ./vendor/github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
step "install build dependencies (versions pinned using go.mod and tools.go)"
|
||||
set -x
|
||||
export GO111MODULE=on # otherwise, a checkout of this repo in GOPATH will disable modules on Go 1.12 and earlier
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/stringer" golang.org/x/tools/cmd/stringer
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/protoc-gen-go" github.com/golang/protobuf/protoc-gen-go
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/enumer" github.com/alvaroloes/enumer
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/goimports" golang.org/x/tools/cmd/goimports
|
||||
go build -v -mod=readonly -o "$GOPATH/bin/golangci-lint" github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
set +x
|
||||
if ! type stringer || ! type protoc-gen-go || ! type enumer || ! type goimports || ! type golangci-lint; then
|
||||
echo "Installed dependencies but can't find them in \$PATH, adjust it to contain \$GOPATH/bin" 1>&2
|
||||
exit 1
|
||||
|
||||
@@ -20,6 +20,7 @@ type Logger interface {
|
||||
WithField(field string, val interface{}) Logger
|
||||
WithFields(fields Fields) Logger
|
||||
WithError(err error) Logger
|
||||
Log(level Level, msg string)
|
||||
Debug(msg string)
|
||||
Info(msg string)
|
||||
Warn(msg string)
|
||||
@@ -158,6 +159,10 @@ func (l *loggerImpl) WithError(err error) Logger {
|
||||
return l.WithField(FieldError, val)
|
||||
}
|
||||
|
||||
func (l *loggerImpl) Log(level Level, msg string) {
|
||||
l.log(level, msg)
|
||||
}
|
||||
|
||||
func (l *loggerImpl) Debug(msg string) {
|
||||
l.log(Debug, msg)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ func (n nullLogger) ReplaceField(field string, val interface{}) Logger { return
|
||||
func (n nullLogger) WithField(field string, val interface{}) Logger { return n }
|
||||
func (n nullLogger) WithFields(fields Fields) Logger { return n }
|
||||
func (n nullLogger) WithError(err error) Logger { return n }
|
||||
func (nullLogger) Log(level Level, msg string) {}
|
||||
func (nullLogger) Debug(msg string) {}
|
||||
func (nullLogger) Info(msg string) {}
|
||||
func (nullLogger) Warn(msg string) {}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/platformtest/tests"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var args struct {
|
||||
createArgs platformtest.ZpoolCreateArgs
|
||||
}
|
||||
flag.StringVar(&args.createArgs.PoolName, "poolname", "", "")
|
||||
flag.StringVar(&args.createArgs.ImagePath, "imagepath", "", "")
|
||||
flag.Int64Var(&args.createArgs.ImageSize, "imagesize", 100*(1<<20), "")
|
||||
flag.StringVar(&args.createArgs.Mountpoint, "mountpoint", "none", "")
|
||||
flag.Parse()
|
||||
|
||||
outlets := logger.NewOutlets()
|
||||
outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
|
||||
LoggingOutletCommon: config.LoggingOutletCommon{
|
||||
Level: "debug",
|
||||
Format: "human",
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
outlets.Add(outlet, level)
|
||||
logger := logger.NewLogger(outlets, 1*time.Second)
|
||||
|
||||
if err := args.createArgs.Validate(); err != nil {
|
||||
logger.Error(err.Error())
|
||||
panic(err)
|
||||
}
|
||||
ctx := platformtest.WithLogger(context.Background(), logger)
|
||||
ex := platformtest.NewEx(logger)
|
||||
|
||||
bold := color.New(color.Bold)
|
||||
for _, c := range tests.Cases {
|
||||
// ATTENTION future parallelism must pass c by value into closure!
|
||||
err := func() error {
|
||||
bold.Printf("BEGIN TEST CASE %s\n", c)
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.createArgs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create test pool")
|
||||
}
|
||||
defer func() {
|
||||
if err := pool.Destroy(ctx, ex); err != nil {
|
||||
fmt.Printf("error destroying test pool: %s", err)
|
||||
}
|
||||
}()
|
||||
ctx := &platformtest.Context{
|
||||
Context: ctx,
|
||||
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
||||
}
|
||||
c(ctx)
|
||||
bold.Printf("DONE TEST CASE %s\n", c)
|
||||
fmt.Println()
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
bold.Printf("TEST CASE FAILED WITH ERROR:\n")
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Idea taken from
|
||||
// https://github.com/openSUSE/umoci/blob/v0.2.1/cmd/umoci/main_test.go
|
||||
//
|
||||
/* How to generate coverage:
|
||||
go test -c -covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/...
|
||||
sudo ../logmockzfs/logzfsenv /tmp/zrepl_platform_test.log /usr/bin/zfs \
|
||||
./harness.test -test.coverprofile=/tmp/harness.out \
|
||||
-test.v __DEVEL--i-heard-you-like-tests \
|
||||
-imagepath /tmp/testpool.img -poolname zreplplatformtest
|
||||
go tool cover -html=/tmp/harness.out -o /tmp/harness.html
|
||||
*/
|
||||
// Merge with existing coverage reports using gocovmerge:
|
||||
// https://github.com/wadey/gocovmerge
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
var (
|
||||
args []string
|
||||
run bool
|
||||
)
|
||||
|
||||
for _, arg := range os.Args {
|
||||
switch {
|
||||
case arg == "__DEVEL--i-heard-you-like-tests":
|
||||
run = true
|
||||
case strings.HasPrefix(arg, "-test"):
|
||||
case strings.HasPrefix(arg, "__DEVEL"):
|
||||
default:
|
||||
args = append(args, arg)
|
||||
}
|
||||
}
|
||||
os.Args = args
|
||||
|
||||
if run {
|
||||
main()
|
||||
}
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ue
|
||||
export ZREPL_MOCK_ZFS_COMMAND_LOG="$1"
|
||||
shift
|
||||
export ZREPL_MOCK_ZFS_PATH="$1"
|
||||
shift
|
||||
export PATH="$(dirname "${BASH_SOURCE[0]}" )":"$PATH"
|
||||
args=("$@")
|
||||
exec "${args[@]}"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
args=("$@")
|
||||
|
||||
if [ -n "$ZREPL_MOCK_ZFS_COMMAND_LOG" ]; then
|
||||
(
|
||||
flock -x 200
|
||||
jq --compact-output -n --argjson args "$(printf '%s\0' "${args[@]}" | jq -Rsc 'split("\u0000")')" '{date: now|strflocaltime("%Y-%m-%dT%H:%M:%S"), command: $args}' >> "$ZREPL_MOCK_ZFS_COMMAND_LOG"
|
||||
) 200>"$ZREPL_MOCK_ZFS_COMMAND_LOG".lock
|
||||
fi
|
||||
|
||||
exec "$ZREPL_MOCK_ZFS_PATH" "${args[@]}"
|
||||
@@ -0,0 +1,24 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
context.Context
|
||||
RootDataset string
|
||||
}
|
||||
|
||||
var _ assert.TestingT = (*Context)(nil)
|
||||
var _ require.TestingT = (*Context)(nil)
|
||||
|
||||
func (c *Context) Errorf(format string, args ...interface{}) {
|
||||
getLog(c).Printf(format, args...)
|
||||
}
|
||||
|
||||
func (c *Context) FailNow() {
|
||||
panic(nil)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/zrepl/zrepl/util/circlog"
|
||||
)
|
||||
|
||||
type ex struct {
|
||||
log Logger
|
||||
}
|
||||
|
||||
func NewEx(log Logger) Execer {
|
||||
return &ex{log}
|
||||
}
|
||||
|
||||
func (e *ex) RunExpectSuccessNoOutput(ctx context.Context, cmd string, args ...string) error {
|
||||
return e.runNoOutput(true, ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (e *ex) RunExpectFailureNoOutput(ctx context.Context, cmd string, args ...string) error {
|
||||
return e.runNoOutput(false, ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (e *ex) runNoOutput(expectSuccess bool, ctx context.Context, cmd string, args ...string) error {
|
||||
log := e.log.WithField("command", fmt.Sprintf("%q %q", cmd, args))
|
||||
log.Debug("begin executing")
|
||||
defer log.Debug("done executing")
|
||||
ecmd := exec.CommandContext(ctx, cmd, args...)
|
||||
buf, _ := circlog.NewCircularLog(32 << 10)
|
||||
ecmd.Stdout, ecmd.Stderr = buf, buf
|
||||
err := ecmd.Run()
|
||||
log.Printf("command output: %s", buf.String())
|
||||
if _, ok := err.(*exec.ExitError); err != nil && !ok {
|
||||
panic(err)
|
||||
}
|
||||
if expectSuccess && err != nil {
|
||||
return fmt.Errorf("expecting no error, got error: %s\n%s", err, buf.String())
|
||||
} else if !expectSuccess && err == nil {
|
||||
return fmt.Errorf("expecting error, got no error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
)
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
contextKeyLogger contextKey = iota
|
||||
)
|
||||
|
||||
func WithLogger(ctx context.Context, logger Logger) context.Context {
|
||||
ctx = context.WithValue(ctx, contextKeyLogger, logger)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func getLog(ctx context.Context) Logger {
|
||||
return ctx.Value(contextKeyLogger).(Logger)
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Execer interface {
|
||||
RunExpectSuccessNoOutput(ctx context.Context, cmd string, args ...string) error
|
||||
RunExpectFailureNoOutput(ctx context.Context, cmd string, args ...string) error
|
||||
}
|
||||
|
||||
type Stmt interface {
|
||||
Run(context context.Context, e Execer) error
|
||||
}
|
||||
|
||||
type Op string
|
||||
|
||||
const (
|
||||
AssertExists Op = "!E"
|
||||
AssertNotExists Op = "!N"
|
||||
Add Op = "+"
|
||||
Del Op = "-"
|
||||
RunCmd Op = "R"
|
||||
DestroyRoot Op = "DESTROYROOT"
|
||||
CreateRoot Op = "CREATEROOT"
|
||||
)
|
||||
|
||||
type DestroyRootOp struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *DestroyRootOp) Run(ctx context.Context, e Execer) error {
|
||||
// early-exit if it doesn't exist
|
||||
if err := e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path); err != nil {
|
||||
getLog(ctx).WithField("root_ds", o.Path).Info("assume root ds doesn't exist")
|
||||
return nil
|
||||
}
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", "-r", o.Path)
|
||||
}
|
||||
|
||||
type FSOp struct {
|
||||
Op Op
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *FSOp) Run(ctx context.Context, e Execer) error {
|
||||
switch o.Op {
|
||||
case AssertExists:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case AssertNotExists:
|
||||
return e.RunExpectFailureNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case Add:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "create", o.Path)
|
||||
case Del:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Path)
|
||||
default:
|
||||
panic(o.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type SnapOp struct {
|
||||
Op Op
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *SnapOp) Run(ctx context.Context, e Execer) error {
|
||||
switch o.Op {
|
||||
case AssertExists:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case AssertNotExists:
|
||||
return e.RunExpectFailureNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case Add:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "snapshot", o.Path)
|
||||
case Del:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Path)
|
||||
default:
|
||||
panic(o.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type RunOp struct {
|
||||
RootDS string
|
||||
Script string
|
||||
}
|
||||
|
||||
func (o *RunOp) Run(ctx context.Context, e Execer) error {
|
||||
cmd := exec.CommandContext(ctx, "/usr/bin/env", "bash", "-c", o.Script)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("ROOTDS=%s", o.RootDS))
|
||||
log := getLog(ctx).WithField("script", o.Script)
|
||||
log.Info("start script")
|
||||
defer log.Info("script done")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if _, ok := err.(*exec.ExitError); err != nil && !ok {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("script output:\n%s", output)
|
||||
return err
|
||||
}
|
||||
|
||||
type LineError struct {
|
||||
Line string
|
||||
What string
|
||||
}
|
||||
|
||||
func (e LineError) Error() string {
|
||||
return fmt.Sprintf("%q: %s", e.Line, e.What)
|
||||
}
|
||||
|
||||
type RunKind int
|
||||
|
||||
const (
|
||||
PanicErr RunKind = 1 << iota
|
||||
RunAll
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, rk RunKind, rootds string, stmtsStr string) {
|
||||
stmt, err := parseSequence(rootds, stmtsStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
execer := NewEx(getLog(ctx))
|
||||
for _, s := range stmt {
|
||||
err := s.Run(ctx, execer)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if rk == PanicErr {
|
||||
panic(err)
|
||||
} else if rk == RunAll {
|
||||
continue
|
||||
} else {
|
||||
panic(rk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isNoSpace(r rune) bool {
|
||||
return !unicode.IsSpace(r)
|
||||
}
|
||||
|
||||
func splitQuotedWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
begin := bytes.IndexFunc(data, isNoSpace)
|
||||
if begin == -1 {
|
||||
return len(data), nil, nil
|
||||
}
|
||||
if data[begin] == '"' {
|
||||
end := begin + 1
|
||||
for end < len(data) {
|
||||
endCandidate := bytes.Index(data[end:], []byte(`"`))
|
||||
if endCandidate == -1 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
end += endCandidate
|
||||
if data[end-1] != '\\' {
|
||||
// unescaped quote, end of this string
|
||||
// remove backslash-escapes
|
||||
withBackslash := data[begin+1 : end]
|
||||
withoutBaskslash := bytes.Replace(withBackslash, []byte("\\\""), []byte("\""), -1)
|
||||
return end + 1, withoutBaskslash, nil
|
||||
} else {
|
||||
// continue to next quote
|
||||
end += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endOffset := bytes.IndexFunc(data[begin:], unicode.IsSpace)
|
||||
var end int
|
||||
if endOffset == -1 {
|
||||
if !atEOF {
|
||||
return 0, nil, nil
|
||||
} else {
|
||||
end = len(data)
|
||||
}
|
||||
} else {
|
||||
end = begin + endOffset
|
||||
}
|
||||
return end, data[begin:end], nil
|
||||
}
|
||||
return 0, nil, fmt.Errorf("unexpected")
|
||||
}
|
||||
|
||||
func parseSequence(rootds, stmtsStr string) (stmts []Stmt, err error) {
|
||||
scan := bufio.NewScanner(strings.NewReader(stmtsStr))
|
||||
nextLine:
|
||||
for scan.Scan() {
|
||||
if len(bytes.TrimSpace(scan.Bytes())) == 0 {
|
||||
continue
|
||||
}
|
||||
comps := bufio.NewScanner(bytes.NewReader(scan.Bytes()))
|
||||
comps.Split(splitQuotedWords)
|
||||
|
||||
expectMoreTokens := func() error {
|
||||
if !comps.Scan() {
|
||||
return &LineError{scan.Text(), "unexpected EOL"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Op
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var op Op
|
||||
switch comps.Text() {
|
||||
|
||||
case string(RunCmd):
|
||||
script := strings.TrimPrefix(strings.TrimSpace(scan.Text()), string(RunCmd))
|
||||
stmts = append(stmts, &RunOp{RootDS: rootds, Script: script})
|
||||
continue nextLine
|
||||
|
||||
case string(DestroyRoot):
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
}
|
||||
stmts = append(stmts, &DestroyRootOp{rootds})
|
||||
continue nextLine
|
||||
|
||||
case string(CreateRoot):
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
}
|
||||
stmts = append(stmts, &FSOp{Op: Add, Path: rootds})
|
||||
continue nextLine
|
||||
|
||||
case string(Add):
|
||||
op = Add
|
||||
case string(Del):
|
||||
op = Del
|
||||
case string(AssertExists):
|
||||
op = AssertExists
|
||||
case string(AssertNotExists):
|
||||
op = AssertNotExists
|
||||
default:
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("invalid op %q", comps.Text())}
|
||||
}
|
||||
|
||||
// FS / SNAP
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.ContainsAny(comps.Text(), "@") {
|
||||
stmts = append(stmts, &SnapOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, comps.Text())})
|
||||
} else {
|
||||
stmts = append(stmts, &FSOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, comps.Text())})
|
||||
}
|
||||
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("unexpected tokens at EOL")}
|
||||
}
|
||||
}
|
||||
return stmts, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSplitQuotedWords(t *testing.T) {
|
||||
s := bufio.NewScanner(strings.NewReader(`
|
||||
foo "bar baz" blah "foo \"with single escape" "blah baz" "\"foo" "foo\""
|
||||
`))
|
||||
s.Split(splitQuotedWords)
|
||||
var words []string
|
||||
for s.Scan() {
|
||||
words = append(words, s.Text())
|
||||
}
|
||||
assert.Equal(
|
||||
t,
|
||||
[]string{
|
||||
"foo",
|
||||
"bar baz",
|
||||
"blah",
|
||||
"foo \"with single escape",
|
||||
"blah baz",
|
||||
"\"foo",
|
||||
"foo\"",
|
||||
},
|
||||
words)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type Zpool struct {
|
||||
args ZpoolCreateArgs
|
||||
}
|
||||
|
||||
type ZpoolCreateArgs struct {
|
||||
PoolName string
|
||||
ImagePath string
|
||||
ImageSize int64
|
||||
Mountpoint string
|
||||
}
|
||||
|
||||
func (a ZpoolCreateArgs) Validate() error {
|
||||
if !filepath.IsAbs(a.ImagePath) {
|
||||
return errors.Errorf("ImagePath must be absolute, got %q", a.ImagePath)
|
||||
}
|
||||
const minImageSize = 1024
|
||||
if a.ImageSize < minImageSize {
|
||||
return errors.Errorf("ImageSize must be > %v, got %v", minImageSize, a.ImageSize)
|
||||
}
|
||||
if a.Mountpoint != "none" {
|
||||
return errors.Errorf("Mountpoint must be \"none\"")
|
||||
}
|
||||
if a.PoolName == "" {
|
||||
return errors.Errorf("PoolName must not be emtpy")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateOrReplaceZpool(ctx context.Context, e Execer, args ZpoolCreateArgs) (*Zpool, error) {
|
||||
if err := args.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "zpool create args validation error")
|
||||
}
|
||||
|
||||
// export pool if it already exists (idempotence)
|
||||
if _, err := zfs.ZFSGetRawAnySource(args.PoolName, []string{"name"}); err != nil {
|
||||
if _, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
||||
// we'll create it shortly
|
||||
} else {
|
||||
return nil, errors.Wrapf(err, "cannot determine whether test pool %q exists", args.PoolName)
|
||||
}
|
||||
} else {
|
||||
// exists, export it, OpenFile will destroy it
|
||||
if err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", args.PoolName); err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot destroy test pool %q", args.PoolName)
|
||||
}
|
||||
}
|
||||
|
||||
// idempotently (re)create the pool image
|
||||
image, err := os.OpenFile(args.ImagePath, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create image file")
|
||||
}
|
||||
defer image.Close()
|
||||
if err := image.Truncate(args.ImageSize); err != nil {
|
||||
return nil, errors.Wrap(err, "create image: truncate")
|
||||
}
|
||||
image.Close()
|
||||
|
||||
// create the pool
|
||||
err = e.RunExpectSuccessNoOutput(ctx, "zpool", "create", "-O", "mountpoint=none", args.PoolName, args.ImagePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "zpool create")
|
||||
}
|
||||
|
||||
return &Zpool{args}, nil
|
||||
}
|
||||
|
||||
func (p *Zpool) Name() string { return p.args.PoolName }
|
||||
|
||||
func (p *Zpool) Destroy(ctx context.Context, e Execer) error {
|
||||
|
||||
if err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", p.args.PoolName); err != nil {
|
||||
return errors.Wrapf(err, "export pool %q", p.args.PoolName)
|
||||
}
|
||||
|
||||
if err := os.Remove(p.args.ImagePath); err != nil {
|
||||
return errors.Wrapf(err, "remove pool image")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func BatchDestroy(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
+ "foo bar@2"
|
||||
+ "foo bar@3"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@2"
|
||||
`)
|
||||
|
||||
reqs := []*zfs.DestroySnapOp{
|
||||
&zfs.DestroySnapOp{
|
||||
ErrOut: new(error),
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "3",
|
||||
},
|
||||
&zfs.DestroySnapOp{
|
||||
ErrOut: new(error),
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "2",
|
||||
},
|
||||
}
|
||||
zfs.ZFSDestroyFilesystemVersions(reqs)
|
||||
if *reqs[0].ErrOut != nil {
|
||||
panic("expecting no error")
|
||||
}
|
||||
err := (*reqs[1].ErrOut).Error()
|
||||
if !strings.Contains(err, fmt.Sprintf("%s/foo bar@2", ctx.RootDataset)) {
|
||||
panic(fmt.Sprintf("expecting error about being unable to destroy @2: %T\n%s", err, err))
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
!N "foo bar@3"
|
||||
!E "foo bar@1"
|
||||
!E "foo bar@2"
|
||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@2"
|
||||
- "foo bar@2"
|
||||
- "foo bar@1"
|
||||
- "foo bar"
|
||||
`)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func GetNonexistent(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
`)
|
||||
defer platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
`)
|
||||
|
||||
// test raw
|
||||
_, err := zfs.ZFSGetRawAnySource(fmt.Sprintf("%s/foo bar", ctx.RootDataset), []string{"name"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// test nonexistent filesystem
|
||||
nonexistent := fmt.Sprintf("%s/nonexistent filesystem", ctx.RootDataset)
|
||||
props, err := zfs.ZFSGetRawAnySource(nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
dsne, ok := err.(*zfs.DatasetDoesNotExist)
|
||||
if !ok {
|
||||
panic(err)
|
||||
} else if dsne.Path != nonexistent {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// test nonexistent snapshot
|
||||
nonexistent = fmt.Sprintf("%s/foo bar@non existent", ctx.RootDataset)
|
||||
props, err = zfs.ZFSGetRawAnySource(nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
dsne, ok = err.(*zfs.DatasetDoesNotExist)
|
||||
if !ok {
|
||||
panic(err)
|
||||
} else if dsne.Path != nonexistent {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
)
|
||||
|
||||
type Case func(*platformtest.Context)
|
||||
|
||||
func (c Case) String() string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(c).Pointer()).Name()
|
||||
}
|
||||
|
||||
var Cases = []Case{
|
||||
BatchDestroy,
|
||||
UndestroyableSnapshotParsing,
|
||||
GetNonexistent,
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func UndestroyableSnapshotParsing(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1 2 3"
|
||||
+ "foo bar@4 5 6"
|
||||
+ "foo bar@7 8 9"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@4 5 6"
|
||||
`)
|
||||
defer platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@4 5 6"
|
||||
DESTROYROOT
|
||||
`)
|
||||
|
||||
err := zfs.ZFSDestroy(fmt.Sprintf("%s/foo bar@1 2 3,4 5 6,7 8 9", t.RootDataset))
|
||||
if err == nil {
|
||||
panic("expecting destroy error due to hold")
|
||||
}
|
||||
if dse, ok := err.(*zfs.DestroySnapshotsError); !ok {
|
||||
panic(fmt.Sprintf("expecting *zfs.DestroySnapshotsError, got %T\n%v\n%s", err, err, err))
|
||||
} else {
|
||||
if dse.Filesystem != fmt.Sprintf("%s/foo bar", t.RootDataset) {
|
||||
panic(dse.Filesystem)
|
||||
}
|
||||
require.Equal(t, []string{"4 5 6"}, dse.Undestroyable)
|
||||
require.Equal(t, []string{"dataset is busy"}, dse.Reason)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Code generated by "enumer -type=errorClass"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package driver
|
||||
|
||||
import (
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
"github.com/zrepl/zrepl/util/bytecounter"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/util/semaphore"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
@@ -147,8 +148,12 @@ type Step struct {
|
||||
parent *Filesystem
|
||||
from, to *pdu.FilesystemVersion // compat
|
||||
|
||||
byteCounter bytecounter.StreamCopier
|
||||
expectedSize int64 // 0 means no size estimate present / possible
|
||||
|
||||
// byteCounter is nil initially, and set later in Step.doReplication
|
||||
// => concurrent read of that pointer from Step.ReportInfo must be protected
|
||||
byteCounter bytecounter.StreamCopier
|
||||
byteCounterMtx chainlock.L
|
||||
}
|
||||
|
||||
func (s *Step) TargetEquals(other driver.Step) bool {
|
||||
@@ -172,10 +177,15 @@ func (s *Step) Step(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *Step) ReportInfo() *report.StepInfo {
|
||||
|
||||
// get current byteCounter value
|
||||
var byteCounter int64
|
||||
s.byteCounterMtx.Lock()
|
||||
if s.byteCounter != nil {
|
||||
byteCounter = s.byteCounter.Count()
|
||||
}
|
||||
s.byteCounterMtx.Unlock()
|
||||
|
||||
// FIXME stick to zfs convention of from and to
|
||||
from := ""
|
||||
if s.from != nil {
|
||||
@@ -457,8 +467,12 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
defer sstreamCopier.Close()
|
||||
|
||||
// Install a byte counter to track progress + for status report
|
||||
s.byteCounter = bytecounter.NewStreamCopier(sstreamCopier)
|
||||
byteCountingStream := bytecounter.NewStreamCopier(sstreamCopier)
|
||||
s.byteCounterMtx.Lock()
|
||||
s.byteCounter = byteCountingStream
|
||||
s.byteCounterMtx.Unlock()
|
||||
defer func() {
|
||||
defer s.byteCounterMtx.Lock().Unlock()
|
||||
s.parent.promBytesReplicated.Add(float64(s.byteCounter.Count()))
|
||||
}()
|
||||
|
||||
@@ -467,7 +481,7 @@ func (s *Step) doReplication(ctx context.Context) error {
|
||||
ClearResumeToken: !sres.UsedResumeToken,
|
||||
}
|
||||
log.Debug("initiate receive request")
|
||||
_, err = s.receiver.Receive(ctx, rr, s.byteCounter)
|
||||
_, err = s.receiver.Receive(ctx, rr, byteCountingStream)
|
||||
if err != nil {
|
||||
log.
|
||||
WithError(err).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Code generated by "enumer -type NoFitBehavior"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package base2bufpool
|
||||
|
||||
import (
|
||||
|
||||
@@ -141,7 +141,7 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, z
|
||||
|
||||
func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, streamCopier zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
||||
|
||||
defer c.log.Info("ReqRecv returns")
|
||||
defer c.log.Debug("ReqRecv returns")
|
||||
conn, err := c.getWire(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -222,6 +223,10 @@ func (c *Conn) writeFrame(payload []byte, frameType uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) ResetWriteTimeout() error {
|
||||
return c.nc.RenewWriteDeadline()
|
||||
}
|
||||
|
||||
func (c *Conn) Shutdown(deadline time.Time) error {
|
||||
// TCP connection teardown is a bit wonky if we are in a situation
|
||||
// where there is still data in flight (DIF) to our side:
|
||||
@@ -291,11 +296,22 @@ func (c *Conn) Shutdown(deadline time.Time) error {
|
||||
|
||||
closeWire := func(step string) error {
|
||||
// TODO SetLinger(0) or similiar (we want RST frames here, not FINS)
|
||||
if closeErr := c.nc.Close(); closeErr != nil {
|
||||
prom.ShutdownCloseErrors.WithLabelValues("close").Inc()
|
||||
return closeErr
|
||||
closeErr := c.nc.Close()
|
||||
if closeErr == nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
// TODO go1.13: https://github.com/zrepl/zrepl/issues/190
|
||||
// https://github.com/golang/go/issues/8319
|
||||
// (use errors.Is(closeErr, syscall.ECONNRESET))
|
||||
if pe, ok := closeErr.(*net.OpError); ok && pe.Err == syscall.ECONNRESET {
|
||||
// connection reset by peer on FreeBSD, see https://github.com/zrepl/zrepl/issues/190
|
||||
// We know from kernel code reading that the FD behind c.nc is closed, so let's not consider this an error
|
||||
return nil
|
||||
}
|
||||
|
||||
prom.ShutdownCloseErrors.WithLabelValues("close").Inc()
|
||||
return closeErr
|
||||
}
|
||||
|
||||
hardclose := func(err error, step string) error {
|
||||
@@ -305,7 +321,7 @@ func (c *Conn) Shutdown(deadline time.Time) error {
|
||||
|
||||
c.shutdown.Begin()
|
||||
// new calls to c.ReadFrame and c.WriteFrame will now return ErrShutdown
|
||||
// Aquiring writeMtx and readMtx ensures that the last calls exit successfully
|
||||
// Aquiring writeMtx and readMtx afterwards ensures that already-running calls exit successfully
|
||||
|
||||
// disable renewing timeouts now, enforce the requested deadline instead
|
||||
// we need to do this before aquiring locks to enforce the timeout on slow
|
||||
|
||||
@@ -123,7 +123,17 @@ func (c *Conn) readFrameFiltered() (frameconn.Frame, error) {
|
||||
return frameconn.Frame{}, fmt.Errorf("unknown frame type %x", f.Header.Type)
|
||||
}
|
||||
// drop heartbeat frame
|
||||
debug("received heartbeat")
|
||||
debug("received heartbeat, resetting write timeout")
|
||||
// the peer's heartbeat proves to us that the peer is still live
|
||||
// => trust the peer at this point (DoS risks are ignored ATM)
|
||||
// => we assume that the connection is symmetric duplex, i.e., if receiving works for us,
|
||||
// sending works for us, too.
|
||||
// So, let's grant the peer another write timeout.
|
||||
err = c.fc.ResetWriteTimeout()
|
||||
debug("renew frameconn write timeout returned errT=%T err=%s", err, err)
|
||||
if err != nil {
|
||||
return frameconn.Frame{}, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// This integration test exercises the behavior of heartbeatconn
|
||||
// where the server is slow at handling the data received over the connection.
|
||||
// Note that the server is still sending heartbeats to the client, it's just the
|
||||
// data handling (usually I/O in case of zrepl endpoint.Receiver) that is slow.
|
||||
//
|
||||
// In commit 082335df5d85e1b0b9faa35ff182c71886142d3e and earlier, heartbeatconn would fail
|
||||
// this benchmark with a writev I/O timeout (here the ss(8) output at the time of failure)
|
||||
//
|
||||
// ESTAB 33369 0 127.0.0.1:12345 127.0.0.1:57282 users:(("heartbeatconn_i",pid=25953,fd=5))
|
||||
// cubic wscale:7,7 rto:203 rtt:2.992/5.849 ato:162 mss:32768 pmtu:65535 rcvmss:32741 advmss:65483 cwnd:10 bytes_sent:48 bytes_acked:48 bytes_received:195401 segs_out:44 segs_in:57 data_segs_out:6 data_segs_in:34 send 876.1Mbps lastsnd:125 lastrcv:9390 lastack:125 pacing_rate 1752.0Mbps delivery_rate 6393.8Mbps delivered:7 app_limited busy:42ms rcv_rtt:1 rcv_space:65483 rcv_ssthresh:65483 minrtt:0.029
|
||||
// --
|
||||
// ESTAB 0 3956805 127.0.0.1:57282 127.0.0.1:12345 users:(("heartbeatconn_i",pid=26100,fd=3))
|
||||
// cubic wscale:7,7 rto:211 backoff:5 rtt:10.38/16.937 ato:40 mss:32768 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:195401 bytes_acked:195402 bytes_received:48 segs_out:57 segs_in:45 data_segs_out:34 data_segs_in:6 send 252.5Mbps lastsnd:9390 lastrcv:125 lastack:125 pacing_rate 505.1Mbps delivery_rate 1971.0Mbps delivered:35 busy:30127ms rwnd_limited:30086ms(99.9%) rcv_space:65495 rcv_ssthresh:65495 notsent:3956805 minrtt:0.007
|
||||
// panic: writev tcp 127.0.0.1:57282->127.0.0.1:12345: i/o timeout
|
||||
//
|
||||
// The assumed reason for those writev timeouts is the following:
|
||||
// - Sporadic server stalls (sever data handling, usually I/O) cause TCP exponential backoff on the client for client->server
|
||||
// - Go runtime unblocks after the deadline expires, resultin gin writev I/O timeout
|
||||
// - That is, even though the client observed heartbeats from the server
|
||||
// -> TCP doesn't assume symmetric connection behavior, but our implementation does.
|
||||
//
|
||||
// The fix contained in the commit this message was committed with resets the deadline whenever
|
||||
// a heartbeat is received from the server.
|
||||
//
|
||||
//
|
||||
// How to run this integration test:
|
||||
//
|
||||
//
|
||||
// Terminal 1:
|
||||
// $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode server -addr 127.0.0.1:12345
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// ...
|
||||
//
|
||||
// Terminal 2:
|
||||
// $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode client -addr 127.0.0.1:12345
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>)
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>)
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// ...
|
||||
//
|
||||
// You should observe
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/util/devnoop"
|
||||
|
||||
"github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn"
|
||||
)
|
||||
|
||||
func orDie(err error) {
|
||||
if err != nil {
|
||||
grepfield := path.Base(os.Args[0])[:10]
|
||||
fmt.Fprintf(os.Stderr, "grepping for %s\n", grepfield)
|
||||
sh := fmt.Sprintf("ss -ntpi | grep -A1 %s", grepfield)
|
||||
cmd := exec.Command("bash", "-c", sh)
|
||||
o, _ := cmd.CombinedOutput()
|
||||
buf := bytes.NewBuffer(o)
|
||||
_, _ = io.Copy(os.Stderr, buf)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
var mode string
|
||||
var addr string
|
||||
|
||||
func main() {
|
||||
|
||||
flag.StringVar(&mode, "mode", "", "server|client")
|
||||
flag.StringVar(&addr, "addr", "INVALID", "")
|
||||
flag.Parse()
|
||||
|
||||
modemap := map[string]func(){
|
||||
"server": server,
|
||||
"client": client,
|
||||
}
|
||||
modemap[mode]()
|
||||
|
||||
}
|
||||
|
||||
func server() {
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
orDie(err)
|
||||
l := ln.(*net.TCPListener)
|
||||
for {
|
||||
c, err := l.AcceptTCP()
|
||||
if err != nil {
|
||||
log.Printf("accept err: %s", err)
|
||||
continue
|
||||
}
|
||||
hc := heartbeatconn.Wrap(c, 5*time.Second, 10*time.Second)
|
||||
|
||||
for {
|
||||
f, err := hc.ReadFrame()
|
||||
orDie(err)
|
||||
// _, err = buf.Write(f.Buffer.Bytes())
|
||||
// orDie(err)
|
||||
sleep := time.Duration(rand.NormFloat64()*500) * time.Millisecond
|
||||
time.Sleep(sleep)
|
||||
f.Buffer.Free()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func client() {
|
||||
c, err := net.Dial("tcp", addr)
|
||||
orDie(err)
|
||||
hc := heartbeatconn.Wrap(c.(*net.TCPConn), 5*time.Second, 10*time.Second)
|
||||
|
||||
// follow API requirements to always ReadFrame
|
||||
go func() {
|
||||
for {
|
||||
f, err := hc.ReadFrame()
|
||||
orDie(err)
|
||||
f.Buffer.Free()
|
||||
}
|
||||
}()
|
||||
|
||||
dn := devnoop.Get()
|
||||
var buf [1 << 10]byte
|
||||
for {
|
||||
n, err := dn.Read(buf[:])
|
||||
orDie(err)
|
||||
err = hc.WriteFrame(buf[:n], 23)
|
||||
orDie(err)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
@@ -81,8 +82,10 @@ func doWriteStream(ctx context.Context, c *heartbeatconn.Conn, stream io.Reader,
|
||||
}
|
||||
|
||||
reads := make(chan read, 5)
|
||||
var stopReading uint32
|
||||
go func() {
|
||||
for {
|
||||
defer close(reads)
|
||||
for atomic.LoadUint32(&stopReading) == 0 {
|
||||
buffer := bufpool.Get(1 << FramePayloadShift)
|
||||
bufferBytes := buffer.Bytes()
|
||||
n, err := io.ReadFull(stream, bufferBytes)
|
||||
@@ -97,12 +100,21 @@ func doWriteStream(ctx context.Context, c *heartbeatconn.Conn, stream io.Reader,
|
||||
}
|
||||
if err != nil {
|
||||
reads <- read{err: err} // RULE1
|
||||
close(reads)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
// stop reading
|
||||
atomic.StoreUint32(&stopReading, 1)
|
||||
// drain in-flight reads
|
||||
for read := range reads {
|
||||
debug("doWriteStream: drain read channel")
|
||||
read.buf.Free()
|
||||
}
|
||||
}()
|
||||
|
||||
for read := range reads {
|
||||
if read.err == nil {
|
||||
// RULE 1: read.buf is valid
|
||||
@@ -201,10 +213,17 @@ type readFrameResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func readFrames(reads chan<- readFrameResult, c *heartbeatconn.Conn) {
|
||||
for {
|
||||
// readFrames reads from c into reads
|
||||
// if a read from c encounters an error, noMoreReads is closed before sending the result into reads
|
||||
func readFrames(reads chan<- readFrameResult, noMoreReads chan<- struct{}, c *heartbeatconn.Conn) {
|
||||
// noMoreReads is already closed, don't re-close it
|
||||
defer close(reads)
|
||||
for { // only exits after a read error, make sure noMoreReads is closed
|
||||
var r readFrameResult
|
||||
r.f, r.err = c.ReadFrame()
|
||||
if r.err != nil && noMoreReads != nil {
|
||||
close(noMoreReads)
|
||||
}
|
||||
reads <- r
|
||||
if r.err != nil {
|
||||
return
|
||||
|
||||
@@ -5,9 +5,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn"
|
||||
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
@@ -21,6 +25,8 @@ type Conn struct {
|
||||
// filled by per-conn readFrames goroutine
|
||||
frameReads chan readFrameResult
|
||||
|
||||
closeState closeState
|
||||
|
||||
// readMtx serializes read stream operations because we inherently only
|
||||
// support a single stream at a time over hc.
|
||||
readMtx sync.Mutex
|
||||
@@ -63,15 +69,29 @@ func isConnCleanAfterWrite(err error) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
var ErrReadFramesStopped = fmt.Errorf("stream: reading frames stopped")
|
||||
|
||||
func (c *Conn) readFrames() {
|
||||
defer close(c.waitReadFramesDone)
|
||||
defer close(c.frameReads)
|
||||
readFrames(c.frameReads, c.hc)
|
||||
readFrames(c.frameReads, c.waitReadFramesDone, c.hc)
|
||||
}
|
||||
|
||||
func (c *Conn) ReadStreamedMessage(ctx context.Context, maxSize uint32, frameType uint32) ([]byte, *ReadStreamError) {
|
||||
func (c *Conn) ReadStreamedMessage(ctx context.Context, maxSize uint32, frameType uint32) (_ []byte, err *ReadStreamError) {
|
||||
|
||||
// if we are closed while reading, return that as an error
|
||||
if closeGuard, cse := c.closeState.RWEntry(); cse != nil {
|
||||
return nil, &ReadStreamError{
|
||||
Kind: ReadStreamErrorKindConn,
|
||||
Err: cse,
|
||||
}
|
||||
} else {
|
||||
defer func(err **ReadStreamError) {
|
||||
if closed := closeGuard.RWExit(); closed != nil {
|
||||
*err = &ReadStreamError{
|
||||
Kind: ReadStreamErrorKindConn,
|
||||
Err: closed,
|
||||
}
|
||||
}
|
||||
}(&err)
|
||||
}
|
||||
|
||||
c.readMtx.Lock()
|
||||
defer c.readMtx.Unlock()
|
||||
if !c.readClean {
|
||||
@@ -92,7 +112,7 @@ func (c *Conn) ReadStreamedMessage(ctx context.Context, maxSize uint32, frameTyp
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
err := readStream(c.frameReads, c.hc, w, frameType)
|
||||
err = readStream(c.frameReads, c.hc, w, frameType)
|
||||
c.readClean = isConnCleanAfterRead(err)
|
||||
_ = w.CloseWithError(readMessageSentinel) // always returns nil
|
||||
wg.Wait()
|
||||
@@ -104,23 +124,47 @@ func (c *Conn) ReadStreamedMessage(ctx context.Context, maxSize uint32, frameTyp
|
||||
}
|
||||
|
||||
// WriteStreamTo reads a stream from Conn and writes it to w.
|
||||
func (c *Conn) ReadStreamInto(w io.Writer, frameType uint32) zfs.StreamCopierError {
|
||||
func (c *Conn) ReadStreamInto(w io.Writer, frameType uint32) (err zfs.StreamCopierError) {
|
||||
|
||||
// if we are closed while writing, return that as an error
|
||||
if closeGuard, cse := c.closeState.RWEntry(); cse != nil {
|
||||
return cse
|
||||
} else {
|
||||
defer func(err *zfs.StreamCopierError) {
|
||||
if closed := closeGuard.RWExit(); closed != nil {
|
||||
*err = closed
|
||||
}
|
||||
}(&err)
|
||||
}
|
||||
|
||||
c.readMtx.Lock()
|
||||
defer c.readMtx.Unlock()
|
||||
if !c.readClean {
|
||||
return writeStreamToErrorUnknownState{}
|
||||
}
|
||||
var err *ReadStreamError = readStream(c.frameReads, c.hc, w, frameType)
|
||||
c.readClean = isConnCleanAfterRead(err)
|
||||
var rse *ReadStreamError = readStream(c.frameReads, c.hc, w, frameType)
|
||||
c.readClean = isConnCleanAfterRead(rse)
|
||||
|
||||
// https://golang.org/doc/faq#nil_error
|
||||
if err == nil {
|
||||
if rse == nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
return rse
|
||||
}
|
||||
|
||||
func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameType uint32) error {
|
||||
func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameType uint32) (err error) {
|
||||
|
||||
// if we are closed while writing, return that as an error
|
||||
if closeGuard, cse := c.closeState.RWEntry(); cse != nil {
|
||||
return cse
|
||||
} else {
|
||||
defer func(err *error) {
|
||||
if closed := closeGuard.RWExit(); closed != nil {
|
||||
*err = closed
|
||||
}
|
||||
}(&err)
|
||||
}
|
||||
|
||||
c.writeMtx.Lock()
|
||||
defer c.writeMtx.Unlock()
|
||||
if !c.writeClean {
|
||||
@@ -134,7 +178,19 @@ func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameTyp
|
||||
return errConn
|
||||
}
|
||||
|
||||
func (c *Conn) SendStream(ctx context.Context, src zfs.StreamCopier, frameType uint32) error {
|
||||
func (c *Conn) SendStream(ctx context.Context, src zfs.StreamCopier, frameType uint32) (err error) {
|
||||
|
||||
// if we are closed while reading, return that as an error
|
||||
if closeGuard, cse := c.closeState.RWEntry(); cse != nil {
|
||||
return cse
|
||||
} else {
|
||||
defer func(err *error) {
|
||||
if closed := closeGuard.RWExit(); closed != nil {
|
||||
*err = closed
|
||||
}
|
||||
}(&err)
|
||||
}
|
||||
|
||||
c.writeMtx.Lock()
|
||||
defer c.writeMtx.Unlock()
|
||||
if !c.writeClean {
|
||||
@@ -186,8 +242,75 @@ func (c *Conn) SendStream(ctx context.Context, src zfs.StreamCopier, frameType u
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
err := c.hc.Shutdown()
|
||||
<-c.waitReadFramesDone
|
||||
return err
|
||||
type closeState struct {
|
||||
closeCount uint32
|
||||
}
|
||||
|
||||
type closeStateErrConnectionClosed struct{}
|
||||
|
||||
var _ zfs.StreamCopierError = (*closeStateErrConnectionClosed)(nil)
|
||||
var _ error = (*closeStateErrConnectionClosed)(nil)
|
||||
var _ net.Error = (*closeStateErrConnectionClosed)(nil)
|
||||
|
||||
func (e *closeStateErrConnectionClosed) Error() string {
|
||||
return "connection closed"
|
||||
}
|
||||
func (e *closeStateErrConnectionClosed) IsReadError() bool { return true }
|
||||
func (e *closeStateErrConnectionClosed) IsWriteError() bool { return true }
|
||||
func (e *closeStateErrConnectionClosed) Timeout() bool { return false }
|
||||
func (e *closeStateErrConnectionClosed) Temporary() bool { return false }
|
||||
|
||||
func (s *closeState) CloseEntry() error {
|
||||
firstCloser := atomic.AddUint32(&s.closeCount, 1) == 1
|
||||
if !firstCloser {
|
||||
return errors.New("duplicate close")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type closeStateEntry struct {
|
||||
s *closeState
|
||||
entryCount uint32
|
||||
}
|
||||
|
||||
func (s *closeState) RWEntry() (e *closeStateEntry, err zfs.StreamCopierError) {
|
||||
entry := &closeStateEntry{s, atomic.LoadUint32(&s.closeCount)}
|
||||
if entry.entryCount > 0 {
|
||||
return nil, &closeStateErrConnectionClosed{}
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (e *closeStateEntry) RWExit() zfs.StreamCopierError {
|
||||
if atomic.LoadUint32(&e.entryCount) == e.entryCount {
|
||||
// no calls to Close() while running rw operation
|
||||
return nil
|
||||
}
|
||||
return &closeStateErrConnectionClosed{}
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if err := c.closeState.CloseEntry(); err != nil {
|
||||
return errors.Wrap(err, "stream conn close")
|
||||
}
|
||||
|
||||
// Shutdown c.hc, which will cause c.readFrames to close c.waitReadFramesDone
|
||||
err := c.hc.Shutdown()
|
||||
// However, c.readFrames may be blocking on a filled c.frameReads
|
||||
// and since the connection is closed, nobody is going to read from it
|
||||
for read := range c.frameReads {
|
||||
debug("Conn.Close() draining queued read")
|
||||
read.f.Buffer.Free()
|
||||
}
|
||||
// if we can't close, don't expect c.readFrames to terminate
|
||||
// this might leak c.readFrames, but we can't do something useful at this point
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// shutdown didn't report errors, so c.readFrames will exit due to read error
|
||||
// This behavior is the contract we have with c.hc.
|
||||
// If that contract is broken, this read will block indefinitely and
|
||||
// cause an easily diagnosable goroutine leak (of this goroutine)
|
||||
<-c.waitReadFramesDone
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestStreamer(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
readFrames(ch, b)
|
||||
readFrames(ch, nil, b)
|
||||
}()
|
||||
err := readStream(ch, b, &buf, stype)
|
||||
log.WithField("errType", fmt.Sprintf("%T %v", err, err)).Debug("ReadStream returned")
|
||||
@@ -113,7 +113,7 @@ func TestMultiFrameStreamErrTraileror(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
readFrames(ch, b)
|
||||
readFrames(ch, nil, b)
|
||||
}()
|
||||
err := readStream(ch, b, &buf, stype)
|
||||
t.Logf("%s", err)
|
||||
|
||||
@@ -80,7 +80,7 @@ func (c *Conn) renewReadDeadline() error {
|
||||
return c.SetReadDeadline(time.Now().Add(c.idleTimeout))
|
||||
}
|
||||
|
||||
func (c *Conn) renewWriteDeadline() error {
|
||||
func (c *Conn) RenewWriteDeadline() error {
|
||||
if atomic.LoadInt32(&c.renewDeadlinesDisabled) != 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -107,7 +107,7 @@ restart:
|
||||
func (c Conn) Write(p []byte) (n int, err error) {
|
||||
n = 0
|
||||
restart:
|
||||
if err := c.renewWriteDeadline(); err != nil {
|
||||
if err := c.RenewWriteDeadline(); err != nil {
|
||||
return n, err
|
||||
}
|
||||
var nCurWrite int
|
||||
@@ -127,7 +127,7 @@ restart:
|
||||
func (c Conn) WritevFull(bufs net.Buffers) (n int64, err error) {
|
||||
n = 0
|
||||
restart:
|
||||
if err := c.renewWriteDeadline(); err != nil {
|
||||
if err := c.RenewWriteDeadline(); err != nil {
|
||||
return n, err
|
||||
}
|
||||
var nCurWrite int64
|
||||
@@ -162,10 +162,14 @@ func buildIovecs(buffers net.Buffers) (totalLen int64, vecs []syscall.Iovec) {
|
||||
if len(buffers[i]) == 0 {
|
||||
continue
|
||||
}
|
||||
vecs = append(vecs, syscall.Iovec{
|
||||
|
||||
v := syscall.Iovec{
|
||||
Base: &buffers[i][0],
|
||||
Len: uint64(len(buffers[i])),
|
||||
})
|
||||
}
|
||||
// syscall.Iovec.Len has platform-dependent size, thus use SetLen
|
||||
v.SetLen(len(buffers[i]))
|
||||
|
||||
vecs = append(vecs, v)
|
||||
}
|
||||
return totalLen, vecs
|
||||
}
|
||||
@@ -244,6 +248,14 @@ func (c Conn) readv(rawConn syscall.RawConn, iovecs []syscall.Iovec) (n int64, e
|
||||
func (c Conn) doOneReadv(rawConn syscall.RawConn, iovecs *[]syscall.Iovec) (n int64, err error) {
|
||||
rawReadErr := rawConn.Read(func(fd uintptr) (done bool) {
|
||||
// iovecs, n and err must not be shadowed!
|
||||
|
||||
// NOTE: unsafe.Pointer safety rules
|
||||
// https://tip.golang.org/pkg/unsafe/#Pointer
|
||||
//
|
||||
// (4) Conversion of a Pointer to a uintptr when calling syscall.Syscall.
|
||||
// ...
|
||||
// uintptr() conversions must appear within the syscall.Syscall argument list.
|
||||
// (even though we are not the escape analysis Likely not )
|
||||
thisReadN, _, errno := syscall.Syscall(
|
||||
syscall.SYS_READV,
|
||||
fd,
|
||||
@@ -260,16 +272,32 @@ func (c Conn) doOneReadv(rawConn syscall.RawConn, iovecs *[]syscall.Iovec) (n in
|
||||
if int(thisReadN) < 0 {
|
||||
panic("unexpected return value")
|
||||
}
|
||||
n += int64(thisReadN)
|
||||
n += int64(thisReadN) // TODO check overflow
|
||||
|
||||
// shift iovecs forward
|
||||
for left := int64(thisReadN); left > 0; {
|
||||
curVecNewLength := int64((*iovecs)[0].Len) - left // TODO assert conversion
|
||||
if curVecNewLength <= 0 {
|
||||
left -= int64((*iovecs)[0].Len)
|
||||
for left := int(thisReadN); left > 0; {
|
||||
// conversion to uint does not change value, see TestIovecLenFieldIsMachineUint, and left > 0
|
||||
thisIovecConsumedCompletely := uint((*iovecs)[0].Len) <= uint(left)
|
||||
if thisIovecConsumedCompletely {
|
||||
// Update left, cannot go below 0 due to
|
||||
// a) definition of thisIovecConsumedCompletely
|
||||
// b) left > 0 due to loop invariant
|
||||
// Convertion .Len to int64 is thus also safe now, because it is < left < INT_MAX
|
||||
left -= int((*iovecs)[0].Len)
|
||||
*iovecs = (*iovecs)[1:]
|
||||
} else {
|
||||
// trim this iovec to remaining length
|
||||
|
||||
// NOTE: unsafe.Pointer safety rules
|
||||
// https://tip.golang.org/pkg/unsafe/#Pointer
|
||||
// (3) Conversion of a Pointer to a uintptr and back, with arithmetic.
|
||||
// ...
|
||||
// Note that both conversions must appear in the same expression,
|
||||
// with only the intervening arithmetic between them:
|
||||
(*iovecs)[0].Base = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer((*iovecs)[0].Base)) + uintptr(left)))
|
||||
(*iovecs)[0].Len = uint64(curVecNewLength)
|
||||
curVecNewLength := uint((*iovecs)[0].Len) - uint(left) // casts to uint do not change value
|
||||
(*iovecs)[0].SetLen(int(curVecNewLength)) // int and uint have the same size, no change of value
|
||||
|
||||
break // inner
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -176,3 +178,14 @@ func TestNoPartialWritesDueToDeadline(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
assert.True(t, netErr.Timeout())
|
||||
}
|
||||
|
||||
func TestIovecLenFieldIsMachineUint(t *testing.T) {
|
||||
iov := syscall.Iovec{}
|
||||
_ = iov // make linter happy (unsafe.Sizeof not recognized as usage)
|
||||
size_t := unsafe.Sizeof(iov.Len)
|
||||
if size_t != unsafe.Sizeof(uint(23)) {
|
||||
t.Fatalf("expecting (struct iov)->Len to be sizeof(uint)")
|
||||
}
|
||||
// ssize_t is defined to be the signed version of size_t,
|
||||
// so we know sizeof(ssize_t) == sizeof(int)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// +build tools
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "golang.org/x/tools/cmd/stringer"
|
||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
||||
_ "github.com/alvaroloes/enumer"
|
||||
_ "golang.org/x/tools/cmd/goimports"
|
||||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||
)
|
||||
@@ -3,6 +3,7 @@ package local
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/transport"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
type LocalConnecter struct {
|
||||
listenerName string
|
||||
clientIdentity string
|
||||
dialTimeout time.Duration
|
||||
}
|
||||
|
||||
func LocalConnecterFromConfig(in *config.LocalConnect) (*LocalConnecter, error) {
|
||||
@@ -20,10 +22,27 @@ func LocalConnecterFromConfig(in *config.LocalConnect) (*LocalConnecter, error)
|
||||
if in.ListenerName == "" {
|
||||
return nil, fmt.Errorf("ListenerName must not be empty")
|
||||
}
|
||||
return &LocalConnecter{listenerName: in.ListenerName, clientIdentity: in.ClientIdentity}, nil
|
||||
if in.DialTimeout < 0 {
|
||||
return nil, fmt.Errorf("DialTimeout must be zero or positive")
|
||||
}
|
||||
cn := &LocalConnecter{
|
||||
listenerName: in.ListenerName,
|
||||
clientIdentity: in.ClientIdentity,
|
||||
dialTimeout: in.DialTimeout,
|
||||
}
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func (c *LocalConnecter) Connect(dialCtx context.Context) (transport.Wire, error) {
|
||||
l := GetLocalListener(c.listenerName)
|
||||
return l.Connect(dialCtx, c.clientIdentity)
|
||||
if c.dialTimeout > 0 {
|
||||
ctx, cancel := context.WithTimeout(dialCtx, c.dialTimeout)
|
||||
defer cancel()
|
||||
dialCtx = ctx // shadow
|
||||
}
|
||||
w, err := l.Connect(dialCtx, c.clientIdentity)
|
||||
if err == context.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("local listener %q not reachable", c.listenerName)
|
||||
}
|
||||
return w, err
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ func (l *LocalListener) Connect(dialCtx context.Context, clientIdentity string)
|
||||
// place request
|
||||
req := connectRequest{
|
||||
clientIdentity: clientIdentity,
|
||||
callback: make(chan connectResult),
|
||||
// ensure non-blocking send in Accept, we don't necessarily read from callback before
|
||||
// Accept writes to it
|
||||
callback: make(chan connectResult, 1),
|
||||
}
|
||||
select {
|
||||
case l.connects <- req:
|
||||
@@ -93,19 +95,35 @@ func (l *LocalListener) Addr() net.Addr { return localAddr{"<listening>"} }
|
||||
|
||||
func (l *LocalListener) Accept(ctx context.Context) (*transport.AuthConn, error) {
|
||||
respondToRequest := func(req connectRequest, res connectResult) (err error) {
|
||||
|
||||
transport.GetLogger(ctx).
|
||||
WithField("res.conn", res.conn).WithField("res.err", res.err).
|
||||
Debug("responding to client request")
|
||||
|
||||
// contract bewteen Connect and Accept is that Connect sends a req.callback
|
||||
// into which we can send one result non-blockingly.
|
||||
// We want to panic if that contract is violated (impl error)
|
||||
//
|
||||
// However, Connect also supports timeouts through context cancellation:
|
||||
// Connect closes the channel into which we might send, which will panic.
|
||||
//
|
||||
// ==> distinguish those cases in defer
|
||||
const clientCallbackBlocked = "client-provided callback did block on send"
|
||||
defer func() {
|
||||
errv := recover()
|
||||
transport.GetLogger(ctx).WithField("recover_err", errv).
|
||||
Debug("panic on send to client callback, likely a legitimate client-side timeout")
|
||||
if errv == clientCallbackBlocked {
|
||||
// this would be a violation of contract betwee Connect and Accept, see above
|
||||
panic(clientCallbackBlocked)
|
||||
} else {
|
||||
transport.GetLogger(ctx).WithField("recover_err", errv).
|
||||
Debug("panic on send to client callback, likely a legitimate client-side timeout")
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case req.callback <- res:
|
||||
err = nil
|
||||
default:
|
||||
err = fmt.Errorf("client-provided callback did block on send")
|
||||
panic(clientCallbackBlocked)
|
||||
}
|
||||
close(req.callback)
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package circlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const CIRCULARLOG_INIT_SIZE int = 32 << 10
|
||||
|
||||
type CircularLog struct {
|
||||
buf []byte
|
||||
size int
|
||||
max int
|
||||
written int
|
||||
writeCursor int
|
||||
/*
|
||||
Mutex prevents:
|
||||
concurrent writes:
|
||||
buf, size, written, writeCursor in Write([]byte)
|
||||
buf, writeCursor in Reset()
|
||||
data races vs concurrent Write([]byte) calls:
|
||||
size in Size()
|
||||
size, writeCursor in Len()
|
||||
buf, size, written, writeCursor in Bytes()
|
||||
*/
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewCircularLog(max int) (*CircularLog, error) {
|
||||
if max <= 0 {
|
||||
return nil, fmt.Errorf("max must be positive")
|
||||
}
|
||||
|
||||
return &CircularLog{
|
||||
size: CIRCULARLOG_INIT_SIZE,
|
||||
buf: make([]byte, CIRCULARLOG_INIT_SIZE),
|
||||
max: max,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func nextPow2Int(n int) int {
|
||||
if n < 1 {
|
||||
panic("can only round up positive integers")
|
||||
}
|
||||
r := uint(n)
|
||||
// Credit: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
|
||||
r--
|
||||
// Assume at least a 32 bit integer
|
||||
r |= r >> 1
|
||||
r |= r >> 2
|
||||
r |= r >> 4
|
||||
r |= r >> 8
|
||||
r |= r >> 16
|
||||
if bits.UintSize == 64 {
|
||||
r |= r >> 32
|
||||
}
|
||||
r++
|
||||
// Can't exceed max positive int value
|
||||
if r > ^uint(0)>>1 {
|
||||
panic("rounded to larger than int()")
|
||||
}
|
||||
return int(r)
|
||||
}
|
||||
|
||||
func (cl *CircularLog) Write(data []byte) (int, error) {
|
||||
cl.mtx.Lock()
|
||||
defer cl.mtx.Unlock()
|
||||
n := len(data)
|
||||
|
||||
// Keep growing the buffer by doubling until
|
||||
// hitting cl.max
|
||||
bufAvail := cl.size - cl.writeCursor
|
||||
// If cl.writeCursor wrapped on the last write,
|
||||
// then the buffer is full, not empty.
|
||||
if cl.writeCursor == 0 && cl.written > 0 {
|
||||
bufAvail = 0
|
||||
}
|
||||
if n > bufAvail && cl.size < cl.max {
|
||||
// Add to size, not writeCursor, so as
|
||||
// to not resize multiple times if this
|
||||
// Write() immediately fills up the buffer
|
||||
newSize := nextPow2Int(cl.size + n)
|
||||
if newSize > cl.max {
|
||||
newSize = cl.max
|
||||
}
|
||||
newBuf := make([]byte, newSize)
|
||||
// Reset write cursor to old size if wrapped
|
||||
if cl.writeCursor == 0 && cl.written > 0 {
|
||||
cl.writeCursor = cl.size
|
||||
}
|
||||
copy(newBuf, cl.buf[:cl.writeCursor])
|
||||
cl.buf = newBuf
|
||||
cl.size = newSize
|
||||
}
|
||||
|
||||
// If data to be written is larger than the max size,
|
||||
// discard all but the last cl.size bytes
|
||||
if n > cl.size {
|
||||
data = data[n-cl.size:]
|
||||
// Overwrite the beginning of data
|
||||
// with a string indicating truncation
|
||||
copy(data, []byte("(...)"))
|
||||
}
|
||||
// First copy data to the end of buf. If that wasn't
|
||||
// all of data, then copy the rest to the beginning
|
||||
// of buf.
|
||||
copied := copy(cl.buf[cl.writeCursor:], data)
|
||||
if copied < n {
|
||||
copied += copy(cl.buf, data[copied:])
|
||||
}
|
||||
|
||||
cl.writeCursor = ((cl.writeCursor + copied) % cl.size)
|
||||
cl.written += copied
|
||||
return copied, nil
|
||||
}
|
||||
|
||||
func (cl *CircularLog) Size() int {
|
||||
cl.mtx.Lock()
|
||||
defer cl.mtx.Unlock()
|
||||
return cl.size
|
||||
}
|
||||
|
||||
func (cl *CircularLog) Len() int {
|
||||
cl.mtx.Lock()
|
||||
defer cl.mtx.Unlock()
|
||||
if cl.written >= cl.size {
|
||||
return cl.size
|
||||
} else {
|
||||
return cl.writeCursor
|
||||
}
|
||||
}
|
||||
|
||||
func (cl *CircularLog) TotalWritten() int {
|
||||
cl.mtx.Lock()
|
||||
defer cl.mtx.Unlock()
|
||||
|
||||
return cl.written
|
||||
}
|
||||
|
||||
func (cl *CircularLog) Reset() {
|
||||
cl.mtx.Lock()
|
||||
defer cl.mtx.Unlock()
|
||||
cl.writeCursor = 0
|
||||
cl.buf = make([]byte, CIRCULARLOG_INIT_SIZE)
|
||||
cl.size = CIRCULARLOG_INIT_SIZE
|
||||
}
|
||||
|
||||
func (cl *CircularLog) Bytes() []byte {
|
||||
cl.mtx.Lock()
|
||||
defer cl.mtx.Unlock()
|
||||
switch {
|
||||
case cl.written >= cl.size && cl.writeCursor == 0:
|
||||
return cl.buf
|
||||
case cl.written > cl.size:
|
||||
ret := make([]byte, cl.size)
|
||||
copy(ret, cl.buf[cl.writeCursor:])
|
||||
copy(ret[cl.size-cl.writeCursor:], cl.buf[:cl.writeCursor])
|
||||
return ret
|
||||
default:
|
||||
return cl.buf[:cl.writeCursor]
|
||||
}
|
||||
}
|
||||
|
||||
func (cl *CircularLog) String() string {
|
||||
return string(cl.Bytes())
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package circlog_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/util/circlog"
|
||||
)
|
||||
|
||||
func TestCircularLog(t *testing.T) {
|
||||
var maxCircularLogSize int = 1 << 20
|
||||
|
||||
writeFixedSize := func(w io.Writer, size int) (int, error) {
|
||||
pattern := []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
||||
writeBytes := bytes.Repeat(pattern, size/4)
|
||||
if len(writeBytes) < size {
|
||||
writeBytes = append(writeBytes, pattern[:size-len(writeBytes)]...)
|
||||
}
|
||||
n, err := w.Write(writeBytes)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
t.Run("negative-size-error", func(t *testing.T) {
|
||||
_, err := circlog.NewCircularLog(-1)
|
||||
require.EqualError(t, err, "max must be positive")
|
||||
})
|
||||
|
||||
t.Run("no-resize", func(t *testing.T) {
|
||||
log, err := circlog.NewCircularLog(maxCircularLogSize)
|
||||
require.NoError(t, err)
|
||||
|
||||
initSize := log.Size()
|
||||
writeSize := circlog.CIRCULARLOG_INIT_SIZE - 1
|
||||
_, err = writeFixedSize(log, writeSize)
|
||||
require.NoError(t, err)
|
||||
finalSize := log.Size()
|
||||
require.Equal(t, initSize, finalSize)
|
||||
require.Equal(t, writeSize, log.Len())
|
||||
})
|
||||
t.Run("one-resize", func(t *testing.T) {
|
||||
log, err := circlog.NewCircularLog(maxCircularLogSize)
|
||||
require.NoError(t, err)
|
||||
|
||||
initSize := log.Size()
|
||||
writeSize := circlog.CIRCULARLOG_INIT_SIZE + 1
|
||||
_, err = writeFixedSize(log, writeSize)
|
||||
require.NoError(t, err)
|
||||
finalSize := log.Size()
|
||||
require.Greater(t, finalSize, initSize)
|
||||
require.LessOrEqual(t, finalSize, maxCircularLogSize)
|
||||
require.Equal(t, writeSize, log.Len())
|
||||
})
|
||||
t.Run("reset", func(t *testing.T) {
|
||||
log, err := circlog.NewCircularLog(maxCircularLogSize)
|
||||
require.NoError(t, err)
|
||||
|
||||
initSize := log.Size()
|
||||
_, err = writeFixedSize(log, maxCircularLogSize)
|
||||
require.NoError(t, err)
|
||||
log.Reset()
|
||||
finalSize := log.Size()
|
||||
require.Equal(t, initSize, finalSize)
|
||||
})
|
||||
t.Run("wrap-exactly-maximum", func(t *testing.T) {
|
||||
log, err := circlog.NewCircularLog(maxCircularLogSize)
|
||||
require.NoError(t, err)
|
||||
|
||||
initSize := log.Size()
|
||||
writeSize := maxCircularLogSize / 4
|
||||
for written := 0; written < maxCircularLogSize; {
|
||||
n, err := writeFixedSize(log, writeSize)
|
||||
written += n
|
||||
require.NoError(t, err)
|
||||
}
|
||||
finalSize := log.Size()
|
||||
require.Greater(t, finalSize, initSize)
|
||||
require.Equal(t, finalSize, maxCircularLogSize)
|
||||
require.Equal(t, finalSize, log.Len())
|
||||
})
|
||||
t.Run("wrap-partial", func(t *testing.T) {
|
||||
log, err := circlog.NewCircularLog(maxCircularLogSize)
|
||||
require.NoError(t, err)
|
||||
|
||||
initSize := log.Size()
|
||||
|
||||
startSentinel := []byte{0x00, 0x00, 0x00, 0x00}
|
||||
_, err = log.Write(startSentinel)
|
||||
require.NoError(t, err)
|
||||
|
||||
nWritesToFill := 4
|
||||
writeSize := maxCircularLogSize / nWritesToFill
|
||||
for i := 0; i < (nWritesToFill + 1); i++ {
|
||||
_, err := writeFixedSize(log, writeSize)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
endSentinel := []byte{0xFF, 0xFF, 0xFF, 0xFF}
|
||||
_, err = log.Write(endSentinel)
|
||||
require.NoError(t, err)
|
||||
|
||||
finalSize := log.Size()
|
||||
require.Greater(t, finalSize, initSize)
|
||||
require.Greater(t, log.TotalWritten(), finalSize)
|
||||
require.Equal(t, finalSize, maxCircularLogSize)
|
||||
require.Equal(t, finalSize, log.Len())
|
||||
|
||||
logBytes := log.Bytes()
|
||||
|
||||
require.Equal(t, endSentinel, logBytes[len(logBytes)-len(endSentinel):])
|
||||
require.NotEqual(t, startSentinel, logBytes[:len(startSentinel)])
|
||||
})
|
||||
t.Run("overflow-write", func(t *testing.T) {
|
||||
log, err := circlog.NewCircularLog(maxCircularLogSize)
|
||||
require.NoError(t, err)
|
||||
|
||||
initSize := log.Size()
|
||||
writeSize := maxCircularLogSize + 1
|
||||
n, err := writeFixedSize(log, writeSize)
|
||||
require.NoError(t, err)
|
||||
finalSize := log.Size()
|
||||
require.Less(t, n, writeSize)
|
||||
require.Greater(t, finalSize, initSize)
|
||||
require.Equal(t, finalSize, maxCircularLogSize)
|
||||
logBytes := log.Bytes()
|
||||
require.Equal(t, []byte("(...)"), logBytes[:5])
|
||||
})
|
||||
t.Run("stringify", func(t *testing.T) {
|
||||
log, err := circlog.NewCircularLog(maxCircularLogSize)
|
||||
require.NoError(t, err)
|
||||
|
||||
writtenString := "A harmful truth is better than a useful lie."
|
||||
n, err := log.Write([]byte(writtenString))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(writtenString), n)
|
||||
loggedString := log.String()
|
||||
require.Equal(t, writtenString, loggedString)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package circlog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNextPow2Int(t *testing.T) {
|
||||
require.Equal(t, 512, nextPow2Int(512), "a power of 2 should round to itself")
|
||||
require.Equal(t, 1024, nextPow2Int(513), "should round up to the next power of 2")
|
||||
require.PanicsWithValue(t, "can only round up positive integers", func() { nextPow2Int(0) }, "unimplemented: zero is not positive; corner case")
|
||||
require.PanicsWithValue(t, "can only round up positive integers", func() { nextPow2Int(-1) }, "unimplemented: cannot round up negative numbers")
|
||||
maxInt := int((^uint(0)) >> 1)
|
||||
require.PanicsWithValue(t, "rounded to larger than int()", func() { nextPow2Int(maxInt - 1) }, "cannot round to a number bigger than the int type")
|
||||
}
|
||||
@@ -25,6 +25,23 @@ func Duration(varname string, def time.Duration) time.Duration {
|
||||
return d
|
||||
}
|
||||
|
||||
func Int(varname string, def int) int {
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v.(int)
|
||||
}
|
||||
e := os.Getenv(varname)
|
||||
if e == "" {
|
||||
return def
|
||||
}
|
||||
d64, err := strconv.ParseInt(e, 10, strconv.IntSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
d := int(d64)
|
||||
cache.Store(varname, d)
|
||||
return d
|
||||
}
|
||||
|
||||
func Int64(varname string, def int64) int64 {
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v.(int64)
|
||||
@@ -56,3 +73,15 @@ func Bool(varname string, def bool) bool {
|
||||
cache.Store(varname, d)
|
||||
return d
|
||||
}
|
||||
|
||||
func String(varname string, def string) string {
|
||||
if v, ok := cache.Load(varname); ok {
|
||||
return v.(string)
|
||||
}
|
||||
e := os.Getenv(varname)
|
||||
if e == "" {
|
||||
return def
|
||||
}
|
||||
cache.Store(varname, e)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ func ZFSGetFilesystemPlaceholderState(p *DatasetPath) (state *FilesystemPlacehol
|
||||
}
|
||||
|
||||
func ZFSCreatePlaceholderFilesystem(p *DatasetPath) (err error) {
|
||||
if p.Length() == 1 {
|
||||
return fmt.Errorf("cannot create %q: pools cannot be created with zfs create", p.ToString())
|
||||
}
|
||||
cmd := exec.Command(ZFS_BINARY, "create",
|
||||
"-o", fmt.Sprintf("%s=%s", PlaceholderPropertyName, placeholderPropertyOn),
|
||||
"-o", "mountpoint=none",
|
||||
|
||||
@@ -154,15 +154,3 @@ func ZFSListFilesystemVersions(fs *DatasetPath, filter FilesystemVersionFilter)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ZFSDestroyFilesystemVersion(filesystem *DatasetPath, version *FilesystemVersion) (err error) {
|
||||
|
||||
datasetPath := version.ToAbsPath(filesystem)
|
||||
|
||||
// Sanity check...
|
||||
if !strings.ContainsAny(datasetPath, "@#") {
|
||||
return fmt.Errorf("sanity check failed: no @ or # character found in %q", datasetPath)
|
||||
}
|
||||
|
||||
return ZFSDestroy(datasetPath)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package zfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
)
|
||||
|
||||
func ZFSDestroyFilesystemVersion(filesystem *DatasetPath, version *FilesystemVersion) (err error) {
|
||||
|
||||
datasetPath := version.ToAbsPath(filesystem)
|
||||
|
||||
// Sanity check...
|
||||
if !strings.ContainsAny(datasetPath, "@#") {
|
||||
return fmt.Errorf("sanity check failed: no @ or # character found in %q", datasetPath)
|
||||
}
|
||||
|
||||
return ZFSDestroy(datasetPath)
|
||||
}
|
||||
|
||||
var destroyerSingleton = destroyerImpl{}
|
||||
|
||||
type DestroySnapOp struct {
|
||||
Filesystem string
|
||||
Name string
|
||||
ErrOut *error
|
||||
}
|
||||
|
||||
func (o *DestroySnapOp) String() string {
|
||||
return fmt.Sprintf("destroy operation %s@%s", o.Filesystem, o.Name)
|
||||
}
|
||||
|
||||
func ZFSDestroyFilesystemVersions(reqs []*DestroySnapOp) {
|
||||
doDestroy(context.TODO(), reqs, destroyerSingleton)
|
||||
}
|
||||
|
||||
func setDestroySnapOpErr(b []*DestroySnapOp, err error) {
|
||||
for _, r := range b {
|
||||
*r.ErrOut = err
|
||||
}
|
||||
}
|
||||
|
||||
type destroyer interface {
|
||||
Destroy(args []string) error
|
||||
DestroySnapshotsCommaSyntaxSupported() (bool, error)
|
||||
}
|
||||
|
||||
func doDestroy(ctx context.Context, reqs []*DestroySnapOp, e destroyer) {
|
||||
|
||||
var validated []*DestroySnapOp
|
||||
for _, req := range reqs {
|
||||
// Filesystem and Snapshot should not be empty
|
||||
// ZFS will generally fail because those are invalid destroy arguments,
|
||||
// but we'd rather apply defensive programming here (doing destroy after all)
|
||||
if req.Filesystem == "" {
|
||||
*req.ErrOut = fmt.Errorf("Filesystem must not be an empty string")
|
||||
} else if req.Name == "" {
|
||||
*req.ErrOut = fmt.Errorf("Name must not be an empty string")
|
||||
} else {
|
||||
validated = append(validated, req)
|
||||
}
|
||||
}
|
||||
reqs = validated
|
||||
|
||||
commaSupported, err := e.DestroySnapshotsCommaSyntaxSupported()
|
||||
if err != nil {
|
||||
debug("destroy: comma syntax support detection failed: %s", err)
|
||||
setDestroySnapOpErr(reqs, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !commaSupported {
|
||||
doDestroySeq(ctx, reqs, e)
|
||||
} else {
|
||||
doDestroyBatched(ctx, reqs, e)
|
||||
}
|
||||
}
|
||||
|
||||
func doDestroySeq(ctx context.Context, reqs []*DestroySnapOp, e destroyer) {
|
||||
for _, r := range reqs {
|
||||
*r.ErrOut = e.Destroy([]string{fmt.Sprintf("%s@%s", r.Filesystem, r.Name)})
|
||||
}
|
||||
}
|
||||
|
||||
func doDestroyBatched(ctx context.Context, reqs []*DestroySnapOp, d destroyer) {
|
||||
perFS := buildBatches(reqs)
|
||||
for _, fsbatch := range perFS {
|
||||
doDestroyBatchedRec(ctx, fsbatch, d)
|
||||
}
|
||||
}
|
||||
|
||||
func buildBatches(reqs []*DestroySnapOp) [][]*DestroySnapOp {
|
||||
if len(reqs) == 0 {
|
||||
return nil
|
||||
}
|
||||
sorted := make([]*DestroySnapOp, len(reqs))
|
||||
copy(sorted, reqs)
|
||||
sort.SliceStable(sorted, func(i, j int) bool {
|
||||
// by filesystem, then snap name
|
||||
fscmp := strings.Compare(sorted[i].Filesystem, sorted[j].Filesystem)
|
||||
if fscmp != 0 {
|
||||
return fscmp == -1
|
||||
}
|
||||
return strings.Compare(sorted[i].Name, sorted[j].Name) == -1
|
||||
})
|
||||
|
||||
// group by fs
|
||||
var perFS [][]*DestroySnapOp
|
||||
consumed := 0
|
||||
for consumed < len(sorted) {
|
||||
batchConsumedUntil := consumed
|
||||
for ; batchConsumedUntil < len(sorted) && sorted[batchConsumedUntil].Filesystem == sorted[consumed].Filesystem; batchConsumedUntil++ {
|
||||
}
|
||||
perFS = append(perFS, sorted[consumed:batchConsumedUntil])
|
||||
consumed = batchConsumedUntil
|
||||
}
|
||||
return perFS
|
||||
}
|
||||
|
||||
// batch must be on same Filesystem, panics otherwise
|
||||
func tryBatch(ctx context.Context, batch []*DestroySnapOp, d destroyer) error {
|
||||
batchFS := batch[0].Filesystem
|
||||
batchNames := make([]string, len(batch))
|
||||
for i := range batchNames {
|
||||
batchNames[i] = batch[i].Name
|
||||
if batchFS != batch[i].Filesystem {
|
||||
panic("inconsistent batch")
|
||||
}
|
||||
}
|
||||
batchArg := fmt.Sprintf("%s@%s", batchFS, strings.Join(batchNames, ","))
|
||||
return d.Destroy([]string{batchArg})
|
||||
}
|
||||
|
||||
// fsbatch must be on same filesystem
|
||||
func doDestroyBatchedRec(ctx context.Context, fsbatch []*DestroySnapOp, d destroyer) {
|
||||
if len(fsbatch) <= 1 {
|
||||
doDestroySeq(ctx, fsbatch, d)
|
||||
return
|
||||
}
|
||||
|
||||
err := tryBatch(ctx, fsbatch, d)
|
||||
if err == nil {
|
||||
setDestroySnapOpErr(fsbatch, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if pe, ok := err.(*os.PathError); ok && pe.Err == syscall.E2BIG {
|
||||
// see TestExcessiveArgumentsResultInE2BIG
|
||||
// try halving batch size, assuming snapshots names are roughly the same length
|
||||
debug("batch destroy: E2BIG encountered: %s", err)
|
||||
doDestroyBatchedRec(ctx, fsbatch[0:len(fsbatch)/2], d)
|
||||
doDestroyBatchedRec(ctx, fsbatch[len(fsbatch)/2:], d)
|
||||
return
|
||||
}
|
||||
|
||||
singleRun := fsbatch // the destroys that will be tried sequentially after "smart" error handling below
|
||||
|
||||
if err, ok := err.(*DestroySnapshotsError); ok {
|
||||
// eliminate undestroyable datasets from batch and try it once again
|
||||
strippedBatch, remaining := make([]*DestroySnapOp, 0, len(fsbatch)), make([]*DestroySnapOp, 0, len(fsbatch))
|
||||
|
||||
for _, b := range fsbatch {
|
||||
isUndestroyable := false
|
||||
for _, undestroyable := range err.Undestroyable {
|
||||
if undestroyable == b.Name {
|
||||
isUndestroyable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isUndestroyable {
|
||||
remaining = append(remaining, b)
|
||||
} else {
|
||||
strippedBatch = append(strippedBatch, b)
|
||||
}
|
||||
}
|
||||
|
||||
err := tryBatch(ctx, strippedBatch, d)
|
||||
if err != nil {
|
||||
// run entire batch sequentially if the stripped one fails
|
||||
// (it shouldn't because we stripped erronous datasets)
|
||||
singleRun = fsbatch // shadow
|
||||
} else {
|
||||
setDestroySnapOpErr(strippedBatch, nil) // these ones worked
|
||||
singleRun = remaining // shadow
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
doDestroySeq(ctx, singleRun, d)
|
||||
|
||||
}
|
||||
|
||||
type destroyerImpl struct{}
|
||||
|
||||
func (d destroyerImpl) Destroy(args []string) error {
|
||||
if len(args) != 1 {
|
||||
// we have no use case for this at the moment, so let's crash (safer than destroying something unexpectedly)
|
||||
panic(fmt.Sprintf("unexpected number of arguments: %v", args))
|
||||
}
|
||||
// we know that we are only using this for snapshots, so also sanity check for an @ in args[0]
|
||||
if !strings.ContainsAny(args[0], "@") {
|
||||
panic(fmt.Sprintf("sanity check: expecting '@' in call to Destroy, got %q", args[0]))
|
||||
}
|
||||
return ZFSDestroy(args[0])
|
||||
}
|
||||
|
||||
var batchDestroyFeatureCheck struct {
|
||||
once sync.Once
|
||||
enable bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (d destroyerImpl) DestroySnapshotsCommaSyntaxSupported() (bool, error) {
|
||||
batchDestroyFeatureCheck.once.Do(func() {
|
||||
// "feature discovery"
|
||||
cmd := exec.Command("zfs", "destroy")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if _, ok := err.(*exec.ExitError); !ok {
|
||||
debug("destroy feature check failed: %T %s", err, err)
|
||||
batchDestroyFeatureCheck.err = err
|
||||
}
|
||||
def := strings.Contains(string(output), "<filesystem|volume>@<snap>[%<snap>][,...]")
|
||||
batchDestroyFeatureCheck.enable = envconst.Bool("ZREPL_EXPERIMENTAL_ZFS_COMMA_SYNTAX_SUPPORTED", def)
|
||||
debug("destroy feature check complete %#v", &batchDestroyFeatureCheck)
|
||||
})
|
||||
return batchDestroyFeatureCheck.enable, batchDestroyFeatureCheck.err
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user