Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdd0c9a199 | |||
| 498036194d |
+176
-94
@@ -1,8 +1,5 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
orbs:
|
|
||||||
# NB: 1.7.2 is not the Go version, but the Orb version
|
|
||||||
# https://circleci.com/developer/orbs/orb/circleci/go#usage-go-modules-cache
|
|
||||||
go: circleci/go@1.7.2
|
|
||||||
commands:
|
commands:
|
||||||
setup-home-local-bin:
|
setup-home-local-bin:
|
||||||
steps:
|
steps:
|
||||||
@@ -27,12 +24,18 @@ commands:
|
|||||||
|
|
||||||
apt-update-and-install-common-deps:
|
apt-update-and-install-common-deps:
|
||||||
steps:
|
steps:
|
||||||
- run: sudo apt-get update
|
- run: sudo apt update && sudo apt install gawk make
|
||||||
- run: sudo apt-get install -y gawk make
|
|
||||||
# CircleCI doesn't update its cimg/go images.
|
restore-cache-gomod:
|
||||||
# So, need to update manually to get up-to-date trust chains.
|
steps:
|
||||||
# The need for this was required for cimg/go:1.12, but let's future proof this here and now.
|
- restore_cache:
|
||||||
- run: sudo apt-get install -y git ca-certificates
|
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||||
|
save-cache-gomod:
|
||||||
|
steps:
|
||||||
|
- save_cache:
|
||||||
|
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||||
|
paths:
|
||||||
|
- "/go/pkg/mod"
|
||||||
|
|
||||||
install-godep:
|
install-godep:
|
||||||
steps:
|
steps:
|
||||||
@@ -47,41 +50,94 @@ commands:
|
|||||||
- invoke-lazy-sh:
|
- invoke-lazy-sh:
|
||||||
subcommand: docdep
|
subcommand: docdep
|
||||||
|
|
||||||
docs-publish-sh:
|
download-and-install-minio-client:
|
||||||
|
steps:
|
||||||
|
- setup-home-local-bin
|
||||||
|
- restore_cache:
|
||||||
|
key: minio-client-v2
|
||||||
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
|
command: |
|
||||||
|
if which mc; then exit 0; fi
|
||||||
|
sudo curl -sSL https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2020-08-20T00-23-01Z \
|
||||||
|
-o "$HOME/.local/bin/mc"
|
||||||
|
sudo chmod +x "$HOME/.local/bin/mc"
|
||||||
|
- save_cache:
|
||||||
|
key: minio-client-v2
|
||||||
|
paths:
|
||||||
|
- "$HOME/.local/bin/mc"
|
||||||
|
|
||||||
|
upload-minio:
|
||||||
parameters:
|
parameters:
|
||||||
push:
|
src:
|
||||||
type: boolean
|
type: string
|
||||||
|
dst:
|
||||||
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
|
||||||
- run:
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
|
when: always
|
||||||
command: |
|
command: |
|
||||||
git config --global user.email "zreplbot@cschwarz.com"
|
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
||||||
git config --global user.name "zrepl-github-io-ci"
|
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
|
||||||
|
|
||||||
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
|
||||||
- run: ssh-add -D
|
|
||||||
# the default circleci ssh config only additional ssh keys for Host !github.com
|
# keep in sync with set-github-minio-status
|
||||||
|
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
|
mkdir -p ./artifacts
|
||||||
|
mc cp -r <<parameters.src>> "zrepl-minio/$jobprefix/<<parameters.dst>>"
|
||||||
|
|
||||||
|
set-github-minio-status:
|
||||||
|
parameters:
|
||||||
|
context:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
minio-dst:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
- run:
|
- run:
|
||||||
|
shell: /bin/bash -eo pipefail
|
||||||
command: |
|
command: |
|
||||||
cat > ~/.ssh/config \<<EOF
|
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
|
||||||
Host *
|
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
|
||||||
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421
|
exit 0
|
||||||
EOF
|
fi
|
||||||
- add_ssh_keys:
|
set -u # from now on
|
||||||
fingerprints:
|
|
||||||
# deploy key for zrepl.github.io
|
|
||||||
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
|
|
||||||
|
|
||||||
# caller must install-docdep
|
# keep in sync with with upload-minio command
|
||||||
- when:
|
jobprefix=zrepl-ci-artifacts/${CIRCLE_SHA1}-pipeline-<<pipeline.number>>/${CIRCLE_JOB}
|
||||||
condition: << parameters.push >>
|
# Push Artifact Link to GitHub
|
||||||
|
REPO="zrepl/zrepl"
|
||||||
|
COMMIT="${CIRCLE_SHA1}"
|
||||||
|
JOB_NAME="${CIRCLE_JOB}"
|
||||||
|
CONTEXT="<<parameters.context>>"
|
||||||
|
DESCRIPTION="<<parameters.description>>"
|
||||||
|
TARGETURL=https://minio.cschwarz.com/minio/"$jobprefix"/"<<parameters.minio-dst>>"
|
||||||
|
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
|
||||||
|
-X POST \
|
||||||
|
-d '{"context":"'"$CONTEXT"'", "state": "success", "description":"'"$DESCRIPTION"'", "target_url":"'"$TARGETURL"'"}'
|
||||||
|
|
||||||
|
|
||||||
|
trigger-pipeline:
|
||||||
|
parameters:
|
||||||
|
body_no_shell_subst:
|
||||||
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- run: bash -x docs/publish.sh -c -a -P
|
- run: |
|
||||||
- when:
|
curl -X POST https://circleci.com/api/v2/project/github/zrepl/zrepl/pipeline \
|
||||||
condition:
|
-H 'Content-Type: application/json' \
|
||||||
not: << parameters.push >>
|
-H 'Accept: application/json' \
|
||||||
steps:
|
-H "Circle-Token: $ZREPL_BOT_CIRCLE_TOKEN" \
|
||||||
- run: bash -x docs/publish.sh -c -a
|
--data '<<parameters.body_no_shell_subst>>'
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
do_ci:
|
do_ci:
|
||||||
@@ -94,7 +150,7 @@ parameters:
|
|||||||
|
|
||||||
release_docker_baseimage_tag:
|
release_docker_baseimage_tag:
|
||||||
type: string
|
type: string
|
||||||
default: "1.21"
|
default: "1.19"
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
@@ -104,19 +160,19 @@ workflows:
|
|||||||
jobs:
|
jobs:
|
||||||
- quickcheck-docs
|
- quickcheck-docs
|
||||||
- quickcheck-go: &quickcheck-go-smoketest
|
- quickcheck-go: &quickcheck-go-smoketest
|
||||||
name: quickcheck-go-amd64-linux-1.21
|
name: quickcheck-go-amd64-linux-1.19
|
||||||
goversion: &latest-go-release "1.21"
|
goversion: &latest-go-release "1.19"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
- test-go-on-latest-go-release:
|
- test-go-on-latest-go-release:
|
||||||
goversion: *latest-go-release
|
goversion: *latest-go-release
|
||||||
- quickcheck-go:
|
- quickcheck-go:
|
||||||
requires:
|
requires:
|
||||||
- quickcheck-go-amd64-linux-1.21 #quickcheck-go-smoketest.name
|
- quickcheck-go-amd64-linux-1.19 #quickcheck-go-smoketest.name
|
||||||
matrix: &quickcheck-go-matrix
|
matrix: &quickcheck-go-matrix
|
||||||
alias: quickcheck-go-matrix
|
alias: quickcheck-go-matrix
|
||||||
parameters:
|
parameters:
|
||||||
goversion: [*latest-go-release, "1.20"]
|
goversion: [*latest-go-release, "1.12"]
|
||||||
goos: ["linux", "freebsd"]
|
goos: ["linux", "freebsd"]
|
||||||
goarch: ["amd64", "arm64"]
|
goarch: ["amd64", "arm64"]
|
||||||
exclude:
|
exclude:
|
||||||
@@ -124,14 +180,10 @@ workflows:
|
|||||||
- goversion: *latest-go-release
|
- goversion: *latest-go-release
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
- platformtest:
|
# not supported by Go 1.12
|
||||||
matrix:
|
- goversion: "1.12"
|
||||||
parameters:
|
goos: freebsd
|
||||||
goversion: [*latest-go-release]
|
goarch: arm64
|
||||||
goos: ["linux"]
|
|
||||||
goarch: ["amd64"]
|
|
||||||
requires:
|
|
||||||
- quickcheck-go-<< matrix.goarch >>-<< matrix.goos >>-<< matrix.goversion >>
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
when: << pipeline.parameters.do_release >>
|
when: << pipeline.parameters.do_release >>
|
||||||
@@ -149,7 +201,20 @@ workflows:
|
|||||||
- release-deb
|
- release-deb
|
||||||
- release-rpm
|
- release-rpm
|
||||||
|
|
||||||
publish-zrepl.github.io:
|
periodic:
|
||||||
|
triggers:
|
||||||
|
- schedule:
|
||||||
|
cron: "00 17 * * *"
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- stable
|
||||||
|
- problame/circleci-build
|
||||||
|
jobs:
|
||||||
|
- periodic-full-pipeline-run
|
||||||
|
|
||||||
|
zrepl.github.io:
|
||||||
jobs:
|
jobs:
|
||||||
- publish-zrepl-github-io:
|
- publish-zrepl-github-io:
|
||||||
filters:
|
filters:
|
||||||
@@ -160,15 +225,20 @@ workflows:
|
|||||||
jobs:
|
jobs:
|
||||||
quickcheck-docs:
|
quickcheck-docs:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/base:2023.09
|
- image: cimg/base:2020.08
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- install-docdep
|
- install-docdep
|
||||||
# do the current docs build
|
|
||||||
- run: make docs
|
- run: make docs
|
||||||
# does the publish.sh script still work?
|
|
||||||
- docs-publish-sh:
|
- download-and-install-minio-client
|
||||||
push: false
|
- upload-minio:
|
||||||
|
src: artifacts
|
||||||
|
dst: ""
|
||||||
|
- set-github-minio-status:
|
||||||
|
context: artifacts/${CIRCLE_JOB}
|
||||||
|
description: artifacts of CI job ${CIRCLE_JOB}
|
||||||
|
minio-dst: ""
|
||||||
|
|
||||||
quickcheck-go:
|
quickcheck-go:
|
||||||
parameters:
|
parameters:
|
||||||
@@ -187,48 +257,29 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
|
||||||
- go/load-cache:
|
- restore-cache-gomod
|
||||||
key: quickcheck-<<parameters.goversion>>
|
|
||||||
- install-godep
|
|
||||||
- run: go mod download
|
- run: go mod download
|
||||||
- run: cd build && go mod download
|
- run: cd build && go mod download
|
||||||
- go/save-cache:
|
- save-cache-gomod
|
||||||
key: quickcheck-<<parameters.goversion>>
|
|
||||||
|
|
||||||
|
- install-godep
|
||||||
- run: make formatcheck
|
- run: make formatcheck
|
||||||
- run: make generate-platform-test-list
|
- run: make generate-platform-test-list
|
||||||
- run: make zrepl-bin test-platform-bin
|
- run: make zrepl-bin test-platform-bin
|
||||||
- run: make vet
|
- run: make vet
|
||||||
- run: make lint
|
- run: make lint
|
||||||
|
|
||||||
|
- download-and-install-minio-client
|
||||||
- run: rm -f artifacts/generate-platform-test-list
|
- run: rm -f artifacts/generate-platform-test-list
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- persist_to_workspace:
|
- upload-minio:
|
||||||
root: .
|
src: artifacts
|
||||||
paths: [.]
|
dst: ""
|
||||||
|
- set-github-minio-status:
|
||||||
platformtest:
|
context: artifacts/${CIRCLE_JOB}
|
||||||
parameters:
|
description: artifacts of CI job ${CIRCLE_JOB}
|
||||||
goversion:
|
minio-dst: ""
|
||||||
type: string
|
|
||||||
goos:
|
|
||||||
type: string
|
|
||||||
goarch:
|
|
||||||
type: string
|
|
||||||
machine:
|
|
||||||
image: ubuntu-2204:current
|
|
||||||
resource_class: medium
|
|
||||||
environment:
|
|
||||||
GOOS: <<parameters.goos>>
|
|
||||||
GOARCH: <<parameters.goarch>>
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: .
|
|
||||||
- run: sudo apt-get update
|
|
||||||
- run: sudo apt-get install -y zfsutils-linux
|
|
||||||
- run: sudo zfs version
|
|
||||||
- run: sudo make test-platform GOOS="$GOOS" GOARCH="$GOARCH"
|
|
||||||
|
|
||||||
test-go-on-latest-go-release:
|
test-go-on-latest-go-release:
|
||||||
parameters:
|
parameters:
|
||||||
@@ -238,11 +289,9 @@ jobs:
|
|||||||
- image: cimg/go:<<parameters.goversion>>
|
- image: cimg/go:<<parameters.goversion>>
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- go/load-cache:
|
- restore-cache-gomod
|
||||||
key: make-test-go
|
|
||||||
- run: make test-go
|
- run: make test-go
|
||||||
- go/save-cache:
|
# don't save-cache-gomod here, test-go doesn't pull all the dependencies
|
||||||
key: make-test-go
|
|
||||||
|
|
||||||
release-build:
|
release-build:
|
||||||
machine:
|
machine:
|
||||||
@@ -283,15 +332,48 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- run: make wrapup-and-checksum
|
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
- download-and-install-minio-client
|
||||||
|
- upload-minio:
|
||||||
|
src: artifacts
|
||||||
|
dst: ""
|
||||||
|
- set-github-minio-status:
|
||||||
|
context: artifacts/release
|
||||||
|
description: CI-generated release artifacts
|
||||||
|
minio-dst: ""
|
||||||
|
|
||||||
|
periodic-full-pipeline-run:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:2020.08
|
||||||
|
steps:
|
||||||
|
- trigger-pipeline:
|
||||||
|
body_no_shell_subst: '{"branch":"<<pipeline.git.branch>>", "parameters": { "do_ci": true, "do_release": true }}'
|
||||||
|
|
||||||
publish-zrepl-github-io:
|
publish-zrepl-github-io:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/base:2023.09
|
- image: cimg/python:3.7
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- install-docdep
|
- invoke-lazy-sh:
|
||||||
- docs-publish-sh:
|
subcommand: docdep
|
||||||
push: true
|
- run:
|
||||||
|
command: |
|
||||||
|
git config --global user.email "zreplbot@cschwarz.com"
|
||||||
|
git config --global user.name "zrepl-github-io-ci"
|
||||||
|
|
||||||
|
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
||||||
|
- run: ssh-add -D
|
||||||
|
# the default circleci ssh config only additional ssh keys for Host !github.com
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
cat > ~/.ssh/config \<<EOF
|
||||||
|
Host *
|
||||||
|
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421
|
||||||
|
EOF
|
||||||
|
- add_ssh_keys:
|
||||||
|
fingerprints:
|
||||||
|
# deploy key for zrepl.github.io
|
||||||
|
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
|
||||||
|
|
||||||
|
- run: bash -x docs/publish.sh -c -a
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
import os
|
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
circle_token = os.environ.get('CIRCLE_TOKEN')
|
|
||||||
if not circle_token:
|
|
||||||
raise ValueError('CIRCLE_TOKEN environment variable not set')
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Download artifacts from CircleCI')
|
|
||||||
parser.add_argument('build_num', type=str, help='Build number')
|
|
||||||
parser.add_argument('dst', type=Path, help='Destination directory')
|
|
||||||
parser.add_argument('--prefix', type=str, default='', help='Filter for prefix')
|
|
||||||
parser.add_argument('--match', type=str, default='.*', help='Only include paths matching the given regex')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
res = requests.get(
|
|
||||||
f"https://circleci.com/api/v1.1/project/github/zrepl/zrepl/{args.build_num}/artifacts",
|
|
||||||
headers={
|
|
||||||
"Circle-Token": circle_token,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
res.raise_for_status()
|
|
||||||
|
|
||||||
# https://circleci.com/docs/api/v1/index.html#artifacts-of-a-job
|
|
||||||
# [ {
|
|
||||||
# "path" : "raw-test-output/go-test-report.xml",
|
|
||||||
# "pretty_path" : "raw-test-output/go-test-report.xml",
|
|
||||||
# "node_index" : 0,
|
|
||||||
# "url" : "https://24-88881093-gh.circle-artifacts.com/0/raw-test-output/go-test-report.xml"
|
|
||||||
# }, {
|
|
||||||
# "path" : "raw-test-output/go-test.out",
|
|
||||||
# "pretty_path" : "raw-test-output/go-test.out",
|
|
||||||
# "node_index" : 0,
|
|
||||||
# "url" : "https://24-88881093-gh.circle-artifacts.com/0/raw-test-output/go-test.out"
|
|
||||||
# } ]
|
|
||||||
res = res.json()
|
|
||||||
|
|
||||||
for artifact in res:
|
|
||||||
if not artifact["pretty_path"].startswith(args.prefix):
|
|
||||||
continue
|
|
||||||
if not re.match(args.match, artifact["pretty_path"]):
|
|
||||||
continue
|
|
||||||
stripped = artifact["pretty_path"][len(args.prefix):]
|
|
||||||
print(f"Downloading {artifact['pretty_path']} to {args.dst / stripped}")
|
|
||||||
artifact_rel = Path(stripped)
|
|
||||||
artifact_dst = args.dst / artifact_rel
|
|
||||||
artifact_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
res = requests.get(
|
|
||||||
artifact["url"],
|
|
||||||
headers={
|
|
||||||
"Circle-Token": circle_token,
|
|
||||||
},
|
|
||||||
stream=True,
|
|
||||||
)
|
|
||||||
res.raise_for_status()
|
|
||||||
|
|
||||||
total_size = int(res.headers.get("Content-Length", 0))
|
|
||||||
block_size = 128 * 1024
|
|
||||||
with open(artifact_dst, "wb") as f:
|
|
||||||
progress = 0
|
|
||||||
start_time = time.time()
|
|
||||||
for chunk in res.iter_content(chunk_size=block_size):
|
|
||||||
f.write(chunk)
|
|
||||||
progress += len(chunk)
|
|
||||||
percent = progress / total_size * 100
|
|
||||||
elapsed_time = time.time() - start_time
|
|
||||||
if elapsed_time >= 5:
|
|
||||||
print(f"Downloaded {progress}/{total_size} bytes ({percent:.2f}%)", end="\r")
|
|
||||||
start_time = time.time()
|
|
||||||
print(f"Downloaded {progress}/{total_size} bytes ({percent:.2f}%)")
|
|
||||||
print("Download complete!")
|
|
||||||
|
|
||||||
print("All files downloaded")
|
|
||||||
@@ -28,7 +28,7 @@ GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
|||||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
GOLANGCI_LINT := golangci-lint
|
GOLANGCI_LINT := golangci-lint
|
||||||
GOCOVMERGE := gocovmerge
|
GOCOVMERGE := gocovmerge
|
||||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.21
|
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.19
|
||||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||||
|
|
||||||
ifneq ($(GOARM),)
|
ifneq ($(GOARM),)
|
||||||
@@ -52,6 +52,12 @@ release: clean
|
|||||||
$(MAKE) test-go
|
$(MAKE) test-go
|
||||||
$(MAKE) bins-all
|
$(MAKE) bins-all
|
||||||
$(MAKE) noarch
|
$(MAKE) noarch
|
||||||
|
$(MAKE) wrapup-and-checksum
|
||||||
|
$(MAKE) check-git-clean
|
||||||
|
ifeq (SIGN, 1)
|
||||||
|
$(MAKE) sign
|
||||||
|
endif
|
||||||
|
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
||||||
|
|
||||||
release-docker: $(ARTIFACTDIR)
|
release-docker: $(ARTIFACTDIR)
|
||||||
sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build.Dockerfile > artifacts/release-docker.Dockerfile
|
sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build.Dockerfile > artifacts/release-docker.Dockerfile
|
||||||
@@ -128,17 +134,11 @@ endif
|
|||||||
|
|
||||||
deb-docker:
|
deb-docker:
|
||||||
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
||||||
# Use a small open file limit to make fakeroot work. If we don't
|
|
||||||
# specify it, docker daemon will use its file limit. I don't know
|
|
||||||
# what changed (Docker, its systemd service, its Go version). But I
|
|
||||||
# observed fakeroot iterating close(i) up to i > 1000000, which costs
|
|
||||||
# a good amount of CPU time and makes the build slow.
|
|
||||||
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
||||||
--ulimit nofile=1024:1024 \
|
|
||||||
zrepl_debian_pkg \
|
zrepl_debian_pkg \
|
||||||
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
||||||
|
|
||||||
# expects `release`, `deb` & `rpm` targets to have run before
|
# expects `release` target to have run before
|
||||||
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||||
wrapup-and-checksum:
|
wrapup-and-checksum:
|
||||||
rm -f $(NOARCH_TARBALL)
|
rm -f $(NOARCH_TARBALL)
|
||||||
@@ -175,21 +175,16 @@ check-git-clean:
|
|||||||
|
|
||||||
tag-release:
|
tag-release:
|
||||||
test -n "$(ZREPL_TAG_VERSION)" || exit 1
|
test -n "$(ZREPL_TAG_VERSION)" || exit 1
|
||||||
git tag -u '328A6627FA98061D!' -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
|
git tag -u E27CA5FC -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
|
||||||
|
|
||||||
sign:
|
sign:
|
||||||
gpg -u '328A6627FA98061D!' \
|
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
|
||||||
--armor \
|
--armor \
|
||||||
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
|
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
|
||||||
|
|
||||||
clean: docs-clean
|
clean: docs-clean
|
||||||
rm -rf "$(ARTIFACTDIR)"
|
rm -rf "$(ARTIFACTDIR)"
|
||||||
|
|
||||||
download-circleci-release:
|
|
||||||
rm -rf "$(ARTIFACTDIR)"
|
|
||||||
mkdir -p "$(ARTIFACTDIR)/release"
|
|
||||||
python3 .circleci/download_artifacts.py --prefix 'artifacts/release/' "$(BUILD_NUM)" "$(ARTIFACTDIR)/release"
|
|
||||||
|
|
||||||
##################### BINARIES #####################
|
##################### BINARIES #####################
|
||||||
.PHONY: bins-all lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
|
.PHONY: bins-all lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,12 @@ FROM !SUBSTITUTED_BY_MAKEFILE
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-venv \
|
|
||||||
unzip \
|
unzip \
|
||||||
gawk
|
gawk
|
||||||
|
|
||||||
ADD build.installprotoc.bash ./
|
ADD build.installprotoc.bash ./
|
||||||
RUN bash build.installprotoc.bash
|
RUN bash build.installprotoc.bash
|
||||||
|
|
||||||
# setup venv
|
|
||||||
ENV VIRTUAL_ENV=/opt/venv
|
|
||||||
RUN python3 -m venv $VIRTUAL_ENV
|
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
|
|
||||||
ADD lazy.sh /tmp/lazy.sh
|
ADD lazy.sh /tmp/lazy.sh
|
||||||
ADD docs/requirements.txt /tmp/requirements.txt
|
ADD docs/requirements.txt /tmp/requirements.txt
|
||||||
ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
|
ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
|
||||||
|
|||||||
+5
-32
@@ -4,37 +4,10 @@ go 1.12
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alvaroloes/enumer v1.1.1
|
github.com/alvaroloes/enumer v1.1.1
|
||||||
github.com/breml/bidichk v0.2.6 // indirect
|
github.com/golangci/golangci-lint v1.50.1
|
||||||
github.com/breml/errchkjson v0.3.5 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/chavacava/garif v0.1.0 // indirect
|
|
||||||
github.com/daixiang0/gci v0.11.1 // indirect
|
|
||||||
github.com/golangci/golangci-lint v1.54.2
|
|
||||||
github.com/golangci/revgrep v0.5.0 // indirect
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/jgautheron/goconst v1.6.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
|
||||||
github.com/mgechev/revive v1.3.3 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
|
||||||
github.com/polyfloyd/go-errorlint v1.4.5 // indirect
|
|
||||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
|
||||||
github.com/prometheus/common v0.44.0 // indirect
|
|
||||||
github.com/prometheus/procfs v0.11.1 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
|
||||||
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
|
|
||||||
github.com/spf13/viper v1.16.0 // indirect
|
|
||||||
github.com/stretchr/objx v0.5.1 // indirect
|
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
|
||||||
github.com/tetafro/godot v1.4.15 // indirect
|
|
||||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
||||||
github.com/xen0n/gosmopolitan v1.2.2 // indirect
|
golang.org/x/tools v0.2.0
|
||||||
gitlab.com/bosi/decorder v0.4.1 // indirect
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
google.golang.org/protobuf v1.28.0
|
||||||
go.uber.org/zap v1.25.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
|
||||||
golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 // indirect
|
|
||||||
golang.org/x/tools v0.13.0
|
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
|
|
||||||
google.golang.org/protobuf v1.31.0
|
|
||||||
mvdan.cc/unparam v0.0.0-20230815095028-f7c6fb1088f0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
+492
-1231
File diff suppressed because it is too large
Load Diff
@@ -374,7 +374,6 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
|
|||||||
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
|
eta = time.Duration((float64(expected)-float64(replicated))/float64(rate)) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
if !latest.State.IsTerminal() {
|
|
||||||
t.Write("Progress: ")
|
t.Write("Progress: ")
|
||||||
t.DrawBar(50, replicated, expected, changeCount)
|
t.DrawBar(50, replicated, expected, changeCount)
|
||||||
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
|
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinaryUint(replicated), ByteCountBinaryUint(expected), ByteCountBinary(rate)))
|
||||||
@@ -382,7 +381,6 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
|
|||||||
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
||||||
}
|
}
|
||||||
t.Newline()
|
t.Newline()
|
||||||
}
|
|
||||||
if containsInvalidSizeEstimates {
|
if containsInvalidSizeEstimates {
|
||||||
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||||
t.Newline()
|
t.Newline()
|
||||||
@@ -495,7 +493,6 @@ func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFun
|
|||||||
}
|
}
|
||||||
|
|
||||||
// global progress bar
|
// global progress bar
|
||||||
if !state.IsTerminal() {
|
|
||||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
||||||
t.Write("Progress: ")
|
t.Write("Progress: ")
|
||||||
t.Write("[")
|
t.Write("[")
|
||||||
@@ -505,7 +502,6 @@ func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFun
|
|||||||
t.Write("]")
|
t.Write("]")
|
||||||
t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
t.Printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
||||||
t.Newline()
|
t.Newline()
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(all, func(i, j int) bool {
|
sort.SliceStable(all, func(i, j int) bool {
|
||||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
||||||
|
|||||||
+56
-4
@@ -6,6 +6,8 @@ import (
|
|||||||
"log/syslog"
|
"log/syslog"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -65,6 +67,7 @@ type ActiveJob struct {
|
|||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Connect ConnectEnum `yaml:"connect"`
|
Connect ConnectEnum `yaml:"connect"`
|
||||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
Pruning PruningSenderReceiver `yaml:"pruning"`
|
||||||
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
Replication *Replication `yaml:"replication,optional,fromdefaults"`
|
||||||
ConflictResolution *ConflictResolution `yaml:"conflict_resolution,optional,fromdefaults"`
|
ConflictResolution *ConflictResolution `yaml:"conflict_resolution,optional,fromdefaults"`
|
||||||
}
|
}
|
||||||
@@ -77,12 +80,14 @@ type PassiveJob struct {
|
|||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Serve ServeEnum `yaml:"serve"`
|
Serve ServeEnum `yaml:"serve"`
|
||||||
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapJob struct {
|
type SnapJob struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Pruning PruningLocal `yaml:"pruning"`
|
Pruning PruningLocal `yaml:"pruning"`
|
||||||
|
Debug JobDebugSettings `yaml:"debug,optional"`
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
||||||
}
|
}
|
||||||
@@ -185,10 +190,13 @@ func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error
|
|||||||
return fmt.Errorf("value must not be empty")
|
return fmt.Errorf("value must not be empty")
|
||||||
default:
|
default:
|
||||||
i.Manual = false
|
i.Manual = false
|
||||||
i.Interval, err = parsePositiveDuration(s)
|
i.Interval, err = time.ParseDuration(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if i.Interval <= 0 {
|
||||||
|
return fmt.Errorf("value must be a positive duration, got %q", s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -222,9 +230,8 @@ type SnapshottingEnum struct {
|
|||||||
type SnapshottingPeriodic struct {
|
type SnapshottingPeriodic struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Prefix string `yaml:"prefix"`
|
Prefix string `yaml:"prefix"`
|
||||||
Interval *PositiveDuration `yaml:"interval"`
|
Interval time.Duration `yaml:"interval,positive"`
|
||||||
Hooks HookList `yaml:"hooks,optional"`
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronSpec struct {
|
type CronSpec struct {
|
||||||
@@ -257,7 +264,6 @@ type SnapshottingCron struct {
|
|||||||
Prefix string `yaml:"prefix"`
|
Prefix string `yaml:"prefix"`
|
||||||
Cron CronSpec `yaml:"cron"`
|
Cron CronSpec `yaml:"cron"`
|
||||||
Hooks HookList `yaml:"hooks,optional"`
|
Hooks HookList `yaml:"hooks,optional"`
|
||||||
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapshottingManual struct {
|
type SnapshottingManual struct {
|
||||||
@@ -477,6 +483,14 @@ type GlobalStdinServer struct {
|
|||||||
SockDir string `yaml:"sockdir,default=/var/run/zrepl/stdinserver"`
|
SockDir string `yaml:"sockdir,default=/var/run/zrepl/stdinserver"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JobDebugSettings struct {
|
||||||
|
Conn *struct {
|
||||||
|
ReadDump string `yaml:"read_dump"`
|
||||||
|
WriteDump string `yaml:"write_dump"`
|
||||||
|
} `yaml:"conn,optional"`
|
||||||
|
RPCLog bool `yaml:"rpc_log,optional,default=false"`
|
||||||
|
}
|
||||||
|
|
||||||
type HookList []HookEnum
|
type HookList []HookEnum
|
||||||
|
|
||||||
type HookEnum struct {
|
type HookEnum struct {
|
||||||
@@ -701,3 +715,41 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
|
|||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
||||||
|
|
||||||
|
func parsePositiveDuration(e string) (d time.Duration, err error) {
|
||||||
|
comps := durationStringRegex.FindStringSubmatch(e)
|
||||||
|
if len(comps) != 3 {
|
||||||
|
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if durationFactor <= 0 {
|
||||||
|
return 0, errors.New("duration must be positive integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationUnit time.Duration
|
||||||
|
switch comps[2] {
|
||||||
|
case "s":
|
||||||
|
durationUnit = time.Second
|
||||||
|
case "m":
|
||||||
|
durationUnit = time.Minute
|
||||||
|
case "h":
|
||||||
|
durationUnit = time.Hour
|
||||||
|
case "d":
|
||||||
|
durationUnit = 24 * time.Hour
|
||||||
|
case "w":
|
||||||
|
durationUnit = 24 * 7 * time.Hour
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d = time.Duration(durationFactor) * durationUnit
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Duration struct{ d time.Duration }
|
|
||||||
|
|
||||||
func (d Duration) Duration() time.Duration { return d.d }
|
|
||||||
|
|
||||||
var _ yaml.Unmarshaler = &Duration{}
|
|
||||||
|
|
||||||
func (d *Duration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
|
||||||
var s string
|
|
||||||
err := unmarshal(&s, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.d, err = parseDuration(s)
|
|
||||||
if err != nil {
|
|
||||||
d.d = 0
|
|
||||||
return &yaml.TypeError{Errors: []string{fmt.Sprintf("cannot parse value %q: %s", s, err)}}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type PositiveDuration struct{ d Duration }
|
|
||||||
|
|
||||||
var _ yaml.Unmarshaler = &PositiveDuration{}
|
|
||||||
|
|
||||||
func (d PositiveDuration) Duration() time.Duration { return d.d.Duration() }
|
|
||||||
|
|
||||||
func (d *PositiveDuration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
|
||||||
err := d.d.UnmarshalYAML(unmarshal)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if d.d.Duration() <= 0 {
|
|
||||||
return fmt.Errorf("duration must be positive, got %s", d.d.Duration())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePositiveDuration(e string) (time.Duration, error) {
|
|
||||||
d, err := parseDuration(e)
|
|
||||||
if err != nil {
|
|
||||||
return d, err
|
|
||||||
}
|
|
||||||
if d <= 0 {
|
|
||||||
return 0, errors.New("duration must be positive integer")
|
|
||||||
}
|
|
||||||
return d, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*([\+-]?\d+)\s*(|s|m|h|d|w)\s*$`)
|
|
||||||
|
|
||||||
func parseDuration(e string) (d time.Duration, err error) {
|
|
||||||
comps := durationStringRegex.FindStringSubmatch(e)
|
|
||||||
if comps == nil {
|
|
||||||
err = fmt.Errorf("must match %s", durationStringRegex)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(comps) != 3 {
|
|
||||||
panic(pretty.Sprint(comps))
|
|
||||||
}
|
|
||||||
|
|
||||||
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var durationUnit time.Duration
|
|
||||||
switch comps[2] {
|
|
||||||
case "":
|
|
||||||
if durationFactor != 0 {
|
|
||||||
err = fmt.Errorf("missing time unit")
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// It's the case where user specified '0'.
|
|
||||||
// We want to allow this, just like time.ParseDuration.
|
|
||||||
}
|
|
||||||
case "s":
|
|
||||||
durationUnit = time.Second
|
|
||||||
case "m":
|
|
||||||
durationUnit = time.Minute
|
|
||||||
case "h":
|
|
||||||
durationUnit = time.Hour
|
|
||||||
case "d":
|
|
||||||
durationUnit = 24 * time.Hour
|
|
||||||
case "w":
|
|
||||||
durationUnit = 24 * 7 * time.Hour
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d = time.Duration(durationFactor) * durationUnit
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -35,23 +35,8 @@ jobs:
|
|||||||
snapshotting:
|
snapshotting:
|
||||||
type: periodic
|
type: periodic
|
||||||
prefix: zrepl_
|
prefix: zrepl_
|
||||||
timestamp_format: dense
|
|
||||||
interval: 10m
|
interval: 10m
|
||||||
`
|
`
|
||||||
cron := `
|
|
||||||
snapshotting:
|
|
||||||
type: cron
|
|
||||||
prefix: zrepl_
|
|
||||||
timestamp_format: human
|
|
||||||
cron: "10 * * * *"
|
|
||||||
`
|
|
||||||
|
|
||||||
periodicDaily := `
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 1d
|
|
||||||
`
|
|
||||||
|
|
||||||
hooks := `
|
hooks := `
|
||||||
snapshotting:
|
snapshotting:
|
||||||
@@ -89,27 +74,10 @@ jobs:
|
|||||||
c = testValidConfig(t, fillSnapshotting(periodic))
|
c = testValidConfig(t, fillSnapshotting(periodic))
|
||||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
||||||
assert.Equal(t, "periodic", snp.Type)
|
assert.Equal(t, "periodic", snp.Type)
|
||||||
assert.Equal(t, 10*time.Minute, snp.Interval.Duration())
|
assert.Equal(t, 10*time.Minute, snp.Interval)
|
||||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("periodicDaily", func(t *testing.T) {
|
|
||||||
c = testValidConfig(t, fillSnapshotting(periodicDaily))
|
|
||||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
|
||||||
assert.Equal(t, "periodic", snp.Type)
|
|
||||||
assert.Equal(t, 24*time.Hour, snp.Interval.Duration())
|
|
||||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
|
||||||
assert.Equal(t, "dense", snp.TimestampFormat)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("cron", func(t *testing.T) {
|
|
||||||
c = testValidConfig(t, fillSnapshotting(cron))
|
|
||||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingCron)
|
|
||||||
assert.Equal(t, "cron", snp.Type)
|
|
||||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
|
||||||
assert.Equal(t, "human", snp.TimestampFormat)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("hooks", func(t *testing.T) {
|
t.Run("hooks", func(t *testing.T) {
|
||||||
c = testValidConfig(t, fillSnapshotting(hooks))
|
c = testValidConfig(t, fillSnapshotting(hooks))
|
||||||
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
|
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
|
||||||
@@ -120,57 +88,3 @@ jobs:
|
|||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSnapshottingTimestampDefaults(t *testing.T) {
|
|
||||||
tmpl := `
|
|
||||||
jobs:
|
|
||||||
- name: foo
|
|
||||||
type: push
|
|
||||||
connect:
|
|
||||||
type: local
|
|
||||||
listener_name: foo
|
|
||||||
client_identity: bar
|
|
||||||
filesystems: {"<": true}
|
|
||||||
%s
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
`
|
|
||||||
|
|
||||||
periodic := `
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
`
|
|
||||||
cron := `
|
|
||||||
snapshotting:
|
|
||||||
type: cron
|
|
||||||
prefix: zrepl_
|
|
||||||
cron: "10 * * * *"
|
|
||||||
`
|
|
||||||
|
|
||||||
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
|
||||||
var c *Config
|
|
||||||
|
|
||||||
t.Run("periodic", func(t *testing.T) {
|
|
||||||
c = testValidConfig(t, fillSnapshotting(periodic))
|
|
||||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
|
||||||
assert.Equal(t, "periodic", snp.Type)
|
|
||||||
assert.Equal(t, 10*time.Minute, snp.Interval.Duration())
|
|
||||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
|
||||||
assert.Equal(t, "dense", snp.TimestampFormat) // default was set correctly
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("cron", func(t *testing.T) {
|
|
||||||
c = testValidConfig(t, fillSnapshotting(cron))
|
|
||||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingCron)
|
|
||||||
assert.Equal(t, "cron", snp.Type)
|
|
||||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
|
||||||
assert.Equal(t, "dense", snp.TimestampFormat) // default was set correctly
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -160,14 +160,6 @@ func (m DatasetMapFilter) Filter(p *zfs.DatasetPath) (pass bool, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m DatasetMapFilter) UserSpecifiedDatasets() (datasets zfs.UserSpecifiedDatasetsSet) {
|
|
||||||
datasets = make(zfs.UserSpecifiedDatasetsSet)
|
|
||||||
for i := range m.entries {
|
|
||||||
datasets[m.entries[i].path.ToString()] = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct a new filter-only DatasetMapFilter from a mapping
|
// Construct a new filter-only DatasetMapFilter from a mapping
|
||||||
// The new filter allows exactly those paths that were not forbidden by the mapping.
|
// The new filter allows exactly those paths that were not forbidden by the mapping.
|
||||||
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
|
||||||
|
|||||||
@@ -199,16 +199,6 @@ const (
|
|||||||
Done
|
Done
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns true in case the State is a terminal state(PlanErr, ExecErr, Done)
|
|
||||||
func (s State) IsTerminal() bool {
|
|
||||||
switch s {
|
|
||||||
case PlanErr, ExecErr, Done:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type updater func(func(*Pruner))
|
type updater func(func(*Pruner))
|
||||||
|
|
||||||
func (p *Pruner) Prune() {
|
func (p *Pruner) Prune() {
|
||||||
|
|||||||
+25
-5
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ func cronFromConfig(fsf zfs.DatasetFilter, in config.SnapshottingCron) (*Cron, e
|
|||||||
}
|
}
|
||||||
planArgs := planArgs{
|
planArgs := planArgs{
|
||||||
prefix: in.Prefix,
|
prefix: in.Prefix,
|
||||||
timestampFormat: in.TimestampFormat,
|
|
||||||
hooks: hooksList,
|
hooks: hooksList,
|
||||||
}
|
}
|
||||||
return &Cron{config: in, fsf: fsf, planArgs: planArgs}, nil
|
return &Cron{config: in, fsf: fsf, planArgs: planArgs}, nil
|
||||||
@@ -44,17 +42,38 @@ type Cron struct {
|
|||||||
|
|
||||||
func (s *Cron) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
func (s *Cron) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||||
|
|
||||||
|
t := time.NewTimer(0)
|
||||||
|
defer func() {
|
||||||
|
if !t.Stop() {
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
for {
|
for {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
s.wakeupTime = s.config.Cron.Schedule.Next(now)
|
s.wakeupTime = s.config.Cron.Schedule.Next(now)
|
||||||
s.mtx.Unlock()
|
s.mtx.Unlock()
|
||||||
|
|
||||||
ctxDone := suspendresumesafetimer.SleepUntil(ctx, s.wakeupTime)
|
// Re-arm the timer.
|
||||||
if ctxDone != nil {
|
// Need to Stop before Reset, see docs.
|
||||||
return
|
if !t.Stop() {
|
||||||
|
// Use non-blocking read from timer channel
|
||||||
|
// because, except for the first loop iteration,
|
||||||
|
// the channel is already drained
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
t.Reset(s.wakeupTime.Sub(now))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
getLogger(ctx).Debug("cron timer fired")
|
getLogger(ctx).Debug("cron timer fired")
|
||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
if s.running {
|
if s.running {
|
||||||
@@ -84,6 +103,7 @@ func (s *Cron) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-18
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ import (
|
|||||||
|
|
||||||
type planArgs struct {
|
type planArgs struct {
|
||||||
prefix string
|
prefix string
|
||||||
timestampFormat string
|
|
||||||
hooks *hooks.List
|
hooks *hooks.List
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,21 +58,6 @@ type snapProgress struct {
|
|||||||
runResults hooks.PlanReport
|
runResults hooks.PlanReport
|
||||||
}
|
}
|
||||||
|
|
||||||
func (plan *plan) formatNow(format string) string {
|
|
||||||
now := time.Now().UTC()
|
|
||||||
switch strings.ToLower(format) {
|
|
||||||
case "dense":
|
|
||||||
format = "20060102_150405_000"
|
|
||||||
case "human":
|
|
||||||
format = "2006-01-02_15:04:05"
|
|
||||||
case "iso-8601":
|
|
||||||
format = "2006-01-02T15:04:05.000Z"
|
|
||||||
case "unix-seconds":
|
|
||||||
return strconv.FormatInt(now.Unix(), 10)
|
|
||||||
}
|
|
||||||
return now.Format(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plan *plan) execute(ctx context.Context, dryRun bool) (ok bool) {
|
func (plan *plan) execute(ctx context.Context, dryRun bool) (ok bool) {
|
||||||
|
|
||||||
hookMatchCount := make(map[hooks.Hook]int, len(*plan.args.hooks))
|
hookMatchCount := make(map[hooks.Hook]int, len(*plan.args.hooks))
|
||||||
@@ -85,7 +68,7 @@ func (plan *plan) execute(ctx context.Context, dryRun bool) (ok bool) {
|
|||||||
anyFsHadErr := false
|
anyFsHadErr := false
|
||||||
// TODO channel programs -> allow a little jitter?
|
// TODO channel programs -> allow a little jitter?
|
||||||
for fs, progress := range plan.snaps {
|
for fs, progress := range plan.snaps {
|
||||||
suffix := plan.formatNow(plan.args.timestampFormat)
|
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||||
snapname := fmt.Sprintf("%s%s", plan.args.prefix, suffix)
|
snapname := fmt.Sprintf("%s%s", plan.args.prefix, suffix)
|
||||||
|
|
||||||
ctx := logging.WithInjectedField(ctx, "fs", fs.ToString())
|
ctx := logging.WithInjectedField(ctx, "fs", fs.ToString())
|
||||||
|
|||||||
+17
-12
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/hooks"
|
"github.com/zrepl/zrepl/daemon/hooks"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ func periodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.Snap
|
|||||||
if in.Prefix == "" {
|
if in.Prefix == "" {
|
||||||
return nil, errors.New("prefix must not be empty")
|
return nil, errors.New("prefix must not be empty")
|
||||||
}
|
}
|
||||||
if in.Interval.Duration() <= 0 {
|
if in.Interval <= 0 {
|
||||||
return nil, errors.New("interval must be positive")
|
return nil, errors.New("interval must be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,11 +32,10 @@ func periodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.Snap
|
|||||||
}
|
}
|
||||||
|
|
||||||
args := periodicArgs{
|
args := periodicArgs{
|
||||||
interval: in.Interval.Duration(),
|
interval: in.Interval,
|
||||||
fsf: fsf,
|
fsf: fsf,
|
||||||
planArgs: planArgs{
|
planArgs: planArgs{
|
||||||
prefix: in.Prefix,
|
prefix: in.Prefix,
|
||||||
timestampFormat: in.TimestampFormat,
|
|
||||||
hooks: hookList,
|
hooks: hookList,
|
||||||
},
|
},
|
||||||
// ctx and log is set in Run()
|
// ctx and log is set in Run()
|
||||||
@@ -173,13 +171,16 @@ func periodicStateSyncUp(a periodicArgs, u updater) state {
|
|||||||
u(func(s *Periodic) {
|
u(func(s *Periodic) {
|
||||||
s.sleepUntil = syncPoint
|
s.sleepUntil = syncPoint
|
||||||
})
|
})
|
||||||
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, syncPoint)
|
t := time.NewTimer(time.Until(syncPoint))
|
||||||
if ctxDone != nil {
|
defer t.Stop()
|
||||||
return onMainCtxDone(a.ctx, u)
|
select {
|
||||||
}
|
case <-t.C:
|
||||||
return u(func(s *Periodic) {
|
return u(func(s *Periodic) {
|
||||||
s.state = Planning
|
s.state = Planning
|
||||||
}).sf()
|
}).sf()
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return onMainCtxDone(a.ctx, u)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func periodicStatePlan(a periodicArgs, u updater) state {
|
func periodicStatePlan(a periodicArgs, u updater) state {
|
||||||
@@ -240,13 +241,17 @@ func periodicStateWait(a periodicArgs, u updater) state {
|
|||||||
logFunc("enter wait-state after error")
|
logFunc("enter wait-state after error")
|
||||||
})
|
})
|
||||||
|
|
||||||
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, sleepUntil)
|
t := time.NewTimer(time.Until(sleepUntil))
|
||||||
if ctxDone != nil {
|
defer t.Stop()
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
select {
|
||||||
|
case <-t.C:
|
||||||
return u(func(snapper *Periodic) {
|
return u(func(snapper *Periodic) {
|
||||||
snapper.state = Planning
|
snapper.state = Planning
|
||||||
}).sf()
|
}).sf()
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return onMainCtxDone(a.ctx, u)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
||||||
|
|||||||
+452
-724
File diff suppressed because it is too large
Load Diff
Vendored
-23
@@ -1,23 +0,0 @@
|
|||||||
#!/sbin/openrc-run
|
|
||||||
|
|
||||||
command='/usr/local/bin/zrepl'
|
|
||||||
command_args='daemon'
|
|
||||||
command_background='true'
|
|
||||||
pidfile="/run/${RC_SVCNAME}.pid"
|
|
||||||
output_log="/var/log/${RC_SVCNAME}.log"
|
|
||||||
error_log="/var/log/${RC_SVCNAME}.log"
|
|
||||||
|
|
||||||
zrepl_runtime_dir='/var/run/zrepl'
|
|
||||||
|
|
||||||
start() {
|
|
||||||
mkdir -p "$zrepl_runtime_dir/stdinserver"
|
|
||||||
chmod -R 0700 "$zrepl_runtime_dir"
|
|
||||||
default_start
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
rm -rf "$zrepl_runtime_dir"
|
|
||||||
default_stop
|
|
||||||
}
|
|
||||||
|
|
||||||
# vi: noet sw=8 sts=0
|
|
||||||
Vendored
+1
-5
@@ -9,9 +9,6 @@ ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
|
|||||||
RuntimeDirectory=zrepl zrepl/stdinserver
|
RuntimeDirectory=zrepl zrepl/stdinserver
|
||||||
RuntimeDirectoryMode=0700
|
RuntimeDirectoryMode=0700
|
||||||
|
|
||||||
# Make Go produce coredumps
|
|
||||||
Environment=GOTRACEBACK='crash'
|
|
||||||
|
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
#PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this?
|
#PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this?
|
||||||
ProtectKernelTunables=yes
|
ProtectKernelTunables=yes
|
||||||
@@ -30,8 +27,7 @@ ProtectHome=read-only
|
|||||||
# SystemCallFilter
|
# SystemCallFilter
|
||||||
# ~@privileged doesn't work with Ubuntu 18.04 ssh
|
# ~@privileged doesn't work with Ubuntu 18.04 ssh
|
||||||
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources
|
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources
|
||||||
# Go1.19 added automatic RLIMIT_NOFILE changes, so, we need to allow that
|
|
||||||
SystemCallFilter= setrlimit
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
+2
-2
@@ -10,11 +10,11 @@ BUILDDIR = _build
|
|||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
# Put it first so that "make" without argument is like "make help".
|
||||||
help:
|
help:
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" -c sphinxconf $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
.PHONY: help Makefile
|
.PHONY: help Makefile
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
%: Makefile
|
%: Makefile
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" -c sphinxconf $(SPHINXOPTS) $(O)
|
||||||
|
|||||||
Vendored
-43
@@ -1,43 +0,0 @@
|
|||||||
/* https://github.com/sphinx-contrib/sphinxcontrib-versioning/blob/0b56210959c6b21bbb730072e81876491b4e1371/sphinxcontrib/versioning/_static/banner.css */
|
|
||||||
|
|
||||||
.scv-banner {
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-size: 80%;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
background: #d40 linear-gradient(-45deg,
|
|
||||||
rgba(255, 255, 255, 0.2) 0%,
|
|
||||||
rgba(255, 255, 255, 0.2) 25%,
|
|
||||||
transparent 25%,
|
|
||||||
transparent 50%,
|
|
||||||
rgba(255, 255, 255, 0.2) 50%,
|
|
||||||
rgba(255, 255, 255, 0.2) 75%,
|
|
||||||
transparent 75%,
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
background-size: 28px 28px;
|
|
||||||
}
|
|
||||||
.scv-banner > a {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.scv-sphinx_rtd_theme {
|
|
||||||
background-color: #2980B9;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.scv-bizstyle {
|
|
||||||
background-color: #336699;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.scv-classic {
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.scv-traditional {
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
{% extends "!page.html" %}
|
|
||||||
{% block body %}
|
|
||||||
{% if current_version and latest_version and current_version != latest_version %}
|
|
||||||
<p class="scv-banner scv-sphinx_rtd_theme">
|
|
||||||
<strong>
|
|
||||||
{% if current_version.is_released %}
|
|
||||||
You're reading an old version of this documentation.
|
|
||||||
If you want up-to-date information, please have a look at <a href="{{ vpathto(latest_version.name) }}">{{latest_version.name}}</a>.
|
|
||||||
{% else %}
|
|
||||||
You're reading the documentation for a development version.
|
|
||||||
For the latest released version, please have a look at <a href="{{ vpathto(latest_version.name) }}">{{latest_version.name}}</a>.
|
|
||||||
{% endif %}
|
|
||||||
</strong>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}%
|
|
||||||
Vendored
-27
@@ -1,27 +0,0 @@
|
|||||||
{%- if current_version %}
|
|
||||||
<div class="rst-versions" data-toggle="rst-versions" role="note" aria-label="versions">
|
|
||||||
<span class="rst-current-version" data-toggle="rst-current-version">
|
|
||||||
<span class="fa fa-book"> Other Versions</span>
|
|
||||||
v: {{ current_version.name }}
|
|
||||||
<span class="fa fa-caret-down"></span>
|
|
||||||
</span>
|
|
||||||
<div class="rst-other-versions">
|
|
||||||
{%- if versions.tags %}
|
|
||||||
<dl>
|
|
||||||
<dt>Tags</dt>
|
|
||||||
{%- for item in versions.tags %}
|
|
||||||
<dd><a href="{{ item.url }}">{{ item.name }}</a></dd>
|
|
||||||
{%- endfor %}
|
|
||||||
</dl>
|
|
||||||
{%- endif %}
|
|
||||||
{%- if versions.branches %}
|
|
||||||
<dl>
|
|
||||||
<dt>Branches</dt>
|
|
||||||
{%- for item in versions.branches %}
|
|
||||||
<dd><a href="{{ item.url }}">{{ item.name }}</a></dd>
|
|
||||||
{%- endfor %}
|
|
||||||
</dl>
|
|
||||||
{%- endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{%- endif %}
|
|
||||||
+13
-62
@@ -16,63 +16,13 @@ Changelog
|
|||||||
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
||||||
Developers should consult the git commit log or GitHub issue tracker.
|
Developers should consult the git commit log or GitHub issue tracker.
|
||||||
|
|
||||||
Next Release
|
0.6 (Unreleased)
|
||||||
------------
|
----------------
|
||||||
|
|
||||||
The plan for the next release is to revisit how zrepl does snapshot management.
|
* `Feature Wishlist on GitHub <https://github.com/zrepl/zrepl/discussions/547>`_
|
||||||
High-level goals:
|
|
||||||
|
|
||||||
- Make it easy to decouple snapshot management (snapshotting, pruning) from replication.
|
|
||||||
- Ability to include/exclude snapshots from replication.
|
|
||||||
This is useful for aforementioned decoupling, e.g., separate snapshot prefixes for local & remote replication.
|
|
||||||
Also, it makes explicit that by default, zrepl replicates all snapshots, and that
|
|
||||||
replication has no concept of "zrepl-created snapshots", which is a common misconception.
|
|
||||||
- Use of ``zfs snapshot`` comma syntax or channel programs to take snapshots of multiple
|
|
||||||
datasets atomically.
|
|
||||||
- Provide an alternative to the ``grid`` pruning policy.
|
|
||||||
Most likely something based on hourly/daily/weekly/monthly "trains" plus a count.
|
|
||||||
- Ability to prune at the granularity of the **group** of snapshots created at a given
|
|
||||||
time, as opposed to the individual snapshots within a dataset.
|
|
||||||
Maybe this will be addressed by the alternative to the ``grid`` pruning policy,
|
|
||||||
as it will likely be more predictable.
|
|
||||||
|
|
||||||
Those changes will likely come with some breakage in the config.
|
|
||||||
However, I want to avoid breaking **use cases** that are satisfied by the current design.
|
|
||||||
There will be beta/RC releases to give users a chance to evaluate.
|
|
||||||
|
|
||||||
0.6.1
|
|
||||||
-----
|
|
||||||
|
|
||||||
* |feature| add metric to detect filesystems rules that don't match any local dataset (thanks, `@gmekicaxcient <https://github.com/gmekicaxcient>`_).
|
|
||||||
* |bugfix| ``zrepl status``: hide progress bar once all filesystems reach terminal state (thanks, `@0x3333 <https://github.com/0x3333>`_).
|
|
||||||
* |bugfix| handling of tenative cursor presence if protection strategy doesn't use it (:issue:`714`).
|
|
||||||
* |docs| address setup with two or more external disks (thanks, `@se-jaeger <https://github.com/se-jaeger>`_).
|
|
||||||
* |docs| document ``replication`` and ``conflict_resolution`` options (thanks, `@InsanePrawn <https://github.com/InsanePrawn>`_).
|
|
||||||
* |docs| docs: talks: add note on keep_bookmarks option (thanks, `@skirmess <https://github.com/skirmess>`_).
|
|
||||||
* |maint| dist: add openrc service file (thanks, `@gramosg <https://github.com/gramosg>`_).
|
|
||||||
* |maint| grafana: update dashboard to Grafana 9.3.6.
|
|
||||||
* |maint| run platform tests as part of CI.
|
|
||||||
* |maint| build: upgrade to Go 1.21 and update golangci-lint; minimum Go version for builds is now 1.20
|
|
||||||
|
|
||||||
.. 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 GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
|
||||||
| Note that PayPal processing fees are relatively high for small donations.
|
|
||||||
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
|
||||||
|
|
||||||
0.6
|
|
||||||
---
|
|
||||||
|
|
||||||
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
|
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
|
||||||
* |feature| Configurable initial replication policy.
|
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited).
|
||||||
When a filesystem is first replicated to a receiver, this control whether just the newest
|
|
||||||
snapshot will be replicated vs. all existing snapshots. Learn more :ref:`in the docs <conflict_resolution-initial_replication>`.
|
|
||||||
* |feature| Configurable timestamp format for snapshot names via :ref:`timestamp_format<job-snapshotting-timestamp_format>`
|
|
||||||
(Thanks, `@ydylla <https://github.com/ydylla>`_).
|
|
||||||
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited)
|
|
||||||
(Thanks, `@3nprob <https://github.com/3nprob>`_).
|
|
||||||
* |feature| Add ``zrepl configcheck --skip-cert-check`` flag (Thanks, `@cole-h <https://github.com/cole-h>`_).
|
|
||||||
* |bugfix| Fix resuming from interrupted replications that use ``send.raw`` on unencrypted datasets.
|
* |bugfix| Fix resuming from interrupted replications that use ``send.raw`` on unencrypted datasets.
|
||||||
|
|
||||||
* The send options introduced in zrepl 0.4 allow users to specify additional zfs send flags for zrepl to use.
|
* The send options introduced in zrepl 0.4 allow users to specify additional zfs send flags for zrepl to use.
|
||||||
@@ -84,7 +34,7 @@ There will be beta/RC releases to give users a chance to evaluate.
|
|||||||
* However, this means that the ``zrepl status`` UI no longer indicates whether a replication step uses encrypted sends or not.
|
* However, this means that the ``zrepl status`` UI no longer indicates whether a replication step uses encrypted sends or not.
|
||||||
The setting is still effective though.
|
The setting is still effective though.
|
||||||
|
|
||||||
* |break| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
|
* |break| |feature| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
|
||||||
|
|
||||||
* The metric still reports the zrepl version in a label.
|
* The metric still reports the zrepl version in a label.
|
||||||
But the metric *value* is now the Unix timestamp at the time the daemon was started.
|
But the metric *value* is now the Unix timestamp at the time the daemon was started.
|
||||||
@@ -93,13 +43,6 @@ There will be beta/RC releases to give users a chance to evaluate.
|
|||||||
* |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF``
|
* |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF``
|
||||||
* |bugfix| don't treat receive-side bookmarks as a replication conflict.
|
* |bugfix| don't treat receive-side bookmarks as a replication conflict.
|
||||||
This facilitates chaining of replication jobs. See :issue:`490`.
|
This facilitates chaining of replication jobs. See :issue:`490`.
|
||||||
* |bugfix| workaround for Go/gRPC problem on Illumos where zrepl would
|
|
||||||
crash when using the ``local`` transport type (:issue:`598`).
|
|
||||||
* |bugfix| fix active child tasks panic that cold occur during replication plannig (:issue:`193abbe`)
|
|
||||||
* |bugfix| ``zrepl status`` off-by-one error in display of completed step count (:commit:`ce6701f`)
|
|
||||||
* |bugfix| Allow using day & week units for ``snapshotting.interval`` (:commit:`ffb1d89`)
|
|
||||||
* |docs| ``docs/overview`` improvements (Thanks, `@jtagcat <https://github.com/jtagcat>`_).
|
|
||||||
* |maint| Update to Go 1.19.
|
|
||||||
|
|
||||||
0.5
|
0.5
|
||||||
---
|
---
|
||||||
@@ -132,6 +75,14 @@ Note to all users: please read up on the following OpenZFS bugs, as you might be
|
|||||||
|
|
||||||
Finally, I'd like to point you to the `GitHub discussion <https://github.com/zrepl/zrepl/discussions/547>`_ about which bugfixes and features should be prioritized in zrepl 0.6 and beyond!
|
Finally, I'd like to point you to the `GitHub discussion <https://github.com/zrepl/zrepl/discussions/547>`_ about which bugfixes and features should be prioritized in zrepl 0.6 and beyond!
|
||||||
|
|
||||||
|
.. 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 GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||||
|
| Note that PayPal processing fees are relatively high for small donations.
|
||||||
|
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
|
|
||||||
0.4.0
|
0.4.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
-187
@@ -1,187 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# zrepl documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Wed Nov 8 22:28:10 2017.
|
|
||||||
#
|
|
||||||
# This file is execfile()d with the current directory set to its
|
|
||||||
# containing dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
#
|
|
||||||
# import os
|
|
||||||
# import sys
|
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
|
||||||
#
|
|
||||||
# needs_sphinx = '1.0'
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
||||||
# ones.
|
|
||||||
extensions = ['sphinx.ext.todo',
|
|
||||||
'sphinx.ext.githubpages',
|
|
||||||
'sphinx.ext.extlinks',
|
|
||||||
"sphinx_multiversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
# suppress_warnings = ['image.nonlocal_uri']
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['./_templates']
|
|
||||||
|
|
||||||
# The suffix(es) of source filenames.
|
|
||||||
# You can specify multiple suffix as a list of string:
|
|
||||||
#
|
|
||||||
# source_suffix = ['.rst', '.md']
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = 'zrepl'
|
|
||||||
copyright = '2017-2023, Christian Schwarz'
|
|
||||||
author = 'Christian Schwarz'
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
#
|
|
||||||
# The short X.Y version.
|
|
||||||
#version = set by sphinxcontrib-versioning
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
#release = version
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#
|
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
|
||||||
# Usually you set "language" from the command line for these cases.
|
|
||||||
language = 'en'
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
# This patterns also effect to html_static_path and html_extra_path
|
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
|
||||||
todo_include_todos = True
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
#
|
|
||||||
html_theme = 'sphinx_rtd_theme'
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
#
|
|
||||||
# html_theme_options = {}
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
html_css_files = [
|
|
||||||
'banner.css',
|
|
||||||
]
|
|
||||||
|
|
||||||
html_logo = '_static/zrepl.svg'
|
|
||||||
|
|
||||||
html_context = {
|
|
||||||
# https://github.com/rtfd/sphinx_rtd_theme/issues/205
|
|
||||||
# Add 'Edit on Github' link instead of 'View page source'
|
|
||||||
"display_github": True,
|
|
||||||
"github_user": "zrepl",
|
|
||||||
"github_repo": "zrepl",
|
|
||||||
"github_version": "master",
|
|
||||||
"conf_py_path": "/docs/",
|
|
||||||
"source_suffix": source_suffix,
|
|
||||||
}
|
|
||||||
|
|
||||||
# -- Options for HTMLHelp output ------------------------------------------
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'zrepldoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
|
||||||
|
|
||||||
latex_elements = {
|
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
|
||||||
#
|
|
||||||
# 'papersize': 'letterpaper',
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
#
|
|
||||||
# 'pointsize': '10pt',
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
#
|
|
||||||
# 'preamble': '',
|
|
||||||
|
|
||||||
# Latex figure (float) alignment
|
|
||||||
#
|
|
||||||
# 'figure_align': 'htbp',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title,
|
|
||||||
# author, documentclass [howto, manual, or own class]).
|
|
||||||
latex_documents = [
|
|
||||||
(master_doc, 'zrepl.tex', 'zrepl Documentation',
|
|
||||||
'Christian Schwarz', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for manual page output ---------------------------------------
|
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
|
||||||
# (source start file, name, description, authors, manual section).
|
|
||||||
man_pages = [
|
|
||||||
(master_doc, 'zrepl', 'zrepl Documentation',
|
|
||||||
[author], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output -------------------------------------------
|
|
||||||
|
|
||||||
# Grouping the document tree into Texinfo files. List of tuples
|
|
||||||
# (source start file, target name, title, author,
|
|
||||||
# dir menu entry, description, category)
|
|
||||||
texinfo_documents = [
|
|
||||||
(master_doc, 'zrepl', 'zrepl Documentation',
|
|
||||||
author, 'zrepl', 'One line description of project.',
|
|
||||||
'Miscellaneous'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for the extlinks extension -----------------------------------
|
|
||||||
# http://www.sphinx-doc.org/en/stable/ext/extlinks.html
|
|
||||||
extlinks = {
|
|
||||||
'issue':('https://github.com/zrepl/zrepl/issues/%s', 'issue #%s'),
|
|
||||||
'repomasterlink':('https://github.com/zrepl/zrepl/blob/master/%s', '%s'),
|
|
||||||
'sampleconf':('https://github.com/zrepl/zrepl/blob/master/config/samples%s', 'config/samples%s'),
|
|
||||||
'commit':('https://github.com/zrepl/zrepl/commit/%s', 'commit %s'),
|
|
||||||
}
|
|
||||||
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
sphinxconf/conf.py
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
.. include:: ../global.rst.inc
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
.. _conflict_resolution-options:
|
|
||||||
|
|
||||||
Conflict Resolution Options
|
Conflict Resolution Options
|
||||||
===========================
|
===========================
|
||||||
@@ -16,7 +15,7 @@ Conflict Resolution Options
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
.. _conflict_resolution-initial_replication:
|
.. _conflict_resolution-initial_replication-option-send_all_snapshots:
|
||||||
|
|
||||||
|
|
||||||
``initial_replication`` option
|
``initial_replication`` option
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ Job Type ``push``
|
|||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
- |pruning-spec|
|
- |pruning-spec|
|
||||||
* - ``replication``
|
|
||||||
- |replication-options|
|
|
||||||
* - ``conflict_resolution``
|
|
||||||
- |conflict-resolution-options|
|
|
||||||
|
|
||||||
Example config: :sampleconf:`/push.yml`
|
Example config: :sampleconf:`/push.yml`
|
||||||
|
|
||||||
@@ -85,10 +81,6 @@ Job Type ``pull``
|
|||||||
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
|
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
- |pruning-spec|
|
- |pruning-spec|
|
||||||
* - ``replication``
|
|
||||||
- |replication-options|
|
|
||||||
* - ``conflict_resolution``
|
|
||||||
- |conflict-resolution-options|
|
|
||||||
|
|
||||||
Example config: :sampleconf:`/pull.yml`
|
Example config: :sampleconf:`/pull.yml`
|
||||||
|
|
||||||
|
|||||||
@@ -44,3 +44,27 @@ Interval & duration fields in job definitions, pruning configurations, etc. must
|
|||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
||||||
// s = second, m = minute, h = hour, d = day, w = week (7 days)
|
// s = second, m = minute, h = hour, d = day, w = week (7 days)
|
||||||
|
|
||||||
|
Super-Verbose Job Debugging
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
You have probably landed here because you opened an issue on GitHub and some developer told you to do this...
|
||||||
|
So just read the annotated comments ;)
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
job:
|
||||||
|
- name: ...
|
||||||
|
...
|
||||||
|
# JOB DEBUGGING OPTIONS
|
||||||
|
# should be equal for all job types, but each job implements the debugging itself
|
||||||
|
debug:
|
||||||
|
conn: # debug the io.ReadWriteCloser connection
|
||||||
|
read_dump: /tmp/connlog_read # dump results of Read() invocations to this file
|
||||||
|
write_dump: /tmp/connlog_write # dump results of Write() invocations to this file
|
||||||
|
rpc: # debug the RPC protocol implementation
|
||||||
|
log: true # log output from rpc layer to the job log
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
Connection dumps will almost certainly contain your or other's private data. Do not share it in a bug report.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
.. include:: ../global.rst.inc
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
.. _replication-options:
|
|
||||||
|
|
||||||
Replication Options
|
Replication Options
|
||||||
===================
|
===================
|
||||||
|
|||||||
@@ -197,8 +197,11 @@ Mount behaviour
|
|||||||
* ``canmount``
|
* ``canmount``
|
||||||
* ``overlay``
|
* ``overlay``
|
||||||
|
|
||||||
Note: Before `OpenZFS 2.0.5 <https://github.com/openzfs/zfs/issues/11416>`_, inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``.
|
Note: inheriting or overriding the ``mountpoint`` property on ZVOLs fails in ``zfs recv``.
|
||||||
If you are on such an older version, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
|
This is an `issue in OpenZFS <https://github.com/openzfs/zfs/issues/11416>`_ .
|
||||||
|
As a workaround, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
|
||||||
|
Please comment at zrepl :issue:`430` if you encounter this issue and/or would like zrepl to automatically work around it.
|
||||||
|
|
||||||
|
|
||||||
Systemd
|
Systemd
|
||||||
-------
|
-------
|
||||||
|
|||||||
@@ -62,9 +62,6 @@ The ``periodic`` and ``cron`` snapshotting types share some common options and b
|
|||||||
type: periodic
|
type: periodic
|
||||||
prefix: zrepl_
|
prefix: zrepl_
|
||||||
interval: 10m
|
interval: 10m
|
||||||
# Timestamp format that is used as snapshot suffix.
|
|
||||||
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
|
|
||||||
timestamp_format: dense
|
|
||||||
hooks: ...
|
hooks: ...
|
||||||
pruning: ...
|
pruning: ...
|
||||||
|
|
||||||
@@ -94,9 +91,6 @@ The snapshotter uses the ``prefix`` to identify which snapshots it created.
|
|||||||
# (second, optional) minute hour day-of-month month day-of-week
|
# (second, optional) minute hour day-of-month month day-of-week
|
||||||
# This example takes snapshots daily at 3:00.
|
# This example takes snapshots daily at 3:00.
|
||||||
cron: "0 3 * * *"
|
cron: "0 3 * * *"
|
||||||
# Timestamp format that is used as snapshot suffix.
|
|
||||||
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
|
|
||||||
timestamp_format: dense
|
|
||||||
pruning: ...
|
pruning: ...
|
||||||
|
|
||||||
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time.
|
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time.
|
||||||
@@ -104,21 +98,6 @@ See https://en.wikipedia.org/wiki/Cron for details on the syntax.
|
|||||||
zrepl uses the ``the github.com/robfig/cron/v3`` Go package for parsing.
|
zrepl uses the ``the github.com/robfig/cron/v3`` Go package for parsing.
|
||||||
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
|
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
|
||||||
|
|
||||||
.. _job-snapshotting-timestamp_format:
|
|
||||||
|
|
||||||
Timestamp Format
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The ``cron`` and ``periodic`` snapshotter support configuring a custom timestamp format that is used as suffix for the snapshot name.
|
|
||||||
It can be used by setting ``timestamp_format`` to any of the following values:
|
|
||||||
|
|
||||||
* ``dense`` (default) looks like ``20060102_150405_000``
|
|
||||||
* ``human`` looks like ``2006-01-02_15:04:05``
|
|
||||||
* ``iso-8601`` looks like ``2006-01-02T15:04:05.000Z``
|
|
||||||
* ``unix-seconds`` looks like ``1136214245``
|
|
||||||
* Any custom Go time format accepted by `time.Time#Format <https://go.dev/src/time/format.go>`_.
|
|
||||||
|
|
||||||
|
|
||||||
``manual`` Snapshotting
|
``manual`` Snapshotting
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import argparse
|
|
||||||
import distutils
|
|
||||||
|
|
||||||
argparser = argparse.ArgumentParser()
|
|
||||||
argparser.add_argument("docsroot")
|
|
||||||
argparser.add_argument("outdir")
|
|
||||||
args = argparser.parse_args()
|
|
||||||
|
|
||||||
output = subprocess.run(["git", "tag", "-l"], capture_output=True, check=True, text=True)
|
output = subprocess.run(["git", "tag", "-l"], capture_output=True, check=True, text=True)
|
||||||
tagRE = re.compile(r"^v(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<rc>\d+))?$")
|
tagRE = re.compile(r"^v(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<rc>\d+))?$")
|
||||||
@@ -57,16 +49,26 @@ for (mm, l) in by_major_minor.items():
|
|||||||
latest_by_major_minor.append(l[-1])
|
latest_by_major_minor.append(l[-1])
|
||||||
latest_by_major_minor.sort(key=lambda tag: (tag.major, tag.minor))
|
latest_by_major_minor.sort(key=lambda tag: (tag.major, tag.minor))
|
||||||
|
|
||||||
cmdline = [
|
# print(by_major_minor)
|
||||||
"sphinx-multiversion",
|
# print(latest_by_major_minor)
|
||||||
"-D", "smv_tag_whitelist=^({})$".format("|".join([re.escape(tag.orig) for tag in latest_by_major_minor])),
|
|
||||||
"-D", "smv_branch_whitelist=^(master|stable)$",
|
cmdline = []
|
||||||
"-D", "smv_remote_whitelist=^.*$",
|
|
||||||
"-D", "smv_latest_version=stable",
|
for latest_patch in latest_by_major_minor:
|
||||||
"-D", r"smv_released_pattern=^refs/(tags|heads|remotes/[^/]+)/(?!master).*$", # treat everything except master as released, that way, the banner message makes sense
|
cmdline.append("--whitelist-tags")
|
||||||
# "--dump-metadata", # for debugging
|
cmdline.append(f"^{re.escape(latest_patch.orig)}$")
|
||||||
args.docsroot,
|
|
||||||
args.outdir,
|
# we want flexibility to update docs for the latest stable release
|
||||||
]
|
# => we have a branch for that, called `stable` which we move manually
|
||||||
print(cmdline)
|
# TODO: in the future, have f"stable-{latest_by_major_minor[-1]}"
|
||||||
subprocess.run(cmdline, check=True)
|
default_version = "stable"
|
||||||
|
cmdline.extend(["--whitelist-branches", default_version])
|
||||||
|
|
||||||
|
cmdline.extend(["--root-ref", f"{default_version}"])
|
||||||
|
cmdline.extend(["--banner-main-ref", f"{default_version}"])
|
||||||
|
cmdline.extend(["--show-banner"])
|
||||||
|
cmdline.extend(["--sort", "semver"])
|
||||||
|
|
||||||
|
cmdline.extend(["--whitelist-branches", "master"])
|
||||||
|
|
||||||
|
print(" ".join(cmdline))
|
||||||
@@ -26,8 +26,6 @@
|
|||||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
||||||
.. |send-options| replace:: :ref:`send options<job-send-options>`, e.g. for encrypted sends
|
.. |send-options| replace:: :ref:`send options<job-send-options>`, e.g. for encrypted sends
|
||||||
.. |recv-options| replace:: :ref:`recv options<job-recv-options>`
|
.. |recv-options| replace:: :ref:`recv options<job-recv-options>`
|
||||||
.. |replication-options| replace:: :ref:`replication options<replication-options>`
|
|
||||||
.. |conflict-resolution-options| replace:: :ref:`conflict resolution options<conflict_resolution-options>`
|
|
||||||
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
||||||
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
||||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||||
|
|||||||
@@ -15,5 +15,3 @@ Talks & Presentations
|
|||||||
`Event <https://wiki.freebsd.org/DevSummit/201709>`__
|
`Event <https://wiki.freebsd.org/DevSummit/201709>`__
|
||||||
)
|
)
|
||||||
|
|
||||||
* Note: The remarks on ``keep_bookmarks`` are irrelevant as of zrepl 0.1 which introduced the zrepl-managed replication cursor bookmark.
|
|
||||||
Read the `Overview <overview-how-replication-works>`_ section to learn more.
|
|
||||||
|
|||||||
+15
-17
@@ -3,8 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
NON_INTERACTIVE=false
|
NON_INTERACTIVE=false
|
||||||
DO_CLONE=false
|
DO_CLONE=false
|
||||||
PUSH=false
|
while getopts "ca" arg; do
|
||||||
while getopts "caP" arg; do
|
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
"a")
|
"a")
|
||||||
NON_INTERACTIVE=true
|
NON_INTERACTIVE=true
|
||||||
@@ -12,9 +11,6 @@ while getopts "caP" arg; do
|
|||||||
"c")
|
"c")
|
||||||
DO_CLONE=true
|
DO_CLONE=true
|
||||||
;;
|
;;
|
||||||
"P")
|
|
||||||
PUSH=true
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "invalid option '-$arg'"
|
echo "invalid option '-$arg'"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -30,8 +26,8 @@ checkout_repo_msg() {
|
|||||||
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! type sphinx-multiversion >/dev/null; then
|
if ! type sphinx-versioning >/dev/null; then
|
||||||
echo "install sphinx-multiversion and come back"
|
echo "install sphinx-versioning and come back"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -77,15 +73,22 @@ popd
|
|||||||
|
|
||||||
echo "building site"
|
echo "building site"
|
||||||
|
|
||||||
python3 run-sphinx-multiversion.py . ./public_git
|
flags="$(python3 gen-sphinx-versioning-flags.py)"
|
||||||
|
set -e
|
||||||
|
sphinx-versioning build \
|
||||||
|
$flags \
|
||||||
|
docs ./public_git \
|
||||||
|
-- -c sphinxconf # older conf.py throw errors because they used
|
||||||
|
# version = subprocess.show_output(["git", "describe"])
|
||||||
|
# which fails when building with sphinxcontrib-versioning
|
||||||
|
set +e
|
||||||
|
|
||||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||||
git status --porcelain
|
git status --porcelain
|
||||||
if [[ "$(git status --porcelain)" != "" ]]; then
|
if [[ "$(git status --porcelain)" != "" ]]; then
|
||||||
CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)"
|
CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)"
|
||||||
fi
|
fi
|
||||||
COMMIT_MSG="render from publish.sh - $(date -u) - ${CURRENT_COMMIT}"
|
COMMIT_MSG="sphinx-versioning render from publish.sh - $(date -u) - ${CURRENT_COMMIT}"
|
||||||
|
|
||||||
pushd "$PUBLICDIR"
|
pushd "$PUBLICDIR"
|
||||||
|
|
||||||
@@ -97,11 +100,6 @@ if [ "$(git status --porcelain)" != "" ]; then
|
|||||||
else
|
else
|
||||||
echo "nothing to commit"
|
echo "nothing to commit"
|
||||||
fi
|
fi
|
||||||
|
echo "pushing to GitHub pages repo"
|
||||||
if $PUSH; then
|
git push origin master
|
||||||
echo "pushing to GitHub pages repo"
|
|
||||||
git push origin master
|
|
||||||
else
|
|
||||||
echo "not pushing to GitHub pages repo, set -P flag to push"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,17 +33,4 @@ You will likely want to customize some aspects mentioned in the top comment in t
|
|||||||
|
|
||||||
.. literalinclude:: ../../config/samples/quickstart_backup_to_external_disk.yml
|
.. literalinclude:: ../../config/samples/quickstart_backup_to_external_disk.yml
|
||||||
|
|
||||||
|
|
||||||
Offline Backups with two (or more) External Disks
|
|
||||||
-------------------------------------------------
|
|
||||||
|
|
||||||
It can be desirable to have multiple disk-based backups of the same machine.
|
|
||||||
To accomplish this,
|
|
||||||
|
|
||||||
* create one zpool per external HDD, each with a unique name, and
|
|
||||||
* define a pair of ``push`` and ``sink`` job **for each** of these zpools, each with a unique ``name``, ``listener_name``, and ``root_fs``.
|
|
||||||
|
|
||||||
The unique names ensure that the jobs don't step on each others' toes when managing :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` .
|
|
||||||
|
|
||||||
|
|
||||||
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||||
+29
-23
@@ -1,24 +1,30 @@
|
|||||||
alabaster==0.7.13
|
alabaster==0.7.12
|
||||||
Babel==2.12.1
|
attrs==19.1.0
|
||||||
certifi==2023.7.22
|
Babel==2.7.0
|
||||||
charset-normalizer==3.2.0
|
certifi==2019.6.16
|
||||||
docutils==0.18.1
|
chardet==3.0.4
|
||||||
idna==3.4
|
Click==7.0
|
||||||
imagesize==1.4.1
|
colorclass==2.2.0
|
||||||
Jinja2==3.1.2
|
docutils==0.15.2
|
||||||
MarkupSafe==2.1.3
|
idna==2.8
|
||||||
packaging==23.1
|
imagesize==1.1.0
|
||||||
Pygments==2.16.1
|
Jinja2==2.10.1
|
||||||
requests==2.31.0
|
MarkupSafe==1.1.1
|
||||||
snowballstemmer==2.2.0
|
packaging==19.1
|
||||||
Sphinx==7.2.5
|
Pygments==2.4.2
|
||||||
sphinx-multiversion @ git+https://github.com/zrepl/sphinx-multiversion/@52c915d7ad898d9641ec48c8bbccb7d4f079db93
|
pyparsing==2.4.2
|
||||||
sphinx-rtd-theme==1.3.0
|
pytz==2019.2
|
||||||
sphinxcontrib-applehelp==1.0.7
|
requests==2.22.0
|
||||||
sphinxcontrib-devhelp==1.0.5
|
six==1.12.0
|
||||||
sphinxcontrib-htmlhelp==2.0.4
|
snowballstemmer==1.9.1
|
||||||
sphinxcontrib-jquery==4.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-jsmath==1.0.1
|
||||||
sphinxcontrib-qthelp==1.0.6
|
sphinxcontrib-qthelp==1.0.2
|
||||||
sphinxcontrib-serializinghtml==1.1.9
|
sphinxcontrib-serializinghtml==1.1.3
|
||||||
urllib3==2.0.4
|
git+https://github.com/rwblair/sphinxcontrib-versioning.git@7e3885a389a809e17ea55261316b7b0e98dbf98f#egg=sphinxcontrib-versioning
|
||||||
|
sphinxcontrib-websupport==1.1.2
|
||||||
|
urllib3==1.25.3
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# zrepl documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Wed Nov 8 22:28:10 2017.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
# import os
|
||||||
|
# import sys
|
||||||
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = ['sphinx.ext.todo',
|
||||||
|
'sphinx.ext.githubpages',
|
||||||
|
'sphinx.ext.extlinks']
|
||||||
|
|
||||||
|
suppress_warnings = ['image.nonlocal_uri']
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['../_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = 'zrepl'
|
||||||
|
copyright = '2017-2019, Christian Schwarz'
|
||||||
|
author = 'Christian Schwarz'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
#version = set by sphinxcontrib-versioning
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
#release = version
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['../_static']
|
||||||
|
|
||||||
|
|
||||||
|
html_logo = '../_static/zrepl.svg'
|
||||||
|
|
||||||
|
html_context = {
|
||||||
|
# https://github.com/rtfd/sphinx_rtd_theme/issues/205
|
||||||
|
# Add 'Edit on Github' link instead of 'View page source'
|
||||||
|
"display_github": True,
|
||||||
|
"github_user": "zrepl",
|
||||||
|
"github_repo": "zrepl",
|
||||||
|
"github_version": "master",
|
||||||
|
"conf_py_path": "/docs/",
|
||||||
|
"source_suffix": source_suffix,
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'zrepldoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'zrepl.tex', 'zrepl Documentation',
|
||||||
|
'Christian Schwarz', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'zrepl', 'zrepl Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'zrepl', 'zrepl Documentation',
|
||||||
|
author, 'zrepl', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for the extlinks extension -----------------------------------
|
||||||
|
# http://www.sphinx-doc.org/en/stable/ext/extlinks.html
|
||||||
|
extlinks = {
|
||||||
|
'issue':('https://github.com/zrepl/zrepl/issues/%s', 'issue #'),
|
||||||
|
'repomasterlink':('https://github.com/zrepl/zrepl/blob/master/%s', ''),
|
||||||
|
'sampleconf':('https://github.com/zrepl/zrepl/blob/master/config/samples%s', 'config/samples'),
|
||||||
|
'commit':('https://github.com/zrepl/zrepl/commit/%s', 'commit '),
|
||||||
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,6 @@ We would like to thank the following people and organizations for supporting zre
|
|||||||
|
|
||||||
<div class="fa fa-code" style="width: 1em;"></div>
|
<div class="fa fa-code" style="width: 1em;"></div>
|
||||||
|
|
||||||
* |supporter-std| `Max Christian Pohle <https://coderonline.de>`_
|
|
||||||
* |supporter-gold| Prominic.NET, Inc.
|
* |supporter-gold| Prominic.NET, Inc.
|
||||||
* |supporter-std| Torsten Blum
|
* |supporter-std| Torsten Blum
|
||||||
* |supporter-gold| Cyberiada GmbH
|
* |supporter-gold| Cyberiada GmbH
|
||||||
|
|||||||
+23
-56
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -234,21 +233,6 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
|||||||
//
|
//
|
||||||
// Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep,
|
// Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep,
|
||||||
// will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup.
|
// will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup.
|
||||||
destroyTypes := AbstractionTypeSet{
|
|
||||||
AbstractionStepHold: true,
|
|
||||||
AbstractionTentativeReplicationCursorBookmark: true,
|
|
||||||
}
|
|
||||||
// The replication planner can also pick an endpoint zfs abstraction as FromVersion.
|
|
||||||
// Keep it, so that the replication will succeed.
|
|
||||||
//
|
|
||||||
// NB: there is no abstraction for snapshots, so, we only need to check bookmarks.
|
|
||||||
if sendArgs.FromVersion != nil && sendArgs.FromVersion.IsBookmark() {
|
|
||||||
dp, err := zfs.NewDatasetPath(sendArgs.FS)
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // sendArgs is validated, this shouldn't happen
|
|
||||||
}
|
|
||||||
liveAbs = append(liveAbs, destroyTypes.ExtractBookmark(dp, sendArgs.FromVersion))
|
|
||||||
}
|
|
||||||
func() {
|
func() {
|
||||||
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
|
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
|
||||||
defer endSpan()
|
defer endSpan()
|
||||||
@@ -261,45 +245,35 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
|||||||
return keep
|
return keep
|
||||||
}
|
}
|
||||||
check := func(obsoleteAbs []Abstraction) {
|
check := func(obsoleteAbs []Abstraction) {
|
||||||
// Ensure that we don't delete `From` or `To`.
|
// last line of defense: check that we don't destroy the incremental `from` and `to`
|
||||||
// Regardless of whether they are in AbstractionTypeSet or not.
|
// if we did that, we might be about to blow away the last common filesystem version between sender and receiver
|
||||||
// And produce a nice error message in case we do, to aid debugging the resulting panic.
|
mustLiveVersions := []zfs.FilesystemVersion{sendArgs.ToVersion}
|
||||||
//
|
|
||||||
// This is especially important for `From`. We could break incremental replication
|
|
||||||
// if we deleted the last common filesystem version between sender and receiver.
|
|
||||||
type Problem struct {
|
|
||||||
sendArgsWhat string
|
|
||||||
fullpath string
|
|
||||||
obsoleteAbs Abstraction
|
|
||||||
}
|
|
||||||
problems := make([]Problem, 0)
|
|
||||||
checkFullpaths := make(map[string]string, 2)
|
|
||||||
checkFullpaths["ToVersion"] = sendArgs.ToVersion.FullPath(sendArgs.FS)
|
|
||||||
if sendArgs.FromVersion != nil {
|
if sendArgs.FromVersion != nil {
|
||||||
checkFullpaths["FromVersion"] = sendArgs.FromVersion.FullPath(sendArgs.FS)
|
mustLiveVersions = append(mustLiveVersions, *sendArgs.FromVersion)
|
||||||
}
|
}
|
||||||
for _, a := range obsoleteAbs {
|
for _, staleVersion := range obsoleteAbs {
|
||||||
for what, fullpath := range checkFullpaths {
|
for _, mustLiveVersion := range mustLiveVersions {
|
||||||
if a.GetFullPath() == fullpath && a.GetType().IsSnapshotOrBookmark() {
|
isSendArg := zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion())
|
||||||
problems = append(problems, Problem{
|
stepHoldBasedGuaranteeStrategy := false
|
||||||
sendArgsWhat: what,
|
k := replicationGuaranteeStrategy.Kind()
|
||||||
fullpath: fullpath,
|
switch k {
|
||||||
obsoleteAbs: a,
|
case ReplicationGuaranteeKindResumability:
|
||||||
})
|
stepHoldBasedGuaranteeStrategy = true
|
||||||
|
case ReplicationGuaranteeKindIncremental:
|
||||||
|
case ReplicationGuaranteeKindNone:
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("this is supposed to be an exhaustive match, got %v", k))
|
||||||
|
}
|
||||||
|
isSnapshot := mustLiveVersion.IsSnapshot()
|
||||||
|
if isSendArg && (!isSnapshot || stepHoldBasedGuaranteeStrategy) {
|
||||||
|
panic(fmt.Sprintf("impl error: %q would be destroyed because it is considered stale but it is part of of sendArgs=%s", mustLiveVersion.String(), pretty.Sprint(sendArgs)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(problems) == 0 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var msg strings.Builder
|
destroyTypes := AbstractionTypeSet{
|
||||||
fmt.Fprintf(&msg, "cleaning up send stale would destroy send args:\n")
|
AbstractionStepHold: true,
|
||||||
fmt.Fprintf(&msg, " SendArgs: %s\n", pretty.Sprint(sendArgs))
|
AbstractionTentativeReplicationCursorBookmark: true,
|
||||||
for _, check := range problems {
|
|
||||||
fmt.Fprintf(&msg, "would delete %s %s because it was deemed an obsolete abstraction: %s\n",
|
|
||||||
check.sendArgsWhat, check.fullpath, check.obsoleteAbs)
|
|
||||||
}
|
|
||||||
panic(msg.String())
|
|
||||||
}
|
}
|
||||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
||||||
}()
|
}()
|
||||||
@@ -454,7 +428,6 @@ func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, _ io.ReadCloser
|
|||||||
|
|
||||||
type FSFilter interface { // FIXME unused
|
type FSFilter interface { // FIXME unused
|
||||||
Filter(path *zfs.DatasetPath) (pass bool, err error)
|
Filter(path *zfs.DatasetPath) (pass bool, err error)
|
||||||
UserSpecifiedDatasets() zfs.UserSpecifiedDatasetsSet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: can we get away without error types here?
|
// FIXME: can we get away without error types here?
|
||||||
@@ -614,12 +587,6 @@ func (f subroot) Filter(p *zfs.DatasetPath) (pass bool, err error) {
|
|||||||
return p.HasPrefix(f.localRoot) && !p.Equal(f.localRoot), nil
|
return p.HasPrefix(f.localRoot) && !p.Equal(f.localRoot), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f subroot) UserSpecifiedDatasets() zfs.UserSpecifiedDatasetsSet {
|
|
||||||
return zfs.UserSpecifiedDatasetsSet{
|
|
||||||
f.localRoot.ToString(): true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
|
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
|
||||||
p, err := zfs.NewDatasetPath(fs)
|
p, err := zfs.NewDatasetPath(fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -89,8 +89,6 @@ func ReplicationGuaranteeFromKind(k ReplicationGuaranteeKind) ReplicationGuarant
|
|||||||
|
|
||||||
type ReplicationGuaranteeNone struct{}
|
type ReplicationGuaranteeNone struct{}
|
||||||
|
|
||||||
func (g ReplicationGuaranteeNone) String() string { return "none" }
|
|
||||||
|
|
||||||
func (g ReplicationGuaranteeNone) Kind() ReplicationGuaranteeKind {
|
func (g ReplicationGuaranteeNone) Kind() ReplicationGuaranteeKind {
|
||||||
return ReplicationGuaranteeKindNone
|
return ReplicationGuaranteeKindNone
|
||||||
}
|
}
|
||||||
@@ -109,8 +107,6 @@ func (g ReplicationGuaranteeNone) SenderPostRecvConfirmed(ctx context.Context, j
|
|||||||
|
|
||||||
type ReplicationGuaranteeIncremental struct{}
|
type ReplicationGuaranteeIncremental struct{}
|
||||||
|
|
||||||
func (g ReplicationGuaranteeIncremental) String() string { return "incremental" }
|
|
||||||
|
|
||||||
func (g ReplicationGuaranteeIncremental) Kind() ReplicationGuaranteeKind {
|
func (g ReplicationGuaranteeIncremental) Kind() ReplicationGuaranteeKind {
|
||||||
return ReplicationGuaranteeKindIncremental
|
return ReplicationGuaranteeKindIncremental
|
||||||
}
|
}
|
||||||
@@ -148,8 +144,6 @@ func (g ReplicationGuaranteeIncremental) SenderPostRecvConfirmed(ctx context.Con
|
|||||||
|
|
||||||
type ReplicationGuaranteeResumability struct{}
|
type ReplicationGuaranteeResumability struct{}
|
||||||
|
|
||||||
func (g ReplicationGuaranteeResumability) String() string { return "resumability" }
|
|
||||||
|
|
||||||
func (g ReplicationGuaranteeResumability) Kind() ReplicationGuaranteeKind {
|
func (g ReplicationGuaranteeResumability) Kind() ReplicationGuaranteeKind {
|
||||||
return ReplicationGuaranteeKindResumability
|
return ReplicationGuaranteeKindResumability
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const (
|
|||||||
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AbstractionTypesAll = AbstractionTypeSet{
|
var AbstractionTypesAll = map[AbstractionType]bool{
|
||||||
AbstractionStepHold: true,
|
AbstractionStepHold: true,
|
||||||
AbstractionLastReceivedHold: true,
|
AbstractionLastReceivedHold: true,
|
||||||
AbstractionTentativeReplicationCursorBookmark: true,
|
AbstractionTentativeReplicationCursorBookmark: true,
|
||||||
@@ -181,38 +181,6 @@ func (s AbstractionTypeSet) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the `BookmarkExtractor()` method of each abstraction type in this set
|
|
||||||
// to try extract an abstraction from the given FilesystemVersion.
|
|
||||||
//
|
|
||||||
// Abstraction types in this set that don't have a bookmark extractor are skipped.
|
|
||||||
//
|
|
||||||
// Panics if more than one abstraction type matches.
|
|
||||||
func (s AbstractionTypeSet) ExtractBookmark(dp *zfs.DatasetPath, v *zfs.FilesystemVersion) Abstraction {
|
|
||||||
matched := make(AbstractionTypeSet, 1)
|
|
||||||
var matchedAbs Abstraction
|
|
||||||
for absType := range s {
|
|
||||||
extractor := absType.BookmarkExtractor()
|
|
||||||
if extractor == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
abstraction := extractor(dp, *v)
|
|
||||||
if abstraction != nil {
|
|
||||||
matched[absType] = true
|
|
||||||
matchedAbs = abstraction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(matched) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(matched) == 1 {
|
|
||||||
if matchedAbs == nil {
|
|
||||||
panic("loop above should always set matchedAbs if there is a match")
|
|
||||||
}
|
|
||||||
return matchedAbs
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("abstraction types extractors should not overlap: %s", matched))
|
|
||||||
}
|
|
||||||
|
|
||||||
type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstraction
|
type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstraction
|
||||||
|
|
||||||
// returns nil if the abstraction type is not bookmark-based
|
// returns nil if the abstraction type is not bookmark-based
|
||||||
@@ -270,23 +238,6 @@ func (t AbstractionType) BookmarkNamer() func(fs string, guid uint64, jobId JobI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t AbstractionType) IsSnapshotOrBookmark() bool {
|
|
||||||
switch t {
|
|
||||||
case AbstractionTentativeReplicationCursorBookmark:
|
|
||||||
return true
|
|
||||||
case AbstractionReplicationCursorBookmarkV1:
|
|
||||||
return true
|
|
||||||
case AbstractionReplicationCursorBookmarkV2:
|
|
||||||
return true
|
|
||||||
case AbstractionStepHold:
|
|
||||||
return false
|
|
||||||
case AbstractionLastReceivedHold:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unimpl: %q", t))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListZFSHoldsAndBookmarksQuery struct {
|
type ListZFSHoldsAndBookmarksQuery struct {
|
||||||
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
||||||
// What abstraction types should match (any contained in the set)
|
// What abstraction types should match (any contained in the set)
|
||||||
|
|||||||
@@ -150,7 +150,9 @@ func NewOutlets() *Outlets {
|
|||||||
func (os *Outlets) DeepCopy() (copy *Outlets) {
|
func (os *Outlets) DeepCopy() (copy *Outlets) {
|
||||||
copy = NewOutlets()
|
copy = NewOutlets()
|
||||||
for level := range os.outs {
|
for level := range os.outs {
|
||||||
copy.outs[level] = append(copy.outs[level], os.outs[level]...)
|
for i := range os.outs[level] {
|
||||||
|
copy.outs[level] = append(copy.outs[level], os.outs[level][i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return copy
|
return copy
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
dh-exec \
|
dh-exec \
|
||||||
binutils-aarch64-linux-gnu \
|
binutils-aarch64-linux-gnu \
|
||||||
binutils-arm-linux-gnueabihf \
|
binutils-arm-linux-gnueabihf \
|
||||||
binutils-i686-linux-gnu \
|
binutils-i686-linux-gnu
|
||||||
binutils-x86-64-linux-gnu
|
|
||||||
|
|
||||||
RUN mkdir -p /build/src && chmod -R 0777 /build
|
RUN mkdir -p /build/src && chmod -R 0777 /build
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,6 @@ export ZREPL_MOCK_ZFS_COMMAND_LOG="$1"
|
|||||||
shift
|
shift
|
||||||
export ZREPL_MOCK_ZFS_PATH="$1"
|
export ZREPL_MOCK_ZFS_PATH="$1"
|
||||||
shift
|
shift
|
||||||
dirname="$(dirname "${BASH_SOURCE[0]}")"
|
export PATH="$(dirname "${BASH_SOURCE[0]}" )":"$PATH"
|
||||||
# If we invoke this script from the top zrepl source tree, like so:
|
|
||||||
# ./platformtest/logmockzfs/logzfsenv ...
|
|
||||||
# then dirname is relative, i.e., ./platformtest/logmockzfs.
|
|
||||||
# If we put that relative dir in PATH, then Go >= 1.19 will refuse to
|
|
||||||
# exec the `./platformtest/logmockzfs/zfs` wrapper script if it finds
|
|
||||||
# it via PATH lookup. For Example:
|
|
||||||
# cmd := exec.Command("zfs")
|
|
||||||
# err := cmd.Run()
|
|
||||||
# will fail with an error that errors.Is(err, exec.ErrDot), message:
|
|
||||||
# cannot run executable found relative to current directory
|
|
||||||
# The solution is to use an abspath.
|
|
||||||
# Learn more at https://pkg.go.dev/os/exec#hdr-Executables_in_the_current_directory
|
|
||||||
# and https://go-review.googlesource.com/c/go/+/381374
|
|
||||||
absdirname="$(readlink -e "$dirname")"
|
|
||||||
export PATH="$absdirname":"$PATH"
|
|
||||||
args=("$@")
|
args=("$@")
|
||||||
exec "${args[@]}"
|
exec "${args[@]}"
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ var Cases = []Case{BatchDestroy,
|
|||||||
ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication,
|
ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication,
|
||||||
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
|
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
|
||||||
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
|
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
|
||||||
ReplicationIncrementalHandlesFromVersionEqTentativeCursorCorrectly,
|
|
||||||
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
|
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
|
||||||
ReplicationInitialAll,
|
ReplicationInitialAll,
|
||||||
ReplicationInitialFail,
|
ReplicationInitialFail,
|
||||||
|
|||||||
@@ -248,10 +248,7 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
|
|||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
snap2Hold, err := endpoint.HoldStep(ctx, sfs, snap2, jobId) // no shadow
|
snap2Hold, err := endpoint.HoldStep(ctx, sfs, snap2, jobId) // no shadow
|
||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
// create artificial tentative cursor
|
return []endpoint.Abstraction{snap2Cursor, snap1Hold, snap2Hold}
|
||||||
snap3TentativeCursor, err := endpoint.CreateTentativeReplicationCursor(ctx, sfs, snap3, jobId)
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
return []endpoint.Abstraction{snap2Cursor, snap1Hold, snap2Hold, snap3TentativeCursor}
|
|
||||||
}
|
}
|
||||||
createArtificalStaleAbstractions(sjid)
|
createArtificalStaleAbstractions(sjid)
|
||||||
ojidSendAbstractions := createArtificalStaleAbstractions(ojid)
|
ojidSendAbstractions := createArtificalStaleAbstractions(ojid)
|
||||||
@@ -336,29 +333,21 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
|
|||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
snap2OjidCursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap2.Guid, ojid)
|
snap2OjidCursorName, err := endpoint.ReplicationCursorBookmarkName(sfs, snap2.Guid, ojid)
|
||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
snap3SjidTentativeCursorName, err := endpoint.TentativeReplicationCursorBookmarkName(sfs, snap3.Guid, sjid)
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
snap3OjidTentativeCursorName, err := endpoint.TentativeReplicationCursorBookmarkName(sfs, snap3.Guid, ojid)
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
var bmNames []string
|
var bmNames []string
|
||||||
for _, bm := range sBms {
|
for _, bm := range sBms {
|
||||||
bmNames = append(bmNames, bm.Name)
|
bmNames = append(bmNames, bm.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if invalidateCacheBeforeSecondReplication {
|
if invalidateCacheBeforeSecondReplication {
|
||||||
require.Len(ctx, sBms, 4)
|
require.Len(ctx, sBms, 3)
|
||||||
require.Contains(ctx, bmNames, snap5SjidCursorName)
|
require.Contains(ctx, bmNames, snap5SjidCursorName)
|
||||||
require.Contains(ctx, bmNames, snap2OjidCursorName)
|
require.Contains(ctx, bmNames, snap2OjidCursorName)
|
||||||
require.Contains(ctx, bmNames, snap3OjidTentativeCursorName)
|
|
||||||
require.Contains(ctx, bmNames, "2")
|
require.Contains(ctx, bmNames, "2")
|
||||||
} else {
|
} else {
|
||||||
require.Len(ctx, sBms, 6)
|
require.Len(ctx, sBms, 4)
|
||||||
ctx.Logf("%s", pretty.Sprint(sBms))
|
|
||||||
require.Contains(ctx, bmNames, snap5SjidCursorName)
|
require.Contains(ctx, bmNames, snap5SjidCursorName)
|
||||||
require.Contains(ctx, bmNames, snap2SjidCursorName)
|
require.Contains(ctx, bmNames, snap2SjidCursorName)
|
||||||
require.Contains(ctx, bmNames, snap2OjidCursorName)
|
require.Contains(ctx, bmNames, snap2OjidCursorName)
|
||||||
require.Contains(ctx, bmNames, snap3SjidTentativeCursorName)
|
|
||||||
require.Contains(ctx, bmNames, snap3OjidTentativeCursorName)
|
|
||||||
require.Contains(ctx, bmNames, "2")
|
require.Contains(ctx, bmNames, "2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,84 +370,6 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReplicationIncrementalHandlesFromVersionEqTentativeCursorCorrectly(ctx *platformtest.Context) {
|
|
||||||
|
|
||||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
|
||||||
CREATEROOT
|
|
||||||
+ "sender"
|
|
||||||
+ "sender@1"
|
|
||||||
+ "receiver"
|
|
||||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
|
||||||
`)
|
|
||||||
|
|
||||||
sjid := endpoint.MustMakeJobID("sender-job")
|
|
||||||
rjid := endpoint.MustMakeJobID("receiver-job")
|
|
||||||
|
|
||||||
sfs := ctx.RootDataset + "/sender"
|
|
||||||
rfsRoot := ctx.RootDataset + "/receiver"
|
|
||||||
|
|
||||||
rep := replicationInvocation{
|
|
||||||
sjid: sjid,
|
|
||||||
rjid: rjid,
|
|
||||||
sfs: sfs,
|
|
||||||
rfsRoot: rfsRoot,
|
|
||||||
// It doesn't really matter what guarantee we use here, as the second replication will configure another.
|
|
||||||
// But, in the real world, the only way for a stale tentative cursor to appear is if the guarantee is set to
|
|
||||||
// incremental replication and we crash before converting the tentative cursor into a regular cursor.
|
|
||||||
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do initial replication to set up the test.
|
|
||||||
rep1 := rep.Do(ctx)
|
|
||||||
ctx.Logf("\n%s", pretty.Sprint(rep1))
|
|
||||||
sfsDs := mustDatasetPath(sfs)
|
|
||||||
snap1_sender := mustGetFilesystemVersion(ctx, sfs+"@1")
|
|
||||||
snap1_replicationCursor_name, err := endpoint.ReplicationCursorBookmarkName(sfs, snap1_sender.Guid, sjid)
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
snap1_replicationCursor := mustGetFilesystemVersion(ctx, sfs+"#"+snap1_replicationCursor_name)
|
|
||||||
|
|
||||||
// The second replication will be done with a guarantee kind that doesn't create tentative cursors by itself.
|
|
||||||
// So, it would generally be right to clean up any tentative cursors on sfs since they're stale abstractions.
|
|
||||||
// However, if the cursor is used as the `from` version in any send step, we must not destroy it, as that
|
|
||||||
// would break incremental replication.
|
|
||||||
// NB: we only need to test the first step as all subsequent steps will be snapshot->snapshot.
|
|
||||||
rep.guarantee = pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeNothing)
|
|
||||||
// create the artificial cursor
|
|
||||||
snap1_tentativeCursor, err := endpoint.CreateTentativeReplicationCursor(ctx, sfs, snap1_sender, sjid)
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
endpoint.AbstractionsCacheInvalidate(sfs)
|
|
||||||
// remove other bookmarks of snap1, and snap1 itself, to force the replication planner to use the tentative cursor
|
|
||||||
err = zfs.ZFSDestroyFilesystemVersion(ctx, sfsDs, &snap1_sender)
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
err = zfs.ZFSDestroyFilesystemVersion(ctx, sfsDs, &snap1_replicationCursor)
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
versions, err := zfs.ZFSListFilesystemVersions(ctx, sfsDs, zfs.ListFilesystemVersionsOptions{})
|
|
||||||
require.NoError(ctx, err)
|
|
||||||
require.Len(ctx, versions, 1)
|
|
||||||
require.Equal(ctx, versions[0].Guid, snap1_tentativeCursor.GetFilesystemVersion().Guid)
|
|
||||||
// create another snapshot so that replication does one incremental step `tentative_cursor` -> `@2`
|
|
||||||
mustSnapshot(ctx, sfs+"@2")
|
|
||||||
mustGetFilesystemVersion(ctx, sfs+"@2")
|
|
||||||
// do the replication
|
|
||||||
rep2 := rep.Do(ctx)
|
|
||||||
ctx.Logf("\n%s", pretty.Sprint(rep2))
|
|
||||||
|
|
||||||
// Ensure that the tentative cursor was used.
|
|
||||||
require.Len(ctx, rep2.Attempts, 1)
|
|
||||||
require.Equal(ctx, rep2.Attempts[0].State, report.AttemptDone)
|
|
||||||
require.Len(ctx, rep2.Attempts[0].Filesystems, 1)
|
|
||||||
require.Nil(ctx, rep2.Attempts[0].Filesystems[0].Error())
|
|
||||||
require.Len(ctx, rep2.Attempts[0].Filesystems[0].Steps, 1)
|
|
||||||
require.EqualValues(ctx, rep2.Attempts[0].Filesystems[0].CurrentStep, 1)
|
|
||||||
require.Len(ctx, rep2.Attempts[0].Filesystems[0].Steps, 1)
|
|
||||||
require.Equal(ctx, rep2.Attempts[0].Filesystems[0].Steps[0].Info.From, snap1_tentativeCursor.GetFilesystemVersion().RelName())
|
|
||||||
|
|
||||||
// Ensure that the tentative cursor was destroyed as part of SendPost.
|
|
||||||
_, err = zfs.ZFSGetFilesystemVersion(ctx, snap1_replicationCursor.FullPath(sfs))
|
|
||||||
_, ok := err.(*zfs.DatasetDoesNotExist)
|
|
||||||
require.True(ctx, ok)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PartialSender struct {
|
type PartialSender struct {
|
||||||
*endpoint.Sender
|
*endpoint.Sender
|
||||||
failAfterByteCount int64
|
failAfterByteCount int64
|
||||||
|
|||||||
@@ -192,14 +192,3 @@ func (r *Report) GetFailedFilesystemsCountInLatestAttempt() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true in case the AttemptState is a terminal
|
|
||||||
// state(AttemptPlanningError, AttemptFanOutError, AttemptDone)
|
|
||||||
func (a AttemptState) IsTerminal() bool {
|
|
||||||
switch a {
|
|
||||||
case AttemptPlanningError, AttemptFanOutError, AttemptDone:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -107,31 +107,34 @@ func (c *Conn) RenewWriteDeadline() error {
|
|||||||
return c.SetWriteDeadline(time.Now().Add(c.idleTimeout))
|
return c.SetWriteDeadline(time.Now().Add(c.idleTimeout))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Read(p []byte) (n int, _ error) {
|
func (c *Conn) Read(p []byte) (n int, err error) {
|
||||||
n = 0
|
n = 0
|
||||||
|
err = nil
|
||||||
restart:
|
restart:
|
||||||
if err := c.renewReadDeadline(); err != nil {
|
if err := c.renewReadDeadline(); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
var nCurRead int
|
var nCurRead int
|
||||||
nCurRead, err := c.Wire.Read(p[n:])
|
nCurRead, err = c.Wire.Read(p[n:])
|
||||||
n += nCurRead
|
n += nCurRead
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && nCurRead > 0 {
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && nCurRead > 0 {
|
||||||
|
err = nil
|
||||||
goto restart
|
goto restart
|
||||||
}
|
}
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Write(p []byte) (n int, _ error) {
|
func (c *Conn) Write(p []byte) (n int, err error) {
|
||||||
n = 0
|
n = 0
|
||||||
restart:
|
restart:
|
||||||
if err := c.RenewWriteDeadline(); err != nil {
|
if err := c.RenewWriteDeadline(); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
var nCurWrite int
|
var nCurWrite int
|
||||||
nCurWrite, err := c.Wire.Write(p[n:])
|
nCurWrite, err = c.Wire.Write(p[n:])
|
||||||
n += nCurWrite
|
n += nCurWrite
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && nCurWrite > 0 {
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && nCurWrite > 0 {
|
||||||
|
err = nil
|
||||||
goto restart
|
goto restart
|
||||||
}
|
}
|
||||||
return n, err
|
return n, err
|
||||||
@@ -141,16 +144,17 @@ restart:
|
|||||||
// but is guaranteed to use the writev system call if the wrapped Wire
|
// but is guaranteed to use the writev system call if the wrapped Wire
|
||||||
// support it.
|
// support it.
|
||||||
// Note the Conn does not support writev through io.Copy(aConn, aNetBuffers).
|
// Note the Conn does not support writev through io.Copy(aConn, aNetBuffers).
|
||||||
func (c *Conn) WritevFull(bufs net.Buffers) (n int64, _ error) {
|
func (c *Conn) WritevFull(bufs net.Buffers) (n int64, err error) {
|
||||||
n = 0
|
n = 0
|
||||||
restart:
|
restart:
|
||||||
if err := c.RenewWriteDeadline(); err != nil {
|
if err := c.RenewWriteDeadline(); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
var nCurWrite int64
|
var nCurWrite int64
|
||||||
nCurWrite, err := io.Copy(c.Wire, &bufs)
|
nCurWrite, err = io.Copy(c.Wire, &bufs)
|
||||||
n += nCurWrite
|
n += nCurWrite
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && nCurWrite > 0 {
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && nCurWrite > 0 {
|
||||||
|
err = nil
|
||||||
goto restart
|
goto restart
|
||||||
}
|
}
|
||||||
return n, err
|
return n, err
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
package suspendresumesafetimer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The returned error is guaranteed to be the ctx.Err()
|
|
||||||
func SleepUntil(ctx context.Context, sleepUntil time.Time) error {
|
|
||||||
|
|
||||||
// We use .Round(0) to strip the monotonic clock reading from the time.Time
|
|
||||||
// returned by time.Now(). That will make the before/after check in the ticker
|
|
||||||
// for-loop compare wall-clock times instead of monotonic time.
|
|
||||||
// Comparing wall clock time is necessary because monotonic time does not progress
|
|
||||||
// while the system is suspended.
|
|
||||||
//
|
|
||||||
// Background
|
|
||||||
//
|
|
||||||
// A time.Time carries a wallclock timestamp and optionally a monotonic clock timestamp.
|
|
||||||
// time.Now() returns a time.Time that carries both.
|
|
||||||
// time.Time.Add() applies the same delta to both timestamps in the time.Time.
|
|
||||||
// x.Sub(y) will return the *monotonic* delta if both x and y carry a monotonic timestamp.
|
|
||||||
// time.Until(x) == x.Sub(now) where `now` will have a monotonic timestamp.
|
|
||||||
// So, time.Until(x) with an `x` that has monotonic timestamp will return monotonic delta.
|
|
||||||
//
|
|
||||||
// Why Do We Care?
|
|
||||||
//
|
|
||||||
// On systems that suspend/resume, wall clock time progresses during suspend but
|
|
||||||
// monotonic time does not.
|
|
||||||
//
|
|
||||||
// So, suppose the following sequence of events:
|
|
||||||
// x <== time.Now()
|
|
||||||
// System suspends for 1 hour
|
|
||||||
// delta <== time.Now().Sub(x)
|
|
||||||
// `delta` will be near 0 because time.Until() subtracts the monotonic
|
|
||||||
// timestamps, and monotonic time didn't progress during suspend.
|
|
||||||
//
|
|
||||||
// Now strip the timestamp using .Round(0)
|
|
||||||
// x <== time.Now().Round(0)
|
|
||||||
// System suspends for 1 hour
|
|
||||||
// delta <== time.Now().Sub(x)
|
|
||||||
// `delta` will be 1 hour because time.Sub() subtracted wallclock timestamps
|
|
||||||
// because x didn't have a monotonic timestamp because we stripped it using .Round(0).
|
|
||||||
//
|
|
||||||
//
|
|
||||||
sleepUntil = sleepUntil.Round(0)
|
|
||||||
|
|
||||||
// Set up a timer so that, if the system doesn't suspend/resume,
|
|
||||||
// we get a precise wake-up time from the native Go timer.
|
|
||||||
monotonicClockTimer := time.NewTimer(time.Until(sleepUntil))
|
|
||||||
defer func() {
|
|
||||||
if !monotonicClockTimer.Stop() {
|
|
||||||
// non-blocking read since we can come here when
|
|
||||||
// we've already drained the channel through
|
|
||||||
// case <-monotonicClockTimer.C
|
|
||||||
// in the `for` loop below.
|
|
||||||
select {
|
|
||||||
case <-monotonicClockTimer.C:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set up a ticker so that we're guaranteed to wake up periodically.
|
|
||||||
// We'll then get the current wall-clock time and check ourselves
|
|
||||||
// whether we're past the requested expiration time.
|
|
||||||
// Pick a 10 second check interval by default since it's rare that
|
|
||||||
// suspend/resume is done more frequently.
|
|
||||||
ticker := time.NewTicker(envconst.Duration("ZREPL_WALLCLOCKTIMER_MAX_DELAY", 10*time.Second))
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-monotonicClockTimer.C:
|
|
||||||
return nil
|
|
||||||
case <-ticker.C:
|
|
||||||
now := time.Now()
|
|
||||||
if now.Before(sleepUntil) {
|
|
||||||
// Continue waiting.
|
|
||||||
|
|
||||||
// Reset the monotonic timer to reset drift.
|
|
||||||
if !monotonicClockTimer.Stop() {
|
|
||||||
<-monotonicClockTimer.C
|
|
||||||
}
|
|
||||||
monotonicClockTimer.Reset(time.Until(sleepUntil))
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,20 +3,12 @@ package zfs
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DatasetFilter interface {
|
type DatasetFilter interface {
|
||||||
Filter(p *DatasetPath) (pass bool, err error)
|
Filter(p *DatasetPath) (pass bool, err error)
|
||||||
// The caller owns the returned set.
|
|
||||||
// Implementations should return a copy.
|
|
||||||
UserSpecifiedDatasets() UserSpecifiedDatasetsSet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A set of dataset names that the user specified in the configuration file.
|
|
||||||
type UserSpecifiedDatasetsSet map[string]bool
|
|
||||||
|
|
||||||
// Returns a DatasetFilter that does not filter (passes all paths)
|
// Returns a DatasetFilter that does not filter (passes all paths)
|
||||||
func NoFilter() DatasetFilter {
|
func NoFilter() DatasetFilter {
|
||||||
return noFilter{}
|
return noFilter{}
|
||||||
@@ -27,7 +19,6 @@ type noFilter struct{}
|
|||||||
var _ DatasetFilter = noFilter{}
|
var _ DatasetFilter = noFilter{}
|
||||||
|
|
||||||
func (noFilter) Filter(p *DatasetPath) (pass bool, err error) { return true, nil }
|
func (noFilter) Filter(p *DatasetPath) (pass bool, err error) { return true, nil }
|
||||||
func (noFilter) UserSpecifiedDatasets() UserSpecifiedDatasetsSet { return nil }
|
|
||||||
|
|
||||||
func ZFSListMapping(ctx context.Context, filter DatasetFilter) (datasets []*DatasetPath, err error) {
|
func ZFSListMapping(ctx context.Context, filter DatasetFilter) (datasets []*DatasetPath, err error) {
|
||||||
res, err := ZFSListMappingProperties(ctx, filter, nil)
|
res, err := ZFSListMappingProperties(ctx, filter, nil)
|
||||||
@@ -70,7 +61,6 @@ func ZFSListMappingProperties(ctx context.Context, filter DatasetFilter, propert
|
|||||||
|
|
||||||
go ZFSListChan(ctx, rchan, properties, nil, "-r", "-t", "filesystem,volume")
|
go ZFSListChan(ctx, rchan, properties, nil, "-r", "-t", "filesystem,volume")
|
||||||
|
|
||||||
unmatchedUserSpecifiedDatasets := filter.UserSpecifiedDatasets()
|
|
||||||
datasets = make([]ZFSListMappingPropertiesResult, 0)
|
datasets = make([]ZFSListMappingPropertiesResult, 0)
|
||||||
for r := range rchan {
|
for r := range rchan {
|
||||||
|
|
||||||
@@ -84,8 +74,6 @@ func ZFSListMappingProperties(ctx context.Context, filter DatasetFilter, propert
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(unmatchedUserSpecifiedDatasets, path.ToString())
|
|
||||||
|
|
||||||
pass, filterErr := filter.Filter(path)
|
pass, filterErr := filter.Filter(path)
|
||||||
if filterErr != nil {
|
if filterErr != nil {
|
||||||
return nil, fmt.Errorf("error calling filter: %s", filterErr)
|
return nil, fmt.Errorf("error calling filter: %s", filterErr)
|
||||||
@@ -99,9 +87,5 @@ func ZFSListMappingProperties(ctx context.Context, filter DatasetFilter, propert
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jobid := zfscmd.GetJobIDOrDefault(ctx, "__nojobid")
|
|
||||||
metric := prom.ZFSListUnmatchedUserSpecifiedDatasetCount.WithLabelValues(jobid)
|
|
||||||
metric.Add(float64(len(unmatchedUserSpecifiedDatasets)))
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ var prom struct {
|
|||||||
ZFSSnapshotDuration *prometheus.HistogramVec
|
ZFSSnapshotDuration *prometheus.HistogramVec
|
||||||
ZFSBookmarkDuration *prometheus.HistogramVec
|
ZFSBookmarkDuration *prometheus.HistogramVec
|
||||||
ZFSDestroyDuration *prometheus.HistogramVec
|
ZFSDestroyDuration *prometheus.HistogramVec
|
||||||
ZFSListUnmatchedUserSpecifiedDatasetCount *prometheus.GaugeVec
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -35,15 +34,6 @@ func init() {
|
|||||||
Name: "destroy_duration",
|
Name: "destroy_duration",
|
||||||
Help: "Duration it took to destroy a dataset",
|
Help: "Duration it took to destroy a dataset",
|
||||||
}, []string{"dataset_type", "filesystem"})
|
}, []string{"dataset_type", "filesystem"})
|
||||||
prom.ZFSListUnmatchedUserSpecifiedDatasetCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "zfs",
|
|
||||||
Name: "list_unmatched_user_specified_dataset_count",
|
|
||||||
Help: "When evaluating a DatsetFilter against zfs list output, this counter " +
|
|
||||||
"is incremented for every DatasetFilter rule that did not match any " +
|
|
||||||
"filesystem name in the zfs list output. Monitor for increases to detect filesystem " +
|
|
||||||
"filter rules that have no effect because they don't match any local filesystem.",
|
|
||||||
}, []string{"jobid"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrometheusRegister(registry prometheus.Registerer) error {
|
func PrometheusRegister(registry prometheus.Registerer) error {
|
||||||
@@ -59,8 +49,5 @@ func PrometheusRegister(registry prometheus.Registerer) error {
|
|||||||
if err := registry.Register(prom.ZFSDestroyDuration); err != nil {
|
if err := registry.Register(prom.ZFSDestroyDuration); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := registry.Register(prom.ZFSListUnmatchedUserSpecifiedDatasetCount); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-23
@@ -343,6 +343,7 @@ const (
|
|||||||
|
|
||||||
type SendStream struct {
|
type SendStream struct {
|
||||||
cmd *zfscmd.Cmd
|
cmd *zfscmd.Cmd
|
||||||
|
kill context.CancelFunc
|
||||||
stdoutReader io.ReadCloser // not *os.File for mocking during platformtest
|
stdoutReader io.ReadCloser // not *os.File for mocking during platformtest
|
||||||
stderrBuf *circlog.CircularLog
|
stderrBuf *circlog.CircularLog
|
||||||
|
|
||||||
@@ -415,29 +416,8 @@ func (s *SendStream) killAndWait() error {
|
|||||||
debug("sendStream: killAndWait enter")
|
debug("sendStream: killAndWait enter")
|
||||||
defer debug("sendStream: killAndWait leave")
|
defer debug("sendStream: killAndWait leave")
|
||||||
|
|
||||||
// ensure this function is only called once
|
|
||||||
if s.state != sendStreamOpen {
|
|
||||||
panic(s.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// send SIGKILL
|
// send SIGKILL
|
||||||
// In an earlier version, we used the starting context.Context's cancel function
|
s.kill()
|
||||||
// for this, but in Go > 1.19, doing so will cause .Wait() to return the
|
|
||||||
// context cancel error instead of the *exec.ExitError.
|
|
||||||
err := s.cmd.Process().Kill()
|
|
||||||
if err != nil {
|
|
||||||
if err == os.ErrProcessDone {
|
|
||||||
// This can happen if
|
|
||||||
// (1) the process has already been .Wait()ed, or
|
|
||||||
// (2) some other goroutine cancels the context, likely further up
|
|
||||||
// the context tree.
|
|
||||||
// Case (1) can't happen to us because we only call
|
|
||||||
// this function in sendStreamOpen state.
|
|
||||||
// In Case (2), it's still our job to .Wait(), so, fallthrough.
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close our read-end of the pipe.
|
// Close our read-end of the pipe.
|
||||||
//
|
//
|
||||||
@@ -983,10 +963,10 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, e
|
|||||||
|
|
||||||
stream := &SendStream{
|
stream := &SendStream{
|
||||||
cmd: cmd,
|
cmd: cmd,
|
||||||
|
kill: cancel,
|
||||||
stdoutReader: stdoutReader,
|
stdoutReader: stdoutReader,
|
||||||
stderrBuf: stderrBuf,
|
stderrBuf: stderrBuf,
|
||||||
}
|
}
|
||||||
_ = cancel // the SendStream.killAndWait() will kill the process
|
|
||||||
|
|
||||||
return stream, nil
|
return stream, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ func WithJobID(ctx context.Context, jobID string) context.Context {
|
|||||||
return context.WithValue(ctx, contextKeyJobID, jobID)
|
return context.WithValue(ctx, contextKeyJobID, jobID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetJobIDOrDefault(ctx context.Context, def string) string {
|
|
||||||
return getJobIDOrDefault(ctx, def)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getJobIDOrDefault(ctx context.Context, def string) string {
|
func getJobIDOrDefault(ctx context.Context, def string) string {
|
||||||
ret, ok := ctx.Value(contextKeyJobID).(string)
|
ret, ok := ctx.Value(contextKeyJobID).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
Reference in New Issue
Block a user