Compare commits
521 Commits
v0.1.0-rc4
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b2fa629187 | |||
| 4fa96f01f8 | |||
| 45daa2149a | |||
| 6a6698211f | |||
| f4e87b4b57 | |||
| 9d25ab2fb1 | |||
| d5f2dc6b8b | |||
| 1428678b58 | |||
| 06f7183608 | |||
| de9ae54de9 | |||
| 1358894d5f | |||
| 8b5a942d39 | |||
| bf7749fae3 | |||
| 2cc7017d74 | |||
| 4611293a2e | |||
| 71ef236e91 | |||
| 53dd46c617 | |||
| 996259b9ce | |||
| a6d4d55b83 | |||
| d916c93cf9 | |||
| 46d9be415c | |||
| b93a06d237 | |||
| 2c780b4d4e | |||
| d75fe80edb | |||
| 05d4563f00 | |||
| f81f66315a | |||
| 5ea0a48224 | |||
| efdef1e8b3 | |||
| 79118ada70 | |||
| b1ea121d53 | |||
| d7ee92f213 | |||
| 2c12675ee1 | |||
| 76981d2037 | |||
| 075ecdac5b | |||
| 1dfae6829b | |||
| c393f35453 | |||
| ed4547166a | |||
| 888283c06f | |||
| 9e5f5e1ec9 | |||
| 17805e32c2 | |||
| 2aff1e45a4 | |||
| 4f950bb60a | |||
| e5704d518f | |||
| e3aca655c1 | |||
| d3b229562c | |||
| 6e6a3454de | |||
| 9d895da41e | |||
| 8cb9eaf32f | |||
| ac9785fabb | |||
| 899e23762a | |||
| b4b16f7bca | |||
| cf3dda5a65 | |||
| 3ade4bebc8 | |||
| e8662c2427 | |||
| 0718602634 | |||
| 0bfb286a0e | |||
| f113f0564f | |||
| 6ad4e708cd | |||
| 020a5bc4c1 | |||
| 3e59f5ffa3 | |||
| da854f929a | |||
| 799f07d8bb | |||
| 9ef7c8b16d | |||
| 860a9be37f | |||
| 4d6583ea5f | |||
| 3f751277c2 | |||
| 40c4827897 | |||
| e524b60f24 | |||
| 27f4ad1f53 | |||
| bae2bd2601 | |||
| b5d853885b | |||
| 3755c97a86 | |||
| d7ede3f82c | |||
| bca37c741c | |||
| 98073c7dca | |||
| f1608aba15 | |||
| dc05cd00f2 | |||
| 2923009689 | |||
| cc0f3b1f13 | |||
| 20abaa2e7f | |||
| 908807bd59 | |||
| b9b9ad10cf | |||
| 904c1512a3 | |||
| 3df5e223be | |||
| b8f55a97ba | |||
| 82adb2b9f5 | |||
| 7b6adab6b1 | |||
| e390aa0c5a | |||
| 5a8f0b9a24 | |||
| 3cb1865909 | |||
| 0ab92d4861 | |||
| 740ab4b1b2 | |||
| 48c5b60024 | |||
| 40fd700855 | |||
| def510abfd | |||
| 08769a8752 | |||
| affe00aefe | |||
| 5615f4929a | |||
| 440b07443f | |||
| e2fcf9ff5b | |||
| a5f6bc3697 | |||
| 9c63736489 | |||
| 830536715e | |||
| ebc46cf1c0 | |||
| 27012e5623 | |||
| 30faaec26a | |||
| 21e0ae63a6 | |||
| 370f40881d | |||
| fb71a7e4b0 | |||
| ef9a63b075 | |||
| faef059edf | |||
| ad9fbf7b6d | |||
| 3bd17b8069 | |||
| 99bf1487ae | |||
| c3b4f01c44 | |||
| 9d5c892023 | |||
| d8d1d25ec2 | |||
| d02d7e5e1d | |||
| 39f8ff62f0 | |||
| 9a434b0e54 | |||
| b5053d2659 | |||
| 0fe2ac6b90 | |||
| 95c924968a | |||
| 523a3bb26b | |||
| 96396b2e86 | |||
| 8749f0bd3d | |||
| bc92660e09 | |||
| 8b0637ddcc | |||
| bbdc6f5465 | |||
| bc5e1ede04 | |||
| 2b3daaf9f1 | |||
| 2b3df7e342 | |||
| 5e4d4188f4 | |||
| 1e8ffe4486 | |||
| 59389b84a2 | |||
| 4fae0bb68e | |||
| 9777a441e9 | |||
| 1a72edea5d | |||
| 96db636582 | |||
| 190ab7c08d | |||
| 6be133f55d | |||
| 5ffd470596 | |||
| 2119dc40ab | |||
| 0df1c4cdcc | |||
| 2658695a35 | |||
| 1ac1635b3d | |||
| 4a2806f6d1 | |||
| 0a264b9b41 | |||
| a3379d6785 | |||
| 6260b75031 | |||
| 3ffb69bfb0 | |||
| 1da8f848f2 | |||
| 6ed4626df9 | |||
| c07f9ec62e | |||
| fd5b0e6831 | |||
| a4cea1b4f3 | |||
| c0b52b92d5 | |||
| 12018b3685 | |||
| a91fb873e4 | |||
| a6aa610165 | |||
| 6c87bdb9fb | |||
| b9250a41a2 | |||
| a967986a18 | |||
| c743c7b03f | |||
| a9c61b4b0b | |||
| 206d359dcd | |||
| 2d8c3692ec | |||
| 7769263c2e | |||
| 89f7c76c4e | |||
| c7771f98f5 | |||
| 299f1c906e | |||
| d3f68ae4e8 | |||
| 193abbe6b1 | |||
| 02b215128e | |||
| dc03db7423 | |||
| 1df0f8912a | |||
| e4112d888c | |||
| 53f9bd6d88 | |||
| 43c2a0d9b0 | |||
| e0c7ceedd5 | |||
| 2642c64303 | |||
| 1acafabb5b | |||
| 19b2deb2cf | |||
| ce6701fb33 | |||
| 0121929164 | |||
| bc96f8f212 | |||
| 459508c9d9 | |||
| 4a27cc63a8 | |||
| 0a6840273a | |||
| 76ef84f83b | |||
| 66946df756 | |||
| 556fac3002 | |||
| 1ad7df2df3 | |||
| a3d010c5f0 | |||
| 12503dc55a | |||
| 7d10a71cc0 | |||
| 3d3d1b5679 | |||
| 5240ab4949 | |||
| 19aebd399f | |||
| 04e03f4d06 | |||
| 2e2a8a1d5d | |||
| a2b2e0fe34 | |||
| af2905d245 | |||
| c3f0041efd | |||
| 083f6001eb | |||
| 2d57ec6ee0 | |||
| fb6a9be954 | |||
| c1e2c9826f | |||
| b00b61e967 | |||
| ac147b5a6f | |||
| 1850a332ed | |||
| 20ff9717bc | |||
| c2fbf93365 | |||
| cf5e8e8f26 | |||
| c600cc1f60 | |||
| c6a9ebc71c | |||
| 1f0f2f8569 | |||
| 5104ad3d0b | |||
| a6dbda1ea8 | |||
| 1edb8014bc | |||
| 845195b7ed | |||
| 2c9fcd7c14 | |||
| 4f9b63aa09 | |||
| a8e92971d0 | |||
| b54e477602 | |||
| 959fb08a89 | |||
| 6ac012aa3c | |||
| 3e93b31f75 | |||
| 08df208149 | |||
| 936ed73a45 | |||
| 8fee536260 | |||
| 01b4792974 | |||
| ad80bb3735 | |||
| f5f269bfd5 | |||
| 5b16769057 | |||
| 009bd410af | |||
| bcfcd7a134 | |||
| bf1276f767 | |||
| 9fa7a18351 | |||
| 50e8ee4549 | |||
| 3b5a1a8b9a | |||
| f661d9429f | |||
| ac4b109872 | |||
| b2c6e51a43 | |||
| ee2336a24b | |||
| 1e85b1cb5f | |||
| 5c6d69a69c | |||
| 299808aaaf | |||
| 7071adc774 | |||
| 8d678eed19 | |||
| ab7abd9686 | |||
| a58ce74ed0 | |||
| 2c8c2cfa14 | |||
| 0ceea1b792 | |||
| 07f2bfff6a | |||
| b9ee14f6d7 | |||
| 393fc10a69 | |||
| 1c937e58f7 | |||
| 70bbdfe760 | |||
| efe7b17d21 | |||
| 166e80bb96 | |||
| 6ac537210b | |||
| 596a39c0f5 | |||
| d118bcc717 | |||
| 48be4032a2 | |||
| 2d545c87cd | |||
| 0b48ba54f8 | |||
| c3d87289bb | |||
| 0d96627ffb | |||
| 61acc7494a | |||
| 96d5288667 | |||
| b8cd3c59f1 | |||
| 91632b52bb | |||
| d39c0e3745 | |||
| 180eaea195 | |||
| 69ed2d7117 | |||
| c420f3c909 | |||
| 0a2dea05a9 | |||
| 293c89d392 | |||
| bb5ef0c8b2 | |||
| 8f44cdc284 | |||
| 53028ed50a | |||
| 17e152c601 | |||
| 2ba6aabd4a | |||
| 98207c904d | |||
| d2b825a8ae | |||
| 913b8b37fe | |||
| f494c5ba31 | |||
| b9503685f0 | |||
| 0f4143c0e0 | |||
| 8839ed1f95 | |||
| 41b4038ad5 | |||
| 88d21eb23a | |||
| e0be7e4d4f | |||
| 0c189265e8 | |||
| b1f8cdf385 | |||
| 428a60870a | |||
| af2d6579c5 | |||
| 0f3da73ef1 | |||
| fecc9416ab | |||
| a7915db4c3 | |||
| 3a4e841c73 | |||
| bcf6ff1c08 | |||
| ad7a104ab4 | |||
| 639359f393 | |||
| 91e310b7e3 | |||
| 5b30ad01ce | |||
| 8f4f9338a9 | |||
| 67bbce3c36 | |||
| 180c3d9ae1 | |||
| 7a5883d404 | |||
| c90acefacb | |||
| d6799e08d8 | |||
| c44dccc34b | |||
| 7f1695c457 | |||
| e500d9ee26 | |||
| d5ce578929 | |||
| 3f8fe3a368 | |||
| 6b4c6fc062 | |||
| e239d6f633 | |||
| 0bbe2befce | |||
| fa4e048169 | |||
| 4f9f21f7f2 | |||
| 480176ba2d | |||
| 1190c0f6d2 | |||
| 720a284db5 | |||
| 83fdffbcef | |||
| 0ee7a49d31 | |||
| 02db5994fe | |||
| 1d7a84e8ae | |||
| a0e3dc7040 | |||
| 43495d70c7 | |||
| fa586b493c | |||
| 30cdc1430e | |||
| 27673a23e9 | |||
| 95fc299733 | |||
| 4e702eedc9 | |||
| 8ff83f2f1a | |||
| 4b8f0ad112 | |||
| dbc8bbeb6a | |||
| b3e856f40d | |||
| 8e1937fe75 | |||
| 073514fc21 | |||
| dab222d95f | |||
| a827894274 | |||
| 9a8d813d14 | |||
| e391fa94f9 | |||
| 509185dfbe | |||
| 4b1b7a8561 | |||
| b330ccca5d | |||
| 05f1237a6d | |||
| 1c270b7e39 | |||
| 1b39e9d03c | |||
| 655a2e5404 | |||
| 9c80eea045 | |||
| 175ad1dd0b | |||
| 728e97700f | |||
| 94a0fbf953 | |||
| b056e7b2b9 | |||
| 6e927f20f9 | |||
| 301f163a44 | |||
| 474652ea51 | |||
| 1bc731e782 | |||
| 292b85b5ef | |||
| dce98d50da | |||
| 10a14a8c50 | |||
| bcb5965617 | |||
| f772b3d39f | |||
| 27db8c0afe | |||
| 0e5c77d2be | |||
| 0280727985 | |||
| b4abebce00 | |||
| d89afe58d4 | |||
| c855546b9f | |||
| 7d6ee4c166 | |||
| 2fbd9d8f8c | |||
| 18e101a04e | |||
| e594421322 | |||
| 0d4bfda2fb | |||
| b83c026cdc | |||
| 46caf31075 | |||
| 2b9d696b49 | |||
| 70afff6e3b | |||
| 01bbda13e5 | |||
| c5a8f6635f | |||
| 6f441c55dc | |||
| 7b34d6cba5 | |||
| 70f9c6482f | |||
| aed6149c8c | |||
| 0834a184b8 | |||
| f61295f76c | |||
| 0920a40751 | |||
| e0b5bd75f8 | |||
| 96e188d7c4 | |||
| 1336c91865 | |||
| 9568e46f05 | |||
| 3187129672 | |||
| 44bd354eae | |||
| 94caf8b8db | |||
| 3ff1966cab | |||
| a3842155c5 | |||
| 02b3b4f80c | |||
| 0882290595 | |||
| b8d9f4ba92 | |||
| 1462c5caa5 | |||
| 58c08c855f | |||
| 9a4763ceee | |||
| f8200a6386 | |||
| e35320f8ee | |||
| 5b52e5e331 | |||
| 6ebd9f1037 | |||
| 99ab16d7be | |||
| e675b35cda | |||
| ddd7acec49 | |||
| e7aa08564b | |||
| 93ccdb8024 | |||
| 501645f918 | |||
| 4fd9d30c98 | |||
| e2b9c16959 | |||
| 4ba85c40cc | |||
| cca95f613b | |||
| 5b50a66c6c | |||
| dd508280f0 | |||
| 2927d0ca15 | |||
| d35e2400b2 | |||
| 47ed599db7 | |||
| f899f4cbe4 | |||
| b3231d2bed | |||
| c24c327151 | |||
| 5e17d7ba80 | |||
| 0261dbfe3d | |||
| 4301f741db | |||
| 7e743c74dc | |||
| ad0b055245 | |||
| 27db3e6f70 | |||
| 4994b7a9ea | |||
| 080f2c0616 | |||
| d469cc04b6 | |||
| e0a25d04ac | |||
| b0f2c79944 | |||
| d2bc40f78d | |||
| 9e54f11960 | |||
| 6eda1f743f | |||
| 6787decef1 | |||
| d56d45a2ab | |||
| fcf16a163a | |||
| dc39c819a3 | |||
| 1048b09487 | |||
| 3806e97404 | |||
| c396f9508a | |||
| b9933f6cb2 | |||
| 0ba4b5eda6 | |||
| 18d2c350de | |||
| f8f9fd11cd | |||
| b422e6f12e | |||
| f8d5082bdd | |||
| ffe677e55a | |||
| 84eefa57bc | |||
| ad77371e38 | |||
| c524acb2df | |||
| 3edfe535c6 | |||
| d3b99e8e39 | |||
| 3c03f21419 | |||
| 5c95c21727 | |||
| a6b578b648 | |||
| 8af824df41 | |||
| 58ab25919e | |||
| 215848f476 | |||
| f9c7766073 | |||
| f976212ec9 | |||
| 8c88e168c1 | |||
| a78c854404 | |||
| 8a5af2f80e | |||
| f7aa26d418 | |||
| b5ff1a9926 | |||
| 729c83ee72 | |||
| 00434f4ac9 | |||
| 2cd9173bfb | |||
| 7ba3ae077f | |||
| a6497b2c6e | |||
| 07956c2299 | |||
| 77d3a1ad4d | |||
| 2e0ff9a582 | |||
| a65d8f1f4c | |||
| 424234c2d1 | |||
| 3bfe0c16d0 | |||
| e5f944c2f8 | |||
| d81a1818d6 | |||
| 921b34235e | |||
| a0cf9cff2a | |||
| 3105609d39 | |||
| 254a292362 | |||
| 95e16f01c1 | |||
| df21b5d1b4 | |||
| c4be60c29f | |||
| ff18181064 | |||
| b167de3096 | |||
| 9dd187e29d | |||
| 413bd0f43b | |||
| 3c3606d516 | |||
| b5dc04e62c | |||
| 234a327a03 | |||
| 90b32c7377 | |||
| 0fc6a63564 | |||
| 5138681c13 | |||
| d6304f4f1d | |||
| 082335df5d | |||
| be506661a5 | |||
| 38385adf22 | |||
| bbcfb47d28 | |||
| 32dd456ac6 | |||
| 4d2765e5f2 | |||
| 000d8bba66 | |||
| 5f909dab76 | |||
| 56e63ff551 | |||
| f31b54582f | |||
| 5b256a92b3 | |||
| cd829bd79a | |||
| 5b97953bfb | |||
| afed762774 | |||
| 5324f29693 |
+368
-61
@@ -1,79 +1,386 @@
|
|||||||
version: 2.0
|
version: 2.1
|
||||||
workflows:
|
orbs:
|
||||||
version: 2
|
# NB: this is not the Go version, but the Orb version
|
||||||
build:
|
# https://circleci.com/developer/orbs/orb/circleci/go#usage-go-modules-cache
|
||||||
jobs:
|
go: circleci/go@1.11.0
|
||||||
- build-1.11
|
|
||||||
- build-1.12
|
commands:
|
||||||
- build-latest
|
setup-home-local-bin:
|
||||||
jobs:
|
|
||||||
# build-latest serves as the template
|
|
||||||
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
|
|
||||||
build-latest: &build-latest
|
|
||||||
description: Builds zrepl
|
|
||||||
parameters:
|
|
||||||
image:
|
|
||||||
description: "the docker image that the job should use"
|
|
||||||
type: string
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:latest
|
|
||||||
environment:
|
|
||||||
# required by lazy.sh
|
|
||||||
TERM: xterm
|
|
||||||
working_directory: /go/src/github.com/zrepl/zrepl
|
|
||||||
steps:
|
steps:
|
||||||
- run:
|
- run:
|
||||||
name: Setup environment variables
|
shell: /bin/bash -euo pipefail
|
||||||
|
command: |
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
line='export PATH="$HOME/.local/bin:$PATH"'
|
||||||
|
if grep "$line" $BASH_ENV >/dev/null; then
|
||||||
|
echo "$line" >> $BASH_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NOTE: UV version is defined in .uv-version file at repository root
|
||||||
|
install-docdep:
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Read UV version from .uv-version
|
||||||
|
command: |
|
||||||
|
UV_VERSION=$(cat .uv-version)
|
||||||
|
echo "export UV_VERSION=$UV_VERSION" >> $BASH_ENV
|
||||||
|
# Python is managed by uv - it will automatically download the version
|
||||||
|
# specified in docs/.python-version when needed
|
||||||
|
- run:
|
||||||
|
name: Install uv
|
||||||
|
command: curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh
|
||||||
|
- run:
|
||||||
|
name: Add uv to PATH and set cache dir
|
||||||
command: |
|
command: |
|
||||||
# used by pip (for docs)
|
|
||||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
|
||||||
|
echo 'export UV_CACHE_DIR="$HOME/.cache/uv"' >> $BASH_ENV
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
|
name: Restore uv cache
|
||||||
keys:
|
keys:
|
||||||
- source
|
- uv-cache-v1-${UV_VERSION}-{{ checksum "docs/uv.lock" }}
|
||||||
- vendor
|
- uv-cache-v1-${UV_VERSION}-
|
||||||
- protobuf
|
|
||||||
|
|
||||||
|
save-uv-cache:
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Prune uv cache for CI
|
||||||
|
command: uv cache prune --ci
|
||||||
|
- save_cache:
|
||||||
|
name: Save uv cache
|
||||||
|
key: uv-cache-v1-${UV_VERSION}-{{ checksum "docs/uv.lock" }}
|
||||||
|
paths:
|
||||||
|
- ~/.cache/uv
|
||||||
|
|
||||||
|
install-zfs-from-source:
|
||||||
|
parameters:
|
||||||
|
zfs_release:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Record kernel version for cache key
|
||||||
|
command: uname -r > /tmp/kernel-version
|
||||||
|
- restore_cache:
|
||||||
|
name: Restore ZFS native debs cache
|
||||||
|
keys:
|
||||||
|
- zfs-debs-v2-<<parameters.zfs_release>>-{{ checksum "/tmp/kernel-version" }}
|
||||||
|
- run:
|
||||||
|
# https://openzfs.github.io/openzfs-docs/Developer%20Resources/Building%20ZFS.html
|
||||||
|
name: Build ZFS <<parameters.zfs_release>> native debs (if not cached)
|
||||||
|
no_output_timeout: 20m
|
||||||
|
command: |
|
||||||
|
# CircleCI machine images have pyenv Python 3.13 shadowing the system
|
||||||
|
# Python 3.12. The apt python3-* packages (setuptools, cffi, etc.) only
|
||||||
|
# install for the system Python, and ZFS's dpkg-buildpackage needs them
|
||||||
|
# for --enable-pyzfs. Use the system Python so apt packages are visible.
|
||||||
|
export PYENV_VERSION=system
|
||||||
|
if [ -d /tmp/zfs-debs ]; then
|
||||||
|
echo "ZFS debs cache hit, skipping build"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
alien autoconf automake build-essential debhelper-compat dh-autoreconf \
|
||||||
|
dh-dkms dh-python dkms fakeroot gawk git libaio-dev libattr1-dev \
|
||||||
|
libblkid-dev libcurl4-openssl-dev libelf-dev libffi-dev libpam0g-dev \
|
||||||
|
libssl-dev libtirpc-dev libtool libudev-dev linux-headers-$(uname -r) lsb-release \
|
||||||
|
parallel po-debconf python3 python3-all-dev python3-cffi python3-dev \
|
||||||
|
python3-packaging python3-setuptools python3-sphinx uuid-dev zlib1g-dev
|
||||||
|
mkdir -p /tmp/zfs-build
|
||||||
|
cd /tmp/zfs-build
|
||||||
|
git clone --depth 1 --branch <<parameters.zfs_release>> https://github.com/openzfs/zfs.git zfs-src
|
||||||
|
cd zfs-src
|
||||||
|
sh autogen.sh
|
||||||
|
./configure
|
||||||
|
make native-deb
|
||||||
|
mkdir -p /tmp/zfs-debs
|
||||||
|
find /tmp/zfs-build -name '*.deb' -exec mv -t /tmp/zfs-debs/ {} +
|
||||||
|
- save_cache:
|
||||||
|
name: Save ZFS native debs cache
|
||||||
|
key: zfs-debs-v2-<<parameters.zfs_release>>-{{ checksum "/tmp/kernel-version" }}
|
||||||
|
paths:
|
||||||
|
- /tmp/zfs-debs
|
||||||
|
- run:
|
||||||
|
name: Install ZFS <<parameters.zfs_release>>
|
||||||
|
command: |
|
||||||
|
ls /tmp/zfs-debs/
|
||||||
|
# Only install the debs we need. Exclude dracut (conflicts with
|
||||||
|
# initramfs-tools), dkms (we have prebuilt modules), and packages
|
||||||
|
# we don't need (pyzfs, test, doc).
|
||||||
|
sudo apt-get install -y $(find /tmp/zfs-debs -name '*.deb' \
|
||||||
|
! -name '*dracut*' \
|
||||||
|
! -name '*dkms*' \
|
||||||
|
! -name '*initramfs*' \
|
||||||
|
! -name '*pyzfs*' \
|
||||||
|
! -name '*doc*' \
|
||||||
|
! -name '*test*' \
|
||||||
|
-print)
|
||||||
|
sudo modprobe zfs
|
||||||
|
- run:
|
||||||
|
name: Verify ZFS version
|
||||||
|
command: |
|
||||||
|
sudo zfs version
|
||||||
|
sudo zpool version
|
||||||
|
|
||||||
|
docs-publish-sh:
|
||||||
|
parameters:
|
||||||
|
push:
|
||||||
|
type: boolean
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
git config --global user.email "zreplbot@cschwarz.com"
|
||||||
|
git config --global user.name "zrepl-github-io-ci"
|
||||||
|
|
||||||
|
# Configure git to use the GitHub token for HTTPS authentication.
|
||||||
|
# The token is stored in the 'zrepl-github-io-deploy' context.
|
||||||
|
- when:
|
||||||
|
condition: << parameters.push >>
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Configure git to use GitHub token for push
|
||||||
|
# GITHUB_PAGES_TOKEN is from the 'zrepl-github-io-deploy' context.
|
||||||
|
# CircleCI's secret masking automatically redacts context variables in logs.
|
||||||
|
command: |
|
||||||
|
# Unset CircleCI's SSH URL rewriting that checkout step configured
|
||||||
|
git config --global --unset-all url."ssh://git@github.com".insteadOf || true
|
||||||
|
# Set up credential helper with GitHub token
|
||||||
|
git config --global credential.helper store
|
||||||
|
echo "https://x-access-token:${GITHUB_PAGES_TOKEN}@github.com" > ~/.git-credentials
|
||||||
|
chmod 600 ~/.git-credentials
|
||||||
|
|
||||||
|
# caller must install-docdep
|
||||||
|
- when:
|
||||||
|
condition: << parameters.push >>
|
||||||
|
steps:
|
||||||
|
- run: bash -x docs/publish.sh -c -a -P
|
||||||
|
- when:
|
||||||
|
condition:
|
||||||
|
not: << parameters.push >>
|
||||||
|
steps:
|
||||||
|
- run: bash -x docs/publish.sh -c -a
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
do_ci:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
do_release:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
ci:
|
||||||
|
when: << pipeline.parameters.do_ci >>
|
||||||
|
jobs:
|
||||||
|
- run-docs-publish-sh:
|
||||||
|
name: quickcheck-docs
|
||||||
|
push: false
|
||||||
|
- quickcheck-go:
|
||||||
|
name: quickcheck-go-amd64-linux-1.25.7
|
||||||
|
goversion: &latest-go-release "1.25.7"
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- test-go:
|
||||||
|
goversion: *latest-go-release
|
||||||
|
- quickcheck-go:
|
||||||
|
requires:
|
||||||
|
- quickcheck-go-amd64-linux-1.25.7 #quickcheck-go-smoketest.name
|
||||||
|
matrix:
|
||||||
|
alias: quickcheck-go-matrix
|
||||||
|
parameters:
|
||||||
|
goversion: [*latest-go-release, "1.24.13"]
|
||||||
|
goos: ["linux", "freebsd"]
|
||||||
|
goarch: ["amd64", "arm64"]
|
||||||
|
exclude:
|
||||||
|
# don't re-do quickcheck-go-smoketest
|
||||||
|
- goversion: *latest-go-release
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- platformtest:
|
||||||
|
matrix:
|
||||||
|
parameters:
|
||||||
|
goversion: [*latest-go-release]
|
||||||
|
goos: ["linux"]
|
||||||
|
goarch: ["amd64"]
|
||||||
|
zfs_release: ["zfs-2.2.9", "zfs-2.3.5", "zfs-2.4.0"]
|
||||||
|
requires:
|
||||||
|
- test-go
|
||||||
|
- quickcheck-go-<< matrix.goarch >>-<< matrix.goos >>-<< matrix.goversion >>
|
||||||
|
|
||||||
|
release:
|
||||||
|
when: << pipeline.parameters.do_release >>
|
||||||
|
jobs:
|
||||||
|
- release-build
|
||||||
|
- release-deb:
|
||||||
|
requires:
|
||||||
|
- release-build
|
||||||
|
- release-rpm:
|
||||||
|
requires:
|
||||||
|
- release-build
|
||||||
|
- release-upload:
|
||||||
|
requires:
|
||||||
|
- release-build
|
||||||
|
- release-deb
|
||||||
|
- release-rpm
|
||||||
|
|
||||||
|
publish-zrepl.github.io:
|
||||||
|
jobs:
|
||||||
|
- run-docs-publish-sh:
|
||||||
|
name: publish-zrepl.github.io
|
||||||
|
push: true
|
||||||
|
context:
|
||||||
|
- zrepl-github-io-deploy
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-docs-publish-sh:
|
||||||
|
parameters:
|
||||||
|
push:
|
||||||
|
type: boolean
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:current
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-docdep
|
||||||
|
- docs-publish-sh:
|
||||||
|
push: << parameters.push >>
|
||||||
|
- save-uv-cache
|
||||||
|
|
||||||
|
quickcheck-go:
|
||||||
|
parameters:
|
||||||
|
goversion:
|
||||||
|
type: string
|
||||||
|
goos:
|
||||||
|
type: string
|
||||||
|
goarch:
|
||||||
|
type: string
|
||||||
|
docker:
|
||||||
|
- image: &cimg_with_modern_go cimg/go:1.25
|
||||||
|
environment:
|
||||||
|
GOOS: <<parameters.goos>>
|
||||||
|
GOARCH: <<parameters.goarch>>
|
||||||
|
GOTOOLCHAIN: "go<<parameters.goversion>>"
|
||||||
|
|
||||||
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
|
||||||
- save_cache:
|
- go/load-cache:
|
||||||
key: source
|
key: quickcheck-<<parameters.goversion>>
|
||||||
paths:
|
- run: make build/install
|
||||||
- ".git"
|
- run: go mod download
|
||||||
|
- run: cd build && go mod download
|
||||||
|
- go/save-cache:
|
||||||
|
key: quickcheck-<<parameters.goversion>>
|
||||||
|
|
||||||
# install deps
|
# ensure all code has been generated
|
||||||
- run: wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
- run: make generate
|
||||||
- run: echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
- run: |
|
||||||
- run: sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
if output=$(git status --porcelain) && [ -z "$output" ]; then
|
||||||
- save_cache:
|
echo "Working directory clean"
|
||||||
key: protobuf
|
else
|
||||||
paths:
|
echo "Uncommitted changes"
|
||||||
- "/usr/include/google/protobuf"
|
echo ""
|
||||||
|
echo "$output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
|
# other checks
|
||||||
- run: ./lazy.sh devsetup
|
- run: make zrepl-bin test-platform-bin
|
||||||
|
|
||||||
- run: make vendordeps
|
|
||||||
- save_cache:
|
|
||||||
key: vendor
|
|
||||||
paths:
|
|
||||||
- "./vendor"
|
|
||||||
|
|
||||||
- run: make
|
|
||||||
- run: make vet
|
- run: make vet
|
||||||
- run: make test
|
- run: make lint
|
||||||
- run: make release
|
|
||||||
|
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: ./artifacts/release
|
path: artifacts
|
||||||
when: always
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths: [.]
|
||||||
|
|
||||||
|
platformtest:
|
||||||
|
parameters:
|
||||||
|
goversion:
|
||||||
|
type: string
|
||||||
|
goos:
|
||||||
|
type: string
|
||||||
|
goarch:
|
||||||
|
type: string
|
||||||
|
zfs_release:
|
||||||
|
type: string
|
||||||
|
machine:
|
||||||
|
# pinned (not :current) to keep ZFS build cache valid across runs
|
||||||
|
image: ubuntu-2404:2025.09.1
|
||||||
|
resource_class: medium
|
||||||
|
environment:
|
||||||
|
GOOS: <<parameters.goos>>
|
||||||
|
GOARCH: <<parameters.goarch>>
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- install-zfs-from-source:
|
||||||
|
zfs_release: <<parameters.zfs_release>>
|
||||||
|
- run: sudo make test-platform GOOS="$GOOS" GOARCH="$GOARCH"
|
||||||
|
|
||||||
build-1.11:
|
test-go:
|
||||||
<<: *build-latest
|
parameters:
|
||||||
|
goversion:
|
||||||
|
type: string
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.11
|
- image: *cimg_with_modern_go
|
||||||
build-1.12:
|
environment:
|
||||||
<<: *build-latest
|
GOTOOLCHAIN: "go<<parameters.goversion>>"
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- go/load-cache:
|
||||||
|
key: make-test-go
|
||||||
|
- run: make test-go
|
||||||
|
- go/save-cache:
|
||||||
|
key: make-test-go
|
||||||
|
|
||||||
|
release-build:
|
||||||
|
machine:
|
||||||
|
image: &release-vm-image "ubuntu-2404:current"
|
||||||
|
resource_class: large
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: make release-docker
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths: [.]
|
||||||
|
release-deb:
|
||||||
|
machine:
|
||||||
|
image: *release-vm-image
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: make debs-docker
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths:
|
||||||
|
- "artifacts/*.deb"
|
||||||
|
|
||||||
|
release-rpm:
|
||||||
|
machine:
|
||||||
|
image: *release-vm-image
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: make rpms-docker
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths:
|
||||||
|
- "artifacts/*.rpm"
|
||||||
|
|
||||||
|
release-upload:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.12
|
- image: cimg/base:2024.09
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: make wrapup-and-checksum
|
||||||
|
- store_artifacts:
|
||||||
|
path: artifacts/release
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
circle_token = os.environ.get('CIRCLE_TOKEN')
|
||||||
|
if not circle_token:
|
||||||
|
cli_yml = Path.home() / ".circleci" / "cli.yml"
|
||||||
|
if cli_yml.exists():
|
||||||
|
with open(cli_yml) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
if data:
|
||||||
|
circle_token = data.get("token")
|
||||||
|
if not circle_token:
|
||||||
|
raise ValueError('CIRCLE_TOKEN not set and no token found in ~/.circleci/cli.yml')
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
description: Generate GitHub release notes from docs/changelog.rst and create a draft release with artifacts
|
||||||
|
argument-hint: <version-tag, e.g. v0.7.0>
|
||||||
|
model: sonnet
|
||||||
|
allowed-tools: Read, Write
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task
|
||||||
|
|
||||||
|
Create a draft GitHub release for zrepl version $ARGUMENTS and upload release artifacts.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
Here is the current changelog RST source:
|
||||||
|
|
||||||
|
!`cat docs/changelog.rst`
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### Phase 1: Pre-flight Sanity Checks
|
||||||
|
|
||||||
|
Before creating the release, verify that the previous build steps have been completed.
|
||||||
|
Run these checks using bash commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test -d artifacts/release && echo "✓ artifacts/release directory exists" || echo "✗ Missing artifacts/release directory - run: make download-circleci-release JOB_NUM=<num>"
|
||||||
|
test -f artifacts/release/sha512sum.txt && echo "✓ sha512sum.txt exists" || echo "✗ Missing sha512sum.txt - run: make download-circleci-release JOB_NUM=<num>"
|
||||||
|
test -f artifacts/release/sha512sum.txt.asc && echo "✓ sha512sum.txt.asc exists (signed)" || echo "✗ Missing signature - run: make verify-and-sign"
|
||||||
|
ls artifacts/release/zrepl-* >/dev/null 2>&1 && echo "✓ Release artifacts present" || echo "✗ No release artifacts found"
|
||||||
|
```
|
||||||
|
|
||||||
|
If ANY check fails (shows ✗), **STOP** and print the error messages. Do not proceed with release creation.
|
||||||
|
|
||||||
|
### Phase 2: Generate Release Notes
|
||||||
|
|
||||||
|
2. Extract the changelog section for version $ARGUMENTS from the RST source above.
|
||||||
|
3. Extract all GitHub usernames mentioned in that changelog section (look for @username patterns or other contributor attributions).
|
||||||
|
4. Write a short "Highlights" section (2-4 bullets) summarizing the most impactful user-facing changes in plain language. Do NOT include commit/issue links.
|
||||||
|
5. Write a "Contributors" thank you line for all GitHub users found in step 3. Format as: "Thanks to @user1, @user2, and @user3 for their contributions to this release!"
|
||||||
|
6. Write a "Breaking Changes" section. Convert RST formatting to plain Markdown text (no commit or issue links):
|
||||||
|
- `:commit:\`abc123\`` → just remove it or describe what it changed
|
||||||
|
- `:issue:\`123\`` → just remove it or describe the issue in plain text
|
||||||
|
- `:ref:\`display text <anchor>\`` → just use `display text` as plain text
|
||||||
|
- `:repomasterlink:\`path\`` → just mention the path without a link
|
||||||
|
- RST inline code ` ``code`` ` → markdown `` `code` ``
|
||||||
|
- RST links `` `text <url>`_ `` → just use `text` without the link
|
||||||
|
If there are no breaking changes, write "No breaking changes. vX.Y.Z-1 is interoperable with vX.Y.Z." (fill in actual versions).
|
||||||
|
7. Assemble the full release notes using the template below. **IMPORTANT**: Do NOT include a detailed changelog list. Only include what is specified in the template.
|
||||||
|
8. Write the result to `artifacts/release-notes.md`.
|
||||||
|
9. Validate all links in the release notes by checking HTTP status codes with curl:
|
||||||
|
- Extract all URLs from the markdown file
|
||||||
|
- Check each URL with `curl -sI -w "%{http_code}" -o /dev/null <url>`
|
||||||
|
- Report any broken links (non-200 status codes)
|
||||||
|
- If any links are broken, stop and notify the user before creating the release
|
||||||
|
|
||||||
|
### Phase 3: Create Draft Release and Upload Artifacts
|
||||||
|
|
||||||
|
10. Run: `gh release create $ARGUMENTS --title "$ARGUMENTS" --notes-file artifacts/release-notes.md --draft`
|
||||||
|
11. Upload all artifacts: `gh release upload $ARGUMENTS artifacts/release/*`
|
||||||
|
12. Verify upload succeeded: `gh release view $ARGUMENTS --json assets --jq '.assets | length'`
|
||||||
|
13. Print the release URL and confirm that artifacts were uploaded successfully.
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
```
|
||||||
|
The full changelog is [on the docs site](https://zrepl.github.io/changelog.html).
|
||||||
|
|
||||||
|
## Highlights
|
||||||
|
|
||||||
|
{2-4 bullet plain-language summary of the most impactful changes}
|
||||||
|
|
||||||
|
{Thank all GitHub users mentioned in the changelog, e.g., "Thanks to @user1, @user2, and @user3 for their contributions to this release!"}
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
{breaking changes, or "No breaking changes."}
|
||||||
|
|
||||||
|
## New Users
|
||||||
|
|
||||||
|
We provide [quick-start guides](https://zrepl.github.io/quickstart.html) for different usage scenarios.
|
||||||
|
We also recommend studying the [overview section of the configuration chapter](https://zrepl.github.io/configuration/overview.html).
|
||||||
|
|
||||||
|
## Testing & Upgrading
|
||||||
|
|
||||||
|
* Read the [Changelog](https://zrepl.github.io/changelog.html)
|
||||||
|
* [Run the platform tests](https://zrepl.github.io/usage.html#platform-tests) on a test system.
|
||||||
|
* Download & deploy the `zrepl` binary / distro package.
|
||||||
|
|
||||||
|
## Donations
|
||||||
|
|
||||||
|
zrepl is a spare-time project primarily developed by [Christian Schwarz](https://cschwarz.com).
|
||||||
|
Express your support through a donation to keep maintenance and feature development going.
|
||||||
|
|
||||||
|
[](https://patreon.com/zrepl) [](https://github.com/sponsors/problame) [](https://liberapay.com/zrepl/donate) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||||
|
```
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
---
|
||||||
|
description: Update supporters list from git history and changelog contributors
|
||||||
|
argument-hint: <time-range-or-release>
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task
|
||||||
|
|
||||||
|
Automatically update `docs/supporters.rst` by extracting GitHub contributors from:
|
||||||
|
1. Git commit history (code contributors)
|
||||||
|
2. `docs/changelog.rst` (all mentioned GitHub users)
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
Specify a time range or release range to filter contributors:
|
||||||
|
|
||||||
|
**Time range examples:**
|
||||||
|
- `--since="2024-01-01"` - Contributors since a specific date
|
||||||
|
- `--since="1 year ago"` - Contributors in the last year
|
||||||
|
- `--since="6 months ago"` - Contributors in the last 6 months
|
||||||
|
|
||||||
|
**Release range examples:**
|
||||||
|
- `v0.6.0..HEAD` - Contributors between v0.6.0 and current HEAD
|
||||||
|
- `v0.6.0..v0.7.0` - Contributors between two releases
|
||||||
|
- `--all` - All contributors (default if no argument provided)
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### Step 1: Extract Contributors from Git History
|
||||||
|
|
||||||
|
Parse the arguments to determine the git log range:
|
||||||
|
- If `--since="..."` is provided, use: `git log --since="..." --format='%aN <%aE>' | sort -u`
|
||||||
|
- If a commit range like `v0.6.0..HEAD` is provided, use: `git log v0.6.0..HEAD --format='%aN <%aE>' | sort -u`
|
||||||
|
- If `--all` or no argument, use: `git log --format='%aN <%aE>' | sort -u`
|
||||||
|
|
||||||
|
Parse the output to identify:
|
||||||
|
- GitHub users (look for @users.noreply.github.com emails or known GitHub patterns)
|
||||||
|
- Real names of code contributors
|
||||||
|
- Filter out the main maintainer (Christian Schwarz / problame) as they're already credited
|
||||||
|
|
||||||
|
### Step 2: Extract GitHub Users from Changelog
|
||||||
|
|
||||||
|
Read `docs/changelog.rst` and extract all GitHub usernames mentioned:
|
||||||
|
- Pattern: `` `@username <https://github.com/username>`_ ``
|
||||||
|
- Pattern: `@username` in text
|
||||||
|
- Collect all unique usernames
|
||||||
|
|
||||||
|
### Step 3: Categorize Contributors
|
||||||
|
|
||||||
|
For each contributor, determine their tier:
|
||||||
|
|
||||||
|
**Code contributors** (`|supporter-code|`):
|
||||||
|
- Anyone who appears in git history as a commit author
|
||||||
|
- Prioritize those with substantial contributions (multiple commits)
|
||||||
|
|
||||||
|
**Other contributors** (keep existing `|supporter-gold|` and `|supporter-std|`):
|
||||||
|
- Keep all existing gold/std supporters as-is (monetary supporters)
|
||||||
|
- Only update code contributors
|
||||||
|
|
||||||
|
### Step 4: Read Current Supporters File
|
||||||
|
|
||||||
|
Read `docs/supporters.rst` and parse existing entries to:
|
||||||
|
- Preserve all `|supporter-gold|` entries (monetary supporters - don't touch these)
|
||||||
|
- Preserve all `|supporter-std|` entries (monetary supporters - don't touch these)
|
||||||
|
- Preserve ALL existing `|supporter-code|` entries in their current order
|
||||||
|
- Extract list of existing code contributor names/GitHub usernames to avoid duplicates
|
||||||
|
|
||||||
|
### Step 5: Identify Contributors to Promote/Add
|
||||||
|
|
||||||
|
**IMPORTANT: Find contributors in the range, and either ADD them (if new) or MOVE them up (if recurring).**
|
||||||
|
|
||||||
|
For each GitHub user found in changelog or git history for the specified range:
|
||||||
|
1. Check if they're already listed as `|supporter-code|` in the existing file
|
||||||
|
2. Categorize them:
|
||||||
|
- **NEW**: Not currently in the file → will be added
|
||||||
|
- **RECURRING**: Already in the file with new contributions in this range → will be moved to top
|
||||||
|
- **UNCHANGED**: Not in this range → stays in original position
|
||||||
|
3. For both NEW and RECURRING contributors:
|
||||||
|
- Get their latest contribution date from the range: `git log <range> --author="email" --format="%ai" -1`
|
||||||
|
- Format as: `* |supporter-code| `Name <https://github.com/username>`_`
|
||||||
|
- Use their real name if available from git history, otherwise use GitHub username
|
||||||
|
4. Sort NEW + RECURRING contributors together by their contribution date (newest first)
|
||||||
|
5. Keep list of UNCHANGED contributors in their original order
|
||||||
|
|
||||||
|
**The final order will be: [NEW + RECURRING from range, sorted newest first] + [UNCHANGED in original order]**
|
||||||
|
|
||||||
|
### Step 6: Generate Updated File
|
||||||
|
|
||||||
|
Create the updated supporters list:
|
||||||
|
- Keep all `|supporter-gold|` entries unchanged and in their exact positions
|
||||||
|
- Keep all `|supporter-std|` entries unchanged and in their exact positions
|
||||||
|
- For `|supporter-code|` entries, reorganize as follows:
|
||||||
|
1. **Top section**: Contributors from the analyzed range (NEW + RECURRING), sorted newest first
|
||||||
|
2. **Bottom section**: UNCHANGED contributors (not in this range), in their original order
|
||||||
|
- Update the spacer comment showing the range that was just processed:
|
||||||
|
```rst
|
||||||
|
..
|
||||||
|
↓ claude --permission-mode default /update-supporters <actual-range-used>
|
||||||
|
```
|
||||||
|
Replace `<actual-range-used>` with the actual argument passed (e.g., `v0.6.1..v0.7.0`)
|
||||||
|
This comment should appear immediately before the first code contributor entry
|
||||||
|
If there's an existing comment in this format, replace it; otherwise add it
|
||||||
|
- Maintain all other file structure and RST formatting, including the "Before /update-supporters" marker comment
|
||||||
|
|
||||||
|
### Step 7: Apply Changes
|
||||||
|
|
||||||
|
- Use Edit tool to update the `docs/supporters.rst` file
|
||||||
|
- Display summary of changes made:
|
||||||
|
- Time/release range analyzed
|
||||||
|
- Number of contributors found
|
||||||
|
- New code contributors added
|
||||||
|
- Final counts by tier
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- **DO NOT modify, remove, or re-sort** any `|supporter-gold|` or `|supporter-std|` entries
|
||||||
|
- **For code contributors**: ADD new ones and MOVE UP recurring ones (those with contributions in the analyzed range)
|
||||||
|
- When extracting from git history, focus on substantial contributors (filter out single-commit or trivial changes if needed)
|
||||||
|
- **CRITICAL:** Contributors from the analyzed range go to the top
|
||||||
|
- Use `git log <range> --author="<email>" --format="%ai" -1` to get latest contribution date in the range
|
||||||
|
- Both NEW and RECURRING contributors from this range are sorted together (newest first) and placed at the TOP
|
||||||
|
- UNCHANGED contributors (not in this range) stay in their original relative order below
|
||||||
|
- This creates a chronological history: most recent release's contributors appear first
|
||||||
|
- Add/update the spacer comment showing the processed range
|
||||||
|
- Handle edge cases:
|
||||||
|
- Contributors mentioned in changelog but not in git history (external contributors, reporters)
|
||||||
|
- Contributors in git history but not mentioned in changelog (decide whether to include)
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Analyzing contributors for range: v0.6.0..v0.7.0
|
||||||
|
- Found 8 code contributors in git history
|
||||||
|
- Found 12 GitHub users mentioned in changelog.rst (v0.7.0 section)
|
||||||
|
- Current supporters.rst has 8 gold, 12 std, 10 code contributors
|
||||||
|
|
||||||
|
New contributors:
|
||||||
|
+ Jane Doe (@janedoe)
|
||||||
|
+ John Smith (@jsmith)
|
||||||
|
|
||||||
|
Promoted (recurring) contributors:
|
||||||
|
↑ Alice Developer (@alice) - new contributions in v0.6.0..v0.7.0
|
||||||
|
|
||||||
|
Updated docs/supporters.rst:
|
||||||
|
- Gold supporters: 8 (unchanged)
|
||||||
|
- Std supporters: 12 (unchanged)
|
||||||
|
- Code contributors: 10 → 12
|
||||||
|
- Promoted to top: 3 contributors (2 new + 1 recurring)
|
||||||
|
```
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
github: problame
|
||||||
|
patreon: zrepl
|
||||||
|
liberapay: zrepl
|
||||||
|
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||||
|
ko_fi: problame
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Docs use Python only for static site generation.
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: /docs
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "*"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# Build
|
# Build
|
||||||
artifacts/
|
artifacts/
|
||||||
|
build/install.tmp
|
||||||
|
|
||||||
# Golang
|
# Golang
|
||||||
vendor/
|
vendor/
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
version: "2"
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- revive
|
||||||
|
settings:
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: time-equal
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
path: _test\.go
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: 'SA9003:'
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: '(QF1001|QF1011|ST1012|QF1008|ST1005|ST1023|QF1003|ST1006|ST1001|QF1004):'
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- goimports
|
||||||
|
settings:
|
||||||
|
goimports:
|
||||||
|
local-prefixes:
|
||||||
|
- github.com/zrepl/zrepl
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
-69
@@ -1,69 +0,0 @@
|
|||||||
dist: xenial
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
env: # for allow_failures: https://docs.travis-ci.com/user/customizing-the-build/
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
|
|
||||||
- language: go
|
|
||||||
name: "Build in Docker (docs/installation.rst)"
|
|
||||||
script:
|
|
||||||
- sudo docker build -t zrepl_build -f build.Dockerfile .
|
|
||||||
- |
|
|
||||||
sudo docker run -it --rm \
|
|
||||||
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
|
|
||||||
--user "$(id -u):$(id -g)" \
|
|
||||||
zrepl_build make vendordeps release
|
|
||||||
|
|
||||||
- &zrepl_build_template
|
|
||||||
language: go
|
|
||||||
go_import_path: github.com/zrepl/zrepl
|
|
||||||
before_install:
|
|
||||||
- wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
|
||||||
- echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
|
||||||
- sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
|
||||||
- ./lazy.sh godep
|
|
||||||
- make vendordeps
|
|
||||||
script:
|
|
||||||
- make
|
|
||||||
- make vet
|
|
||||||
- make test
|
|
||||||
- make artifacts/zrepl-freebsd-amd64
|
|
||||||
- make artifacts/zrepl-linux-amd64
|
|
||||||
- make artifacts/zrepl-darwin-amd64
|
|
||||||
go:
|
|
||||||
- "1.11"
|
|
||||||
|
|
||||||
- <<: *zrepl_build_template
|
|
||||||
go:
|
|
||||||
- "1.12"
|
|
||||||
|
|
||||||
- <<: *zrepl_build_template
|
|
||||||
go:
|
|
||||||
- "master"
|
|
||||||
|
|
||||||
- &zrepl_docs_template
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- "3.4"
|
|
||||||
install:
|
|
||||||
- sudo apt-get install libgirepository1.0-dev
|
|
||||||
- pip install -r docs/requirements.txt
|
|
||||||
script:
|
|
||||||
- make docs
|
|
||||||
- <<: *zrepl_docs_template
|
|
||||||
python:
|
|
||||||
- "3.5"
|
|
||||||
- <<: *zrepl_docs_template
|
|
||||||
python:
|
|
||||||
- "3.6"
|
|
||||||
- <<: *zrepl_docs_template
|
|
||||||
python:
|
|
||||||
- "3.7"
|
|
||||||
|
|
||||||
|
|
||||||
allow_failures:
|
|
||||||
- <<: *zrepl_build_template
|
|
||||||
go:
|
|
||||||
- "master"
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0.9.30
|
||||||
Generated
-477
@@ -1,477 +0,0 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:8cf2cf1ab10480b5e0df950dac1517aaabde05d055d9d955652997ae4b9ecbbf"
|
|
||||||
name = "github.com/alvaroloes/enumer"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "6bcfe2edaac32ad71b88ce4cf92d34cd643e4ecb"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:c0bec5f9b98d0bc872ff5e834fac186b807b656683bd29cb82fb207a1513fabb"
|
|
||||||
name = "github.com/beorn7/perks"
|
|
||||||
packages = ["quantile"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b"
|
|
||||||
name = "github.com/davecgh/go-spew"
|
|
||||||
packages = ["spew"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:e988ed0ca0d81f4d28772760c02ee95084961311291bdfefc1b04617c178b722"
|
|
||||||
name = "github.com/fatih/color"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
|
||||||
version = "v1.7.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:5d0a2385edf4ba44f3b7b76bc0436ceb8f62bf55aa5d540a9eb9ec6c58d86809"
|
|
||||||
name = "github.com/ftrvxmtrx/fd"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "c6d800382fff6dc1412f34269f71b7f83bd059ad"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:4416ad04caf950d004921268f777e418f8c328080ccf6a91e799f977dc128808"
|
|
||||||
name = "github.com/gdamore/encoding"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:d8088c614a5bffaf38a40c116226e7d230750bafb77942dca25eab779ceeaf1f"
|
|
||||||
name = "github.com/gdamore/tcell"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"termbox",
|
|
||||||
"terminfo",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "de7e78efa4a71b3f36c7154989c529dbdf9ae623"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:6a4a01d58b227c4b6b11111b9f172ec5c17682b82724e58e6daf3f19f4faccd8"
|
|
||||||
name = "github.com/go-logfmt/logfmt"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
|
|
||||||
version = "v0.3.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18"
|
|
||||||
name = "github.com/golang/protobuf"
|
|
||||||
packages = [
|
|
||||||
"proto",
|
|
||||||
"protoc-gen-go",
|
|
||||||
"protoc-gen-go/descriptor",
|
|
||||||
"protoc-gen-go/generator",
|
|
||||||
"protoc-gen-go/generator/internal/remap",
|
|
||||||
"protoc-gen-go/grpc",
|
|
||||||
"protoc-gen-go/plugin",
|
|
||||||
"ptypes",
|
|
||||||
"ptypes/any",
|
|
||||||
"ptypes/duration",
|
|
||||||
"ptypes/timestamp",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
|
||||||
version = "v1.2.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:ad92aa49f34cbc3546063c7eb2cabb55ee2278b72842eda80e2a20a8a06a8d73"
|
|
||||||
name = "github.com/google/uuid"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
|
|
||||||
version = "v1.1.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:cb09475f771b9167fb9333629f5d6a7161572602ea040f1094602b0dc8709878"
|
|
||||||
name = "github.com/jinzhu/copier"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "db4671f3a9b8df855e993f7c94ec5ef1ffb0a23b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:1ed9eeebdf24aadfbca57eb50e6455bd1d2474525e0f0d4454de8c8e9bc7ee9a"
|
|
||||||
name = "github.com/kr/logfmt"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:591a2778aa6e896980757ea87e659b3aa13d8c0e790310614028463a31c0998b"
|
|
||||||
name = "github.com/kr/pretty"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:109e0a9b9e74c5c8adf94a2dc4dd4a9ca2a183d4d87ffecd4e62db69a5ede55a"
|
|
||||||
name = "github.com/kr/text"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "7cafcd837844e784b526369c9bce262804aebc60"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:863ba095ae0954100963826333bd1503c169424bb798d9ce800d9f6651f4c234"
|
|
||||||
name = "github.com/lucasb-eyer/go-colorful"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82"
|
|
||||||
version = "v1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:9ea83adf8e96d6304f394d40436f2eb44c1dc3250d223b74088cc253a6cd0a1c"
|
|
||||||
name = "github.com/mattn/go-colorable"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
|
||||||
version = "v0.0.9"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:78229b46ddb7434f881390029bd1af7661294af31f6802e0e1bedaad4ab0af3c"
|
|
||||||
name = "github.com/mattn/go-isatty"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
|
||||||
version = "v0.0.3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:82b912465c1da0668582a7d1117339c278e786c2536b3c3623029a0c7141c2d0"
|
|
||||||
name = "github.com/mattn/go-runewidth"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
|
|
||||||
version = "v0.0.3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:4c23ced97a470b17d9ffd788310502a077b9c1f60221a85563e49696276b4147"
|
|
||||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
|
||||||
packages = ["pbutil"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:4ff67dde814694496d7aa31be44b900f9717a10c8bc9136b13f49c8ef97f439a"
|
|
||||||
name = "github.com/montanaflynn/stats"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "63fbb2597b7a13043b453a4b819945badb8f8926"
|
|
||||||
version = "v0.5.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:f60ff065b58bd53e641112b38bbda9d2684deb828393c7ffb89c69a1ee301d17"
|
|
||||||
name = "github.com/pascaldekloe/name"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "0fd16699aae1833640fca52a937944c6f3b1d58c"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
|
||||||
version = "v0.8.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:1cbc6b98173422a756ae79e485952cb37a0a460c710541c75d3e9961c5a60782"
|
|
||||||
name = "github.com/pkg/profile"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "5b67d428864e92711fcbd2f8629456121a56d91f"
|
|
||||||
version = "v1.2.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
|
|
||||||
name = "github.com/pmezard/go-difflib"
|
|
||||||
packages = ["difflib"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:fa72f780ae3b4820ed12cef7a034291ab10d83e2da4ab5ba81afa44d5bf3a529"
|
|
||||||
name = "github.com/problame/go-netssh"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "09d6bc45d284784cb3e5aaa1998513f37eb19cc6"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:ebf8ffdde9bdbf9c83e22121875c68c01d821776523546554b2a3ff6f72773ab"
|
|
||||||
name = "github.com/prometheus/client_golang"
|
|
||||||
packages = [
|
|
||||||
"prometheus",
|
|
||||||
"prometheus/promhttp",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "e11c6ff8170beca9d5fd8b938e71165eeec53ac6"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:60aca47f4eeeb972f1b9da7e7db51dee15ff6c59f7b401c1588b8e6771ba15ef"
|
|
||||||
name = "github.com/prometheus/client_model"
|
|
||||||
packages = ["go"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:af21ee3e0a8212f17bb317cd7237f9920bcb2641a291ac111f30f63b3cab817f"
|
|
||||||
name = "github.com/prometheus/common"
|
|
||||||
packages = [
|
|
||||||
"expfmt",
|
|
||||||
"internal/bitbucket.org/ww/goautoneg",
|
|
||||||
"model",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "d0f7cd64bda49e08b22ae8a730aa57aa0db125d6"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:61df0898746840afc7be5dc2c3eeec83022fab70df11ecee5b16c85e912cf5ed"
|
|
||||||
name = "github.com/prometheus/procfs"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"internal/util",
|
|
||||||
"nfs",
|
|
||||||
"xfs",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:3962f553b77bf6c03fc07cd687a22dd3b00fe11aa14d31194f5505f5bb65cdc8"
|
|
||||||
name = "github.com/sergi/go-diff"
|
|
||||||
packages = ["diffmatchpatch"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:146327ce93be37e68bd3ff8541090d96da8cb3adc9e35d57570e9170a29f6bf6"
|
|
||||||
name = "github.com/spf13/cobra"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "b78744579491c1ceeaaa3b40205e56b0591b93a3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:261bc565833ef4f02121450d74eb88d5ae4bd74bfe5d0e862cddb8550ec35000"
|
|
||||||
name = "github.com/spf13/pflag"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:3926a4ec9a4ff1a072458451aa2d9b98acd059a45b38f7335d31e06c3d6a0159"
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
packages = [
|
|
||||||
"assert",
|
|
||||||
"require",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
|
||||||
version = "v1.1.4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:f80053a92d9ac874ad97d665d3005c1118ed66e9e88401361dc32defe6bef21c"
|
|
||||||
name = "github.com/theckman/goconstraint"
|
|
||||||
packages = ["go1.11/gte"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "93babf24513d0e8277635da8169fcc5a46ae3f6a"
|
|
||||||
version = "v1.11.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:529ed3f98838f69e13761788d0cc71b44e130058fab13bae2ce09f7a176bced4"
|
|
||||||
name = "github.com/yudai/gojsondiff"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"formatter",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:9857bb2293f372b2181004d8b62179bbdb4ab0982ec6f762abe6cf2bfedaff85"
|
|
||||||
name = "github.com/yudai/golcs"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "v2"
|
|
||||||
digest = "1:6b8a6afafde7ed31cd0c577ba40d88ce39e8f1c5eb76d7836be7d5b74f1c534a"
|
|
||||||
name = "github.com/zrepl/yaml-config"
|
|
||||||
packages = ["."]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "08227ad854131f7dfcdfb12579fb73dd8a38a03a"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:ea539c13b066dac72a940b62f37600a20ab8e88057397c78f3197c1a48475425"
|
|
||||||
name = "golang.org/x/net"
|
|
||||||
packages = [
|
|
||||||
"context",
|
|
||||||
"http/httpguts",
|
|
||||||
"http2",
|
|
||||||
"http2/hpack",
|
|
||||||
"idna",
|
|
||||||
"internal/timeseries",
|
|
||||||
"trace",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "351d144fa1fc0bd934e2408202be0c29f25e35a0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:f358024b019f87eecaadcb098113a40852c94fe58ea670ef3c3e2d2c7bd93db1"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
packages = ["unix"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "4ed8d59d0b35e1e29334a206d1b3f38b1e5dfb31"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4"
|
|
||||||
name = "golang.org/x/text"
|
|
||||||
packages = [
|
|
||||||
"collate",
|
|
||||||
"collate/build",
|
|
||||||
"encoding",
|
|
||||||
"encoding/internal/identifier",
|
|
||||||
"internal/colltab",
|
|
||||||
"internal/gen",
|
|
||||||
"internal/tag",
|
|
||||||
"internal/triegen",
|
|
||||||
"internal/ucd",
|
|
||||||
"language",
|
|
||||||
"secure/bidirule",
|
|
||||||
"transform",
|
|
||||||
"unicode/bidi",
|
|
||||||
"unicode/cldr",
|
|
||||||
"unicode/norm",
|
|
||||||
"unicode/rangetable",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
|
||||||
version = "v0.3.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:4cd780b2ee42c8eac9c02bfb6e6b52dcbaef770774458c8938f5cbfb73a7b6d3"
|
|
||||||
name = "golang.org/x/tools"
|
|
||||||
packages = ["cmd/stringer"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "d0ca3933b724e6be513276cc2edb34e10d667438"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:5fc6c317675b746d0c641b29aa0aab5fcb403c0d07afdbf0de86b0d447a0502a"
|
|
||||||
name = "google.golang.org/genproto"
|
|
||||||
packages = ["googleapis/rpc/status"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "bd91e49a0898e27abb88c339b432fa53d7497ac0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
digest = "1:d141efe4aaad714e3059c340901aab3147b6253e58c85dafbcca3dd8b0e88ad6"
|
|
||||||
name = "google.golang.org/grpc"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"balancer",
|
|
||||||
"balancer/base",
|
|
||||||
"balancer/roundrobin",
|
|
||||||
"binarylog/grpc_binarylog_v1",
|
|
||||||
"codes",
|
|
||||||
"connectivity",
|
|
||||||
"credentials",
|
|
||||||
"credentials/internal",
|
|
||||||
"encoding",
|
|
||||||
"encoding/proto",
|
|
||||||
"grpclog",
|
|
||||||
"internal",
|
|
||||||
"internal/backoff",
|
|
||||||
"internal/binarylog",
|
|
||||||
"internal/channelz",
|
|
||||||
"internal/envconfig",
|
|
||||||
"internal/grpcrand",
|
|
||||||
"internal/grpcsync",
|
|
||||||
"internal/syscall",
|
|
||||||
"internal/transport",
|
|
||||||
"keepalive",
|
|
||||||
"metadata",
|
|
||||||
"naming",
|
|
||||||
"peer",
|
|
||||||
"resolver",
|
|
||||||
"resolver/dns",
|
|
||||||
"resolver/passthrough",
|
|
||||||
"stats",
|
|
||||||
"status",
|
|
||||||
"tap",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "df014850f6dee74ba2fc94874043a9f3f75fbfd8"
|
|
||||||
version = "v1.17.0"
|
|
||||||
|
|
||||||
[solve-meta]
|
|
||||||
analyzer-name = "dep"
|
|
||||||
analyzer-version = 1
|
|
||||||
input-imports = [
|
|
||||||
"github.com/alvaroloes/enumer",
|
|
||||||
"github.com/fatih/color",
|
|
||||||
"github.com/gdamore/tcell/termbox",
|
|
||||||
"github.com/go-logfmt/logfmt",
|
|
||||||
"github.com/golang/protobuf/proto",
|
|
||||||
"github.com/golang/protobuf/protoc-gen-go",
|
|
||||||
"github.com/google/uuid",
|
|
||||||
"github.com/jinzhu/copier",
|
|
||||||
"github.com/kr/pretty",
|
|
||||||
"github.com/mattn/go-isatty",
|
|
||||||
"github.com/montanaflynn/stats",
|
|
||||||
"github.com/pkg/errors",
|
|
||||||
"github.com/pkg/profile",
|
|
||||||
"github.com/problame/go-netssh",
|
|
||||||
"github.com/prometheus/client_golang/prometheus",
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
|
||||||
"github.com/spf13/cobra",
|
|
||||||
"github.com/spf13/pflag",
|
|
||||||
"github.com/stretchr/testify/assert",
|
|
||||||
"github.com/stretchr/testify/require",
|
|
||||||
"github.com/yudai/gojsondiff",
|
|
||||||
"github.com/yudai/gojsondiff/formatter",
|
|
||||||
"github.com/zrepl/yaml-config",
|
|
||||||
"golang.org/x/net/context",
|
|
||||||
"golang.org/x/sys/unix",
|
|
||||||
"golang.org/x/tools/cmd/stringer",
|
|
||||||
"google.golang.org/grpc",
|
|
||||||
"google.golang.org/grpc/codes",
|
|
||||||
"google.golang.org/grpc/credentials",
|
|
||||||
"google.golang.org/grpc/keepalive",
|
|
||||||
"google.golang.org/grpc/peer",
|
|
||||||
"google.golang.org/grpc/status",
|
|
||||||
]
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
||||||
-65
@@ -1,65 +0,0 @@
|
|||||||
ignored = [
|
|
||||||
"github.com/inconshreveable/mousetrap",
|
|
||||||
]
|
|
||||||
|
|
||||||
required = [
|
|
||||||
"golang.org/x/tools/cmd/stringer",
|
|
||||||
"github.com/alvaroloes/enumer",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/jinzhu/copier"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/kr/pretty"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
version = "0.8.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
version = "1.1.4"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "v2"
|
|
||||||
name = "github.com/zrepl/yaml-config"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/go-logfmt/logfmt"
|
|
||||||
version = "*"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/problame/go-netssh"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/prometheus/client_golang"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/golang/protobuf"
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/fatih/color"
|
|
||||||
version = "1.7.0"
|
|
||||||
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/gdamore/tcell"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "google.golang.org/grpc"
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
version = "1.1.0"
|
|
||||||
name = "github.com/google/uuid"
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
.PHONY: generate build test vet cover release docs docs-clean clean vendordeps
|
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
|
||||||
.DEFAULT_GOAL := build
|
.PHONY: release release-noarch
|
||||||
|
.DEFAULT_GOAL := zrepl-bin
|
||||||
|
|
||||||
ARTIFACTDIR := artifacts
|
ARTIFACTDIR := artifacts
|
||||||
|
|
||||||
@@ -12,87 +13,380 @@ ifndef _ZREPL_VERSION
|
|||||||
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
|
|
||||||
|
|
||||||
GO_BUILD := go build -ldflags $(GO_LDFLAGS)
|
ZREPL_PACKAGE_RELEASE := 1
|
||||||
|
|
||||||
RELEASE_BINS := $(ARTIFACTDIR)/zrepl-freebsd-amd64
|
GO := go
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-amd64
|
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
||||||
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-darwin-amd64
|
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
|
||||||
|
GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"')
|
||||||
|
GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"')
|
||||||
|
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
|
||||||
|
GO_ENV_VARS := CGO_ENABLED=0
|
||||||
|
GO_LDFLAGS := "-X github.com/zrepl/zrepl/internal/version.zreplVersion=$(_ZREPL_VERSION)"
|
||||||
|
GO_MOD_READONLY := -mod=readonly
|
||||||
|
GO_EXTRA_BUILDFLAGS :=
|
||||||
|
GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
||||||
|
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||||
|
GOCOVMERGE := gocovmerge
|
||||||
|
RELEASE_GOVERSION ?= go1.25.7
|
||||||
|
STRIPPED_GOVERSION := $(subst go,,$(RELEASE_GOVERSION))
|
||||||
|
RELEASE_DOCKER_BASEIMAGE ?= golang:$(STRIPPED_GOVERSION)
|
||||||
|
RELEASE_DOCKER_CACHEMOUNT :=
|
||||||
|
|
||||||
RELEASE_NOARCH := $(ARTIFACTDIR)/zrepl-noarch.tar
|
ifneq ($(GOARM),)
|
||||||
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <(go env) && echo "zrepl-$${GOOS}-$${GOARCH}"' )
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||||
|
else
|
||||||
vendordeps:
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)
|
||||||
dep ensure -v -vendor-only
|
endif
|
||||||
|
|
||||||
generate: #not part of the build, must do that manually
|
|
||||||
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
|
|
||||||
go generate -x ./...
|
|
||||||
|
|
||||||
build:
|
|
||||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
|
||||||
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl"
|
|
||||||
|
|
||||||
test:
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
vet:
|
|
||||||
# for each supported platform to cover conditional compilation
|
|
||||||
GOOS=linux go vet ./...
|
|
||||||
GOOS=darwin go vet ./...
|
|
||||||
GOOS=freebsd go vet ./...
|
|
||||||
|
|
||||||
$(ARTIFACTDIR):
|
|
||||||
mkdir -p "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
|
||||||
mkdir -p "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/bash_completion: $(RELEASE_BINS)
|
|
||||||
artifacts/$(THIS_PLATFORM_RELEASE_BIN) bashcomp "$@"
|
|
||||||
|
|
||||||
$(ARTIFACTDIR)/go_version.txt:
|
|
||||||
go version > $@
|
|
||||||
|
|
||||||
docs: $(ARTIFACTDIR)/docs
|
|
||||||
make -C docs \
|
|
||||||
html \
|
|
||||||
BUILDDIR=../artifacts/docs \
|
|
||||||
|
|
||||||
docs-clean:
|
|
||||||
make -C docs \
|
|
||||||
clean \
|
|
||||||
BUILDDIR=../artifacts/docs
|
|
||||||
|
|
||||||
|
|
||||||
.PHONY: $(RELEASE_BINS)
|
ifneq ($(RELEASE_DOCKER_CACHEMOUNT),)
|
||||||
# TODO: two wildcards possible
|
_RELEASE_DOCKER_CACHEMOUNT := -v $(RELEASE_DOCKER_CACHEMOUNT)/mod:/go/pkg/mod -v $(RELEASE_DOCKER_CACHEMOUNT)/xdg-cache:/.cache/go-build
|
||||||
$(RELEASE_BINS): $(ARTIFACTDIR)/zrepl-%-amd64: generate $(ARTIFACTDIR) vet test
|
.PHONY: release-docker-mkcachemount
|
||||||
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
|
release-docker-mkcachemount:
|
||||||
GOOS=$* GOARCH=amd64 $(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$*-amd64"
|
mkdir -p $(RELEASE_DOCKER_CACHEMOUNT)
|
||||||
|
mkdir -p $(RELEASE_DOCKER_CACHEMOUNT)/mod
|
||||||
|
mkdir -p $(RELEASE_DOCKER_CACHEMOUNT)/xdg-cache
|
||||||
|
else
|
||||||
|
_RELEASE_DOCKER_CACHEMOUNT :=
|
||||||
|
.PHONY: release-docker-mkcachemount
|
||||||
|
release-docker-mkcachemount:
|
||||||
|
# nothing to do
|
||||||
|
endif
|
||||||
|
|
||||||
$(RELEASE_NOARCH): docs $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_version.txt
|
##################### PRODUCING A RELEASE #############
|
||||||
|
.PHONY: release wrapup-and-checksum check-git-clean verify-and-sign clean ensure-release-toolchain
|
||||||
|
|
||||||
|
ensure-release-toolchain:
|
||||||
|
# ensure the toolchain is actually the one we expect
|
||||||
|
test $(RELEASE_GOVERSION) = "$$($(GO_ENV_VARS) $(GO) env GOVERSION)"
|
||||||
|
|
||||||
|
release: ensure-release-toolchain
|
||||||
|
$(MAKE) _run_make_foreach_target_tuple RUN_MAKE_FOREACH_TARGET_TUPLE_ARG="vet"
|
||||||
|
$(MAKE) _run_make_foreach_target_tuple RUN_MAKE_FOREACH_TARGET_TUPLE_ARG="lint"
|
||||||
|
$(MAKE) _run_make_foreach_target_tuple RUN_MAKE_FOREACH_TARGET_TUPLE_ARG="zrepl-bin"
|
||||||
|
$(MAKE) _run_make_foreach_target_tuple RUN_MAKE_FOREACH_TARGET_TUPLE_ARG="test-platform-bin"
|
||||||
|
$(MAKE) noarch
|
||||||
|
|
||||||
|
release-docker: $(ARTIFACTDIR) release-docker-mkcachemount
|
||||||
|
sed 's/FROM.*!SUBSTITUTED_BY_MAKEFILE/FROM $(RELEASE_DOCKER_BASEIMAGE)/' build/build.Dockerfile > $(ARTIFACTDIR)/build.Dockerfile
|
||||||
|
docker build -t zrepl_release --pull \
|
||||||
|
--build-arg BUILD_UID=$$(id -u) \
|
||||||
|
--build-arg BUILD_GID=$$(id -g) \
|
||||||
|
-f $(ARTIFACTDIR)/build.Dockerfile .
|
||||||
|
docker run --rm -i$$(test -t 0 && echo t) \
|
||||||
|
$(_RELEASE_DOCKER_CACHEMOUNT) \
|
||||||
|
-v $(CURDIR):/src \
|
||||||
|
zrepl_release \
|
||||||
|
make release \
|
||||||
|
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
|
||||||
|
ZREPL_VERSION=$(ZREPL_VERSION) ZREPL_PACKAGE_RELEASE=$(ZREPL_PACKAGE_RELEASE) \
|
||||||
|
RELEASE_GOVERSION=$(RELEASE_GOVERSION)
|
||||||
|
|
||||||
|
debs-docker:
|
||||||
|
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=deb
|
||||||
|
rpms-docker:
|
||||||
|
$(MAKE) _debs_or_rpms_docker _DEB_OR_RPM=rpm
|
||||||
|
_debs_or_rpms_docker: # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
||||||
|
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=amd64
|
||||||
|
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm64
|
||||||
|
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=arm GOARM=7
|
||||||
|
$(MAKE) $(_DEB_OR_RPM)-docker GOOS=linux GOARCH=386
|
||||||
|
|
||||||
|
rpm: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
||||||
|
$(eval _ZREPL_RPM_VERSION := $(subst -,.,$(_ZREPL_VERSION)))
|
||||||
|
$(eval _ZREPL_RPM_TOPDIR_ABS := $(CURDIR)/$(ARTIFACTDIR)/rpmbuild)
|
||||||
|
rm -rf "$(_ZREPL_RPM_TOPDIR_ABS)"
|
||||||
|
mkdir "$(_ZREPL_RPM_TOPDIR_ABS)"
|
||||||
|
for d in BUILD BUILDROOT RPMS SOURCES SPECS SRPMS; do \
|
||||||
|
mkdir -p "$(_ZREPL_RPM_TOPDIR_ABS)/$$d"; \
|
||||||
|
done
|
||||||
|
sed \
|
||||||
|
-e "s/^Version:.*/Version: $(_ZREPL_RPM_VERSION)/g" \
|
||||||
|
-e "s/^Release:.*/Release: $(ZREPL_PACKAGE_RELEASE)/g" \
|
||||||
|
packaging/rpm/zrepl.spec \
|
||||||
|
> $(_ZREPL_RPM_TOPDIR_ABS)/SPECS/zrepl.spec
|
||||||
|
|
||||||
|
# see /usr/lib/rpm/platform
|
||||||
|
ifeq ($(GOARCH),amd64)
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := x86_64)
|
||||||
|
else ifeq ($(GOARCH), 386)
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := i386)
|
||||||
|
else ifeq ($(GOARCH), arm64)
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := aarch64)
|
||||||
|
else ifeq ($(GOARCH), arm)
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := armv7hl)
|
||||||
|
else
|
||||||
|
$(eval _ZREPL_RPMBUILD_TARGET := $(GOARCH))
|
||||||
|
endif
|
||||||
|
rpmbuild \
|
||||||
|
--build-in-place \
|
||||||
|
--define "_sourcedir $(CURDIR)" \
|
||||||
|
--define "_topdir $(_ZREPL_RPM_TOPDIR_ABS)" \
|
||||||
|
--define "_zrepl_binary_filename zrepl-$(ZREPL_TARGET_TUPLE)" \
|
||||||
|
--target $(_ZREPL_RPMBUILD_TARGET) \
|
||||||
|
-bb "$(_ZREPL_RPM_TOPDIR_ABS)"/SPECS/zrepl.spec
|
||||||
|
cp "$(_ZREPL_RPM_TOPDIR_ABS)"/RPMS/$(_ZREPL_RPMBUILD_TARGET)/zrepl-$(_ZREPL_RPM_VERSION)*.rpm $(ARTIFACTDIR)/
|
||||||
|
|
||||||
|
rpm-docker:
|
||||||
|
docker build -t zrepl_rpm_pkg --pull -f packaging/rpm/Dockerfile .
|
||||||
|
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
||||||
|
zrepl_rpm_pkg \
|
||||||
|
make rpm \
|
||||||
|
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
|
||||||
|
ZREPL_VERSION=$(ZREPL_VERSION) ZREPL_PACKAGE_RELEASE=$(ZREPL_PACKAGE_RELEASE)
|
||||||
|
|
||||||
|
|
||||||
|
deb: $(ARTIFACTDIR) # artifacts/_zrepl.zsh_completion artifacts/bash_completion docs zrepl-bin
|
||||||
|
|
||||||
|
cp packaging/deb/debian/changelog.template packaging/deb/debian/changelog
|
||||||
|
sed -i 's/DATE_DASH_R_OUTPUT/$(shell date -R)/' packaging/deb/debian/changelog
|
||||||
|
VERSION="$(subst -,.,$(_ZREPL_VERSION))-$(ZREPL_PACKAGE_RELEASE)"; \
|
||||||
|
export VERSION="$${VERSION#v}"; \
|
||||||
|
sed -i 's/VERSION/'"$$VERSION"'/' packaging/deb/debian/changelog
|
||||||
|
|
||||||
|
ifeq ($(GOARCH), arm)
|
||||||
|
$(eval DEB_HOST_ARCH := armhf)
|
||||||
|
else ifeq ($(GOARCH), 386)
|
||||||
|
$(eval DEB_HOST_ARCH := i386)
|
||||||
|
else
|
||||||
|
$(eval DEB_HOST_ARCH := $(GOARCH))
|
||||||
|
endif
|
||||||
|
|
||||||
|
export ZREPL_DPKG_ZREPL_BINARY_FILENAME=zrepl-$(ZREPL_TARGET_TUPLE); \
|
||||||
|
dpkg-buildpackage -b --no-sign --host-arch $(DEB_HOST_ARCH)
|
||||||
|
cp ../*.deb artifacts/
|
||||||
|
|
||||||
|
deb-docker:
|
||||||
|
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
||||||
|
# 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) \
|
||||||
|
--ulimit nofile=1024:1024 \
|
||||||
|
zrepl_debian_pkg \
|
||||||
|
make deb \
|
||||||
|
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
|
||||||
|
ZREPL_VERSION=$(ZREPL_VERSION) ZREPL_PACKAGE_RELEASE=$(ZREPL_PACKAGE_RELEASE)
|
||||||
|
|
||||||
|
# expects `release`, `deb` & `rpm` targets to have run before
|
||||||
|
NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
|
||||||
|
wrapup-and-checksum:
|
||||||
|
rm -f $(NOARCH_TARBALL)
|
||||||
tar --mtime='1970-01-01' --sort=name \
|
tar --mtime='1970-01-01' --sort=name \
|
||||||
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
||||||
-acf $@ \
|
--transform 's#dist#zrepl-$(_ZREPL_VERSION)-noarch/dist#' \
|
||||||
|
--transform 's#internal/config/samples#zrepl-$(_ZREPL_VERSION)-noarch/config#' \
|
||||||
|
-acf $(NOARCH_TARBALL) \
|
||||||
$(ARTIFACTDIR)/docs/html \
|
$(ARTIFACTDIR)/docs/html \
|
||||||
$(ARTIFACTDIR)/bash_completion \
|
$(ARTIFACTDIR)/bash_completion \
|
||||||
$(ARTIFACTDIR)/go_version.txt
|
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
||||||
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
release: $(RELEASE_BINS) $(RELEASE_NOARCH)
|
dist \
|
||||||
|
internal/config/samples
|
||||||
rm -rf "$(ARTIFACTDIR)/release"
|
rm -rf "$(ARTIFACTDIR)/release"
|
||||||
mkdir -p "$(ARTIFACTDIR)/release"
|
mkdir -p "$(ARTIFACTDIR)/release"
|
||||||
cp $^ "$(ARTIFACTDIR)/release"
|
cp -l $(ARTIFACTDIR)/zrepl* \
|
||||||
|
$(ARTIFACTDIR)/platformtest-* \
|
||||||
|
"$(ARTIFACTDIR)/release"
|
||||||
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
|
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
|
||||||
|
|
||||||
|
check-git-clean:
|
||||||
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
|
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
|
||||||
@if git describe --always --dirty 2>/dev/null | grep dirty >/dev/null; then \
|
@if git describe --always --dirty 2>/dev/null | grep dirty >/dev/null; then \
|
||||||
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
|
||||||
if [ "$(ZREPL_VERSION)" = "" ]; then \
|
if [ "$(ZREPL_VERSION)" = "" ]; then \
|
||||||
echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
|
echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
|
||||||
|
git status; \
|
||||||
|
echo "git diff:"; \
|
||||||
|
git diff | cat; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi; \
|
fi; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
|
tag-release:
|
||||||
|
test -n "$(ZREPL_TAG_VERSION)" || exit 1
|
||||||
|
git tag -u '328A6627FA98061D!' -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
|
||||||
|
|
||||||
|
verify-and-sign:
|
||||||
|
cd "$(ARTIFACTDIR)/release" && sha512sum -c sha512sum.txt
|
||||||
|
gpg -u '328A6627FA98061D!' \
|
||||||
|
--armor \
|
||||||
|
--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/' "$(JOB_NUM)" "$(ARTIFACTDIR)/release"
|
||||||
|
|
||||||
|
##################### MULTI-ARCH HELPERS #####################
|
||||||
|
|
||||||
|
_run_make_foreach_target_tuple:
|
||||||
|
if [ "$(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG)" = "" ]; then \
|
||||||
|
echo "RUN_MAKE_FOREACH_TARGET_TUPLE_ARG must be set"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=freebsd GOARCH=amd64
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=freebsd GOARCH=386
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=freebsd GOARCH=arm GOARM=7
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=freebsd GOARCH=arm64
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=linux GOARCH=amd64
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=linux GOARCH=arm64
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=linux GOARCH=arm GOARM=7
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=linux GOARCH=386
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=darwin GOARCH=amd64
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=solaris GOARCH=amd64
|
||||||
|
$(MAKE) $(RUN_MAKE_FOREACH_TARGET_TUPLE_ARG) GOOS=illumos GOARCH=amd64
|
||||||
|
|
||||||
|
##################### REGULAR TARGETS #####################
|
||||||
|
.PHONY: lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin
|
||||||
|
|
||||||
|
lint: build/install
|
||||||
|
$(GO_ENV_VARS) build/install/golangci_lint/golangci-lint run ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||||
|
|
||||||
|
test-go: $(ARTIFACTDIR)
|
||||||
|
rm -f "$(ARTIFACTDIR)/gotest.cover"
|
||||||
|
ifeq ($(COVER),1)
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
|
-coverpkg github.com/zrepl/zrepl/... \
|
||||||
|
-covermode atomic \
|
||||||
|
-coverprofile "$(ARTIFACTDIR)/gotest.cover" \
|
||||||
|
./...
|
||||||
|
else
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
|
./...
|
||||||
|
endif
|
||||||
|
|
||||||
|
zrepl-bin:
|
||||||
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||||
|
|
||||||
|
COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
|
||||||
|
cover-platform-bin:
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
|
-c -o "$(COVER_PLATFORM_BIN_PATH)" \
|
||||||
|
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
|
||||||
|
./platformtest/harness
|
||||||
|
cover-platform:
|
||||||
|
# do not track dependency on cover-platform-bin to allow build of binary outside of test VM
|
||||||
|
export _TEST_PLATFORM_CMD="$(COVER_PLATFORM_BIN_PATH) \
|
||||||
|
-test.coverprofile \"$(ARTIFACTDIR)/platformtest.cover\" \
|
||||||
|
-test.v \
|
||||||
|
__DEVEL--i-heard-you-like-tests"; \
|
||||||
|
$(MAKE) _test-or-cover-platform-impl
|
||||||
|
|
||||||
|
TEST_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)
|
||||||
|
test-platform-bin:
|
||||||
|
$(GO_BUILD) -o "$(TEST_PLATFORM_BIN_PATH)" ./internal/platformtest/harness
|
||||||
|
test-platform:
|
||||||
|
export _TEST_PLATFORM_CMD="\"$(TEST_PLATFORM_BIN_PATH)\""; \
|
||||||
|
$(MAKE) _test-or-cover-platform-impl
|
||||||
|
|
||||||
|
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
|
||||||
|
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
|
||||||
|
ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
|
||||||
|
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
|
||||||
|
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
|
||||||
|
ZREPL_PLATFORMTEST_ARGS :=
|
||||||
|
_test-or-cover-platform-impl: $(ARTIFACTDIR)
|
||||||
|
ifndef _TEST_PLATFORM_CMD
|
||||||
|
$(error _TEST_PLATFORM_CMD is undefined, caller 'cover-platform' or 'test-platform' should have defined it)
|
||||||
|
endif
|
||||||
|
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
|
||||||
|
rm -f "$(ARTIFACTDIR)/platformtest.cover"
|
||||||
|
internal/platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
|
||||||
|
$(_TEST_PLATFORM_CMD) \
|
||||||
|
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
|
||||||
|
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
|
||||||
|
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
|
||||||
|
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
|
||||||
|
$(ZREPL_PLATFORMTEST_ARGS)
|
||||||
|
|
||||||
|
cover-merge: $(ARTIFACTDIR)
|
||||||
|
$(GOCOVMERGE) $(ARTIFACTDIR)/platformtest.cover $(ARTIFACTDIR)/gotest.cover > $(ARTIFACTDIR)/merged.cover
|
||||||
|
cover-html: cover-merge
|
||||||
|
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
|
||||||
|
|
||||||
|
cover-full:
|
||||||
|
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
|
||||||
|
$(MAKE) test-go COVER=1
|
||||||
|
$(MAKE) cover-platform-bin
|
||||||
|
$(MAKE) cover-platform
|
||||||
|
$(MAKE) cover-html
|
||||||
|
|
||||||
|
##################### DEV TARGETS #####################
|
||||||
|
# not part of the build, must do that manually
|
||||||
|
.PHONY: generate format
|
||||||
|
|
||||||
|
build/install:
|
||||||
|
rm -rf build/install.tmp
|
||||||
|
mkdir build/install.tmp
|
||||||
|
|
||||||
|
-echo "installing protoc"
|
||||||
|
mkdir build/install.tmp/protoc
|
||||||
|
bash -x build/get_protoc.bash build/install.tmp/protoc
|
||||||
|
|
||||||
|
-echo "installing golangci-lint"
|
||||||
|
mkdir -p build/install.tmp/golangci_lint
|
||||||
|
GOHOSTARCH=$(GOHOSTARCH) \
|
||||||
|
build/get_golangci_lint.bash build/install.tmp/golangci_lint
|
||||||
|
|
||||||
|
-echo "installing go tools"
|
||||||
|
build/go_install_tools.bash build/install.tmp/gobin
|
||||||
|
|
||||||
|
mv build/install.tmp build/install
|
||||||
|
|
||||||
|
|
||||||
|
generate: build/install
|
||||||
|
# TODO: would be nice to run with a pure path here
|
||||||
|
PATH="$(CURDIR)/build/install/gobin:$(CURDIR)/build/install/protoc/bin:$$PATH" && \
|
||||||
|
build/install/protoc/bin/protoc -I=internal/replication/logic/pdu --go_out=internal/replication/logic/pdu --go-grpc_out=internal/replication/logic/pdu internal/replication/logic/pdu/pdu.proto && \
|
||||||
|
build/install/protoc/bin/protoc -I=internal/rpc/grpcclientidentity/example --go_out=internal/rpc/grpcclientidentity/example/pdu --go-grpc_out=internal/rpc/grpcclientidentity/example/pdu internal/rpc/grpcclientidentity/example/grpcauth.proto && \
|
||||||
|
$(GO) generate $(GO_BUILDFLAGS) -x ./... && \
|
||||||
|
true
|
||||||
|
|
||||||
|
GOIMPORTS := goimports -srcdir . -local 'github.com/zrepl/zrepl'
|
||||||
|
|
||||||
|
format: build/install
|
||||||
|
@ build/install/gobin/goimports -w -local 'github.com/zrepl/zrepl' $$(find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
|
||||||
|
|
||||||
|
##################### NOARCH #####################
|
||||||
|
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||||
|
|
||||||
|
|
||||||
|
$(ARTIFACTDIR):
|
||||||
|
mkdir -p "$@"
|
||||||
|
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||||
|
mkdir -p "$@"
|
||||||
|
|
||||||
|
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||||
|
# pass
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/bash_completion:
|
||||||
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@"
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/_zrepl.zsh_completion:
|
||||||
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/go_env.txt:
|
||||||
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
|
$(GO) version >> $@
|
||||||
|
|
||||||
|
docs: $(ARTIFACTDIR)/docs
|
||||||
|
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html
|
||||||
|
cd docs && uv sync --frozen
|
||||||
|
cd docs && uv run sphinx-build -W --keep-going -n . ../artifacts/docs/html
|
||||||
|
|
||||||
|
docs-clean:
|
||||||
|
rm -rf artifacts/docs
|
||||||
|
rm -rf docs/.venv
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://zrepl.github.io)
|
[](https://zrepl.github.io)
|
||||||
|
[](https://patreon.com/zrepl)
|
||||||
|
[](https://github.com/sponsors/problame)
|
||||||
|
[](https://liberapay.com/zrepl/donate)
|
||||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||||
[](https://liberapay.com/zrepl/donate)
|
|
||||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
||||||
|
[](https://matrix.to/#/#zrepl:matrix.org)
|
||||||
|
|
||||||
# zrepl
|
# zrepl
|
||||||
zrepl is a one-stop ZFS backup & replication solution.
|
zrepl is a one-stop ZFS backup & replication solution.
|
||||||
@@ -19,117 +22,207 @@ zrepl is a one-stop ZFS backup & replication solution.
|
|||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
1. Does you feature request require default values / some kind of configuration?
|
1. Does your feature request require default values / some kind of configuration?
|
||||||
If so, think of an expressive configuration example.
|
If so, think of an expressive configuration example.
|
||||||
2. Think of at least one use case that generalizes from your concrete application.
|
2. Think of at least one use case that generalizes from your concrete application.
|
||||||
3. Open an issue on GitHub with example conf & use case attached.
|
3. Open an issue on GitHub with example conf & use case attached.
|
||||||
|
4. **Optional**: [Contact Christian Schwarz](https://cschwarz.com) for contract work.
|
||||||
|
|
||||||
The above does not apply if you already implemented everything.
|
The above does not apply if you already implemented everything.
|
||||||
Check out the *Coding Workflow* section below for details.
|
Check out the *Coding Workflow* section below for details.
|
||||||
|
|
||||||
## Package Maintainer Information
|
## Development
|
||||||
|
|
||||||
* Follow the steps in `docs/installation.rst -> Compiling from Source` and read the Makefile / shell scripts used in this process.
|
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
||||||
* Make sure your distro is compatible with the paths in `docs/installation.rst`.
|
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
#### Go Code
|
||||||
|
|
||||||
|
Dependencies:
|
||||||
|
|
||||||
|
* Go
|
||||||
|
* GNU Make
|
||||||
|
* Git
|
||||||
|
* wget (`make generate`)
|
||||||
|
* unzip (`make generate`)
|
||||||
|
|
||||||
|
Some Go code is **generated**, and generated code is committed to the source tree.
|
||||||
|
Therefore, building does not require having code generation tools set up.
|
||||||
|
When making changes that require code to be (re-)generated, run `make generate`.
|
||||||
|
I downloads and installs pinned versions of the code generation tools into `./build/install`.
|
||||||
|
There is a CI check that ensures Git state is clean, i.e., code generation has been done by a PR and is deterministic.
|
||||||
|
|
||||||
|
#### Docs
|
||||||
|
|
||||||
|
Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then run `make docs`.
|
||||||
|
uv automatically manages Python and dependencies.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
The **test suite** is split into pure **Go tests** (`make test-go`) and **platform tests** that interact with ZFS and thus generally **require root privileges** (`sudo make test-platform`).
|
||||||
|
Platform tests run on their own pool with the name `zreplplatformtest`, which is created using the file vdev in `/tmp`.
|
||||||
|
|
||||||
|
For a full **code coverage** profile, run `make test-go COVER=1 && sudo make test-platform && make cover-merge`.
|
||||||
|
An HTML report can be generated using `make cover-html`.
|
||||||
|
|
||||||
|
### Circle CI
|
||||||
|
|
||||||
|
We use CircleCI for automated build & test pre- and post-merge.
|
||||||
|
|
||||||
|
There are two workflows:
|
||||||
|
|
||||||
|
* `ci` runs for every commit / branch / tag pushed to GitHub.
|
||||||
|
It is supposed to run very fast (<5min and provides quick feedback to developers).
|
||||||
|
It runs formatting checks, lints and tests on the most important OSes / architectures.
|
||||||
|
|
||||||
|
* `release` runs
|
||||||
|
* on manual triggers through the CircleCI API (in order to produce a release)
|
||||||
|
* periodically on `master`
|
||||||
|
|
||||||
|
Artifacts are stored in CircleCI.
|
||||||
|
|
||||||
|
### Releasing
|
||||||
|
|
||||||
|
All zrepl releases are git-tagged and then published as a GitHub Release.
|
||||||
|
There is a git tag for each zrepl release, usually `vMAJOR.MINOR.0`.
|
||||||
|
We don't move git tags once the release has been published.
|
||||||
|
|
||||||
|
The procedure to issue a release is as follows:
|
||||||
|
|
||||||
|
* Prepare the release (as a PR to `master`):
|
||||||
|
* Finalize `docs/changelog.rst` for the release.
|
||||||
|
* Update the supporters list based on contributors for this release:
|
||||||
|
```bash
|
||||||
|
claude --permission-mode default '/update-supporters v0.6.1..v0.7.0'
|
||||||
|
```
|
||||||
|
Replace version tags with the appropriate range for your release.
|
||||||
|
* Merge the PR. Docs are auto-published to zrepl.github.io on merge.
|
||||||
|
* Tag the release:
|
||||||
|
* Git tag the release on the `master` branch (e.g., `vMAJOR.MINOR.0`).
|
||||||
|
```
|
||||||
|
make tag-release ZREPL_TAG_VERSION=v0.7.0
|
||||||
|
```
|
||||||
|
* Push the tag.
|
||||||
|
* Build and publish:
|
||||||
|
* Run the `release` pipeline against the `master` branch (trigger via CircleCI UI).
|
||||||
|
This URL: https://app.circleci.com/pipelines/github/zrepl/zrepl?branch=master.
|
||||||
|
Example pipeline: https://app.circleci.com/pipelines/github/zrepl/zrepl/8547
|
||||||
|
* Download artifacts using this handy makefile target.
|
||||||
|
Note: `JOB_NUM` must be the **job number** from the `release-upload` job, **not the pipeline number**.
|
||||||
|
Find it via: pipeline → `release` workflow → `release-upload` job number.
|
||||||
|
Example URL: https://app.circleci.com/pipelines/github/zrepl/zrepl/8547/workflows/65feb2c9-15d7-46ab-a551-46d62a5769b0/jobs/66079/steps
|
||||||
|
|
||||||
|
```
|
||||||
|
make download-circleci-release JOB_NUM=66079
|
||||||
|
```
|
||||||
|
* Verify checksums and sign the checksums file:
|
||||||
|
```
|
||||||
|
make verify-and-sign
|
||||||
|
```
|
||||||
|
* Create GitHub draft release and upload artifacts:
|
||||||
|
```bash
|
||||||
|
claude --permission-mode default '/draft-release v0.7.0'
|
||||||
|
```
|
||||||
|
This command will verify that artifacts are ready, create the draft release, and upload all artifacts.
|
||||||
|
* Review the draft release on GitHub, then publish,
|
||||||
|
with the box checked to create a GitHub "Discussion" for the release.
|
||||||
|
* Immediately after hitting publish.
|
||||||
|
* Update the release on GitHub to link to the discussion.
|
||||||
|
* Update `docs/changelog.rst` to link to the GitHub release.
|
||||||
|
* Update `docs/_templates/versions.html` to link to the GitHub release.
|
||||||
|
* Announce release in the zrepl Matrix channel & other socials as applicable.
|
||||||
|
Link to the GitHub release as the entrypoint.
|
||||||
|
* After a couple of days
|
||||||
|
* Add the .rpm and .deb files to the official zrepl repos.
|
||||||
|
* Code for management of these repos: https://github.com/zrepl/package-repo-ops (private repo at this time)
|
||||||
|
|
||||||
|
#### Patch releases, Go toolchain updates, APT/RPM Package rebuilds
|
||||||
|
|
||||||
|
`vMAJOR.MINOR.0` is typically a tagged commit on `master`, because development velocity isn't high
|
||||||
|
and thus release branches for stabilization aren't necessary.
|
||||||
|
|
||||||
|
Occasionally though there is a need for patch changes to a release, e.g.
|
||||||
|
- security issue in a dependency
|
||||||
|
- Go toolchain update (e.g. security issue in standard library)
|
||||||
|
|
||||||
|
The procedure for this is the following
|
||||||
|
- create a branch off the release tag we need to patch, named `releases/MAJOR.MINOR.X`
|
||||||
|
- that branch will never be merged into `master`, it'll be a dead-end for this specific patch
|
||||||
|
- make changes in that branch
|
||||||
|
- make the final commit that bumps version numbers
|
||||||
|
- create the git tag
|
||||||
|
- follow general procedure for publishing the release (previous sectino
|
||||||
|
|
||||||
|
For Go toolchain updates and package rebuilds with no source code changes, leverage the APT/RPM package revision field.
|
||||||
|
Control via the `ZREPL_PACKAGE_RELEASE` Makefile variable, see `origin/releases/0.6.1-2` for an example.
|
||||||
|
|
||||||
|
|
||||||
|
### Updating Dependencies
|
||||||
|
|
||||||
|
- Update the `go` directive and `toolchain` directive in `go.mod`
|
||||||
|
- `go` is the minimum supported version
|
||||||
|
- `toolchain` is the preferred toolchain version if `GOTOOLCHAIN` is not specified
|
||||||
|
|
||||||
|
Run `go mod tidy` to ensure consistency.
|
||||||
|
|
||||||
|
Update Go module dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update all other dependencies
|
||||||
|
go get -u -t ./...
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# Above might fail if there are version selection conflicts.
|
||||||
|
# Figure out what's going on by updating packages from error messages first.
|
||||||
|
# Example:
|
||||||
|
go get -u google.golang.org/genproto google.golang.org/grpc google.golang.org/protobuf
|
||||||
|
```
|
||||||
|
|
||||||
|
Update codegen & lint tools
|
||||||
|
- `protoc` => `build/get_protoc.bash`
|
||||||
|
- GH releases publish sha256 sums
|
||||||
|
- `golangci-lint` => `build/get_golangci_lint.bash`
|
||||||
|
- bump versions in `build/tools.go`
|
||||||
|
- we use the tools.go trick:
|
||||||
|
- `go get -tags tools -u example.com/tool ; go mod tidy`
|
||||||
|
- review whether we're ready to switch to `go tool`: https://github.com/zrepl/zrepl/pull/909
|
||||||
|
|
||||||
|
Now run `make generate`.
|
||||||
|
|
||||||
|
Run `make lint` and `make vet`.
|
||||||
|
|
||||||
|
Update the CI configuration `.circleci/config.yml`:
|
||||||
|
- Update Go version references (we reference the minimum and max supported version)
|
||||||
|
- Set `Makefile` `RELEASE_GOVERSION` to the new Go version
|
||||||
|
- Update the pinned ZFS release tags in the `platformtest` matrix (`zfs_release` parameter) to the latest patch releases of each OpenZFS branch (currently 2.2, 2.3, 2.4)
|
||||||
|
- Update the pinned Ubuntu machine image for `platformtest` (e.g. `ubuntu-2404:2025.09.1`) — this is pinned rather than `current` so the ZFS build cache stays valid across runs
|
||||||
|
|
||||||
|
Update docs build tooling:
|
||||||
|
- Update `uv` version in `.circleci/config.yml` (search for `astral.sh/uv/` and cache keys containing the version)
|
||||||
|
- Check if there's now a CircleCI orb for uv that we could use
|
||||||
|
- Update Python version in `docs/.python-version`
|
||||||
|
|
||||||
|
Update docs dependencies (Sphinx, sphinx-rtd-theme):
|
||||||
|
- Check current versions in `docs/pyproject.toml`
|
||||||
|
- Review upstream changelogs for breaking changes
|
||||||
|
- Update version constraints in `pyproject.toml` and the `uv` lockfile (see [uv docs on dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/)):
|
||||||
|
- Test locally with `make docs`
|
||||||
|
|
||||||
|
Kick a full CI pipeline run (`do_ci=true` and `do_release=true`).
|
||||||
|
|
||||||
|
Merge PR with merge commit.
|
||||||
|
|
||||||
|
## Notes to Distro Package Maintainers
|
||||||
|
|
||||||
|
* The `Makefile` in this project is not suitable for builds in distros.
|
||||||
|
* Run the platform tests (Docs -> Usage -> Platform Tests) **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
|
||||||
* Ship a default config that adheres to your distro's `hier` and logging system.
|
* Ship a default config that adheres to your distro's `hier` and logging system.
|
||||||
* Ship a service manager file and _please_ try to upstream it to this repository.
|
* Ship a service manager file and _please_ try to upstream it to this repository.
|
||||||
* `dist/systemd` contains a Systemd unit template.
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||||
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
|
* Have a look at the `Makefile`'s `ZREPL_VERSION` variable and how it passed to Go's `ldFlags`.
|
||||||
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
|
This is how `zrepl version` knows what version number to show.
|
||||||
|
Your build system should set the `ldFlags` flags appropriately and add a prefix or suffix that indicates that the given zrepl binary is a distro build, not an official one.
|
||||||
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
|
||||||
|
|
||||||
## Developer Documentation
|
|
||||||
|
|
||||||
zrepl is written in [Go](https://golang.org) and uses [`dep`](https://github.com/golang/dep) to manage dependencies.
|
|
||||||
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
|
||||||
|
|
||||||
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
|
|
||||||
|
|
||||||
### Overall Architecture
|
|
||||||
|
|
||||||
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
|
|
||||||
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── artifacts # build artifcats generate by make
|
|
||||||
├── cli # wrapper around CLI package cobra
|
|
||||||
├── client # all subcommands that are not `daemon`
|
|
||||||
├── config # config data types (=> package yaml-config)
|
|
||||||
│ └── samples
|
|
||||||
├── daemon # the implementation of `zrepl daemon` subcommand
|
|
||||||
│ ├── filters
|
|
||||||
│ ├── job # job implementations
|
|
||||||
│ ├── logging # logging outlets + formatters
|
|
||||||
│ ├── nethelpers
|
|
||||||
│ ├── prometheus
|
|
||||||
│ ├── pruner # pruner implementation
|
|
||||||
│ ├── snapper # snapshotter implementation
|
|
||||||
├── docs # sphinx-based documentation
|
|
||||||
├── dist # supplemental material for users & package maintainers
|
|
||||||
│ ├── **/*.rst # documentation in reStructuredText
|
|
||||||
│ ├── sphinxconf
|
|
||||||
│ │ └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
|
|
||||||
│ ├── requirements.txt # pip3 requirements to build documentation
|
|
||||||
│ ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo
|
|
||||||
│ └── public_git # checkout of zrepl.github.io managed by above shell script
|
|
||||||
├── endpoint # implementation of replication endpoints (=> package replication)
|
|
||||||
├── logger # our own logger package
|
|
||||||
├── pruning # pruning rules (the logic, not the actual execution)
|
|
||||||
│ └── retentiongrid
|
|
||||||
├── replication
|
|
||||||
│ ├── driver # the driver of the replication logic (status reporting, error handling)
|
|
||||||
│ ├── logic # planning & executing replication steps via rpc
|
|
||||||
| | └── pdu # the generated gRPC & protobuf code used in replication (and endpoints)
|
|
||||||
│ └── report # the JSON-serializable report datastructures exposed to the client
|
|
||||||
├── rpc # the hybrid gRPC + ./dataconn RPC client: connects to a remote replication.Endpoint
|
|
||||||
│ ├── dataconn # Bulk data-transfer RPC protocol
|
|
||||||
│ ├── grpcclientidentity # adaptor to inject package transport's 'client identity' concept into gRPC contexts
|
|
||||||
│ ├── netadaptor # adaptor to convert a package transport's Connecter and Listener into net.* primitives
|
|
||||||
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
|
|
||||||
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
|
|
||||||
├── tlsconf # abstraction for Go TLS server + client config
|
|
||||||
├── transport # transports implementation
|
|
||||||
│ ├── fromconfig
|
|
||||||
│ ├── local
|
|
||||||
│ ├── ssh
|
|
||||||
│ ├── tcp
|
|
||||||
│ └── tls
|
|
||||||
├── util
|
|
||||||
├── vendor # managed by dep
|
|
||||||
├── version # abstraction for versions (filled during build by Makefile)
|
|
||||||
└── zfs # zfs(8) wrappers
|
|
||||||
```
|
|
||||||
|
|
||||||
### Coding Workflow
|
|
||||||
|
|
||||||
* Open an issue when starting to hack on a new feature
|
|
||||||
* Commits should reference the issue they are related to
|
|
||||||
* Docs improvements not documenting new features do not require an issue.
|
|
||||||
|
|
||||||
### Breaking Changes
|
|
||||||
|
|
||||||
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
|
|
||||||
|
|
||||||
* Config-breaking changes must contain a line `BREAK CONFIG` in the commit message
|
|
||||||
* Other breaking changes must contain a line `BREAK` in the commit message
|
|
||||||
|
|
||||||
### Glossary & Naming Inconsistencies
|
|
||||||
|
|
||||||
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
|
||||||
However, we need a word for *filesystem* & *ZVOL* but not a snapshot, bookmark, etc.
|
|
||||||
|
|
||||||
Toward the user, the following terminology is used:
|
|
||||||
|
|
||||||
* **filesystem**: a ZFS filesystem or a ZVOL
|
|
||||||
* **filesystem version**: a ZFS snapshot or a bookmark
|
|
||||||
|
|
||||||
Sadly, the zrepl implementation is inconsistent in its use of these words:
|
|
||||||
variables and types are often named *dataset* when they in fact refer to a *filesystem*.
|
|
||||||
|
|
||||||
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
|
|
||||||
|
|
||||||
However, new contributions & patches should fix naming without further notice in the commit message.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
FROM golang:latest
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
python3-pip \
|
|
||||||
unzip
|
|
||||||
|
|
||||||
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
|
|
||||||
RUN echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
|
|
||||||
RUN unzip -d /usr protoc-3.6.1-linux-x86_64.zip
|
|
||||||
|
|
||||||
ADD lazy.sh /tmp/lazy.sh
|
|
||||||
ADD docs/requirements.txt /tmp/requirements.txt
|
|
||||||
ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
|
|
||||||
RUN /tmp/lazy.sh docdep
|
|
||||||
|
|
||||||
# prepare volume mount of git checkout to /zrepl
|
|
||||||
RUN mkdir -p /go/src/github.com/zrepl/zrepl
|
|
||||||
RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
|
||||||
WORKDIR /go/src/github.com/zrepl/zrepl
|
|
||||||
|
|
||||||
ADD Gopkg.toml Gopkg.lock ./
|
|
||||||
|
|
||||||
# godep will install the Go dependencies to vendor in order to then build and install
|
|
||||||
# build dependencies like stringer to $GOPATH/bin.
|
|
||||||
# However, since users volume-mount their Git checkout into /go/src/github.com/zrepl/zrepl
|
|
||||||
# the vendor directory will be empty at build time, allowing them to experiment with
|
|
||||||
# new checkouts, etc.
|
|
||||||
# Thus, we only use the vendored deps for building dependencies.
|
|
||||||
RUN /tmp/lazy.sh godep
|
|
||||||
RUN chmod -R 0777 /go
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
install/
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
FROM !SUBSTITUTED_BY_MAKEFILE
|
||||||
|
|
||||||
|
ARG BUILD_UID=1000
|
||||||
|
ARG BUILD_GID=1000
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
unzip \
|
||||||
|
gawk \
|
||||||
|
curl
|
||||||
|
|
||||||
|
# Create build user with the host's UID/GID before installing uv
|
||||||
|
RUN groupadd -g ${BUILD_GID} zrepl_build && \
|
||||||
|
useradd -u ${BUILD_UID} -g ${BUILD_GID} -m zrepl_build
|
||||||
|
|
||||||
|
# Go toolchain uses xdg-cache
|
||||||
|
RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
||||||
|
|
||||||
|
# Install uv as the build user - version from .uv-version file
|
||||||
|
ADD .uv-version /tmp/.uv-version
|
||||||
|
USER zrepl_build
|
||||||
|
RUN UV_VERSION=$(cat /tmp/.uv-version) && \
|
||||||
|
curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh
|
||||||
|
|
||||||
|
# Go devtools are managed by Makefile
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
ENV PATH="/home/zrepl_build/.local/bin:$PATH" \
|
||||||
|
GOCACHE="/.cache/go-build"
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
//+build windows
|
|
||||||
|
|
||||||
// This package cannot actually be built, and since the Travis
|
|
||||||
// tests run go test ./..., we have to avoid a build attempt.
|
|
||||||
// Windows is not supported atm, so this works ¯\_(ツ)_/¯
|
|
||||||
|
|
||||||
// This package is a pseudo-user of build-time dependencies for zrepl,
|
|
||||||
// mostly for various code-generation tools.
|
|
||||||
//
|
|
||||||
// The imports are necessary to satisfy go dep
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "fmt"
|
|
||||||
_ "github.com/alvaroloes/enumer"
|
|
||||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("just a placeholder")
|
|
||||||
}
|
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# golangci-lint recommends against using `go get` and friends to install it
|
||||||
|
|
||||||
|
cd "$1"
|
||||||
|
|
||||||
|
VERSION=2.8.0
|
||||||
|
FILENAME=golangci-lint-"$VERSION"-linux-"$GOHOSTARCH".tar.gz
|
||||||
|
|
||||||
|
if [ -e "$FILENAME" ]; then
|
||||||
|
echo "$FILENAME" already exists 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget --continue https://github.com/golangci/golangci-lint/releases/download/v"$VERSION"/"$FILENAME"
|
||||||
|
|
||||||
|
stat "$FILENAME"
|
||||||
|
|
||||||
|
# Select the correct checksum for the downloaded architecture
|
||||||
|
case "$GOHOSTARCH" in
|
||||||
|
arm64) EXPECTED_SHA256="2a58388db8af5ab9330791cea0ebdd4100723cd05ad7185d92febaaee272ec9a" ;;
|
||||||
|
amd64) EXPECTED_SHA256="7048bc6b25c9515ed092c83f9fa8709ca97937ead52d9ff317a143299ee97a50" ;;
|
||||||
|
*) echo "Unknown architecture: $GOHOSTARCH" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Verify checksum explicitly - fails if hash doesn't match
|
||||||
|
echo "$EXPECTED_SHA256 $FILENAME" | sha256sum -c -
|
||||||
|
|
||||||
|
tar -x --strip-components=1 -f "$FILENAME"
|
||||||
|
|
||||||
|
stat ./golangci-lint
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
cd "$1"
|
||||||
|
|
||||||
|
MACH=$(uname -m)
|
||||||
|
MACH="${MACH/aarch64/aarch_64}"
|
||||||
|
|
||||||
|
VERSION=33.5
|
||||||
|
FILENAME=protoc-"$VERSION"-linux-"$MACH".zip
|
||||||
|
|
||||||
|
if [ -e "$FILENAME" ]; then
|
||||||
|
echo "$FILENAME" already exists 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget --continue https://github.com/protocolbuffers/protobuf/releases/download/v"$VERSION"/"$FILENAME"
|
||||||
|
|
||||||
|
stat "$FILENAME"
|
||||||
|
|
||||||
|
# Select the correct checksum for the downloaded architecture
|
||||||
|
case "$MACH" in
|
||||||
|
aarch_64) EXPECTED_SHA256="2b0fcf9b2c32cbadccc0eb7a88b841fffecd4a06fc80acdba2b5be45e815c38a" ;;
|
||||||
|
x86_64) EXPECTED_SHA256="24e58fb231d50306ee28491f33a170301e99540f7e29ca461e0e80fd1239f8d1" ;;
|
||||||
|
*) echo "Unknown architecture: $MACH" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Verify checksum explicitly - fails if hash doesn't match
|
||||||
|
echo "$EXPECTED_SHA256 $FILENAME" | sha256sum -c -
|
||||||
|
|
||||||
|
unzip -d . "$FILENAME"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
module github.com/zrepl/zrepl/build
|
||||||
|
|
||||||
|
go 1.24.13
|
||||||
|
|
||||||
|
toolchain go1.25.7
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alvaroloes/enumer v1.1.2
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
||||||
|
golang.org/x/tools v0.41.0
|
||||||
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/pascaldekloe/name v1.0.1 // indirect
|
||||||
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/telemetry v0.0.0-20260205145544-86a5c4bf3c8d // indirect
|
||||||
|
golang.org/x/text v0.31.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
||||||
|
google.golang.org/grpc v1.75.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
github.com/alvaroloes/enumer v1.1.2 h1:5khqHB33TZy1GWCO/lZwcroBFh7u+0j40T83VUbfAMY=
|
||||||
|
github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
||||||
|
github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0=
|
||||||
|
github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0=
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/telemetry v0.0.0-20260205145544-86a5c4bf3c8d h1:xjbyaj6khjO0ocYYa7k9sQLbdi4R1KwHnsS1QGijbsM=
|
||||||
|
golang.org/x/telemetry v0.0.0-20260205145544-86a5c4bf3c8d/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||||
|
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||||
|
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 h1:/WILD1UcXj/ujCxgoL/DvRgt2CP3txG8+FwkUbb9110=
|
||||||
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1/go.mod h1:YNKnb2OAApgYn2oYY47Rn7alMr1zWjb2U8Q0aoGWiNc=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export GO111MODULE=on # otherwise, a checkout of this repo in GOPATH will disable modules on Go 1.12 and earlier
|
||||||
|
source <(go env)
|
||||||
|
# build tools for the host platform
|
||||||
|
export GOOS="$GOHOSTOS"
|
||||||
|
export GOARCH="$GOHOSTARCH"
|
||||||
|
# TODO GOARM=$GOHOSTARM?
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
OUTDIR="$(readlink -f "$1")"
|
||||||
|
if [ -e "$OUTDIR" ]; then
|
||||||
|
echo "$OUTDIR" already exists 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# go install command below will install tools to $GOBIN
|
||||||
|
export GOBIN="$OUTDIR"
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
source ./go_install_host_tool.source
|
||||||
|
|
||||||
|
cat tools.go | grep _ | awk -F'"' '{print $2}' | tee | xargs -tI '{}' go install '{}'
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build tools
|
||||||
|
// +build tools
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// the lines are parsed by lazy.sh, do not edit
|
||||||
|
import (
|
||||||
|
_ "github.com/alvaroloes/enumer"
|
||||||
|
_ "github.com/wadey/gocovmerge"
|
||||||
|
_ "golang.org/x/tools/cmd/goimports"
|
||||||
|
_ "golang.org/x/tools/cmd/stringer"
|
||||||
|
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
||||||
|
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||||
|
)
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rootArgs struct {
|
|
||||||
configPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "zrepl",
|
|
||||||
Short: "One-stop ZFS replication solution",
|
|
||||||
}
|
|
||||||
|
|
||||||
var bashcompCmd = &cobra.Command{
|
|
||||||
Use: "bashcomp path/to/out/file",
|
|
||||||
Short: "generate bash completions",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if len(args) != 1 {
|
|
||||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
|
||||||
cmd.Usage()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Hidden: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
|
||||||
rootCmd.AddCommand(bashcompCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Subcommand struct {
|
|
||||||
Use string
|
|
||||||
Short string
|
|
||||||
Example string
|
|
||||||
NoRequireConfig bool
|
|
||||||
Run func(subcommand *Subcommand, args []string) error
|
|
||||||
SetupFlags func(f *pflag.FlagSet)
|
|
||||||
SetupSubcommands func() []*Subcommand
|
|
||||||
|
|
||||||
config *config.Config
|
|
||||||
configErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subcommand) ConfigParsingError() error {
|
|
||||||
return s.configErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subcommand) Config() *config.Config {
|
|
||||||
if !s.NoRequireConfig && s.config == nil {
|
|
||||||
panic("command that requires config is running and has no config set")
|
|
||||||
}
|
|
||||||
return s.config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subcommand) run(cmd *cobra.Command, args []string) {
|
|
||||||
s.tryParseConfig()
|
|
||||||
err := s.Run(s, args)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subcommand) tryParseConfig() {
|
|
||||||
config, err := config.ParseConfig(rootArgs.configPath)
|
|
||||||
s.configErr = err
|
|
||||||
if err != nil {
|
|
||||||
if s.NoRequireConfig {
|
|
||||||
// doesn't matter
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "could not parse config: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.config = config
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddSubcommand(s *Subcommand) {
|
|
||||||
addSubcommandToCobraCmd(rootCmd, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) {
|
|
||||||
cmd := cobra.Command{
|
|
||||||
Use: s.Use,
|
|
||||||
Short: s.Short,
|
|
||||||
Example: s.Example,
|
|
||||||
}
|
|
||||||
if s.SetupSubcommands == nil {
|
|
||||||
cmd.Run = s.run
|
|
||||||
} else {
|
|
||||||
for _, sub := range s.SetupSubcommands() {
|
|
||||||
addSubcommandToCobraCmd(&cmd, sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.SetupFlags != nil {
|
|
||||||
s.SetupFlags(cmd.Flags())
|
|
||||||
}
|
|
||||||
c.AddCommand(&cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func Run() {
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
MigrateCmd = &cli.Subcommand{
|
|
||||||
Use: "migrate",
|
|
||||||
Short: "perform migration of the on-disk / zfs properties",
|
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
|
||||||
return migrations
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type migration struct {
|
|
||||||
name string
|
|
||||||
method func(config *config.Config, args []string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
var migrations = []*cli.Subcommand{
|
|
||||||
&cli.Subcommand{
|
|
||||||
Use: "0.0.X:0.1:placeholder",
|
|
||||||
Run: doMigratePlaceholder0_1,
|
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
|
||||||
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var migratePlaceholder0_1Args struct {
|
|
||||||
dryRun bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
|
|
||||||
if len(args) != 0 {
|
|
||||||
return fmt.Errorf("migration does not take arguments, got %v", args)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := sc.Config()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "cannot list filesystems")
|
|
||||||
}
|
|
||||||
|
|
||||||
type workItem struct {
|
|
||||||
jobName string
|
|
||||||
rootFS *zfs.DatasetPath
|
|
||||||
fss []*zfs.DatasetPath
|
|
||||||
}
|
|
||||||
var wis []workItem
|
|
||||||
for i, j := range cfg.Jobs {
|
|
||||||
var rfsS string
|
|
||||||
switch job := j.Ret.(type) {
|
|
||||||
case *config.SinkJob:
|
|
||||||
rfsS = job.RootFS
|
|
||||||
case *config.PullJob:
|
|
||||||
rfsS = job.RootFS
|
|
||||||
default:
|
|
||||||
fmt.Printf("ignoring job %q (%d/%d, type %T)\n", j.Name(), i, len(cfg.Jobs), j.Ret)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rfs, err := zfs.NewDatasetPath(rfsS)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "root fs for job %q is not a valid dataset path", j.Name())
|
|
||||||
}
|
|
||||||
var fss []*zfs.DatasetPath
|
|
||||||
for _, fs := range allFSS {
|
|
||||||
if fs.HasPrefix(rfs) {
|
|
||||||
fss = append(fss, fs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wis = append(wis, workItem{j.Name(), rfs, fss})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, wi := range wis {
|
|
||||||
fmt.Printf("job %q => migrate filesystems below root_fs %q\n", wi.jobName, wi.rootFS.ToString())
|
|
||||||
if len(wi.fss) == 0 {
|
|
||||||
fmt.Printf("\tno filesystems\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, fs := range wi.fss {
|
|
||||||
fmt.Printf("\t%q ... ", fs.ToString())
|
|
||||||
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(fs, migratePlaceholder0_1Args.dryRun)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error: %s\n", err)
|
|
||||||
} else if !r.NeedsModification {
|
|
||||||
fmt.Printf("unchanged (placeholder=%v)\n", r.OriginalState.IsPlaceholder)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("migrate (placeholder=%v) (old value = %q)\n",
|
|
||||||
r.OriginalState.IsPlaceholder, r.OriginalState.RawLocalPropertyValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,641 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/gdamore/tcell/termbox"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type byteProgressMeasurement struct {
|
|
||||||
time time.Time
|
|
||||||
val int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type bytesProgressHistory struct {
|
|
||||||
last *byteProgressMeasurement // pointer as poor man's optional
|
|
||||||
changeCount int
|
|
||||||
lastChange time.Time
|
|
||||||
bpsAvg float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
|
||||||
|
|
||||||
if p.last == nil {
|
|
||||||
p.last = &byteProgressMeasurement{
|
|
||||||
time: time.Now(),
|
|
||||||
val: currentVal,
|
|
||||||
}
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.last.val != currentVal {
|
|
||||||
p.changeCount++
|
|
||||||
p.lastChange = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().Sub(p.lastChange) > 3 * time.Second {
|
|
||||||
p.last = nil
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
deltaV := currentVal - p.last.val;
|
|
||||||
deltaT := time.Now().Sub(p.last.time)
|
|
||||||
rate := float64(deltaV) / deltaT.Seconds()
|
|
||||||
|
|
||||||
factor := 0.3
|
|
||||||
p.bpsAvg = (1-factor) * p.bpsAvg + factor * rate
|
|
||||||
|
|
||||||
p.last.time = time.Now()
|
|
||||||
p.last.val = currentVal
|
|
||||||
|
|
||||||
return int64(p.bpsAvg), p.changeCount
|
|
||||||
}
|
|
||||||
|
|
||||||
type tui struct {
|
|
||||||
x, y int
|
|
||||||
indent int
|
|
||||||
|
|
||||||
lock sync.Mutex //For report and error
|
|
||||||
report map[string]job.Status
|
|
||||||
err error
|
|
||||||
|
|
||||||
replicationProgress map[string]*bytesProgressHistory // by job name
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTui() tui {
|
|
||||||
return tui{
|
|
||||||
replicationProgress: make(map[string]*bytesProgressHistory, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) moveCursor(x, y int) {
|
|
||||||
t.x += x
|
|
||||||
t.y += y
|
|
||||||
}
|
|
||||||
|
|
||||||
const INDENT_MULTIPLIER = 4
|
|
||||||
|
|
||||||
func (t *tui) moveLine(dl int, col int) {
|
|
||||||
t.y += dl
|
|
||||||
t.x = t.indent*INDENT_MULTIPLIER + col
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) write(text string) {
|
|
||||||
for _, c := range text {
|
|
||||||
if c == '\n' {
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
termbox.SetCell(t.x, t.y, c, termbox.ColorDefault, termbox.ColorDefault)
|
|
||||||
t.x += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) printf(text string, a ...interface{}) {
|
|
||||||
t.write(fmt.Sprintf(text, a...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrap(s string, width int) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for len(s) > 0 {
|
|
||||||
rem := width
|
|
||||||
if rem > len(s) {
|
|
||||||
rem = len(s)
|
|
||||||
}
|
|
||||||
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
|
|
||||||
rem = idx+1
|
|
||||||
}
|
|
||||||
untilNewline := strings.TrimRight(s[:rem], "\n\r")
|
|
||||||
s = s[rem:]
|
|
||||||
if len(untilNewline) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b.WriteString(untilNewline)
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
return strings.TrimRight(b.String(), "\n\r")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) {
|
|
||||||
whole := fmt.Sprintf(format, a...)
|
|
||||||
width, _ := termbox.Size()
|
|
||||||
if !strings.ContainsAny(whole, "\n\r") && t.x + len(whole) <= width {
|
|
||||||
t.printf(format, a...)
|
|
||||||
} else {
|
|
||||||
t.addIndent(1)
|
|
||||||
t.newline()
|
|
||||||
t.write(wrap(whole, width - INDENT_MULTIPLIER*t.indent))
|
|
||||||
t.addIndent(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) newline() {
|
|
||||||
t.moveLine(1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) setIndent(indent int) {
|
|
||||||
t.indent = indent
|
|
||||||
t.moveLine(0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) addIndent(indent int) {
|
|
||||||
t.indent += indent
|
|
||||||
t.moveLine(0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var statusFlags struct {
|
|
||||||
Raw bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var StatusCmd = &cli.Subcommand{
|
|
||||||
Use: "status",
|
|
||||||
Short: "show job activity or dump as JSON for monitoring",
|
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
|
||||||
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
|
|
||||||
},
|
|
||||||
Run: runStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
func runStatus(s *cli.Subcommand, args []string) error {
|
|
||||||
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if statusFlags.Raw {
|
|
||||||
resp, err := httpc.Get("http://unix"+daemon.ControlJobEndpointStatus)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
fmt.Fprintf(os.Stderr, "Received error response:\n")
|
|
||||||
io.CopyN(os.Stderr, resp.Body, 4096)
|
|
||||||
return errors.Errorf("exit")
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t := newTui()
|
|
||||||
t.lock.Lock()
|
|
||||||
t.err = errors.New("Got no report yet")
|
|
||||||
t.lock.Unlock()
|
|
||||||
|
|
||||||
err = termbox.Init()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer termbox.Close()
|
|
||||||
|
|
||||||
update := func() {
|
|
||||||
m := make(map[string]job.Status)
|
|
||||||
|
|
||||||
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
|
|
||||||
struct{}{},
|
|
||||||
&m,
|
|
||||||
)
|
|
||||||
|
|
||||||
t.lock.Lock()
|
|
||||||
t.err = err2
|
|
||||||
t.report = m
|
|
||||||
t.lock.Unlock()
|
|
||||||
t.draw()
|
|
||||||
}
|
|
||||||
update()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
|
||||||
defer ticker.Stop()
|
|
||||||
go func() {
|
|
||||||
for _ = range ticker.C {
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
termbox.HideCursor()
|
|
||||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
switch ev := termbox.PollEvent(); ev.Type {
|
|
||||||
case termbox.EventKey:
|
|
||||||
switch ev.Key {
|
|
||||||
case termbox.KeyEsc:
|
|
||||||
break loop
|
|
||||||
case termbox.KeyCtrlC:
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
case termbox.EventResize:
|
|
||||||
t.draw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
|
|
||||||
p, ok := t.replicationProgress[jobName]
|
|
||||||
if !ok {
|
|
||||||
p = &bytesProgressHistory{}
|
|
||||||
t.replicationProgress[jobName] = p
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) draw() {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
|
|
||||||
t.x = 0
|
|
||||||
t.y = 0
|
|
||||||
t.indent = 0
|
|
||||||
|
|
||||||
if t.err != nil {
|
|
||||||
t.write(t.err.Error())
|
|
||||||
} else {
|
|
||||||
//Iterate over map in alphabetical order
|
|
||||||
keys := make([]string, len(t.report))
|
|
||||||
i := 0
|
|
||||||
for k, _ := range t.report {
|
|
||||||
keys[i] = k
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
v := t.report[k]
|
|
||||||
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.setIndent(0)
|
|
||||||
|
|
||||||
t.printf("Job: %s", k)
|
|
||||||
t.setIndent(1)
|
|
||||||
t.newline()
|
|
||||||
t.printf("Type: %s", v.Type)
|
|
||||||
t.setIndent(1)
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
if v.Type == job.TypePush || v.Type == job.TypePull {
|
|
||||||
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
|
|
||||||
if !ok || activeStatus == nil {
|
|
||||||
t.printf("ActiveSideStatus is null")
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.printf("Replication:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgresHistory(k))
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
t.printf("Pruning Sender:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderPrunerReport(activeStatus.PruningSender)
|
|
||||||
t.addIndent(-1)
|
|
||||||
|
|
||||||
t.printf("Pruning Receiver:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderPrunerReport(activeStatus.PruningReceiver)
|
|
||||||
t.addIndent(-1)
|
|
||||||
} else if v.Type == job.TypeSnap {
|
|
||||||
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
|
|
||||||
if !ok || snapStatus == nil {
|
|
||||||
t.printf("SnapJobStatus is null")
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.printf("Pruning snapshots:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
t.renderPrunerReport(snapStatus.Pruning)
|
|
||||||
t.addIndent(-1)
|
|
||||||
} else {
|
|
||||||
t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
|
|
||||||
t.newline()
|
|
||||||
asYaml, err := yaml.Marshal(v.JobSpecific)
|
|
||||||
if err != nil {
|
|
||||||
t.printf("Error marshaling status to YAML: %s", err)
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.write(string(asYaml))
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
termbox.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgressHistory) {
|
|
||||||
if rep == nil {
|
|
||||||
t.printf("...\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if rep.WaitReconnectError != nil {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
if !rep.WaitReconnectSince.IsZero() {
|
|
||||||
delta := rep.WaitReconnectUntil.Sub(time.Now()).Round(time.Second)
|
|
||||||
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
|
|
||||||
var until string
|
|
||||||
if rep.WaitReconnectUntil.IsZero() {
|
|
||||||
until = "waiting indefinitely"
|
|
||||||
} else {
|
|
||||||
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
|
|
||||||
}
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
|
|
||||||
rep.WaitReconnectSince, until)
|
|
||||||
} else {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
|
|
||||||
}
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO visualize more than the latest attempt by folding all attempts into one
|
|
||||||
if len(rep.Attempts) == 0 {
|
|
||||||
t.printf("no attempts made yet")
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
t.printf("Attempt #%d", len(rep.Attempts))
|
|
||||||
if len(rep.Attempts) > 1 {
|
|
||||||
t.printf(". Previous attempts failed with the follwing statuses:")
|
|
||||||
t.newline()
|
|
||||||
t.addIndent(1)
|
|
||||||
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)", i + 1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
t.addIndent(-1)
|
|
||||||
} else {
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
latest := rep.Attempts[len(rep.Attempts)-1]
|
|
||||||
sort.Slice(latest.Filesystems, func(i, j int) bool {
|
|
||||||
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
|
|
||||||
})
|
|
||||||
|
|
||||||
t.printf("Status: %s", latest.State)
|
|
||||||
t.newline()
|
|
||||||
if latest.State == report.AttemptPlanningError {
|
|
||||||
t.printf("Problem: ")
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
|
|
||||||
t.newline()
|
|
||||||
} else if latest.State == report.AttemptFanOutError {
|
|
||||||
t.printf("Problem: one or more of the filesystems encountered errors")
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
|
|
||||||
// Draw global progress bar
|
|
||||||
// Progress: [---------------]
|
|
||||||
expected, replicated := latest.BytesSum()
|
|
||||||
rate, changeCount := history.Update(replicated)
|
|
||||||
t.write("Progress: ")
|
|
||||||
t.drawBar(50, replicated, expected, changeCount)
|
|
||||||
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
var maxFSLen int
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
if len(fs.Info.Name) > maxFSLen {
|
|
||||||
maxFSLen = len(fs.Info.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, fs := range latest.Filesystems {
|
|
||||||
t.printFilesystemStatus(fs, false, maxFSLen) // FIXME bring 'active' flag back
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) renderPrunerReport(r *pruner.Report) {
|
|
||||||
if r == nil {
|
|
||||||
t.printf("...\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := pruner.StateString(r.State)
|
|
||||||
if err != nil {
|
|
||||||
t.printf("Status: %q (parse error: %q)\n", r.State, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.printf("Status: %s", state)
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
if r.Error != "" {
|
|
||||||
t.printf("Error: %s\n", r.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type commonFS struct {
|
|
||||||
*pruner.FSReport
|
|
||||||
completed bool
|
|
||||||
}
|
|
||||||
all := make([]commonFS, 0, len(r.Pending) + len(r.Completed))
|
|
||||||
for i := range r.Pending {
|
|
||||||
all = append(all, commonFS{&r.Pending[i], false})
|
|
||||||
}
|
|
||||||
for i := range r.Completed {
|
|
||||||
all = append(all, commonFS{&r.Completed[i], true})
|
|
||||||
}
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case pruner.Plan: fallthrough
|
|
||||||
case pruner.PlanErr:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(all) == 0 {
|
|
||||||
t.printf("nothing to do\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalDestroyCount, completedDestroyCount int
|
|
||||||
var maxFSname int
|
|
||||||
for _, fs := range all {
|
|
||||||
totalDestroyCount += len(fs.DestroyList)
|
|
||||||
if fs.completed {
|
|
||||||
completedDestroyCount += len(fs.DestroyList)
|
|
||||||
}
|
|
||||||
if maxFSname < len(fs.Filesystem) {
|
|
||||||
maxFSname = len(fs.Filesystem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// global progress bar
|
|
||||||
progress := int(math.Round(80 * float64(completedDestroyCount) / float64(totalDestroyCount)))
|
|
||||||
t.write("Progress: ")
|
|
||||||
t.write("[")
|
|
||||||
t.write(times("=", progress))
|
|
||||||
t.write(">")
|
|
||||||
t.write(times("-", 80 - progress))
|
|
||||||
t.write("]")
|
|
||||||
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
|
|
||||||
t.newline()
|
|
||||||
|
|
||||||
sort.SliceStable(all, func(i, j int) bool {
|
|
||||||
return strings.Compare(all[i].Filesystem, all[j].Filesystem) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
// Draw a table-like representation of 'all'
|
|
||||||
for _, fs := range all {
|
|
||||||
t.write(rightPad(fs.Filesystem, maxFSname, " "))
|
|
||||||
t.write(" ")
|
|
||||||
if !fs.SkipReason.NotSkipped() {
|
|
||||||
t.printf("skipped: %s\n", fs.SkipReason)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fs.LastError != "" {
|
|
||||||
if strings.ContainsAny(fs.LastError, "\r\n") {
|
|
||||||
t.printf("ERROR:")
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
|
|
||||||
} else {
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
|
|
||||||
}
|
|
||||||
t.newline()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneRuleActionStr := fmt.Sprintf("(destroy %d of %d snapshots)",
|
|
||||||
len(fs.DestroyList), len(fs.SnapshotList))
|
|
||||||
|
|
||||||
if fs.completed {
|
|
||||||
t.printf( "Completed %s\n", pruneRuleActionStr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.write("Pending ") // whitespace is padding 10
|
|
||||||
if len(fs.DestroyList) == 1 {
|
|
||||||
t.write(fs.DestroyList[0].Name)
|
|
||||||
} else {
|
|
||||||
t.write(pruneRuleActionStr)
|
|
||||||
}
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func times(str string, n int) (out string) {
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
out += str
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func rightPad(str string, length int, pad string) string {
|
|
||||||
if len(str) > length {
|
|
||||||
return str[:length]
|
|
||||||
}
|
|
||||||
return str + times(pad, length-len(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func leftPad(str string, length int, pad string) string {
|
|
||||||
if len(str) > length {
|
|
||||||
return str[len(str)-length:]
|
|
||||||
}
|
|
||||||
return times(pad, length-len(str)) + str
|
|
||||||
}
|
|
||||||
|
|
||||||
var arrowPositions = `>\|/`
|
|
||||||
|
|
||||||
// changeCount = 0 indicates stall / no progresss
|
|
||||||
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
|
|
||||||
var completedLength int
|
|
||||||
if totalBytes > 0 {
|
|
||||||
completedLength = int(int64(length) * bytes / totalBytes)
|
|
||||||
if completedLength > length {
|
|
||||||
completedLength = length
|
|
||||||
}
|
|
||||||
} else if totalBytes == bytes {
|
|
||||||
completedLength = length
|
|
||||||
}
|
|
||||||
|
|
||||||
t.write("[")
|
|
||||||
t.write(times("=", completedLength))
|
|
||||||
t.write( string(arrowPositions[changeCount%len(arrowPositions)]))
|
|
||||||
t.write(times("-", length-completedLength))
|
|
||||||
t.write("]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
|
|
||||||
|
|
||||||
expected, replicated := rep.BytesSum()
|
|
||||||
status := fmt.Sprintf("%s (step %d/%d, %s/%s)",
|
|
||||||
strings.ToUpper(string(rep.State)),
|
|
||||||
rep.CurrentStep, len(rep.Steps),
|
|
||||||
ByteCountBinary(replicated), ByteCountBinary(expected),
|
|
||||||
)
|
|
||||||
|
|
||||||
activeIndicator := " "
|
|
||||||
if active {
|
|
||||||
activeIndicator = "*"
|
|
||||||
}
|
|
||||||
t.printf("%s %s %s ",
|
|
||||||
activeIndicator,
|
|
||||||
rightPad(rep.Info.Name, maxFS, " "),
|
|
||||||
status)
|
|
||||||
|
|
||||||
next := ""
|
|
||||||
if err := rep.Error(); err != nil {
|
|
||||||
next = err.Err
|
|
||||||
} else if rep.State != report.FilesystemDone {
|
|
||||||
if nextStep := rep.NextStep(); nextStep != nil {
|
|
||||||
if nextStep.IsIncremental() {
|
|
||||||
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
|
|
||||||
} else {
|
|
||||||
next = fmt.Sprintf("next: %s (full)", nextStep.Info.To)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next = "" // individual FSes may still be in planning state
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
t.printfDrawIndentedAndWrappedIfMultiline("%s", next)
|
|
||||||
|
|
||||||
t.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ByteCountBinary(b int64) string {
|
|
||||||
const unit = 1024
|
|
||||||
if b < unit {
|
|
||||||
return fmt.Sprintf("%d B", b)
|
|
||||||
}
|
|
||||||
div, exp := int64(unit), 0
|
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
"io/ioutil"
|
|
||||||
"log/syslog"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Jobs []JobEnum `yaml:"jobs"`
|
|
||||||
Global *Global `yaml:"global,optional,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Job(name string) (*JobEnum, error) {
|
|
||||||
for _, j := range c.Jobs {
|
|
||||||
if j.Name() == name {
|
|
||||||
return &j, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("job %q not defined in config", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobEnum struct {
|
|
||||||
Ret interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j JobEnum) Name() string {
|
|
||||||
var name string
|
|
||||||
switch v := j.Ret.(type) {
|
|
||||||
case *SnapJob: name = v.Name
|
|
||||||
case *PushJob: name = v.Name
|
|
||||||
case *SinkJob: name = v.Name
|
|
||||||
case *PullJob: name = v.Name
|
|
||||||
case *SourceJob: name = v.Name
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown job type %T", v))
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActiveJob struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Connect ConnectEnum `yaml:"connect"`
|
|
||||||
Pruning PruningSenderReceiver `yaml:"pruning"`
|
|
||||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PassiveJob struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Serve ServeEnum `yaml:"serve"`
|
|
||||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapJob struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Pruning PruningLocal `yaml:"pruning"`
|
|
||||||
Debug JobDebugSettings `yaml:"debug,optional"`
|
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PushJob struct {
|
|
||||||
ActiveJob `yaml:",inline"`
|
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PullJob struct {
|
|
||||||
ActiveJob `yaml:",inline"`
|
|
||||||
RootFS string `yaml:"root_fs"`
|
|
||||||
Interval PositiveDurationOrManual `yaml:"interval"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PositiveDurationOrManual struct {
|
|
||||||
Interval time.Duration
|
|
||||||
Manual bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ yaml.Unmarshaler = (*PositiveDurationOrManual)(nil)
|
|
||||||
|
|
||||||
func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
var s string
|
|
||||||
if err := u(&s, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch s {
|
|
||||||
case "manual":
|
|
||||||
i.Manual = true
|
|
||||||
i.Interval = 0
|
|
||||||
case "":
|
|
||||||
return fmt.Errorf("value must not be empty")
|
|
||||||
default:
|
|
||||||
i.Manual = false
|
|
||||||
i.Interval, err = time.ParseDuration(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if i.Interval <= 0 {
|
|
||||||
return fmt.Errorf("value must be a positive duration, got %q", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SinkJob struct {
|
|
||||||
PassiveJob `yaml:",inline"`
|
|
||||||
RootFS string `yaml:"root_fs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SourceJob struct {
|
|
||||||
PassiveJob `yaml:",inline"`
|
|
||||||
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
|
|
||||||
Filesystems FilesystemsFilter `yaml:"filesystems"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilesystemsFilter map[string]bool
|
|
||||||
|
|
||||||
type SnapshottingEnum struct {
|
|
||||||
Ret interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshottingPeriodic struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Prefix string `yaml:"prefix"`
|
|
||||||
Interval time.Duration `yaml:"interval,positive"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshottingManual struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PruningSenderReceiver struct {
|
|
||||||
KeepSender []PruningEnum `yaml:"keep_sender"`
|
|
||||||
KeepReceiver []PruningEnum `yaml:"keep_receiver"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PruningLocal struct {
|
|
||||||
Keep []PruningEnum `yaml:"keep"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoggingOutletEnumList []LoggingOutletEnum
|
|
||||||
|
|
||||||
func (l *LoggingOutletEnumList) SetDefault() {
|
|
||||||
def := `
|
|
||||||
type: "stdout"
|
|
||||||
time: true
|
|
||||||
level: "warn"
|
|
||||||
format: "human"
|
|
||||||
`
|
|
||||||
s := &StdoutLoggingOutlet{}
|
|
||||||
err := yaml.UnmarshalStrict([]byte(def), &s)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
*l = []LoggingOutletEnum{LoggingOutletEnum{Ret: s}}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ yaml.Defaulter = &LoggingOutletEnumList{}
|
|
||||||
|
|
||||||
type Global struct {
|
|
||||||
Logging *LoggingOutletEnumList `yaml:"logging,optional,fromdefaults"`
|
|
||||||
Monitoring []MonitoringEnum `yaml:"monitoring,optional"`
|
|
||||||
Control *GlobalControl `yaml:"control,optional,fromdefaults"`
|
|
||||||
Serve *GlobalServe `yaml:"serve,optional,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Default(i interface{}) {
|
|
||||||
v := reflect.ValueOf(i)
|
|
||||||
if v.Kind() != reflect.Ptr {
|
|
||||||
panic(v)
|
|
||||||
}
|
|
||||||
y := `{}`
|
|
||||||
err := yaml.Unmarshal([]byte(y), v.Interface())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectEnum struct {
|
|
||||||
Ret interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectCommon struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPConnect struct {
|
|
||||||
ConnectCommon `yaml:",inline"`
|
|
||||||
Address string `yaml:"address"`
|
|
||||||
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TLSConnect struct {
|
|
||||||
ConnectCommon `yaml:",inline"`
|
|
||||||
Address string `yaml:"address"`
|
|
||||||
Ca string `yaml:"ca"`
|
|
||||||
Cert string `yaml:"cert"`
|
|
||||||
Key string `yaml:"key"`
|
|
||||||
ServerCN string `yaml:"server_cn"`
|
|
||||||
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SSHStdinserverConnect struct {
|
|
||||||
ConnectCommon `yaml:",inline"`
|
|
||||||
Host string `yaml:"host"`
|
|
||||||
User string `yaml:"user"`
|
|
||||||
Port uint16 `yaml:"port"`
|
|
||||||
IdentityFile string `yaml:"identity_file"`
|
|
||||||
TransportOpenCommand []string `yaml:"transport_open_command,optional"` //TODO unused
|
|
||||||
SSHCommand string `yaml:"ssh_command,optional"` //TODO unused
|
|
||||||
Options []string `yaml:"options,optional"`
|
|
||||||
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocalConnect struct {
|
|
||||||
ConnectCommon `yaml:",inline"`
|
|
||||||
ListenerName string `yaml:"listener_name"`
|
|
||||||
ClientIdentity string `yaml:"client_identity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServeEnum struct {
|
|
||||||
Ret interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServeCommon struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPServe struct {
|
|
||||||
ServeCommon `yaml:",inline"`
|
|
||||||
Listen string `yaml:"listen"`
|
|
||||||
Clients map[string]string `yaml:"clients"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TLSServe struct {
|
|
||||||
ServeCommon `yaml:",inline"`
|
|
||||||
Listen string `yaml:"listen"`
|
|
||||||
Ca string `yaml:"ca"`
|
|
||||||
Cert string `yaml:"cert"`
|
|
||||||
Key string `yaml:"key"`
|
|
||||||
ClientCNs []string `yaml:"client_cns"`
|
|
||||||
HandshakeTimeout time.Duration `yaml:"handshake_timeout,zeropositive,default=10s"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StdinserverServer struct {
|
|
||||||
ServeCommon `yaml:",inline"`
|
|
||||||
ClientIdentities []string `yaml:"client_identities"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocalServe struct {
|
|
||||||
ServeCommon `yaml:",inline"`
|
|
||||||
ListenerName string `yaml:"listener_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PruningEnum struct {
|
|
||||||
Ret interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PruneKeepNotReplicated struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
KeepSnapshotAtCursor bool `yaml:"keep_snapshot_at_cursor,optional,default=true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PruneKeepLastN struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Count int `yaml:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PruneKeepRegex struct { // FIXME rename to KeepRegex
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Regex string `yaml:"regex"`
|
|
||||||
Negate bool `yaml:"negate,optional,default=false"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoggingOutletEnum struct {
|
|
||||||
Ret interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoggingOutletCommon struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Level string `yaml:"level"`
|
|
||||||
Format string `yaml:"format"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StdoutLoggingOutlet struct {
|
|
||||||
LoggingOutletCommon `yaml:",inline"`
|
|
||||||
Time bool `yaml:"time,default=true"`
|
|
||||||
Color bool `yaml:"color,default=true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SyslogLoggingOutlet struct {
|
|
||||||
LoggingOutletCommon `yaml:",inline"`
|
|
||||||
Facility *SyslogFacility `yaml:"facility,optional,fromdefaults"`
|
|
||||||
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPLoggingOutlet struct {
|
|
||||||
LoggingOutletCommon `yaml:",inline"`
|
|
||||||
Address string `yaml:"address"`
|
|
||||||
Net string `yaml:"net,default=tcp"`
|
|
||||||
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
|
|
||||||
TLS *TCPLoggingOutletTLS `yaml:"tls,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPLoggingOutletTLS struct {
|
|
||||||
CA string `yaml:"ca"`
|
|
||||||
Cert string `yaml:"cert"`
|
|
||||||
Key string `yaml:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MonitoringEnum struct {
|
|
||||||
Ret interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrometheusMonitoring struct {
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Listen string `yaml:"listen"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SyslogFacility syslog.Priority
|
|
||||||
|
|
||||||
func (f *SyslogFacility) SetDefault() {
|
|
||||||
*f = SyslogFacility(syslog.LOG_LOCAL0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ yaml.Defaulter = (*SyslogFacility)(nil)
|
|
||||||
|
|
||||||
type GlobalControl struct {
|
|
||||||
SockPath string `yaml:"sockpath,default=/var/run/zrepl/control"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GlobalServe struct {
|
|
||||||
StdinServer *GlobalStdinServer `yaml:"stdinserver,optional,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GlobalStdinServer struct {
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}) (interface{}, error) {
|
|
||||||
var in struct {
|
|
||||||
Type string
|
|
||||||
}
|
|
||||||
if err := u(&in, true); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if in.Type == "" {
|
|
||||||
return nil, &yaml.TypeError{Errors: []string{"must specify type"}}
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := types[in.Type]
|
|
||||||
if !ok {
|
|
||||||
return nil, &yaml.TypeError{Errors: []string{fmt.Sprintf("invalid type name %q", in.Type)}}
|
|
||||||
}
|
|
||||||
if err := u(v, false); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *JobEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
|
||||||
"snap": &SnapJob{},
|
|
||||||
"push": &PushJob{},
|
|
||||||
"sink": &SinkJob{},
|
|
||||||
"pull": &PullJob{},
|
|
||||||
"source": &SourceJob{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *ConnectEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
|
||||||
"tcp": &TCPConnect{},
|
|
||||||
"tls": &TLSConnect{},
|
|
||||||
"ssh+stdinserver": &SSHStdinserverConnect{},
|
|
||||||
"local": &LocalConnect{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *ServeEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
|
||||||
"tcp": &TCPServe{},
|
|
||||||
"tls": &TLSServe{},
|
|
||||||
"stdinserver": &StdinserverServer{},
|
|
||||||
"local" : &LocalServe{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PruningEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
|
||||||
"not_replicated": &PruneKeepNotReplicated{},
|
|
||||||
"last_n": &PruneKeepLastN{},
|
|
||||||
"grid": &PruneGrid{},
|
|
||||||
"regex": &PruneKeepRegex{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
|
||||||
"periodic": &SnapshottingPeriodic{},
|
|
||||||
"manual": &SnapshottingManual{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *LoggingOutletEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
|
||||||
"stdout": &StdoutLoggingOutlet{},
|
|
||||||
"syslog": &SyslogLoggingOutlet{},
|
|
||||||
"tcp": &TCPLoggingOutlet{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *MonitoringEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
|
||||||
"prometheus": &PrometheusMonitoring{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *SyslogFacility) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
|
|
||||||
var s string
|
|
||||||
if err := u(&s, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var level syslog.Priority
|
|
||||||
switch s {
|
|
||||||
case "kern": level = syslog.LOG_KERN
|
|
||||||
case "user": level = syslog.LOG_USER
|
|
||||||
case "mail": level = syslog.LOG_MAIL
|
|
||||||
case "daemon": level = syslog.LOG_DAEMON
|
|
||||||
case "auth": level = syslog.LOG_AUTH
|
|
||||||
case "syslog": level = syslog.LOG_SYSLOG
|
|
||||||
case "lpr": level = syslog.LOG_LPR
|
|
||||||
case "news": level = syslog.LOG_NEWS
|
|
||||||
case "uucp": level = syslog.LOG_UUCP
|
|
||||||
case "cron": level = syslog.LOG_CRON
|
|
||||||
case "authpriv": level = syslog.LOG_AUTHPRIV
|
|
||||||
case "ftp": level = syslog.LOG_FTP
|
|
||||||
case "local0": level = syslog.LOG_LOCAL0
|
|
||||||
case "local1": level = syslog.LOG_LOCAL1
|
|
||||||
case "local2": level = syslog.LOG_LOCAL2
|
|
||||||
case "local3": level = syslog.LOG_LOCAL3
|
|
||||||
case "local4": level = syslog.LOG_LOCAL4
|
|
||||||
case "local5": level = syslog.LOG_LOCAL5
|
|
||||||
case "local6": level = syslog.LOG_LOCAL6
|
|
||||||
case "local7": level = syslog.LOG_LOCAL7
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid syslog level: %q", s)
|
|
||||||
}
|
|
||||||
*t = SyslogFacility(level)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ConfigFileDefaultLocations = []string{
|
|
||||||
"/etc/zrepl/zrepl.yml",
|
|
||||||
"/usr/local/etc/zrepl/zrepl.yml",
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseConfig(path string) (i *Config, err error) {
|
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
// Try default locations
|
|
||||||
for _, l := range ConfigFileDefaultLocations {
|
|
||||||
stat, statErr := os.Stat(l)
|
|
||||||
if statErr != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !stat.Mode().IsRegular() {
|
|
||||||
err = errors.Errorf("file at default location is not a regular file: %s", l)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
path = l
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes []byte
|
|
||||||
|
|
||||||
if bytes, err = ioutil.ReadFile(path); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseConfigBytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseConfigBytes(bytes []byte) (*Config, error) {
|
|
||||||
var c *Config
|
|
||||||
if err := yaml.UnmarshalStrict(bytes, &c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if c == nil {
|
|
||||||
return nil, fmt.Errorf("config is empty or only consists of comments")
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
|
||||||
|
|
||||||
func parsePostitiveDuration(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,57 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSnapshotting(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
|
|
||||||
`
|
|
||||||
manual := `
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
`
|
|
||||||
periodic := `
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
`
|
|
||||||
|
|
||||||
fillSnapshotting := func(s string) string {return fmt.Sprintf(tmpl, s)}
|
|
||||||
var c *Config
|
|
||||||
|
|
||||||
t.Run("manual", func(t *testing.T) {
|
|
||||||
c = testValidConfig(t, fillSnapshotting(manual))
|
|
||||||
snm := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingManual)
|
|
||||||
assert.Equal(t, "manual", snm.Type)
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
|
||||||
assert.Equal(t, "zrepl_" , snp.Prefix)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
|
||||||
paths, err := filepath.Glob("./samples/*")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("glob failed: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range paths {
|
|
||||||
|
|
||||||
if path.Ext(p) != ".yml" {
|
|
||||||
t.Logf("skipping file %s", p)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(p, func(t *testing.T) {
|
|
||||||
c, err := ParseConfig(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error parsing %s:\n%+v", p, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("file: %s", p)
|
|
||||||
t.Log(pretty.Sprint(c))
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// template must be a template/text template with a single '{{ . }}' as placehodler for val
|
|
||||||
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
|
||||||
tmp, err := template.New("master").Parse(tmpl)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = tmp.Execute(&buf, val)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return testValidConfig(t, buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func testValidConfig(t *testing.T, input string) *Config {
|
|
||||||
t.Helper()
|
|
||||||
conf, err := testConfig(t, input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, conf)
|
|
||||||
return conf
|
|
||||||
}
|
|
||||||
|
|
||||||
func testConfig(t *testing.T, input string) (*Config, error) {
|
|
||||||
t.Helper()
|
|
||||||
return ParseConfigBytes([]byte(input))
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package filters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AnyFSVFilter struct{}
|
|
||||||
|
|
||||||
func NewAnyFSVFilter() AnyFSVFilter {
|
|
||||||
return AnyFSVFilter{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ zfs.FilesystemVersionFilter = AnyFSVFilter{}
|
|
||||||
|
|
||||||
func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type PrefixFilter struct {
|
|
||||||
prefix string
|
|
||||||
fstype zfs.VersionType
|
|
||||||
fstypeSet bool // optionals anyone?
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ zfs.FilesystemVersionFilter = &PrefixFilter{}
|
|
||||||
|
|
||||||
func NewPrefixFilter(prefix string) *PrefixFilter {
|
|
||||||
return &PrefixFilter{prefix: prefix}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTypedPrefixFilter(prefix string, versionType zfs.VersionType) *PrefixFilter {
|
|
||||||
return &PrefixFilter{prefix, versionType, true}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *PrefixFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
|
|
||||||
fstypeMatches := (!f.fstypeSet || t == f.fstype)
|
|
||||||
prefixMatches := strings.HasPrefix(name, f.prefix)
|
|
||||||
return fstypeMatches && prefixMatches, nil
|
|
||||||
}
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/replication"
|
|
||||||
"github.com/zrepl/zrepl/replication/driver"
|
|
||||||
"github.com/zrepl/zrepl/replication/logic"
|
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
|
||||||
"github.com/zrepl/zrepl/rpc"
|
|
||||||
"github.com/zrepl/zrepl/transport"
|
|
||||||
"github.com/zrepl/zrepl/transport/fromconfig"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ActiveSide struct {
|
|
||||||
mode activeMode
|
|
||||||
name string
|
|
||||||
connecter transport.Connecter
|
|
||||||
|
|
||||||
prunerFactory *pruner.PrunerFactory
|
|
||||||
|
|
||||||
promRepStateSecs *prometheus.HistogramVec // labels: state
|
|
||||||
promPruneSecs *prometheus.HistogramVec // labels: prune_side
|
|
||||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
|
||||||
|
|
||||||
tasksMtx sync.Mutex
|
|
||||||
tasks activeSideTasks
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:generate enumer -type=ActiveSideState
|
|
||||||
type ActiveSideState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ActiveSideReplicating ActiveSideState = 1 << iota
|
|
||||||
ActiveSidePruneSender
|
|
||||||
ActiveSidePruneReceiver
|
|
||||||
ActiveSideDone // also errors
|
|
||||||
)
|
|
||||||
|
|
||||||
type activeSideTasks struct {
|
|
||||||
state ActiveSideState
|
|
||||||
|
|
||||||
// valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
|
||||||
replicationReport driver.ReportFunc
|
|
||||||
replicationCancel context.CancelFunc
|
|
||||||
|
|
||||||
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
|
||||||
prunerSender, prunerReceiver *pruner.Pruner
|
|
||||||
|
|
||||||
// valid for state ActiveSidePruneReceiver, ActiveSideDone
|
|
||||||
prunerSenderCancel, prunerReceiverCancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
|
||||||
a.tasksMtx.Lock()
|
|
||||||
defer a.tasksMtx.Unlock()
|
|
||||||
var copy activeSideTasks
|
|
||||||
copy = a.tasks
|
|
||||||
if u == nil {
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
u(©)
|
|
||||||
a.tasks = copy
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
|
|
||||||
type activeMode interface {
|
|
||||||
ConnectEndpoints(rpcLoggers rpc.Loggers, connecter transport.Connecter)
|
|
||||||
DisconnectEndpoints()
|
|
||||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
|
||||||
Type() Type
|
|
||||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
|
||||||
ResetConnectBackoff()
|
|
||||||
}
|
|
||||||
|
|
||||||
type modePush struct {
|
|
||||||
setupMtx sync.Mutex
|
|
||||||
sender *endpoint.Sender
|
|
||||||
receiver *rpc.Client
|
|
||||||
fsfilter endpoint.FSFilter
|
|
||||||
snapper *snapper.PeriodicOrManual
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
|
||||||
m.setupMtx.Lock()
|
|
||||||
defer m.setupMtx.Unlock()
|
|
||||||
if m.receiver != nil || m.sender != nil {
|
|
||||||
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
|
||||||
}
|
|
||||||
m.sender = endpoint.NewSender(m.fsfilter)
|
|
||||||
m.receiver = rpc.NewClient(connecter, loggers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePush) DisconnectEndpoints() {
|
|
||||||
m.setupMtx.Lock()
|
|
||||||
defer m.setupMtx.Unlock()
|
|
||||||
m.receiver.Close()
|
|
||||||
m.sender = nil
|
|
||||||
m.receiver = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePush) SenderReceiver() (logic.Sender, logic.Receiver) {
|
|
||||||
m.setupMtx.Lock()
|
|
||||||
defer m.setupMtx.Unlock()
|
|
||||||
return m.sender, m.receiver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePush) Type() Type { return TypePush }
|
|
||||||
|
|
||||||
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
|
||||||
m.snapper.Run(ctx, wakeUpCommon)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePush) ResetConnectBackoff() {
|
|
||||||
m.setupMtx.Lock()
|
|
||||||
defer m.setupMtx.Unlock()
|
|
||||||
if m.receiver != nil {
|
|
||||||
m.receiver.ResetConnectBackoff()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error) {
|
|
||||||
m := &modePush{}
|
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
|
||||||
}
|
|
||||||
m.fsfilter = fsf
|
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type modePull struct {
|
|
||||||
setupMtx sync.Mutex
|
|
||||||
receiver *endpoint.Receiver
|
|
||||||
sender *rpc.Client
|
|
||||||
rootFS *zfs.DatasetPath
|
|
||||||
interval config.PositiveDurationOrManual
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
|
|
||||||
m.setupMtx.Lock()
|
|
||||||
defer m.setupMtx.Unlock()
|
|
||||||
if m.receiver != nil || m.sender != nil {
|
|
||||||
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
|
|
||||||
}
|
|
||||||
m.receiver = endpoint.NewReceiver(m.rootFS, false)
|
|
||||||
m.sender = rpc.NewClient(connecter, loggers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePull) DisconnectEndpoints() {
|
|
||||||
m.setupMtx.Lock()
|
|
||||||
defer m.setupMtx.Unlock()
|
|
||||||
m.sender.Close()
|
|
||||||
m.sender = nil
|
|
||||||
m.receiver = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePull) SenderReceiver() (logic.Sender, logic.Receiver) {
|
|
||||||
m.setupMtx.Lock()
|
|
||||||
defer m.setupMtx.Unlock()
|
|
||||||
return m.sender, m.receiver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*modePull) Type() Type { return TypePull }
|
|
||||||
|
|
||||||
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
|
||||||
if m.interval.Manual {
|
|
||||||
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
|
||||||
// "waiting for wakeups" is printed in common ActiveSide.do
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t := time.NewTicker(m.interval.Interval)
|
|
||||||
defer t.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
select {
|
|
||||||
case wakeUpCommon <- struct{}{}:
|
|
||||||
default:
|
|
||||||
GetLogger(ctx).
|
|
||||||
WithField("pull_interval", m.interval).
|
|
||||||
Warn("pull job took longer than pull interval")
|
|
||||||
wakeUpCommon <- struct{}{} // block anyways, to queue up the wakeup
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modePull) ResetConnectBackoff() {
|
|
||||||
m.setupMtx.Lock()
|
|
||||||
defer m.setupMtx.Unlock()
|
|
||||||
if m.sender != nil {
|
|
||||||
m.sender.ResetConnectBackoff()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err error) {
|
|
||||||
m = &modePull{}
|
|
||||||
m.interval = in.Interval
|
|
||||||
|
|
||||||
m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("RootFS is not a valid zfs filesystem path")
|
|
||||||
}
|
|
||||||
if m.rootFS.Length() <= 0 {
|
|
||||||
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *ActiveSide, err error) {
|
|
||||||
|
|
||||||
j = &ActiveSide{mode: mode}
|
|
||||||
j.name = in.Name
|
|
||||||
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "replication",
|
|
||||||
Name: "state_time",
|
|
||||||
Help: "seconds spent during replication",
|
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
|
||||||
}, []string{"state"})
|
|
||||||
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "replication",
|
|
||||||
Name: "bytes_replicated",
|
|
||||||
Help: "number of bytes replicated from sender to receiver per filesystem",
|
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
|
||||||
}, []string{"filesystem"})
|
|
||||||
|
|
||||||
j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build client")
|
|
||||||
}
|
|
||||||
|
|
||||||
j.promPruneSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
||||||
Namespace: "zrepl",
|
|
||||||
Subsystem: "pruning",
|
|
||||||
Name: "time",
|
|
||||||
Help: "seconds spent in pruner",
|
|
||||||
ConstLabels: prometheus.Labels{"zrepl_job": j.name},
|
|
||||||
}, []string{"prune_side"})
|
|
||||||
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return j, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
|
|
||||||
registerer.MustRegister(j.promRepStateSecs)
|
|
||||||
registerer.MustRegister(j.promPruneSecs)
|
|
||||||
registerer.MustRegister(j.promBytesReplicated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *ActiveSide) Name() string { return j.name }
|
|
||||||
|
|
||||||
type ActiveSideStatus struct {
|
|
||||||
Replication *report.Report
|
|
||||||
PruningSender, PruningReceiver *pruner.Report
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *ActiveSide) Status() *Status {
|
|
||||||
tasks := j.updateTasks(nil)
|
|
||||||
|
|
||||||
s := &ActiveSideStatus{}
|
|
||||||
t := j.mode.Type()
|
|
||||||
if tasks.replicationReport != nil {
|
|
||||||
s.Replication = tasks.replicationReport()
|
|
||||||
}
|
|
||||||
if tasks.prunerSender != nil {
|
|
||||||
s.PruningSender = tasks.prunerSender.Report()
|
|
||||||
}
|
|
||||||
if tasks.prunerReceiver != nil {
|
|
||||||
s.PruningReceiver = tasks.prunerReceiver.Report()
|
|
||||||
}
|
|
||||||
return &Status{Type: t, JobSpecific: s}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|
||||||
pull, ok := j.mode.(*modePull)
|
|
||||||
if !ok {
|
|
||||||
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return pull.rootFS.Copy(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *ActiveSide) Run(ctx context.Context) {
|
|
||||||
log := GetLogger(ctx)
|
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
|
||||||
|
|
||||||
defer log.Info("job exiting")
|
|
||||||
|
|
||||||
periodicDone := make(chan struct{})
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
go j.mode.RunPeriodic(ctx, periodicDone)
|
|
||||||
|
|
||||||
invocationCount := 0
|
|
||||||
outer:
|
|
||||||
for {
|
|
||||||
log.Info("wait for wakeups")
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.WithError(ctx.Err()).Info("context")
|
|
||||||
break outer
|
|
||||||
|
|
||||||
case <-wakeup.Wait(ctx):
|
|
||||||
j.mode.ResetConnectBackoff()
|
|
||||||
case <-periodicDone:
|
|
||||||
}
|
|
||||||
invocationCount++
|
|
||||||
invLog := log.WithField("invocation", invocationCount)
|
|
||||||
j.do(WithLogger(ctx, invLog))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *ActiveSide) do(ctx context.Context) {
|
|
||||||
|
|
||||||
log := GetLogger(ctx)
|
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
|
||||||
loggers := rpc.GetLoggersOrPanic(ctx) // filled by WithSubsystemLoggers
|
|
||||||
j.mode.ConnectEndpoints(loggers, j.connecter)
|
|
||||||
defer j.mode.DisconnectEndpoints()
|
|
||||||
|
|
||||||
// allow cancellation of an invocation (this function)
|
|
||||||
ctx, cancelThisRun := context.WithCancel(ctx)
|
|
||||||
defer cancelThisRun()
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-reset.Wait(ctx):
|
|
||||||
log.Info("reset received, cancelling current invocation")
|
|
||||||
cancelThisRun()
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sender, receiver := j.mode.SenderReceiver()
|
|
||||||
|
|
||||||
{
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
ctx, repCancel := context.WithCancel(ctx)
|
|
||||||
var repWait driver.WaitFunc
|
|
||||||
j.updateTasks(func(tasks *activeSideTasks) {
|
|
||||||
// reset it
|
|
||||||
*tasks = activeSideTasks{}
|
|
||||||
tasks.replicationCancel = repCancel
|
|
||||||
tasks.replicationReport, repWait = replication.Do(
|
|
||||||
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver),
|
|
||||||
)
|
|
||||||
tasks.state = ActiveSideReplicating
|
|
||||||
})
|
|
||||||
log.Info("start replication")
|
|
||||||
repWait(true) // wait blocking
|
|
||||||
repCancel() // always cancel to free up context resources
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
ctx, senderCancel := context.WithCancel(ctx)
|
|
||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
|
||||||
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
|
||||||
tasks.prunerSenderCancel = senderCancel
|
|
||||||
tasks.state = ActiveSidePruneSender
|
|
||||||
})
|
|
||||||
log.Info("start pruning sender")
|
|
||||||
tasks.prunerSender.Prune()
|
|
||||||
log.Info("finished pruning sender")
|
|
||||||
senderCancel()
|
|
||||||
}
|
|
||||||
{
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
ctx, receiverCancel := context.WithCancel(ctx)
|
|
||||||
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
|
||||||
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
|
||||||
tasks.prunerReceiverCancel = receiverCancel
|
|
||||||
tasks.state = ActiveSidePruneReceiver
|
|
||||||
})
|
|
||||||
log.Info("start pruning receiver")
|
|
||||||
tasks.prunerReceiver.Prune()
|
|
||||||
log.Info("finished pruning receiver")
|
|
||||||
receiverCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
j.updateTasks(func(tasks *activeSideTasks) {
|
|
||||||
tasks.state = ActiveSideDone
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
|
|
||||||
type testCase struct {
|
|
||||||
err bool
|
|
||||||
input []string
|
|
||||||
}
|
|
||||||
tcs := []testCase{
|
|
||||||
{false, nil},
|
|
||||||
{false, []string{}},
|
|
||||||
{false, []string{""}}, // not our job to determine valid paths
|
|
||||||
{false, []string{"a"}},
|
|
||||||
{false, []string{"some/path"}},
|
|
||||||
{false, []string{"zroot/sink1", "zroot/sink2", "zroot/sink3"}},
|
|
||||||
{true, []string{"zroot/b", "zroot/b"}},
|
|
||||||
{true, []string{"zroot/foo", "zroot/foo/bar", "zroot/baz"}},
|
|
||||||
{false, []string{"a/x", "b/x"}},
|
|
||||||
{false, []string{"a", "b"}},
|
|
||||||
{true, []string{"a", "a"}},
|
|
||||||
{true, []string{"a/x/y", "a/x"}},
|
|
||||||
{true, []string{"a/x", "a/x/y"}},
|
|
||||||
{true, []string{"a/x", "b/x", "a/x/y"}},
|
|
||||||
{true, []string{"a", "a/b", "a/c", "a/b"}},
|
|
||||||
{true, []string{"a/b", "a/c", "a/b", "a/d", "a/c"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tcs {
|
|
||||||
t.Logf("input: %v", tc.input)
|
|
||||||
err := validateReceivingSidesDoNotOverlap(tc.input)
|
|
||||||
if tc.err {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
|
||||||
"github.com/zrepl/zrepl/rpc"
|
|
||||||
"github.com/zrepl/zrepl/transport"
|
|
||||||
"github.com/zrepl/zrepl/transport/fromconfig"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PassiveSide struct {
|
|
||||||
mode passiveMode
|
|
||||||
name string
|
|
||||||
listen transport.AuthenticatedListenerFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
type passiveMode interface {
|
|
||||||
Handler() rpc.Handler
|
|
||||||
RunPeriodic(ctx context.Context)
|
|
||||||
Type() Type
|
|
||||||
}
|
|
||||||
|
|
||||||
type modeSink struct {
|
|
||||||
rootDataset *zfs.DatasetPath
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modeSink) Type() Type { return TypeSink }
|
|
||||||
|
|
||||||
func (m *modeSink) Handler() rpc.Handler {
|
|
||||||
return endpoint.NewReceiver(m.rootDataset, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modeSink) RunPeriodic(_ context.Context) {}
|
|
||||||
|
|
||||||
func modeSinkFromConfig(g *config.Global, in *config.SinkJob) (m *modeSink, err error) {
|
|
||||||
m = &modeSink{}
|
|
||||||
m.rootDataset, err = zfs.NewDatasetPath(in.RootFS)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("root dataset is not a valid zfs filesystem path")
|
|
||||||
}
|
|
||||||
if m.rootDataset.Length() <= 0 {
|
|
||||||
return nil, errors.New("root dataset must not be empty") // duplicates error check of receiver
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type modeSource struct {
|
|
||||||
fsfilter zfs.DatasetFilter
|
|
||||||
snapper *snapper.PeriodicOrManual
|
|
||||||
}
|
|
||||||
|
|
||||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource, err error) {
|
|
||||||
// FIXME exact dedup of modePush
|
|
||||||
m = &modeSource{}
|
|
||||||
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannnot build filesystem filter")
|
|
||||||
}
|
|
||||||
m.fsfilter = fsf
|
|
||||||
|
|
||||||
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build snapper")
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modeSource) Type() Type { return TypeSource }
|
|
||||||
|
|
||||||
func (m *modeSource) Handler() rpc.Handler {
|
|
||||||
return endpoint.NewSender(m.fsfilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modeSource) RunPeriodic(ctx context.Context) {
|
|
||||||
m.snapper.Run(ctx, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) {
|
|
||||||
|
|
||||||
s = &PassiveSide{mode: mode, name: in.Name}
|
|
||||||
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build listener factory")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *PassiveSide) Name() string { return j.name }
|
|
||||||
|
|
||||||
type PassiveStatus struct{}
|
|
||||||
|
|
||||||
func (s *PassiveSide) Status() *Status {
|
|
||||||
return &Status{Type: s.mode.Type()} // FIXME PassiveStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
|
|
||||||
sink, ok := j.mode.(*modeSink)
|
|
||||||
if !ok {
|
|
||||||
_ = j.mode.(*modeSource) // make sure we didn't introduce a new job type
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return sink.rootDataset.Copy(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
|
|
||||||
|
|
||||||
func (j *PassiveSide) Run(ctx context.Context) {
|
|
||||||
|
|
||||||
log := GetLogger(ctx)
|
|
||||||
defer log.Info("job exiting")
|
|
||||||
ctx = logging.WithSubsystemLoggers(ctx, log)
|
|
||||||
{
|
|
||||||
ctx, cancel := context.WithCancel(ctx) // shadowing
|
|
||||||
defer cancel()
|
|
||||||
go j.mode.RunPeriodic(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := j.mode.Handler()
|
|
||||||
if handler == nil {
|
|
||||||
panic(fmt.Sprintf("implementation error: j.mode.Handler() returned nil: %#v", j))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxInterceptor := func(handlerCtx context.Context) context.Context {
|
|
||||||
return logging.WithSubsystemLoggers(handlerCtx, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcLoggers := rpc.GetLoggersOrPanic(ctx) // WithSubsystemLoggers above
|
|
||||||
server := rpc.NewServer(handler, rpcLoggers, ctxInterceptor)
|
|
||||||
|
|
||||||
listener, err := j.listen()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("cannot listen")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
server.Serve(ctx, listener)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package daemon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger = logger.Logger
|
|
||||||
|
|
||||||
var DaemonCmd = &cli.Subcommand {
|
|
||||||
Use: "daemon",
|
|
||||||
Short: "run the zrepl daemon",
|
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
|
||||||
return Run(subcommand.Config())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"time"
|
|
||||||
"context"
|
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
"fmt"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"sort"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
//go:generate stringer -type=SnapState
|
|
||||||
type SnapState uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
SnapPending SnapState = 1 << iota
|
|
||||||
SnapStarted
|
|
||||||
SnapDone
|
|
||||||
SnapError
|
|
||||||
)
|
|
||||||
|
|
||||||
type snapProgress struct {
|
|
||||||
state SnapState
|
|
||||||
|
|
||||||
// SnapStarted, SnapDone, SnapError
|
|
||||||
name string
|
|
||||||
startAt time.Time
|
|
||||||
|
|
||||||
// SnapDone
|
|
||||||
doneAt time.Time
|
|
||||||
|
|
||||||
// SnapErr
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
ctx context.Context
|
|
||||||
log Logger
|
|
||||||
prefix string
|
|
||||||
interval time.Duration
|
|
||||||
fsf *filters.DatasetMapFilter
|
|
||||||
snapshotsTaken chan<-struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Snapper struct {
|
|
||||||
args args
|
|
||||||
|
|
||||||
mtx sync.Mutex
|
|
||||||
state State
|
|
||||||
|
|
||||||
// set in state Plan, used in Waiting
|
|
||||||
lastInvocation time.Time
|
|
||||||
|
|
||||||
// valid for state Snapshotting
|
|
||||||
plan map[*zfs.DatasetPath]snapProgress
|
|
||||||
|
|
||||||
// valid for state SyncUp and Waiting
|
|
||||||
sleepUntil time.Time
|
|
||||||
|
|
||||||
// valid for state Err
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:generate stringer -type=State
|
|
||||||
type State uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
SyncUp State = 1<<iota
|
|
||||||
SyncUpErrWait
|
|
||||||
Planning
|
|
||||||
Snapshotting
|
|
||||||
Waiting
|
|
||||||
ErrorWait
|
|
||||||
Stopped
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s State) sf() state {
|
|
||||||
m := map[State]state{
|
|
||||||
SyncUp: syncUp,
|
|
||||||
SyncUpErrWait: wait,
|
|
||||||
Planning: plan,
|
|
||||||
Snapshotting: snapshot,
|
|
||||||
Waiting: wait,
|
|
||||||
ErrorWait: wait,
|
|
||||||
Stopped: nil,
|
|
||||||
}
|
|
||||||
return m[s]
|
|
||||||
}
|
|
||||||
|
|
||||||
type updater func(u func(*Snapper)) State
|
|
||||||
type state func(a args, u updater) state
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextKeyLog contextKey = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger = logger.Logger
|
|
||||||
|
|
||||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKeyLog, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLogger(ctx context.Context) Logger {
|
|
||||||
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
|
||||||
return log
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
|
||||||
if in.Prefix == "" {
|
|
||||||
return nil, errors.New("prefix must not be empty")
|
|
||||||
}
|
|
||||||
if in.Interval <= 0 {
|
|
||||||
return nil, errors.New("interval must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := args{
|
|
||||||
prefix: in.Prefix,
|
|
||||||
interval: in.Interval,
|
|
||||||
fsf: fsf,
|
|
||||||
// ctx and log is set in Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Snapper{state: SyncUp, args: args}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|
||||||
|
|
||||||
getLogger(ctx).Debug("start")
|
|
||||||
defer getLogger(ctx).Debug("stop")
|
|
||||||
|
|
||||||
s.args.snapshotsTaken = snapshotsTaken
|
|
||||||
s.args.ctx = ctx
|
|
||||||
s.args.log = getLogger(ctx)
|
|
||||||
|
|
||||||
u := func(u func(*Snapper)) State {
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
if u != nil {
|
|
||||||
u(s)
|
|
||||||
}
|
|
||||||
return s.state
|
|
||||||
}
|
|
||||||
|
|
||||||
var st state = syncUp
|
|
||||||
|
|
||||||
for st != nil {
|
|
||||||
pre := u(nil)
|
|
||||||
st = st(s.args, u)
|
|
||||||
post := u(nil)
|
|
||||||
getLogger(ctx).
|
|
||||||
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
|
||||||
Debug("state transition")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func onErr(err error, u updater) state {
|
|
||||||
return u(func(s *Snapper) {
|
|
||||||
s.err = err
|
|
||||||
preState := s.state
|
|
||||||
switch s.state {
|
|
||||||
case SyncUp:
|
|
||||||
s.state = SyncUpErrWait
|
|
||||||
case Planning:
|
|
||||||
fallthrough
|
|
||||||
case Snapshotting:
|
|
||||||
s.state = ErrorWait
|
|
||||||
}
|
|
||||||
s.args.log.WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func onMainCtxDone(ctx context.Context, u updater) state {
|
|
||||||
return u(func(s *Snapper) {
|
|
||||||
s.err = ctx.Err()
|
|
||||||
s.state = Stopped
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncUp(a args, u updater) state {
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
snapper.lastInvocation = time.Now()
|
|
||||||
})
|
|
||||||
fss, err := listFSes(a.ctx, a.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
syncPoint, err := findSyncPoint(a.log, fss, a.prefix, a.interval)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
u(func(s *Snapper){
|
|
||||||
s.sleepUntil = syncPoint
|
|
||||||
})
|
|
||||||
t := time.NewTimer(syncPoint.Sub(time.Now()))
|
|
||||||
defer t.Stop()
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return u(func(s *Snapper) {
|
|
||||||
s.state = Planning
|
|
||||||
}).sf()
|
|
||||||
case <-a.ctx.Done():
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func plan(a args, u updater) state {
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
snapper.lastInvocation = time.Now()
|
|
||||||
})
|
|
||||||
fss, err := listFSes(a.ctx, a.fsf)
|
|
||||||
if err != nil {
|
|
||||||
return onErr(err, u)
|
|
||||||
}
|
|
||||||
|
|
||||||
plan := make(map[*zfs.DatasetPath]snapProgress, len(fss))
|
|
||||||
for _, fs := range fss {
|
|
||||||
plan[fs] = snapProgress{state: SnapPending}
|
|
||||||
}
|
|
||||||
return u(func(s *Snapper) {
|
|
||||||
s.state = Snapshotting
|
|
||||||
s.plan = plan
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func snapshot(a args, u updater) state {
|
|
||||||
|
|
||||||
var plan map[*zfs.DatasetPath]snapProgress
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
plan = snapper.plan
|
|
||||||
})
|
|
||||||
|
|
||||||
hadErr := false
|
|
||||||
// TODO channel programs -> allow a little jitter?
|
|
||||||
for fs, progress := range plan {
|
|
||||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
|
||||||
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
|
||||||
|
|
||||||
l := a.log.
|
|
||||||
WithField("fs", fs.ToString()).
|
|
||||||
WithField("snap", snapname)
|
|
||||||
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
progress.name = snapname
|
|
||||||
progress.startAt = time.Now()
|
|
||||||
progress.state = SnapStarted
|
|
||||||
})
|
|
||||||
|
|
||||||
l.Debug("create snapshot")
|
|
||||||
err := zfs.ZFSSnapshot(fs, snapname, false)
|
|
||||||
if err != nil {
|
|
||||||
hadErr = true
|
|
||||||
l.WithError(err).Error("cannot create snapshot")
|
|
||||||
}
|
|
||||||
doneAt := time.Now()
|
|
||||||
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
progress.doneAt = doneAt
|
|
||||||
progress.state = SnapDone
|
|
||||||
if err != nil {
|
|
||||||
progress.state = SnapError
|
|
||||||
progress.err = err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case a.snapshotsTaken <- struct{}{}:
|
|
||||||
default:
|
|
||||||
if a.snapshotsTaken != nil {
|
|
||||||
a.log.Warn("callback channel is full, discarding snapshot update event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return u(func(snapper *Snapper) {
|
|
||||||
if hadErr {
|
|
||||||
snapper.state = ErrorWait
|
|
||||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
|
||||||
} else {
|
|
||||||
snapper.state = Waiting
|
|
||||||
}
|
|
||||||
}).sf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func wait(a args, u updater) state {
|
|
||||||
var sleepUntil time.Time
|
|
||||||
u(func(snapper *Snapper) {
|
|
||||||
lastTick := snapper.lastInvocation
|
|
||||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
|
||||||
sleepUntil = snapper.sleepUntil
|
|
||||||
log := a.log.WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
|
||||||
logFunc := log.Debug
|
|
||||||
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
|
||||||
logFunc = log.Error
|
|
||||||
}
|
|
||||||
logFunc("enter wait-state after error")
|
|
||||||
})
|
|
||||||
|
|
||||||
t := time.NewTimer(sleepUntil.Sub(time.Now()))
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return u(func(snapper *Snapper) {
|
|
||||||
snapper.state = Planning
|
|
||||||
}).sf()
|
|
||||||
case <-a.ctx.Done():
|
|
||||||
return onMainCtxDone(a.ctx, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) {
|
|
||||||
return zfs.ZFSListMapping(ctx, mf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
|
||||||
type snapTime struct {
|
|
||||||
ds *zfs.DatasetPath
|
|
||||||
time time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fss) == 0 {
|
|
||||||
return time.Now(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
snaptimes := make([]snapTime, 0, len(fss))
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
log.Debug("examine filesystem state")
|
|
||||||
for _, d := range fss {
|
|
||||||
|
|
||||||
l := log.WithField("fs", d.ToString())
|
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Error("cannot list filesystem versions")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(fsvs) <= 0 {
|
|
||||||
l.WithField("prefix", prefix).Debug("no filesystem versions with prefix")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort versions by creation
|
|
||||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
|
||||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
|
||||||
})
|
|
||||||
|
|
||||||
latest := fsvs[len(fsvs)-1]
|
|
||||||
l.WithField("creation", latest.Creation).
|
|
||||||
Debug("found latest snapshot")
|
|
||||||
|
|
||||||
since := now.Sub(latest.Creation)
|
|
||||||
if since < 0 {
|
|
||||||
l.WithField("snapshot", latest.Name).
|
|
||||||
WithField("creation", latest.Creation).
|
|
||||||
Error("snapshot is from the future")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
next := now
|
|
||||||
if since < interval {
|
|
||||||
next = latest.Creation.Add(interval)
|
|
||||||
}
|
|
||||||
snaptimes = append(snaptimes, snapTime{d, next})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(snaptimes) == 0 {
|
|
||||||
snaptimes = append(snaptimes, snapTime{nil, now})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(snaptimes, func(i, j int) bool {
|
|
||||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
|
||||||
})
|
|
||||||
|
|
||||||
return snaptimes[0].time, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package snapper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FIXME: properly abstract snapshotting:
|
|
||||||
// - split up things that trigger snapshotting from the mechanism
|
|
||||||
// - timer-based trigger (periodic)
|
|
||||||
// - call from control socket (manual)
|
|
||||||
// - mixed modes?
|
|
||||||
// - support a `zrepl snapshot JOBNAME` subcommand for config.SnapshottingManual
|
|
||||||
type PeriodicOrManual struct {
|
|
||||||
s *Snapper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan <- struct{}) {
|
|
||||||
if s.s != nil {
|
|
||||||
s.s.Run(ctx, wakeUpCommon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
|
||||||
switch v := in.Ret.(type) {
|
|
||||||
case *config.SnapshottingPeriodic:
|
|
||||||
snapper, err := PeriodicFromConfig(g, fsf, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &PeriodicOrManual{snapper}, nil
|
|
||||||
case *config.SnapshottingManual:
|
|
||||||
return &PeriodicOrManual{}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown snapshotting type %T", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-595
@@ -1,595 +0,0 @@
|
|||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"$$hashKey": "object:3351",
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": "-- Grafana --",
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"gnetId": null,
|
|
||||||
"graphTooltip": 1,
|
|
||||||
"id": 7,
|
|
||||||
"iteration": 1552746418826,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"content": "# zrepl Prometheus Metrics\n\nzrepl exposes Prometheus metrics and ships with this Grafana dashboard.\nThe exported metrics are suitable for health checks:\n\n* The log should generally be warning & error-free\n * The `Log Messages that require attention` graph visualizes log message counts indicating problems.\n* The number of goroutines should not grow unboundedly over time.\n * During replication, the number of goroutines can be way higher than during idle time.\n * If the goroutine count grows with each replication, there is clearly a goroutine leak. Please open a bug report.\n* The sys memory consumption should not grow unboundedly over time.\n * Note that the Go runtime pre-allocates some of its heap from the OS.\n * zrepl actually uses much less memory than allocated from the OS.\n * Since Go 1.11, Go pre-allocates more aggressively.\n* Monitor that some data is replicated, although that metric does not guarantee that replication was successful.\n\n**In general, note that the exported metrics are not stable unless declared otherwise.**",
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 35,
|
|
||||||
"mode": "markdown",
|
|
||||||
"title": "Panel Title",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 9
|
|
||||||
},
|
|
||||||
"id": 15,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": true,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"$$hashKey": "object:3436",
|
|
||||||
"expr": "up{job='$prom_job_name'}",
|
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "zrepl Instances Up",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": "5",
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 9
|
|
||||||
},
|
|
||||||
"id": 22,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "increase(zrepl_daemon_log_entries{job='$prom_job_name',level=~'warn|error'}[$__interval])",
|
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Log Messages that require attention",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 18
|
|
||||||
},
|
|
||||||
"id": 33,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [
|
|
||||||
{
|
|
||||||
"alias": "/replicated bytes in last.*/",
|
|
||||||
"yaxis": 2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "sum(rate(zrepl_replication_bytes_replicated{job='$prom_job_name'}[$__interval])) by (zrepl_job)",
|
|
||||||
"format": "time_series",
|
|
||||||
"hide": false,
|
|
||||||
"interval": "",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"legendFormat": "replication data rate zrepl_job={{zrepl_job}}",
|
|
||||||
"refId": "A"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"expr": "sum(increase(zrepl_replication_bytes_replicated{job='$prom_job_name'}[10m])) by (zrepl_job)",
|
|
||||||
"format": "time_series",
|
|
||||||
"hide": false,
|
|
||||||
"interval": "",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"legendFormat": "replicated bytes in last 10min zrepl_job={{zrepl_job}}",
|
|
||||||
"refId": "B"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Replication Data Rate and Volume(integrates last 10min)",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "Bps",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "bytes",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 18
|
|
||||||
},
|
|
||||||
"id": 23,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "sum(increase(zrepl_daemon_log_entries{job='$prom_job_name',zrepl_job=~\"^[^_].*\"}[$__interval])) by (instance,zrepl_job)",
|
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Log Activity (without internal jobs)",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 27
|
|
||||||
},
|
|
||||||
"id": 17,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"$$hashKey": "object:3535",
|
|
||||||
"expr": "go_memstats_sys_bytes{job='$prom_job_name'}",
|
|
||||||
"format": "time_series",
|
|
||||||
"hide": false,
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Memory Allocated by the Go runtime from the OS (should not grow unboundedly)",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "bytes",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"aliasColors": {},
|
|
||||||
"bars": false,
|
|
||||||
"dashLength": 10,
|
|
||||||
"dashes": false,
|
|
||||||
"datasource": null,
|
|
||||||
"fill": 1,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 9,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 27
|
|
||||||
},
|
|
||||||
"id": 19,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 1,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "null",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"spaceLength": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"expr": "go_goroutines{job='$prom_job_name'}",
|
|
||||||
"format": "time_series",
|
|
||||||
"intervalFactor": 1,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "number of goroutines (should not grow unboundedly)",
|
|
||||||
"tooltip": {
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "individual"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"buckets": null,
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": "0",
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"label": null,
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"refresh": "1m",
|
|
||||||
"schemaVersion": 16,
|
|
||||||
"style": "dark",
|
|
||||||
"tags": [],
|
|
||||||
"templating": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"allValue": null,
|
|
||||||
"current": {
|
|
||||||
"text": "zrepl",
|
|
||||||
"value": "zrepl"
|
|
||||||
},
|
|
||||||
"datasource": "prometheus",
|
|
||||||
"hide": 0,
|
|
||||||
"includeAll": false,
|
|
||||||
"label": "Prometheus Job Name",
|
|
||||||
"multi": false,
|
|
||||||
"name": "prom_job_name",
|
|
||||||
"options": [],
|
|
||||||
"query": "label_values(up, job)",
|
|
||||||
"refresh": 1,
|
|
||||||
"regex": "",
|
|
||||||
"sort": 1,
|
|
||||||
"tagValuesQuery": "",
|
|
||||||
"tags": [],
|
|
||||||
"tagsQuery": "",
|
|
||||||
"type": "query",
|
|
||||||
"useTags": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-2d",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {
|
|
||||||
"refresh_intervals": [
|
|
||||||
"5s",
|
|
||||||
"10s",
|
|
||||||
"30s",
|
|
||||||
"1m",
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"30m",
|
|
||||||
"1h",
|
|
||||||
"2h",
|
|
||||||
"1d"
|
|
||||||
],
|
|
||||||
"time_options": [
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"1h",
|
|
||||||
"6h",
|
|
||||||
"12h",
|
|
||||||
"24h",
|
|
||||||
"2d",
|
|
||||||
"7d",
|
|
||||||
"30d"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timezone": "",
|
|
||||||
"title": "zrepl 0.1",
|
|
||||||
"uid": "xTljn4qmk",
|
|
||||||
"version": 6
|
|
||||||
}
|
|
||||||
+1647
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/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
+4
-29
@@ -4,38 +4,13 @@ Documentation=https://zrepl.github.io
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
ExecStartPre=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml configcheck
|
||||||
ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
|
ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
|
||||||
RuntimeDirectory=zrepl
|
RuntimeDirectory=zrepl zrepl/stdinserver
|
||||||
RuntimeDirectoryMode=0700
|
RuntimeDirectoryMode=0700
|
||||||
|
|
||||||
ProtectSystem=strict
|
# Make Go produce coredumps
|
||||||
#PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this?
|
Environment=GOTRACEBACK='crash'
|
||||||
ProtectKernelTunables=yes
|
|
||||||
ProtectControlGroups=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
#PrivateUsers=yes # TODO Does not work, why?
|
|
||||||
ProtectKernelModules=true
|
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
||||||
RestrictNamespaces=true
|
|
||||||
RestrictRealtime=yes
|
|
||||||
SystemCallArchitectures=native
|
|
||||||
|
|
||||||
# BEGIN ProtectHome
|
|
||||||
ProtectHome=read-only # DEBIAN STRETCH
|
|
||||||
# ProtectHome=tmpfs # FEDORA 28 / 29
|
|
||||||
# END ProtectHome
|
|
||||||
|
|
||||||
# BEGIN SystemCallFilter
|
|
||||||
## BEGIN DEBIAN STRETCH
|
|
||||||
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @privileged @raw-io @debug @clock @resources
|
|
||||||
## END DEBIAN STRETCH
|
|
||||||
## BEGIN FEDORA 28/29
|
|
||||||
## Syscall blacklist (should be fairly stable)
|
|
||||||
#SystemCallFilter=~ @mount @aio @cpu-emulation @keyring @memlock @module @obsolete @privileged @raw-io @reboot @setuid @swap @sync @timer @debug @clock @chown @resources
|
|
||||||
## Syscall whitelist (not sure how stable)
|
|
||||||
#SystemCallFilter=@default @file-system @process @basic-io @ipc @network-io @signal @io-event brk mprotect sched_getaffinity ioctl getrandom
|
|
||||||
## END END FEDORA 28/29
|
|
||||||
# END SystemCallFilter
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
3.14
|
||||||
+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)" -c sphinxconf $(SPHINXOPTS) $(O)
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(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)" -c sphinxconf $(SPHINXOPTS) $(O)
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|||||||
Vendored
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/* 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
+1
@@ -0,0 +1 @@
|
|||||||
|
{% extends "!page.html" %}
|
||||||
Vendored
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<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: latest
|
||||||
|
<span class="fa fa-caret-down"></span>
|
||||||
|
</span>
|
||||||
|
<div class="rst-other-versions">
|
||||||
|
<dl>
|
||||||
|
<dt>Releases</dt>
|
||||||
|
<dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.7.0">v0.7.0</a></dd>
|
||||||
|
<dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.6.1">v0.6.1</a></dd>
|
||||||
|
<dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.5.0">v0.5.0</a></dd>
|
||||||
|
<dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.4.0">v0.4.0</a></dd>
|
||||||
|
<dd><a href="https://github.com/zrepl/zrepl/releases">All releases...</a></dd>
|
||||||
|
</dl>
|
||||||
|
<dl>
|
||||||
|
<dt>Note</dt>
|
||||||
|
<dd style="white-space: normal;">
|
||||||
|
Release-specific docs are in the
|
||||||
|
<span style="font-family: monospace;">zrepl-noarch.tar</span>
|
||||||
|
asset on each GitHub release.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+275
-13
@@ -1,29 +1,290 @@
|
|||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
.. |break_config| replace:: **[CONFIG]**
|
.. |break_config| replace:: **[CONFIG]**
|
||||||
.. |break| replace:: **[BREAK]**
|
.. |break| replace:: **[BREAK]**
|
||||||
.. |bugfix| replace:: [BUG]
|
.. |bugfix| replace:: [BUG]
|
||||||
.. |docs| replace:: [DOCS]
|
.. |docs| replace:: [DOCS]
|
||||||
.. |feature| replace:: [FEATURE]
|
.. |feature| replace:: [FEATURE]
|
||||||
.. |mig| replace:: **[MIGRATION]**
|
.. |mig| replace:: **[MIGRATION]**
|
||||||
|
.. |maint| replace:: [MAINT]
|
||||||
|
|
||||||
.. _changelog:
|
.. _changelog:
|
||||||
|
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
0.7.0
|
||||||
Developers should consult the git commit log or GitHub issue tracker.
|
-----
|
||||||
|
|
||||||
We use the following annotations for classifying changes:
|
`GitHub Release <https://github.com/zrepl/zrepl/releases/tag/v0.7.0>`_
|
||||||
|
|
||||||
* |break_config| Change that breaks the config.
|
* |feature| Config file inclusion using ``include`` directive.
|
||||||
As a package maintainer, make sure to warn your users about config breakage somehow.
|
This allows distributing zrepl job definitions across multiple YAML files in a ``conf.d`` style directory.
|
||||||
* |break| Change that breaks interoperability or persistent state representation with previous releases.
|
(:commit:`4d6583e`, thanks, `@ZeyadTamimi <https://github.com/zeyadtamimi>`_).
|
||||||
As a package maintainer, make sure to warn your users about config breakage somehow.
|
* |feature| Add configurable timezone for snapshot name timestamps. The default remains UTC. (:commit:`b9b9ad1`, thanks, `@mjasnik <https://github.com/mjasnik>`_).
|
||||||
Note that even updating the package on both sides might not be sufficient, e.g. if persistent state needs to be migrated to a new format.
|
* |feature| Improve Grafana dashboard with additional panels and refinements (:commit:`3f75127`, thanks, `@deajan <https://github.com/deajan>`_).
|
||||||
* |mig| Migration that must be run by the user.
|
* |bugfix| Fix replication of placeholder filesystems (:commit:`5615f49`).
|
||||||
* |feature| Change that introduces new functionality.
|
* |bugfix| Avoid using the word "error" in info-level log lines about ZFS command execution (:commit:`d3b2295`, thanks, `@bakhtiyarneyman <https://github.com/bakhtiyarneyman>`_).
|
||||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
* |bugfix| Detect duplicate and internal job names at config parse time.
|
||||||
* |docs| Change to the documentation.
|
zrepl will now refuse to load a config with duplicate or internally reserved job names, instead of failing later at daemon startup.
|
||||||
|
(:commit:`860a9be`).
|
||||||
|
* |bugfix| Fix ``last_n`` keep rule behavior (:commit:`ebc46cf`, thanks, `@dsh2dsh <https://github.com/dsh2dsh>`_).
|
||||||
|
* |bugfix| Allow different jobs to use the same ``root_fs`` for sinks (:commit:`27012e5`, thanks, `@dsh2dsh <https://github.com/dsh2dsh>`_).
|
||||||
|
* |docs| Add installation instructions for openSUSE (:commit:`40c4827`, thanks, `@findesgh <https://github.com/findesgh>`_).
|
||||||
|
* |docs| Improve documentation on ``send_properties`` configuration and NFS/SMB considerations (:commit:`b5d8538`, thanks, `@Malvineous <https://github.com/Malvineous>`_).
|
||||||
|
* |docs| Warn more prominently about the risks of the ``not_replicated`` keep rule (:commit:`affe00a`, thanks, `@wxiaoguang <https://github.com/wxiaoguang>`_).
|
||||||
|
* |docs| Improve TLS/EasyRSA setup instructions (:commit:`e524b60`, thanks, `@alorimer <https://github.com/alorimer>`_).
|
||||||
|
* |docs| Remove dead Bountysource link (:commit:`440b074`, thanks, `@lpulley <https://github.com/lpulley>`_).
|
||||||
|
* |docs| Fix missing newline in compile-from-source documentation (:commit:`e2fcf9f`, thanks, `@Raupinger <https://github.com/Raupinger>`_).
|
||||||
|
* |docs| Fix apt repository installation snippet (:commit:`8305367`, thanks, `@fermino <https://github.com/fermino>`_).
|
||||||
|
* |docs| ``zrepl.github.io``: auto-publish from ``master`` branch, retire ``stable`` branch (:commit:`4f950bb`).
|
||||||
|
* |maint| ``zrepl status``: switch from the unmaintained ``cview`` fork back to the actively maintained ``tview`` library (:commit:`d7ede3f`).
|
||||||
|
* |maint| Update to Go 1.25 toolchain with Go 1.24 language level & Go dependencies
|
||||||
|
* |maint| Debian packages: proper bash completion placement (:commit:`2923009`)
|
||||||
|
* |maint| RPM packages: AlmaLinux 8 for RPM builds (:commit:`27f4ad1`).
|
||||||
|
* |maint| Improve build system & CI setup
|
||||||
|
|
||||||
|
.. 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.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
|
||||||
|
|
||||||
|
0.6
|
||||||
|
---
|
||||||
|
|
||||||
|
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
|
||||||
|
* |feature| Configurable initial replication policy.
|
||||||
|
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.
|
||||||
|
|
||||||
|
* The send options introduced in zrepl 0.4 allow users to specify additional zfs send flags for zrepl to use.
|
||||||
|
Before this fix, when setting ``send.raw=true`` on a job that replicates unencrypted datasets,
|
||||||
|
zrepl would not allow an interrupted replication to resume.
|
||||||
|
The reason were overly cautious checks to support the ``send.encrypted`` option.
|
||||||
|
* This bugfix removes these checks from the replication planner.
|
||||||
|
This makes ``send.encrypted`` a sender-side-only concern, much like all other ``send.*`` flags.
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
* |break| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
The Grafana dashboard in :repomasterlink:`dist/grafana` has been updated.
|
||||||
|
|
||||||
|
* |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF``
|
||||||
|
* |bugfix| don't treat receive-side bookmarks as a replication conflict.
|
||||||
|
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
|
||||||
|
---
|
||||||
|
|
||||||
|
* |feature| :ref:`Bandwidth limiting <job-send-recv-options--bandwidth-limit>` (Thanks, Prominic.NET, Inc.)
|
||||||
|
* |feature| zrepl status: use a ``*`` to indicate which filesystem is currently replicating
|
||||||
|
* |feature| include daemon environment variables in zrepl status (currently only in ``--raw``)
|
||||||
|
* |bugfix| **fix encrypt-on-receive + placeholders use case** (:issue:`504`)
|
||||||
|
|
||||||
|
* Before this fix, **plain sends** to a receiver with an encrypted ``root_fs`` **could be received unencrypted** if zrepl needed to create placeholders on the receiver.
|
||||||
|
* Existing zrepl users should :ref:`read the docs <job-recv-options--placeholder>` and check ``zfs get -r encryption,zrepl:placeholder PATH_TO_ROOTFS`` on the receiver.
|
||||||
|
* Thanks to `@mologie <https://github.com/mologie>`_ and `@razielgn <https://github.com/razielgn>`_ for reporting and testing!
|
||||||
|
|
||||||
|
* |bugfix| Rename mis-spelled :ref:`send option <job-send-options>` ``embbeded_data`` to ``embedded_data``.
|
||||||
|
* |bugfix| zrepl status: replication step numbers should start at 1
|
||||||
|
* |bugfix| incorrect bandwidth averaging in ``zrepl status``.
|
||||||
|
* |bugfix| FreeBSD with OpenZFS 2.0: zrepl would wait indefinitely for zfs send to exit on timeouts.
|
||||||
|
* |bugfix| fix ``strconv.ParseInt: value out of range`` bug (and use the control RPCs).
|
||||||
|
* |docs| improve description of multiple pruning rules.
|
||||||
|
* |docs| document :ref:`platform tests <usage-platform-tests>`.
|
||||||
|
* |docs| quickstart: make users aware that prune rules apply to all snapshots.
|
||||||
|
* |maint| some platformtests were broken.
|
||||||
|
* |maint| FreeBSD: release armv7 and arm64 binaries.
|
||||||
|
* |maint| apt repo: update instructions due to ``apt-key`` deprecation.
|
||||||
|
|
||||||
|
Note to all users: please read up on the following OpenZFS bugs, as you might be affected:
|
||||||
|
|
||||||
|
* `ZFS send/recv with ashift 9->12 leads to data corruption <https://github.com/openzfs/zfs/issues/12762>`_.
|
||||||
|
* Various bugs with encrypted send/recv (`Leadership meeting notes <https://openzfs.topicbox.com/groups/developer/T24bdaa2886c6cbf5-Mc039a11c3f1507ea0664817b/december-openzfs-leadership-meeting>`_)
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
0.4.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
||||||
|
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
||||||
|
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
||||||
|
* |feature| New ``zrepl status`` UI:
|
||||||
|
|
||||||
|
* Interactive job selection.
|
||||||
|
* Interactively ``zrepl signal`` jobs.
|
||||||
|
* Filter filesystems in the job view by name.
|
||||||
|
* An approximation of the old UI is still included as `--mode legacy` but will be removed in a future release of zrepl.
|
||||||
|
|
||||||
|
* |bugfix| Actually use concurrency when listing zrepl abstractions & doing size estimation.
|
||||||
|
These operations were accidentally made sequential in zrepl 0.3.
|
||||||
|
* |bugfix| Job hang-up during second replication attempt.
|
||||||
|
* |bugfix| Data races conditions in the dataconn rpc stack.
|
||||||
|
* |maint| Update to protobuf v1.25 and grpc 1.35.
|
||||||
|
|
||||||
|
For users who skipped the 0.3.1 update: please make sure your pruning grid config is correct.
|
||||||
|
The following bugfix in 0.3.1 :issue:`caused problems for some users <400>`:
|
||||||
|
|
||||||
|
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
||||||
|
|
||||||
|
0.3.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
Mostly a bugfix release for :ref:`zrepl 0.3 <release-0.3>`.
|
||||||
|
|
||||||
|
* |feature| pruning: add optional ``regex`` field to ``last_n`` rule
|
||||||
|
* |docs| pruning: ``grid`` : improve documentation and add an example
|
||||||
|
* |bugfix| pruning: ``grid``: add all snapshots that do not match the regex to the rule's destroy list.
|
||||||
|
This brings the implementation in line with the docs.
|
||||||
|
* |bugfix| ``easyrsa`` script in docs
|
||||||
|
* |bugfix| platformtest: fix skipping encryption-only tests on systems that don't support encryption
|
||||||
|
* |bugfix| replication: report AttemptDone if no filesystems are replicated
|
||||||
|
* |feature| status + replication: warning if replication succeeeded without any filesystem being replicated
|
||||||
|
* |docs| update multi-job & multi-host setup section
|
||||||
|
* RPM Packaging
|
||||||
|
* CI infrastructure rework
|
||||||
|
* Continuous deployment of that new `stable` branch to zrepl.github.io.
|
||||||
|
|
||||||
|
.. _release-0.3:
|
||||||
|
|
||||||
|
0.3
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a big one! Headlining features:
|
||||||
|
|
||||||
|
* **Resumable Send & Recv Support**
|
||||||
|
No knobs required, automatically used where supported.
|
||||||
|
* **Encrypted Send & Recv Support** for OpenZFS native encryption,
|
||||||
|
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
|
||||||
|
* **Replication Guarantees**
|
||||||
|
Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver.
|
||||||
|
By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
We highly recommend studying the updated :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Go 1.15 changed the default TLS validation policy to **require Subject Alternative Names (SAN) in certificates**.
|
||||||
|
The openssl commands we provided in the quick-start guides up to and including the zrepl 0.3 docs seem not to work properly.
|
||||||
|
If you encounter certificate validation errors regarding SAN and wish to continue to use your old certificates, start the zrepl daemon with env var ``GODEBUG=x509ignoreCN=0``.
|
||||||
|
Alternatively, generate new certificates with SANs (see :ref:`both options int the TLS transport docs <transport-tcp+tlsclientauth-certgen>` ).
|
||||||
|
|
||||||
|
Quick-start guides:
|
||||||
|
|
||||||
|
* We have added :ref:`another quick-start guide for a typical workstation use case for zrepl <quickstart-backup-to-external-disk>`.
|
||||||
|
Check it out to learn how you can use zrepl to back up your workstation's OpenZFS natively-encrypted root filesystem to an external disk.
|
||||||
|
|
||||||
|
Additional changelog:
|
||||||
|
|
||||||
|
* |break| Go 1.15 TLS changes mentioned above.
|
||||||
|
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
|
||||||
|
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`this section for details <zrepl-zfs-abstractions>`).
|
||||||
|
Therefore you might need to adjust your job names.
|
||||||
|
**Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.**
|
||||||
|
* |break| |mig| replication cursor representation changed
|
||||||
|
|
||||||
|
* zrepl now manages the :ref:`replication cursor bookmark <zrepl-zfs-abstractions>` per job-filesystem tuple instead of a single replication cursor per filesystem.
|
||||||
|
In the future, this will permit multiple sending jobs to send from the same filesystems.
|
||||||
|
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
|
||||||
|
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
|
||||||
|
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
|
||||||
|
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
|
||||||
|
|
||||||
|
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||||
|
* |feature| :issue:`341` Prometheus metric for failing replications + corresponding Grafana panel
|
||||||
|
* |feature| :issue:`265` transport/tcp: support for CIDR masks in client IP whitelist
|
||||||
|
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||||
|
* |feature| :issue:`307` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
|
||||||
|
* |feature| logging: trace IDs for better log entry correlation with concurrent replication jobs
|
||||||
|
* |feature| experimental environment variable for parallel replication (see :issue:`306` )
|
||||||
|
* |bugfix| missing logger context vars in control connection handlers
|
||||||
|
* |bugfix| improved error messages on ``zfs send`` errors
|
||||||
|
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
||||||
|
* |bugfix| transport/ssh: do not leak zombie ssh process on connection failures
|
||||||
|
that will not be snapshotted until the sync-up phase is over
|
||||||
|
* |docs| Installation: :ref:`FreeBSD jail with iocage <installation-freebsd-jail-with-iocage>`
|
||||||
|
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
|
||||||
|
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
||||||
|
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
|
||||||
|
|
||||||
|
0.2.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* |feature| Illumos (and Solaris) compatibility and binary builds (thanks, `MNX.io <https://mnx.io>`_ )
|
||||||
|
* |feature| 32bit binaries for Linux and FreeBSD (untested, though)
|
||||||
|
* |bugfix| better error messages in ``ssh+stdinserver`` transport
|
||||||
|
* |bugfix| systemd + ``ssh+stdinserver``: automatically create ``/var/run/zrepl/stdinserver``
|
||||||
|
* |bugfix| crash if Prometheus listening socket cannot be opened
|
||||||
|
|
||||||
|
* [MAINTAINER NOTICE] ``Makefile`` refactoring, see commit :commit:`080f2c0`
|
||||||
|
|
||||||
|
0.2
|
||||||
|
---
|
||||||
|
|
||||||
|
* |feature| :ref:`Pre- and Post-Snapshot Hooks <job-snapshotting-hooks>`
|
||||||
|
with built-in support for MySQL and Postgres checkpointing
|
||||||
|
as well as custom scripts (thanks, `@overhacked <https://github.com/overhacked>`_!)
|
||||||
|
* |feature| Use ``zfs destroy pool/fs@snap1,snap2,...`` CLI feature if available
|
||||||
|
* |feature| Linux ARM64 Docker build support & binary builds
|
||||||
|
* |feature| ``zrepl status`` now displays snapshotting reports
|
||||||
|
* |feature| ``zrepl status --job <JOBNAME>`` filter flag
|
||||||
|
* |bugfix| i386 build
|
||||||
|
* |bugfix| early validation of host:port tuples in config
|
||||||
|
* |bugfix| ``zrepl status`` now supports ``TERM=screen`` (tmux on FreeBSD / FreeNAS)
|
||||||
|
* |bugfix| ignore *connection reset by peer* errors when shutting down connections
|
||||||
|
* |bugfix| correct error messages when receive-side pool or ``root_fs`` dataset is not imported
|
||||||
|
* |bugfix| fail fast for misconfigured local transport
|
||||||
|
* |bugfix| race condition in replication report generation would crash the daemon when running ``zrepl status``
|
||||||
|
* |bugfix| rpc goroutine leak in ``push`` mode if zfs recv fails on the ``sink`` side
|
||||||
|
* [MAINTAINER NOTICE] Go modules for dependency management both inside and outside of GOPATH
|
||||||
|
(``lazy.sh`` and ``Makefile`` force ``GO111MODULE=on``)
|
||||||
|
* [MAINTAINER NOTICE] ``make platformtest`` target to check zrepl's ZFS abstractions (screen scraping, etc.).
|
||||||
|
These tests only work on a system with ZFS installed, and must be run as root because they create a file-backed pool for each test case.
|
||||||
|
The pool name ``zreplplatformtest`` is reserved for this use case.
|
||||||
|
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
|
||||||
|
|
||||||
|
0.1.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* |bugfix| :issue:`162` commit :commit:`d6304f4` : fix I/O timeout errors on variable receive rate
|
||||||
|
|
||||||
|
* A significant reduction or sudden stall of the receive rate (e.g. recv pool has other I/O to do)
|
||||||
|
would cause a ``writev I/O timeout`` error after approximately ten seconds.
|
||||||
|
|
||||||
0.1
|
0.1
|
||||||
---
|
---
|
||||||
@@ -42,7 +303,7 @@ It breaks both configuration and transport format, and thus requires manual inte
|
|||||||
Notes to Package Maintainers
|
Notes to Package Maintainers
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* Notify users about migrations (see changes attributed with |mig| below)
|
* Notify users about config changes and migrations (see changes attributed with |break| and |mig| below)
|
||||||
* If the daemon crashes, the stack trace produced by the Go runtime and possibly diagnostic output of zrepl will be written to stderr.
|
* If the daemon crashes, the stack trace produced by the Go runtime and possibly diagnostic output of zrepl will be written to stderr.
|
||||||
This behavior is independent from the ``stdout`` outlet type.
|
This behavior is independent from the ``stdout`` outlet type.
|
||||||
Please make sure the stderr output of the daemon is captured somewhere.
|
Please make sure the stderr output of the daemon is captured somewhere.
|
||||||
@@ -51,6 +312,7 @@ Notes to Package Maintainers
|
|||||||
This functionality will cause SIGABRT on panics and can be used to capture a coredump of the panicking process.
|
This functionality will cause SIGABRT on panics and can be used to capture a coredump of the panicking process.
|
||||||
To that extend, make sure that your package build system, your OS's coredump collection and the Go delve debugger work together.
|
To that extend, make sure that your package build system, your OS's coredump collection and the Go delve debugger work together.
|
||||||
Use your build system to package the Go program in `this tutorial on Go coredumps and the delve debugger <https://rakyll.org/coredumps/>`_ , and make sure the symbol resolution etc. work on coredumps captured from the binary produced by your build system. (Special focus on symbol stripping, etc.)
|
Use your build system to package the Go program in `this tutorial on Go coredumps and the delve debugger <https://rakyll.org/coredumps/>`_ , and make sure the symbol resolution etc. work on coredumps captured from the binary produced by your build system. (Special focus on symbol stripping, etc.)
|
||||||
|
* Consider using the ``zrepl configcheck`` subcommand in startup scripts to abort a restart that would fail due to an invalid config.
|
||||||
|
|
||||||
Changes
|
Changes
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
sphinxconf/conf.py
|
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
#!/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-2026, 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 = 'latest'
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = 'latest'
|
||||||
|
|
||||||
|
# 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', '.venv']
|
||||||
|
|
||||||
|
# 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/internal/config/samples%s', 'internal/config/samples%s'),
|
||||||
|
'commit':('https://github.com/zrepl/zrepl/commit/%s', '%s'),
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,10 +7,14 @@ Configuration
|
|||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
||||||
configuration/preface
|
configuration/overview
|
||||||
configuration/jobs
|
configuration/jobs
|
||||||
configuration/transports
|
configuration/transports
|
||||||
configuration/filter_syntax
|
configuration/filter_syntax
|
||||||
|
configuration/sendrecvoptions
|
||||||
|
configuration/replication
|
||||||
|
configuration/conflict_resolution
|
||||||
|
configuration/snapshotting
|
||||||
configuration/prune
|
configuration/prune
|
||||||
configuration/logging
|
configuration/logging
|
||||||
configuration/monitoring
|
configuration/monitoring
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _conflict_resolution-options:
|
||||||
|
|
||||||
|
Conflict Resolution Options
|
||||||
|
===========================
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
filesystems: ...
|
||||||
|
conflict_resolution:
|
||||||
|
initial_replication: most_recent | all | fail # default: most_recent
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
.. _conflict_resolution-initial_replication:
|
||||||
|
|
||||||
|
|
||||||
|
``initial_replication`` option
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
The ``initial_replication`` option determines how many snapshots zrepl replicates if the filesystem has not been replicated before.
|
||||||
|
If ``most_recent`` (the default), the initial replication will only transfer the most recent snapshot, while ignoring previous snapshots.
|
||||||
|
If all snapshots should be replicated, specify ``all``.
|
||||||
|
Use ``fail`` to make replication of the filesystem fail in case there is no corresponding fileystem on the receiver.
|
||||||
|
|
||||||
|
For example, suppose there are snapshosts ``tank@1``, ``tank@2``, ``tank@3`` on a sender.
|
||||||
|
Then ``most_recent`` will replicate just ``@3``, but ``all`` will replicate ``@1``, ``@2``, and ``@3``.
|
||||||
|
|
||||||
|
If initial replication is interrupted, and there is at least one (maybe partial) snapshot on the receiver, zrepl will always resume in **incremental mode**.
|
||||||
|
And that is regardless of where the initial replication was interrupted.
|
||||||
|
|
||||||
|
For example, if ``initial_replication: all`` and the transfer of ``@1`` is interrupted, zrepl would retry/resume at ``@1``.
|
||||||
|
And even if the user changes the config to ``initial_replication: most_recent`` before resuming, **incremental mode** will still resume at ``@1``.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
Filter Syntax
|
Filter Syntax
|
||||||
=============
|
=============
|
||||||
|
|
||||||
For :ref:`source jobs <job-source>` and :ref:`push jobs <job-push>`, a filesystem filter must be defined (field ``filesystems``).
|
For :ref:`source<job-source>`, :ref:`push<job-push>` and :ref:`snap<job-snap>` jobs, a filesystem filter must be defined (field ``filesystems``).
|
||||||
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
A filter takes a filesystem path (in the ZFS filesystem hierarchy) as parameter and returns ``true`` (pass) or ``false`` (block).
|
||||||
|
|
||||||
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
A filter is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
||||||
|
|||||||
+19
-220
@@ -1,222 +1,9 @@
|
|||||||
.. include:: ../global.rst.inc
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
|
||||||
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
|
||||||
.. |snapshotting-spec| replace:: :ref:`snapshotting specification <job-snapshotting-spec>`
|
|
||||||
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
|
||||||
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
|
||||||
|
|
||||||
.. _job:
|
.. _job:
|
||||||
|
|
||||||
Job Types & Replication
|
Job Types in Detail
|
||||||
=======================
|
===================
|
||||||
|
|
||||||
.. _job-overview:
|
|
||||||
|
|
||||||
Overview & Terminology
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
A *job* is the unit of activity tracked by the zrepl daemon.
|
|
||||||
Every job has a unique ``name``, a ``type`` and type-dependent fields which are documented on this page.
|
|
||||||
|
|
||||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
|
||||||
The active side executes the replication logic whereas the passive side responds to requests after checking the active side's permissions.
|
|
||||||
For communication, the active side connects to the passive side using a :ref:`transport <transport>` and starts issuing remote procedure calls (RPCs).
|
|
||||||
|
|
||||||
The following table shows how different job types can be combined to achieve both push and pull mode setups:
|
|
||||||
|
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
|
||||||
| Setup name | active side | passive side | use case |
|
|
||||||
+=======================+==============+==================================+====================================================================================+
|
|
||||||
| Push mode | ``push`` | ``sink`` | * Laptop backup |
|
|
||||||
| | | | * NAS behind NAT to offsite |
|
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
|
||||||
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
|
||||||
| | | | * Remote server to NAS behind NAT |
|
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
|
||||||
| Local replication | | ``push`` + ``sink`` in one config | * Backup FreeBSD boot pool |
|
|
||||||
| | | with :ref:`local transport <transport-local>` | |
|
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
|
||||||
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
|
||||||
| | | | | required |
|
|
||||||
| | | | * Workaround for :ref:`source-side pruning <prune-workaround-source-side-pruning>` |
|
|
||||||
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
|
||||||
|
|
||||||
How the Active Side Works
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
|
|
||||||
|
|
||||||
* Wakeup because of finished snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
|
||||||
* Connect to the corresponding passive side using a :ref:`transport <transport>` and instantiate an RPC client.
|
|
||||||
* Replicate data from the sending to the receiving side.
|
|
||||||
* Prune on sender & receiver.
|
|
||||||
|
|
||||||
.. TIP::
|
|
||||||
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
|
||||||
|
|
||||||
How the Passive Side Works
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
|
||||||
using the transport listener type specified in the ``serve`` field of the job configuration.
|
|
||||||
Each transport listener provides a client's identity to the passive side job.
|
|
||||||
It uses the client identity for access control:
|
|
||||||
|
|
||||||
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
|
||||||
* The ``source`` job has a whitelist of client identities that are allowed pull access.
|
|
||||||
|
|
||||||
.. TIP::
|
|
||||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
|
||||||
|
|
||||||
How Replication Works
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
One of the major design goals of the replication module is to avoid any duplication of the nontrivial logic.
|
|
||||||
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
|
||||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
|
||||||
|
|
||||||
The following steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
|
||||||
|
|
||||||
* Plan the replication:
|
|
||||||
|
|
||||||
* Compare sender and receiver filesystem snapshots
|
|
||||||
* Build the **replication plan**
|
|
||||||
|
|
||||||
* Per filesystem, compute a diff between sender and receiver snapshots
|
|
||||||
* Build a list of replication steps
|
|
||||||
|
|
||||||
* If possible, use incremental sends (``zfs send -i``)
|
|
||||||
* Otherwise, use full send of most recent snapshot on sender
|
|
||||||
* Give up on filesystems that cannot be replicated without data loss
|
|
||||||
|
|
||||||
* Retry on errors that are likely temporary (i.e. network failures).
|
|
||||||
* Give up on filesystems where a permanent error was received over RPC.
|
|
||||||
|
|
||||||
* Execute the plan
|
|
||||||
|
|
||||||
* Perform replication steps in the following order:
|
|
||||||
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
|
||||||
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
|
||||||
* After a successful replication step, update the replication cursor bookmark (see below).
|
|
||||||
|
|
||||||
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
|
||||||
|
|
||||||
.. _replication-cursor-bookmark:
|
|
||||||
|
|
||||||
The **replication cursor bookmark** ``#zrepl_replication_cursor`` is kept per filesystem on the sending side of a replication setup:
|
|
||||||
It is a bookmark of the most recent successfully replicated snapshot to the receiving side.
|
|
||||||
It is is used by the :ref:`not_replicated <prune-keep-not-replicated>` keep rule to identify all snapshots that have not yet been replicated to the receiving side.
|
|
||||||
Regardless of whether that keep rule is used, the bookmark ensures that replication can always continue incrementally.
|
|
||||||
Note that there is only one cursor bookmark per filesystem, which prohibits multiple jobs to replicate the same filesystem (:ref:`see below<jobs-multiple-jobs>`).
|
|
||||||
|
|
||||||
.. _replication-placeholder-property:
|
|
||||||
|
|
||||||
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
|
|
||||||
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
|
|
||||||
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
|
||||||
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
|
||||||
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
|
||||||
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
|
||||||
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
|
||||||
|
|
||||||
.. ATTENTION::
|
|
||||||
|
|
||||||
Currently, zrepl does not replicate filesystem properties.
|
|
||||||
Whe receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
|
||||||
This is temporary and being worked on :issue:`24`.
|
|
||||||
|
|
||||||
|
|
||||||
.. _job-snapshotting-spec:
|
|
||||||
|
|
||||||
Taking Snaphots
|
|
||||||
---------------
|
|
||||||
|
|
||||||
The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
|
|
||||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
|
||||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
|
||||||
|
|
||||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- type: push
|
|
||||||
filesystems: {
|
|
||||||
"<": true,
|
|
||||||
"tmp": false
|
|
||||||
}
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
...
|
|
||||||
|
|
||||||
There is also a ``manual`` snapshotting type, which covers the following use cases:
|
|
||||||
|
|
||||||
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
|
||||||
* Run scripts before and after taking snapshots (like locking database tables).
|
|
||||||
We are working on better integration for this use case: see :issue:`74`.
|
|
||||||
* Handling snapshotting through a separate ``snap`` job.
|
|
||||||
|
|
||||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- type: push
|
|
||||||
filesystems: {
|
|
||||||
"<": true,
|
|
||||||
"tmp": false
|
|
||||||
}
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
...
|
|
||||||
|
|
||||||
.. _jobs-multiple-jobs:
|
|
||||||
|
|
||||||
Multiple Jobs & More than 2 Machines
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
.. ATTENTION::
|
|
||||||
|
|
||||||
When using multiple jobs across single or multiple machines, the following rules are critical to avoid race conditions & data loss:
|
|
||||||
|
|
||||||
1. The sets of ZFS filesystems matched by the ``filesystems`` filter fields must be disjoint across all jobs configured on a machine.
|
|
||||||
2. The ZFS filesystem subtrees of jobs with ``root_fs`` must be disjoint.
|
|
||||||
3. Across all zrepl instances on all machines in the replication domain, there must be a 1:1 correspondence between active and passive jobs.
|
|
||||||
|
|
||||||
Explanations & exceptions to above rules are detailed below.
|
|
||||||
|
|
||||||
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
|
||||||
|
|
||||||
No Overlapping
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jobs run independently of each other.
|
|
||||||
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
|
|
||||||
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
|
|
||||||
More subtle race conditions can occur with the :ref:`replication cursor bookmark <replication-cursor-bookmark>`, which currently only exists once per filesystem.
|
|
||||||
|
|
||||||
N push jobs to 1 sink
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The :ref:`sink job <job-sink>` namespaces by client identity.
|
|
||||||
It is thus safe to push to one sink job with different client identities.
|
|
||||||
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
|
|
||||||
|
|
||||||
N pull jobs from 1 source
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
|
|
||||||
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
|
|
||||||
|
|
||||||
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
|
|
||||||
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
|
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
.. _job-push:
|
.. _job-push:
|
||||||
|
|
||||||
@@ -232,15 +19,21 @@ Job Type ``push``
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``push``
|
- = ``push``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``connect``
|
* - ``connect``
|
||||||
- |connect-transport|
|
- |connect-transport|
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
- |pruning-spec|
|
- |pruning-spec|
|
||||||
|
* - ``replication``
|
||||||
|
- |replication-options|
|
||||||
|
* - ``conflict_resolution``
|
||||||
|
- |conflict-resolution-options|
|
||||||
|
|
||||||
Example config: :sampleconf:`/push.yml`
|
Example config: :sampleconf:`/push.yml`
|
||||||
|
|
||||||
@@ -258,7 +51,7 @@ Job Type ``sink``
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``sink``
|
- = ``sink``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``serve``
|
* - ``serve``
|
||||||
- |serve-transport|
|
- |serve-transport|
|
||||||
* - ``root_fs``
|
* - ``root_fs``
|
||||||
@@ -281,7 +74,7 @@ Job Type ``pull``
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``pull``
|
- = ``pull``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``connect``
|
* - ``connect``
|
||||||
- |connect-transport|
|
- |connect-transport|
|
||||||
* - ``root_fs``
|
* - ``root_fs``
|
||||||
@@ -292,6 +85,10 @@ 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`
|
||||||
|
|
||||||
@@ -309,11 +106,13 @@ Job Type ``source``
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``source``
|
- = ``source``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``serve``
|
* - ``serve``
|
||||||
- |serve-transport|
|
- |serve-transport|
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
- |snapshotting-spec|
|
- |snapshotting-spec|
|
||||||
|
|
||||||
@@ -346,7 +145,7 @@ Job type that only takes snapshots and performs pruning on the local machine.
|
|||||||
* - ``type``
|
* - ``type``
|
||||||
- = ``snap``
|
- = ``snap``
|
||||||
* - ``name``
|
* - ``name``
|
||||||
- unique name of the job
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
* - ``filesystems``
|
* - ``filesystems``
|
||||||
- |filter-spec| for filesystems to be snapshotted
|
- |filter-spec| for filesystems to be snapshotted
|
||||||
* - ``snapshotting``
|
* - ``snapshotting``
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ The latter is particularly useful in combination with log aggregation services.
|
|||||||
.. WARNING::
|
.. WARNING::
|
||||||
|
|
||||||
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
|
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
|
||||||
Note that TCP buffering in the kernel must first run full becfore messages are dropped.
|
Note that TCP buffering in the kernel must first run full before messages are dropped.
|
||||||
|
|
||||||
Make sure to always configure a ``stdout`` outlet as the special error outlet to be informed about problems
|
Make sure to always configure a ``stdout`` outlet as the special error outlet to be informed about problems
|
||||||
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
|
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
|
||||||
|
|||||||
+15
-31
@@ -1,3 +1,5 @@
|
|||||||
|
.. _miscellaneous:
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@@ -6,16 +8,16 @@ Miscellaneous
|
|||||||
Runtime Directories & UNIX Sockets
|
Runtime Directories & UNIX Sockets
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
zrepl daemon creates various UNIX sockets to allow communicating with it:
|
The zrepl daemon needs to open various UNIX sockets in a runtime directory:
|
||||||
|
|
||||||
* the :ref:`transport-ssh+stdinserver` transport connects to a socket named after ``client_identity`` parameter
|
* a ``control`` socket that the CLI commands use to interact with the daemon
|
||||||
* the ``control`` CLI subcommand connects to a defined control socket
|
* the :ref:`transport-ssh+stdinserver` listener opens one socket per configured client, named after ``client_identity`` parameter
|
||||||
|
|
||||||
There is no further authentication on these sockets.
|
There is no authentication on these sockets except the UNIX permissions.
|
||||||
Therefore we have to make sure they can only be created and accessed by ``zrepl daemon``.
|
The zrepl daemon will refuse to bind any of the above sockets in a directory that is world-accessible.
|
||||||
In fact, ``zrepl daemon`` will not bind a socket to a path in a directory that is world-accessible.
|
|
||||||
|
|
||||||
The directories can be configured in the main configuration file, the defaults are provided below:
|
The following sections of the ``global`` config shows the default paths.
|
||||||
|
The shell script below shows how the default runtime directory can be created.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -27,6 +29,12 @@ The directories can be configured in the main configuration file, the defaults a
|
|||||||
sockdir: /var/run/zrepl/stdinserver
|
sockdir: /var/run/zrepl/stdinserver
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p /var/run/zrepl/stdinserver
|
||||||
|
chmod -R 0700 /var/run/zrepl
|
||||||
|
|
||||||
|
|
||||||
Durations & Intervals
|
Durations & Intervals
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
@@ -36,27 +44,3 @@ 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.
|
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ Prometheus & Grafana
|
|||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9811`` or ``127.0.0.1:9811`` (port 9811 was reserved to zrepl `on the official list <https://github.com/prometheus/prometheus/wiki/Default-port-allocations/_compare/43e495dd251ee328ac0d08b58084665b5c0f7a7e...459195059b55b414193ebeb80c5ba463d2606951>`_).
|
||||||
The Prometheues monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
|
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
||||||
|
|
||||||
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
||||||
see :repomasterlink:`dist/grafana`.
|
see :repomasterlink:`dist/grafana`.
|
||||||
@@ -29,7 +30,8 @@ The dashboard also contains some advice on which metrics are important to monito
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9091'
|
listen: ':9811'
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
|
||||||
|
Overview & Terminology
|
||||||
|
======================
|
||||||
|
|
||||||
|
All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup.
|
||||||
|
The following paths are searched, in this order:
|
||||||
|
|
||||||
|
1. The path specified via the global ``--config`` flag
|
||||||
|
2. ``/etc/zrepl/zrepl.yml``
|
||||||
|
3. ``/usr/local/etc/zrepl/zrepl.yml``
|
||||||
|
|
||||||
|
``zrepl configcheck`` can be used to validate the configuration.
|
||||||
|
If the configuration is valid, it will output nothing and exit with code ``0``.
|
||||||
|
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
||||||
|
|
||||||
|
Full example configs are available at :ref:`quick-start guides <quickstart-toc>` and :sampleconf:`/`.
|
||||||
|
However, copy-pasting examples is no substitute for reading documentation!
|
||||||
|
|
||||||
|
Config File Structure
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
global: ...
|
||||||
|
jobs:
|
||||||
|
- name: backup
|
||||||
|
type: push
|
||||||
|
- ...
|
||||||
|
|
||||||
|
A zrepl configuration file is divided in to two main sections: ``global`` and ``jobs``.
|
||||||
|
``global`` has sensible defaults. It is covered in :ref:`logging <logging>`, :ref:`monitoring <monitoring>` \& :ref:`miscellaneous <miscellaneous>`.
|
||||||
|
|
||||||
|
|
||||||
|
``conf.d``: including config files
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
It is possible to distribute zrepl configurations over multiple YAML configuration
|
||||||
|
files. This is achieved by using the `include` key which can only exist in the main
|
||||||
|
configuration file.
|
||||||
|
|
||||||
|
The list of included paths must point to either individual YAML files or directories.
|
||||||
|
If the path points to a directory then all YAML files with a `.yml` extention in the
|
||||||
|
directory will be included.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
# /etc/zrepl/zrep.yml
|
||||||
|
global: ...
|
||||||
|
include:
|
||||||
|
- ./jobs.d/
|
||||||
|
- /opt/some_job.yml
|
||||||
|
|
||||||
|
# /etc/zrepl/jobs.d/backup.yml
|
||||||
|
jobs:
|
||||||
|
- name: backup
|
||||||
|
type: push
|
||||||
|
- ...
|
||||||
|
|
||||||
|
# /opt/some_job.yml
|
||||||
|
jobs:
|
||||||
|
- name: another_job
|
||||||
|
...
|
||||||
|
- ...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The paths are treated as absolute when starting with `/`, otherwise
|
||||||
|
as a relative path to the main config file's parent directory.
|
||||||
|
|
||||||
|
Job names must be unique across all included configuration files.
|
||||||
|
|
||||||
|
.. _job-overview:
|
||||||
|
|
||||||
|
Jobs \& How They Work Together
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
A *job* is the unit of activity tracked by the zrepl daemon.
|
||||||
|
The ``type`` of a job determines its role in a replication setup and in snapshot management.
|
||||||
|
Jobs are identified by their ``name``, both in log files and the ``zrepl status`` command.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`.
|
||||||
|
|
||||||
|
Replication always happens between a pair of jobs: one **active side** and one **passive side**.
|
||||||
|
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
||||||
|
The passive side responds to requests from the active side after checking its permissions.
|
||||||
|
|
||||||
|
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
|
||||||
|
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
|
||||||
|
|
||||||
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
|
| Setup name | active side | passive side | use case |
|
||||||
|
+=======================+==============+==================================+====================================================================================+
|
||||||
|
| Push mode | ``push`` | ``sink`` | * Laptop backup |
|
||||||
|
| | (snap) | | * NAS behind NAT to offsite |
|
||||||
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
|
| Pull mode | ``pull`` | ``source`` | * Central backup-server for many nodes |
|
||||||
|
| | | (snap) | * Remote server to NAS behind NAT |
|
||||||
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
|
| Local replication | | ``push`` + ``sink`` in one config | * Backup to :ref:`locally attached disk <quickstart-backup-to-external-disk>` |
|
||||||
|
| | | with :ref:`local transport <transport-local>` | * Backup FreeBSD boot pool |
|
||||||
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
|
| Snap & prune-only | ``snap`` | N/A | * | Snapshots & pruning but no replication |
|
||||||
|
| | (snap) | | | required |
|
||||||
|
| | | | * Workaround for :ref:`source-side pruning <prune-workaround-source-side-pruning>` |
|
||||||
|
+-----------------------+--------------+----------------------------------+------------------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
How the Active Side Works
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
|
||||||
|
|
||||||
|
1. Wakeup after snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
||||||
|
2. Connect to the passive side and instantiate an RPC client.
|
||||||
|
3. Replicate data from the sender to the receiver.
|
||||||
|
4. Prune on sender & receiver.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
The progress of the active side can be watched live using ``zrepl status``.
|
||||||
|
|
||||||
|
.. _overview-passive-side--client-identity:
|
||||||
|
|
||||||
|
How the Passive Side Works
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the active side,
|
||||||
|
on the :ref:`transport <transport>` specified with ``serve`` in the job configuration.
|
||||||
|
The respective transport then perfoms authentication & authorization, resulting in a stable *client identity*.
|
||||||
|
The passive side job uses this *client identity* as follows:
|
||||||
|
|
||||||
|
* In ``sink`` jobs, to map requests from different *client identities* to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
||||||
|
* *In the future, ``source`` might embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>`, to support multi-host replication.*
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
The use of the client identity in the ``sink`` job implies that it must be usable as a ZFS ZFS filesystem name component.
|
||||||
|
|
||||||
|
.. _overview-how-replication-works:
|
||||||
|
|
||||||
|
How Replication Works
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
One of the major design goals of the replication module is to avoid any duplication of the nontrivial logic.
|
||||||
|
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
||||||
|
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
||||||
|
|
||||||
|
The following high-level steps take place during replication and can be monitored using ``zrepl status``:
|
||||||
|
|
||||||
|
* Plan the replication:
|
||||||
|
|
||||||
|
* Compare sender and receiver filesystem snapshots
|
||||||
|
* Build the **replication plan**
|
||||||
|
|
||||||
|
* Per filesystem, compute a diff between sender and receiver snapshots
|
||||||
|
* Build a list of **replication steps**
|
||||||
|
|
||||||
|
* If possible, use incremental and resumable sends
|
||||||
|
* Otherwise, use full send of most recent snapshot on sender
|
||||||
|
|
||||||
|
* Retry on errors that are likely temporary (i.e. network failures).
|
||||||
|
* Give up on filesystems where a permanent error was received over RPC.
|
||||||
|
|
||||||
|
* Execute the plan
|
||||||
|
|
||||||
|
* Perform replication steps in the following order:
|
||||||
|
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
|
||||||
|
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
|
||||||
|
* Acquire send-side *step-holds* on the step's `from` and `to` snapshots.
|
||||||
|
* Perform the replication step.
|
||||||
|
* Move the **replication cursor** bookmark on the sending side (see below).
|
||||||
|
* Move the **last-received-hold** on the receiving side (see below).
|
||||||
|
* Release the send-side step-holds.
|
||||||
|
|
||||||
|
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
|
||||||
|
|
||||||
|
ZFS Background Knowledge
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem.
|
||||||
|
Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
|
||||||
|
|
||||||
|
**ZFS Send Modes & Bookmarks**
|
||||||
|
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
|
||||||
|
Full sends are used to create a new filesystem on the receiver with the send-side state of ``fs@to``.
|
||||||
|
Incremental sends only transfer the delta between ``@from`` and ``@to``.
|
||||||
|
Incremental sends require that ``@from`` be present on the receiving side when receiving the incremental stream.
|
||||||
|
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
|
||||||
|
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
|
||||||
|
|
||||||
|
.. _zfs-background-knowledge-plain-vs-raw-sends:
|
||||||
|
|
||||||
|
**Plain and raw sends**
|
||||||
|
By default, ``zfs send`` sends the most generic, backwards-compatible data stream format (so-called 'plain send').
|
||||||
|
If the sent uses newer features, e.g. compression or encryption, ``zfs send`` has to un-do these operations on the fly to produce the plain send stream.
|
||||||
|
If the receiver uses newer features (e.g. compression or encryption inherited from the parent FS), it applies the necessary transformations again on the fly during ``zfs recv``.
|
||||||
|
|
||||||
|
Flags such as ``-e``, ``-c`` and ``-L`` tell ZFS to produce a send stream that is closer to how the data is stored on disk.
|
||||||
|
Sending with those flags removes computational overhead from sender and receiver.
|
||||||
|
However, the receiver will not apply certain transformations, e.g., it will not compress with the receive-side ``compression`` algorithm.
|
||||||
|
|
||||||
|
The ``-w`` (``--raw``) flag produces a send stream that is as *raw* as possible.
|
||||||
|
For unencrypted datasets, its current effect is the same as ``-Lce``.
|
||||||
|
|
||||||
|
Encrypted datasets can only be sent plain (unencrypted) or raw (encrypted) using the ``-w`` flag.
|
||||||
|
|
||||||
|
**Resumable Send & Recv**
|
||||||
|
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
|
||||||
|
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
|
||||||
|
A full send can only be resumed if ``@to`` still exists.
|
||||||
|
An incremental send can only be resumed if ``@to`` still exists *and* either ``@from`` still exists *or* a bookmark ``#fbm`` of ``@from`` still exists.
|
||||||
|
|
||||||
|
**ZFS Holds**
|
||||||
|
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
|
||||||
|
Holds are created and referred to by a *tag*. They can be thought of as a named, persistent lock on the snapshot.
|
||||||
|
|
||||||
|
|
||||||
|
.. _zrepl-zfs-abstractions:
|
||||||
|
|
||||||
|
ZFS Abstractions Managed By zrepl
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
With the background knowledge from the previous paragraph, we now summarize the different on-disk ZFS objects that zrepl manages to provide its functionality.
|
||||||
|
|
||||||
|
.. _replication-placeholder-property:
|
||||||
|
|
||||||
|
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the ZFS property ``zrepl:placeholder=on``.
|
||||||
|
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
|
||||||
|
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
|
||||||
|
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
|
||||||
|
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
|
||||||
|
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
|
||||||
|
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
|
||||||
|
|
||||||
|
.. _replication-cursor-and-last-received-hold:
|
||||||
|
|
||||||
|
The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
|
||||||
|
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
|
||||||
|
and the last-received-hold is a hold of that snapshot on the receiving side.
|
||||||
|
Both are moved atomically after the receiving side has confirmed that a replication step is complete.
|
||||||
|
|
||||||
|
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
|
||||||
|
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
|
||||||
|
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
|
||||||
|
|
||||||
|
.. _tentative-replication-cursor-bookmarks:
|
||||||
|
|
||||||
|
**Tentative replication cursor bookmarks** are short-lived bookmarks that protect the atomic moving-forward of the replication cursor and last-received-hold (see :issue:`this issue <340>`).
|
||||||
|
They are only necessary if step holds are not used as per the :ref:`replication.protection <replication-option-protection>` setting.
|
||||||
|
The tentative replication cursor has the format ``#zrepl_CUSORTENTATIVE_G_<GUID>_J_<JOBNAME>``.
|
||||||
|
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||||
|
|
||||||
|
.. _step-holds:
|
||||||
|
|
||||||
|
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
|
||||||
|
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
|
||||||
|
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
|
||||||
|
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
|
||||||
|
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
|
||||||
|
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
|
||||||
|
A job only ever has one active send per filesystem.
|
||||||
|
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
|
||||||
|
|
||||||
|
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
|
||||||
|
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` where ``#bm`` is not managed by zrepl.
|
||||||
|
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
|
||||||
|
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
|
||||||
|
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
|
||||||
|
Subscribe to zrepl :issue:`326` for details.
|
||||||
|
|
||||||
|
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
More details can be found in the design document :repomasterlink:`replication/design.md`.
|
||||||
|
|
||||||
|
|
||||||
|
Caveats With Complex Setups (More Than 2 Jobs or Machines)
|
||||||
|
----------------------------------------------------------
|
||||||
|
|
||||||
|
Most users are served well with a single sender and a single receiver job.
|
||||||
|
This section documents considerations for more complex setups.
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
Before you continue, make sure you have a working understanding of :ref:`how zrepl works <overview-how-replication-works>`
|
||||||
|
and :ref:`what zrepl does to ensure <zrepl-zfs-abstractions>` that replication between sender and receiver is always
|
||||||
|
possible without conflicts.
|
||||||
|
This will help you understand why certain kinds of multi-machine setups do not (yet) work.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
If you can't find your desired configuration, have questions or would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
|
||||||
|
|
||||||
|
Multiple Jobs on One Machine
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
As a general rule, multiple jobs configured on one machine **must operate on disjoint sets of filesystems**.
|
||||||
|
Otherwise, concurrently running jobs might interfere when operating on the same filesystem.
|
||||||
|
|
||||||
|
On your setup, ensure that
|
||||||
|
|
||||||
|
* all ``filesystems`` filter specifications are disjoint
|
||||||
|
* no ``root_fs`` is a prefix or equal to another ``root_fs``
|
||||||
|
|
||||||
|
* For ``sink`` jobs, consider all possible ``root_fs/${client_identity}``.
|
||||||
|
|
||||||
|
* no ``filesystems`` filter matches any ``root_fs``
|
||||||
|
|
||||||
|
**Exceptions to the rule**:
|
||||||
|
|
||||||
|
* A ``snap`` and ``push`` job on the same machine can match the same ``filesystems``.
|
||||||
|
To avoid interference, only one of the jobs should be pruning snapshots on the sender, the other one should keep all snapshots.
|
||||||
|
Since the jobs won't coordinate, errors in the log are to be expected, but :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` ensure that ``push`` and ``sink`` can always replicate incrementally.
|
||||||
|
This scenario is detailed in one of the :ref:`quick-start guides <quickstart-backup-to-external-disk>`.
|
||||||
|
|
||||||
|
|
||||||
|
Two Or More Machines
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
This section might be relevant to users who wish to *fan-in* (N machines replicate to 1) or *fan-out* (replicate 1 machine to N machines).
|
||||||
|
|
||||||
|
**Working setups**:
|
||||||
|
|
||||||
|
* **Fan-in: N servers replicated to one receiver, disjoint dataset trees.**
|
||||||
|
|
||||||
|
* This is the common use case of a centralized backup server.
|
||||||
|
|
||||||
|
* Implementation:
|
||||||
|
|
||||||
|
* N ``push`` jobs (one per sender server), 1 ``sink`` (as long as the different push jobs have a different :ref:`client identity <overview-passive-side--client-identity>`)
|
||||||
|
* N ``source`` jobs (one per sender server), N ``pull`` on the receiver server (unique names, disjoint ``root_fs``)
|
||||||
|
|
||||||
|
* The ``sink`` job automatically constrains each client to a disjoint sub-tree of the sink-side dataset hierarchy ``${root_fs}/${client_identity}``.
|
||||||
|
Therefore, the different clients cannot interfere.
|
||||||
|
|
||||||
|
* The ``pull`` job only pulls from one host, so it's up to the zrepl user to ensure that the different ``pull`` jobs don't interfere.
|
||||||
|
|
||||||
|
.. _fan-out-replication:
|
||||||
|
|
||||||
|
* **Fan-out: 1 server replicated to N receivers**
|
||||||
|
|
||||||
|
* Can be implemented either in a pull or push fashion.
|
||||||
|
|
||||||
|
* **pull setup**: 1 ``pull`` job on each receiver server, each with a corresponding **unique** ``source`` job on the sender server.
|
||||||
|
* **push setup**: 1 ``sink`` job on each receiver server, each with a corresponding **unique** ``push`` job on the sender server.
|
||||||
|
|
||||||
|
* It is critical that we have one sending-side job (``source``, ``push``) per receiver.
|
||||||
|
The reason is that :ref:`zrepl's ZFS abstractions <zrepl-zfs-abstractions>` (``zrepl zfs-abstraction list``) include the name of the ``source``/``push`` job, but not the receive-side job name or client identity (see :issue:`380`).
|
||||||
|
As a counter-example, suppose we used multiple ``pull`` jobs with only one ``source`` job.
|
||||||
|
All ``pull`` jobs would share the same :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` and trip over each other, breaking incremental replication guarantees quickly.
|
||||||
|
The anlogous problem exists for 1 ``push`` to N ``sink`` jobs.
|
||||||
|
|
||||||
|
* The ``filesystems`` matched by the sending side jobs (``source``, ``push``) need not necessarily be disjoint.
|
||||||
|
For this to work, we need to avoid interference between snapshotting and pruning of the different sending jobs.
|
||||||
|
The solution is to centralize sender-side snapshot management in a separate ``snap`` job.
|
||||||
|
Snapshotting in the ``source``/``push`` job should then be disabled (``type: manual``).
|
||||||
|
And sender-side pruning (``keep_sender``) needs to be disabled in the active side (``pull`` / ``push``), since that'll be done by the ``snap job``.
|
||||||
|
|
||||||
|
* **Restore limitations**: when restoring from one of the ``pull`` targets (e.g., using ``zfs send -R``), the replication cursor bookmarks don't exist on the restored system.
|
||||||
|
This can break incremental replication to all other receive-sides after restore.
|
||||||
|
|
||||||
|
* See :ref:`the fan-out replication quick-start guide <quickstart-fan-out-replication>` for an example of this setup.
|
||||||
|
|
||||||
|
|
||||||
|
**Setups that do not work**:
|
||||||
|
|
||||||
|
* N ``pull`` identities, 1 ``source`` job. Tracking :issue:`380`.
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
.. _configuration_preface:
|
|
||||||
|
|
||||||
=======
|
|
||||||
Preface
|
|
||||||
=======
|
|
||||||
|
|
||||||
-----------------------
|
|
||||||
Configuration File Path
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
zrepl searches for its main configuration file in the following locations (in that order):
|
|
||||||
|
|
||||||
* If set, the location specified via the global ``--config`` flag
|
|
||||||
* ``/etc/zrepl/zrepl.yml``
|
|
||||||
* ``/usr/local/etc/zrepl/zrepl.yml``
|
|
||||||
|
|
||||||
The examples in the :ref:`tutorial` or the :sampleconf:`/` directory should provide a good starting point.
|
|
||||||
|
|
||||||
-------------------
|
|
||||||
Runtime Directories
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
zrepl requires runtime directories for various UNIX sockets --- they are documented in the :ref:`config file<conf-runtime-directories>`.
|
|
||||||
Your package maintainer / init script should take care of creating them.
|
|
||||||
Alternatively, for default settings, the following should to the trick.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
mkdir -p /var/run/zrepl/stdinserver
|
|
||||||
chmod -R 0700 /var/run/zrepl
|
|
||||||
|
|
||||||
|
|
||||||
----------
|
|
||||||
Validating
|
|
||||||
----------
|
|
||||||
|
|
||||||
The config can be validated using the ``zrepl configcheck`` subcommand.
|
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ Pruning Policies
|
|||||||
================
|
================
|
||||||
|
|
||||||
In zrepl, *pruning* means *destroying snapshots*.
|
In zrepl, *pruning* means *destroying snapshots*.
|
||||||
Pruning must happen on both sides of a replication or the systems would inevitable run out of disk space at some point.
|
Pruning must happen on both sides of a replication or the systems would inevitably run out of disk space at some point.
|
||||||
|
|
||||||
Typically, the requirements to temporal resolution and maximum retention time differ per side.
|
Typically, the requirements to temporal resolution and maximum retention time differ per side.
|
||||||
For example, when using zrepl to back up a busy database server, you will want high temporal resolution (snapshots every 10 min) for the last 24h in case of administrative disasters, but cannot afford to store them for much longer because you might have high turnover volume in the database.
|
For example, when using zrepl to back up a busy database server, you will want high temporal resolution (snapshots every 10 min) for the last 24h in case of administrative disasters, but cannot afford to store them for much longer because you might have high turnover volume in the database.
|
||||||
@@ -46,8 +46,11 @@ Example Configuration:
|
|||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
# manually created snapshots will be kept forever on receiver
|
# manually created snapshots will be kept forever on receiver
|
||||||
|
- type: regex
|
||||||
|
regex: "^manual_.*"
|
||||||
|
|
||||||
.. DANGER::
|
.. DANGER::
|
||||||
|
|
||||||
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
You might have **existing snapshots** of filesystems affected by pruning which you want to keep, i.e. not be destroyed by zrepl.
|
||||||
Make sure to actually add the necessary ``regex`` keep rules on both sides, like with ``manual`` in the example above.
|
Make sure to actually add the necessary ``regex`` keep rules on both sides, like with ``manual`` in the example above.
|
||||||
|
|
||||||
@@ -65,8 +68,17 @@ Policy ``not_replicated``
|
|||||||
...
|
...
|
||||||
|
|
||||||
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||||
It only makes sense to specify this rule on a sender (source or push job).
|
It only makes sense to specify this rule for the ``keep_sender``.
|
||||||
The state required to evaluate this rule is stored in the :ref:`replication cursor bookmark <replication-cursor-bookmark>` on the sending side.
|
The reason is that, by definition, all snapshots on the receiver have already been replicated to there from the sender.
|
||||||
|
To determine whether a sender-side snapshot has already been replicated, zrepl uses the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` which corresponds to the most recent successfully replicated snapshot.
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
If you use ``not_replicated`` in your pruning rules, make sure to :ref:`monitor <monitoring>` replication.
|
||||||
|
If your replication gets stuck then ``not_replicated`` causes snapshots to pile uip on the sender.
|
||||||
|
ZFS and especially the ``zfs`` management command are known to degrade in performance with a lot of snapshots.
|
||||||
|
Such degradation impacts zrepl, any other scripts, and human ability to manage your zpool.
|
||||||
|
|
||||||
|
|
||||||
.. _prune-keep-retention-grid:
|
.. _prune-keep-retention-grid:
|
||||||
|
|
||||||
@@ -82,35 +94,91 @@ Policy ``grid``
|
|||||||
- type: grid
|
- type: grid
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
│ │
|
│ │ │
|
||||||
└─ one hour interval
|
└─ 1 repetition of a one-hour interval with keep=all
|
||||||
│
|
│ │
|
||||||
└─ 24 adjacent one-hour intervals
|
└─ 24 repetitions of a one-hour interval with keep=1
|
||||||
|
│
|
||||||
|
└─ 6 repetitions of a 30-day interval with keep=1
|
||||||
...
|
...
|
||||||
|
|
||||||
The retention grid can be thought of as a time-based sieve:
|
The retention grid can be thought of as a time-based sieve that thins out snapshots as they get older.
|
||||||
The ``grid`` field specifies a list of adjacent time intervals:
|
|
||||||
the left edge of the leftmost (first) interval is the ``creation`` date of the youngest snapshot.
|
|
||||||
All intervals to its right describe time intervals further in the past.
|
|
||||||
|
|
||||||
Each interval carries a maximum number of snapshots to keep.
|
|
||||||
It is specified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
|
|
||||||
The default value is **keep=1**.
|
|
||||||
|
|
||||||
|
The ``grid`` field specifies a list of adjacent time intervals.
|
||||||
|
Each interval is a bucket with a maximum capacity of ``keep`` snapshots.
|
||||||
The following procedure happens during pruning:
|
The following procedure happens during pruning:
|
||||||
|
|
||||||
#. The list of snapshots is filtered by the regular expression in ``regex``.
|
#. The list of snapshots is filtered by the regular expression in ``regex``.
|
||||||
Only snapshots names that match the regex are considered for this rule, all others are not affected.
|
Only snapshots names that match the regex are considered for this rule, all others will be pruned unless another rule keeps them.
|
||||||
#. The filtered list of snapshots is sorted by ``creation``
|
#. The snapshots that match ``regex`` are placed onto a time axis according to their ``creation`` date.
|
||||||
#. The left edge of the first interval is aligned to the ``creation`` date of the youngest snapshot
|
The youngest snapshot is on the left, the oldest on the right.
|
||||||
#. A list of buckets is created, one for each interval
|
#. The first buckets are placed "under" that axis so that the ``grid`` spec's first bucket's left edge aligns with youngest snapshot.
|
||||||
#. The list of snapshots is split up into the buckets.
|
#. All subsequent buckets are placed adjacent to their predecessor bucket.
|
||||||
#. For each bucket
|
#. Now each snapshot on the axis either falls into one bucket or it is older than our rightmost bucket.
|
||||||
|
Buckets are left-inclusive and right-exclusive which means that a snapshot on the edge of bucket will always 'fall into the right one'.
|
||||||
|
#. Snapshots older than the rightmost bucket are **not kept** by the grid specification.
|
||||||
|
#. For each bucket, we only keep the ``keep`` oldest snapshots.
|
||||||
|
|
||||||
#. the contained snapshot list is sorted by creation.
|
The syntax to describe the bucket list is as follows:
|
||||||
#. snapshots from the list, oldest first, are destroyed until the specified ``keep`` count is reached.
|
|
||||||
#. all remaining snapshots on the list are kept.
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Repeat x Duration (keep=all)
|
||||||
|
|
||||||
|
* The **duration** specifies the length of the interval.
|
||||||
|
* The **keep** count specifies the number of snapshots that fit into the bucket.
|
||||||
|
It can be either a positive integer or ``all`` (all snapshots are kept).
|
||||||
|
* The **repeat** count repeats the bucket definition for the specified number of times.
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Assume the following grid specification:
|
||||||
|
|
||||||
|
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||||
|
|
||||||
|
This grid specification produces the following constellation of buckets:
|
||||||
|
|
||||||
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Now assume that we have a set of snapshots @a, @b, ..., @D.
|
||||||
|
Snapshot @a is the most recent snapshot.
|
||||||
|
Snapshot @D is the oldest snapshot, it is almost 9 hours older than snapshot @a.
|
||||||
|
We place the snapshots on the same timeline as the buckets:
|
||||||
|
|
||||||
|
|
||||||
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||||
|
| | | | |
|
||||||
|
| a b c | d e f g h i j k l m n o p |q r s t u v w x y z |A B C D
|
||||||
|
|
||||||
|
We obtain the following mapping of snapshots to buckets:
|
||||||
|
|
||||||
|
Bucket1: a,b,c
|
||||||
|
Bucket2: d,e,f,g,h,i
|
||||||
|
Bucket3: j,k,l,m,n,o,p
|
||||||
|
Bucket4: q,r,s,t,u,v,w,x,y,z
|
||||||
|
No bucket: A,B,C,D
|
||||||
|
|
||||||
|
For each bucket, we now prune snapshots until it only contains `keep` snapshots.
|
||||||
|
Newer snapshots are destroyed first.
|
||||||
|
Snapshots that do not fall into a bucket are always destroyed.
|
||||||
|
|
||||||
|
Result after pruning:
|
||||||
|
|
||||||
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
|
| | | | | | | | | |
|
||||||
|
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
||||||
|
| | | | |
|
||||||
|
| a b c | i | p | z |
|
||||||
|
|
||||||
.. _prune-keep-last-n:
|
.. _prune-keep-last-n:
|
||||||
|
|
||||||
@@ -125,9 +193,11 @@ Policy ``last_n``
|
|||||||
keep_receiver:
|
keep_receiver:
|
||||||
- type: last_n
|
- type: last_n
|
||||||
count: 10
|
count: 10
|
||||||
|
regex: ^zrepl_.*$ # optional
|
||||||
...
|
...
|
||||||
|
|
||||||
``last_n`` keeps the last ``count`` snapshots (last = youngest = most recent creation date).
|
``last_n`` filters the snapshot list by ``regex``, then keeps the last ``count`` snapshots in that list (last = youngest = most recent creation date)
|
||||||
|
All snapshots that don't match ``regex`` or exceed ``count`` in the filtered list are destroyed unless matched by other rules.
|
||||||
|
|
||||||
.. _prune-keep-regex:
|
.. _prune-keep-regex:
|
||||||
|
|
||||||
@@ -154,7 +224,7 @@ Policy ``regex``
|
|||||||
negate: true
|
negate: true
|
||||||
regex: "^zrepl_.*"
|
regex: "^zrepl_.*"
|
||||||
|
|
||||||
``regex`` keeps all snapshots whose names are matched by the regular expressionin ``regex``.
|
``regex`` keeps all snapshots whose names are matched by the regular expression in ``regex``.
|
||||||
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
|
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
|
||||||
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _replication-options:
|
||||||
|
|
||||||
|
Replication Options
|
||||||
|
===================
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
filesystems: ...
|
||||||
|
replication:
|
||||||
|
protection:
|
||||||
|
initial: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
||||||
|
incremental: guarantee_resumability # guarantee_{resumability,incremental,nothing}
|
||||||
|
concurrency:
|
||||||
|
size_estimates: 4
|
||||||
|
steps: 1
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
.. _replication-option-protection:
|
||||||
|
|
||||||
|
``protection`` option
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The ``protection`` variable controls the degree to which a replicated filesystem is protected from getting out of sync through a zrepl pruner or external tools that destroy snapshots.
|
||||||
|
zrepl can guarantee :ref:`resumability <step-holds>` or just :ref:`incremental replication <replication-cursor-and-last-received-hold>`.
|
||||||
|
|
||||||
|
``guarantee_resumability`` is the **default** value and guarantees that a replication step is always resumable and that incremental replication will always be possible.
|
||||||
|
The implementation uses replication cursors, last-received-hold and step holds.
|
||||||
|
|
||||||
|
``guarantee_incremental`` only guarantees that incremental replication will always be possible.
|
||||||
|
If a step ``from -> to`` is interrupted and its `to` snapshot is destroyed, zrepl will remove the half-received ``to``'s resume state and start a new step ``from -> to2``.
|
||||||
|
The implementation uses replication cursors, tentative replication cursors and last-received-hold.
|
||||||
|
|
||||||
|
``guarantee_nothing`` does not make any guarantees with regards to keeping sending and receiving side in sync.
|
||||||
|
No bookmarks or holds are created to protect sender and receiver from diverging.
|
||||||
|
|
||||||
|
**Tradeoffs**
|
||||||
|
|
||||||
|
Using ``guarantee_incremental`` instead of ``guarantee_resumability`` obviously removes the resumability guarantee.
|
||||||
|
This means that replication progress is no longer monotonic which might lead to a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages).
|
||||||
|
However, the advantage and :issue:`reason for existence <288>` of the ``incremental`` mode is that it allows the pruner to delete snapshots of interrupted replication steps
|
||||||
|
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
When changing this flag, obsoleted zrepl-managed bookmarks and holds will be destroyed on the next replication step that is attempted for each filesystem.
|
||||||
|
|
||||||
|
|
||||||
|
.. _replication-option-concurrency:
|
||||||
|
|
||||||
|
``concurrency`` option
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The ``concurrency`` options control the maximum amount of concurrency during replication.
|
||||||
|
The default values allow some concurrency during size estimation but no parallelism for the actual replication.
|
||||||
|
|
||||||
|
* ``concurrency.steps`` (default = 1) controls the maximum number of concurrently executed :ref:`replication steps <overview-how-replication-works>`.
|
||||||
|
The planning step for each file system is counted as a single step.
|
||||||
|
* ``concurrency.size_estimates`` (default = 4) controls the maximum number of concurrent step size estimations done by the job.
|
||||||
|
|
||||||
|
Note that initial replication cannot start replicating child filesystems before the parent filesystem's initial replication step has completed.
|
||||||
|
|
||||||
|
Some notes on tuning these values:
|
||||||
|
|
||||||
|
* Disk: Size estimation is less I/O intensive than step execution because it does not need to access the data blocks.
|
||||||
|
* CPU: Size estimation is usually a dense CPU burst whereas step execution CPU utilization is stretched out over time because of disk IO.
|
||||||
|
Faster disks, sending a compressed dataset in :ref:`plain mode <zfs-background-knowledge-plain-vs-raw-sends>` and the zrepl transport mode all contribute to higher CPU requirements.
|
||||||
|
* Network bandwidth: Size estimation does not consume meaningful amounts of bandwidth, step execution does.
|
||||||
|
* :ref:`zrepl ZFS abstractions <zrepl-zfs-abstractions>`: for each replication step zrepl needs to update its ZFS abstractions through the ``zfs`` command which often waits multiple seconds for the zpool to sync.
|
||||||
|
Thus, if the actual send & recv time of a step is small compared to the time spent on zrepl ZFS abstractions then increasing step execution concurrency will result in a lower overall turnaround time.
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
|
||||||
|
Send & Recv Options
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. _job-send-options:
|
||||||
|
|
||||||
|
Send Options
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
filesystems: ...
|
||||||
|
send:
|
||||||
|
# flags from the table below go here
|
||||||
|
...
|
||||||
|
|
||||||
|
The following table specifies the list of (boolean) options.
|
||||||
|
Flags with an entry in the ``zfs send`` column map directly to the zfs send CLI flags.
|
||||||
|
zrepl does not perform feature checks for these flags.
|
||||||
|
If you enable a flag that is not supported by the installed version of ZFS, the zfs error will show up at runtime in the logs and zrepl status.
|
||||||
|
See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-send.8.html>`_ (``man zfs-send``) for their semantics.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 10 70
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - ``send.``
|
||||||
|
- ``zfs send``
|
||||||
|
- Comment
|
||||||
|
* - ``encrypted``
|
||||||
|
-
|
||||||
|
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
||||||
|
* - ``bandwidth_limit``
|
||||||
|
-
|
||||||
|
- Specific to zrepl, :ref:`see below <job-send-recv-options--bandwidth-limit>`.
|
||||||
|
* - ``raw``
|
||||||
|
- ``-w``
|
||||||
|
- Use ``encrypted`` to only allow encrypted sends. Mixed sends are not supported.
|
||||||
|
* - ``send_properties``
|
||||||
|
- ``-p``
|
||||||
|
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
|
* - ``backup_properties``
|
||||||
|
- ``-b``
|
||||||
|
- **Be careful**, read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
|
* - ``large_blocks``
|
||||||
|
- ``-L``
|
||||||
|
- **Potential data loss on OpenZFS < 2.0**, see :ref:`warning below <job-send-options-large-blocks>`.
|
||||||
|
* - ``compressed``
|
||||||
|
- ``-c``
|
||||||
|
-
|
||||||
|
* - ``embedded_data``
|
||||||
|
- ``-e``
|
||||||
|
-
|
||||||
|
* - ``saved``
|
||||||
|
- ``-S``
|
||||||
|
-
|
||||||
|
|
||||||
|
.. _job-send-options-encrypted:
|
||||||
|
|
||||||
|
``encrypted``
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The ``encrypted`` option controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
|
||||||
|
More specifically, if ``encrypted=true``, zrepl
|
||||||
|
|
||||||
|
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
|
||||||
|
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
|
||||||
|
* expects the receiving side to support OpenZFS native encryption (recv will fail otherwise)
|
||||||
|
|
||||||
|
Filesystems matched by ``filesystems`` that are not encrypted are not sent and will cause error log messages.
|
||||||
|
|
||||||
|
If ``encrypted=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
Use ``encrypted`` instead of ``raw`` to make your intent clear that zrepl must only replicate filesystems that are actually encrypted by OpenZFS native encryption.
|
||||||
|
It is meant as a safeguard to prevent unintended sends of unencrypted filesystems in raw mode.
|
||||||
|
|
||||||
|
.. _job-send-options-properties:
|
||||||
|
|
||||||
|
``send_properties``
|
||||||
|
-------------------
|
||||||
|
Sends the dataset properties along with snapshots.
|
||||||
|
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
|
|
||||||
|
.. _job-send-options-backup-properties:
|
||||||
|
|
||||||
|
``backup_properties``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
When properties are modified on a filesystem that was received from a send stream with ``send.properties=true``, ZFS archives the original received value internally.
|
||||||
|
This also applies to :ref:`inheriting or overriding properties during zfs receive <job-recv-options--inherit-and-override>`.
|
||||||
|
|
||||||
|
When sending those received filesystems another hop, the ``backup_properties`` flag instructs ZFS to send the original property values rather than the current locally set values.
|
||||||
|
|
||||||
|
This is useful for replicating properties across multiple levels of backup machines.
|
||||||
|
**Example:**
|
||||||
|
Suppose we want to flow snapshots from Machine A to B, then from B to C.
|
||||||
|
A will enable the :ref:`properties send option <job-send-options-properties>`.
|
||||||
|
B will want to override :ref:`critical properties such as mountpoint or canmount <job-note-property-replication>`.
|
||||||
|
But the job that replicates from B to C should be sending the original property values received from A.
|
||||||
|
Thus, B sets the ``backup_properties`` option.
|
||||||
|
|
||||||
|
Please be careful with this option and read the :ref:`note on property replication below <job-note-property-replication>`.
|
||||||
|
|
||||||
|
.. _job-send-options-large-blocks:
|
||||||
|
|
||||||
|
``large_blocks``
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This flag should not be changed after initial replication.
|
||||||
|
Prior to `OpenZFS commit 7bcb7f08 <https://github.com/openzfs/zfs/pull/10383/files#diff-4c1e47568f46fb63546e984943b09a3e6b051e2242649523f7835bbdfe2a9110R337-R342>`_
|
||||||
|
it was possible to change this setting which resulted in **data loss on the receiver**.
|
||||||
|
The commit in question is included in OpenZFS 2.0 and works around the problem by prohibiting receives of incremental streams with a flipped setting.
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
This bug has **not been fixed in the OpenZFS 0.8 releases** which means that changing this flag after initial replication might cause **data loss** on the receiver.
|
||||||
|
|
||||||
|
.. _job-recv-options:
|
||||||
|
|
||||||
|
Recv Options
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:ref:`Sink<job-sink>` and :ref:`pull<job-pull>` jobs have an optional ``recv`` configuration section:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: pull
|
||||||
|
recv:
|
||||||
|
properties:
|
||||||
|
inherit:
|
||||||
|
- "mountpoint"
|
||||||
|
override: {
|
||||||
|
"org.openzfs.systemd:ignore": "on"
|
||||||
|
}
|
||||||
|
bandwidth_limit: ...
|
||||||
|
placeholder:
|
||||||
|
encryption: unspecified | off | inherit
|
||||||
|
...
|
||||||
|
|
||||||
|
Jump to
|
||||||
|
:ref:`properties <job-recv-options--inherit-and-override>` ,
|
||||||
|
:ref:`bandwidth_limit <job-send-recv-options--bandwidth-limit>` , and
|
||||||
|
:ref:`placeholder <job-recv-options--placeholder>`.
|
||||||
|
|
||||||
|
.. _job-recv-options--inherit-and-override:
|
||||||
|
|
||||||
|
``properties``
|
||||||
|
--------------
|
||||||
|
|
||||||
|
``override`` maps directly to the `zfs recv -o flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
|
||||||
|
Property name-value pairs specified in this map will apply to all received filesystems, regardless of whether the send stream contains properties or not.
|
||||||
|
|
||||||
|
``inherit`` maps directly to the `zfs recv -x flag <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_.
|
||||||
|
Property names specified in this list will be inherited from the receiving side's parent filesystem (e.g. ``root_fs``).
|
||||||
|
|
||||||
|
With both options, the sending side's property value is still stored on the receiver, but the local override or inherit is the one that takes effect.
|
||||||
|
You can send the original properties from the first receiver to another receiver using :ref:`send.backup_properties<job-send-options-backup-properties>`.
|
||||||
|
|
||||||
|
|
||||||
|
.. _job-note-property-replication:
|
||||||
|
|
||||||
|
A Note on Property Replication
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If a send stream contains properties, as per ``send.send_properties`` or ``send.backup_properties``,
|
||||||
|
the default ZFS behavior is to use those properties on the receiving side, verbatim.
|
||||||
|
|
||||||
|
In many use cases for zrepl, this can have devastating consequences.
|
||||||
|
For example, when backing up a filesystem that has ``mountpoint=/`` to a storage server,
|
||||||
|
that storage server's root filesystem will be shadowed by the received file system on some platforms.
|
||||||
|
Also, many scripts and tools use ZFS user properties for configuration and do not check the property source (``local`` vs. ``received``).
|
||||||
|
If they are installed on the receiving side as well as the sending side, property replication could have unintended effects.
|
||||||
|
|
||||||
|
**zrepl currently does not provide any automatic safe-guards for property replication:**
|
||||||
|
|
||||||
|
* Make sure to read the entire man page on zfs recv (`man zfs recv <https://openzfs.github.io/openzfs-docs/man/8/zfs-recv.8.html>`_) before enabling this feature.
|
||||||
|
* Use ``recv.properties.override`` whenever possible, e.g. for ``mountpoint=none`` or ``canmount=off``.
|
||||||
|
* Use ``recv.properties.inherit`` if that makes more sense to you.
|
||||||
|
|
||||||
|
Below is an **non-exhaustive list of problematic properties**.
|
||||||
|
Please open a pull request if you find a property that is missing from this list.
|
||||||
|
(Both with regards to core ZFS tools and other software in the broader ecosystem.)
|
||||||
|
|
||||||
|
Mount behaviour
|
||||||
|
---------------
|
||||||
|
|
||||||
|
* ``mountpoint``
|
||||||
|
* ``canmount``
|
||||||
|
* ``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``.
|
||||||
|
If you are on such an older version, consider creating separate zrepl jobs for your ZVOL and filesystem datasets.
|
||||||
|
|
||||||
|
Systemd
|
||||||
|
-------
|
||||||
|
|
||||||
|
With systemd, you should also consider the properties processed by the `zfs-mount-generator <https://manpages.debian.org/buster-backports/zfsutils-linux/zfs-mount-generator.8.en.html>`_ .
|
||||||
|
|
||||||
|
Most notably:
|
||||||
|
|
||||||
|
* ``org.openzfs.systemd:ignore``
|
||||||
|
* ``org.openzfs.systemd:wanted-by``
|
||||||
|
* ``org.openzfs.systemd:required-by``
|
||||||
|
|
||||||
|
Encryption
|
||||||
|
----------
|
||||||
|
|
||||||
|
If the sender filesystems are encrypted but the sender does :ref:`plain sends <zfs-background-knowledge-plain-vs-raw-sends>`
|
||||||
|
and property replication is enabled, the receiver must :ref:`inherit the following properties<job-recv-options--inherit-and-override>`:
|
||||||
|
|
||||||
|
* ``keylocation``
|
||||||
|
* ``keyformat``
|
||||||
|
* ``encryption``
|
||||||
|
|
||||||
|
Sharing
|
||||||
|
-------
|
||||||
|
|
||||||
|
You may not want the replicated filesystem shared in the same way as the source is.
|
||||||
|
|
||||||
|
* ``sharenfs``
|
||||||
|
* ``sharesmb``
|
||||||
|
|
||||||
|
.. _job-recv-options--placeholder:
|
||||||
|
|
||||||
|
Placeholders
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
placeholder:
|
||||||
|
encryption: unspecified | off | inherit
|
||||||
|
|
||||||
|
During replication, zrepl :ref:`creates placeholder datasets <replication-placeholder-property>` on the receiving side if the sending side's ``filesystems`` filter creates gaps in the dataset hierarchy.
|
||||||
|
This is generally fully transparent to the user.
|
||||||
|
However, with OpenZFS Native Encryption, placeholders require zrepl user attention.
|
||||||
|
Specifically, the problem is that, when zrepl attempts to create the placeholder dataset on the receiver, and that placeholder's parent dataset is encrypted, ZFS wants to inherit encryption to the placeholder.
|
||||||
|
This is relevant to two use cases that zrepl supports:
|
||||||
|
|
||||||
|
1. **encrypted-send-to-untrusted-receiver** In this use case, the sender sends an :ref:`encrypted send stream <job-send-options-encrypted>` and the receiver doesn't have the key loaded.
|
||||||
|
2. **send-plain-encrypt-on-receive** The receive-side ``root_fs`` dataset is encrypted, and the senders are unencrypted.
|
||||||
|
The key of ``root_fs`` is loaded, and the goal is that the plain sends (e.g., from production) are encrypted on-the-fly during receive, with ``root_fs``'s key.
|
||||||
|
|
||||||
|
For **encrypted-send-to-untrusted-receiver**, the placeholder datasets need to be created with ``-o encryption=off``.
|
||||||
|
Without it, creation would fail with an error, indicating that the placeholder's parent dataset's key needs to be loaded.
|
||||||
|
But we don't trust the receiver, so we can't expect that to ever happen.
|
||||||
|
|
||||||
|
However, for **send-plain-encrypt-on-receive**, we cannot set ``-o encryption=off``.
|
||||||
|
The reason is that if we did, any of the (non-placeholder) child datasets below the placeholder would inherit ``encryption=off``, thereby silently breaking our encrypt-on-receive use case.
|
||||||
|
So, to cover this use case, we need to create placeholders without specifying ``-o encryption``.
|
||||||
|
This will make ``zfs create`` inherit the encryption mode from the parent dataset, and thereby transitively from ``root_fs``.
|
||||||
|
|
||||||
|
The zrepl config provides the `recv.placeholder.encryption` knob to control this behavior.
|
||||||
|
In ``undefined`` mode (default), placeholder creation bails out and asks the user to configure a behavior.
|
||||||
|
In ``off`` mode, the placeholder is created with ``encryption=off``, i.e., **encrypted-send-to-untrusted-rceiver** use case.
|
||||||
|
In ``inherit`` mode, the placeholder is created without specifying ``-o encryption`` at all, i.e., the **send-plain-encrypt-on-receive** use case.
|
||||||
|
|
||||||
|
|
||||||
|
Common Options
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. _job-send-recv-options--bandwidth-limit:
|
||||||
|
|
||||||
|
Bandwidth Limit (send & recv)
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
bandwidth_limit:
|
||||||
|
max: 23.5 MiB # -1 is the default and disabled rate limiting
|
||||||
|
bucket_capacity: # token bucket capacity in bytes; defaults to 128KiB
|
||||||
|
|
||||||
|
Both ``send`` and ``recv`` can be limited to a maximum bandwidth through ``bandwidth_limit``.
|
||||||
|
For most users, it should be sufficient to just set ``bandwidth_limit.max``.
|
||||||
|
The ``bandwidth_limit.bucket_capacity`` refers to the `token bucket size <https://github.com/juju/ratelimit>`_.
|
||||||
|
|
||||||
|
The bandwidth limit only applies to the payload data, i.e., the ZFS send stream.
|
||||||
|
It does not account for transport protocol overheads.
|
||||||
|
The scope is the job level, i.e., all :ref:`concurrent <replication-option-concurrency>` sends or incoming receives of a job share the bandwidth limit.
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _job-snapshotting-spec:
|
||||||
|
|
||||||
|
Taking Snaphots
|
||||||
|
===============
|
||||||
|
|
||||||
|
You can configure zrepl to take snapshots of the filesystems in the ``filesystems`` field specified in ``push``, ``source`` and ``snap`` jobs.
|
||||||
|
|
||||||
|
The following snapshotting types are supported:
|
||||||
|
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 70
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - ``snapshotting.type``
|
||||||
|
- Comment
|
||||||
|
* - ``periodic``
|
||||||
|
- Ensure that snapshots are taken at a particular interval.
|
||||||
|
* - ``cron``
|
||||||
|
- Use cron spec to take snapshots at particular points in time.
|
||||||
|
* - ``manual``
|
||||||
|
- zrepl does not take any snapshots by itself.
|
||||||
|
|
||||||
|
The ``periodic`` and ``cron`` snapshotting types share some common options and behavior:
|
||||||
|
|
||||||
|
* **Naming:** The snapshot names are composed of a user-defined ``prefix`` followed by a UTC date formatted like ``20060102_150405_000`` by default.
|
||||||
|
We either use UTC or timestamp with timezone information because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||||
|
Note that if timestamp with time zone information is used, the "+" of the timezone (i.e. +02:00) is replaced by "_" (i.e. _02:00) to conform to allowed characters in ZFS snapshots.
|
||||||
|
* **Hooks:** You can configure hooks to run before or after zrepl takes the snapshots. See :ref:`below <job-snapshotting-hooks>` for details.
|
||||||
|
* **Push replication:** After creating all snapshots, the snapshotter will wake up the replication part of the job, if it's a ``push`` job.
|
||||||
|
Note that snapshotting is decoupled from replication, i.e., if it is down or takes too long, snapshots will still be taken.
|
||||||
|
Note further that other jobs are not woken up by snapshotting.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
There is **no concept of ownership** of the snapshots that are created by ``periodic`` or ``cron``.
|
||||||
|
Thus, there is no distinction between zrepl-created snapshots and user-created snapshots during replication or pruning.
|
||||||
|
|
||||||
|
In particular, pruning will take all snapshots into consideration by default.
|
||||||
|
To constrain pruning to just zrepl-created snapshots:
|
||||||
|
|
||||||
|
1. Assign a unique `prefix` to the snapshotter and
|
||||||
|
2. Use the ``regex`` functionality of the various pruning ``keep`` rules to just consider snapshots with that prefix.
|
||||||
|
|
||||||
|
There is currently no way to constrain replication to just zrepl-created snapshots.
|
||||||
|
Follow and comment at :issue:`403` if you need this functionality.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
The ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||||
|
|
||||||
|
``periodic`` Snapshotting
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- ...
|
||||||
|
filesystems: { ... }
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
prefix: zrepl_
|
||||||
|
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
|
||||||
|
# Specifies in which time zone the snapshot suffix is generated, optional, defaults to UTC, used only if time zone information is part of timestamp_format.
|
||||||
|
# Can be "UTC" (default), "Local" (time zone information from the OS) or any of the IANA Time Zone names (see https://nodatime.org/TimeZones)
|
||||||
|
timestamp_location: UTC
|
||||||
|
hooks: ...
|
||||||
|
pruning: ...
|
||||||
|
|
||||||
|
The ``periodic`` snapshotter ensures that snapshots are taken in the specified ``interval``.
|
||||||
|
If you use zrepl for backup, this translates into your recovery point objective (RPO).
|
||||||
|
To meet your RPO, you still need to monitor that replication, which happens asynchronously to snapshotting, actually works.
|
||||||
|
|
||||||
|
It is desirable to get all ``filesystems`` snapshotted simultaneously because it results in a more consistent backup.
|
||||||
|
To accomplish this while still maintaining the ``interval``, the ``periodic`` snapshotter attempts to get the snapshotting rhythms in sync.
|
||||||
|
To find that sync point, the most recent snapshot, created by the snapshotter, in any of the matched ``filesystems`` is used.
|
||||||
|
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||||
|
The snapshotter uses the ``prefix`` to identify which snapshots it created.
|
||||||
|
|
||||||
|
.. _job-snapshotting--cron:
|
||||||
|
|
||||||
|
``cron`` Snapshotting
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: snap
|
||||||
|
filesystems: { ... }
|
||||||
|
snapshotting:
|
||||||
|
type: cron
|
||||||
|
prefix: zrepl_
|
||||||
|
# (second, optional) minute hour day-of-month month day-of-week
|
||||||
|
# This example takes snapshots daily at 3:00.
|
||||||
|
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
|
||||||
|
# Specifies in which time zone the snapshot suffix is generated, optional, defaults to UTC, used only if time zone information is part of timestamp_format.
|
||||||
|
# Can be "UTC" (default), "Local" (time zone information from the OS) or any of the IANA Time Zone names (see https://nodatime.org/TimeZones)
|
||||||
|
timestamp_location: UTC
|
||||||
|
pruning: ...
|
||||||
|
|
||||||
|
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time.
|
||||||
|
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.
|
||||||
|
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
|
||||||
|
|
||||||
|
|
||||||
|
``manual`` Snapshotting
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
...
|
||||||
|
|
||||||
|
In ``manual`` mode, zrepl does not take snapshots by itself.
|
||||||
|
Manual snapshotting is most useful if you have existing infrastructure for snapshot management.
|
||||||
|
Or, if you want to decouple snapshot management from replication using a zrepl ``snap`` job.
|
||||||
|
See :ref:`this quickstart guide <quickstart-backup-to-external-disk>` for an example.
|
||||||
|
|
||||||
|
To trigger replication after taking snapshots, use the ``zrepl signal wakeup JOB`` command.
|
||||||
|
|
||||||
|
.. _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.
|
||||||
|
|
||||||
|
By default, the current time is converted to UTC and then formatted using the ``dense`` format.
|
||||||
|
|
||||||
|
Both time zone and format can be customized by setting ``timestamp_format`` and/or ``timestamp_location``.
|
||||||
|
|
||||||
|
``timestamp_location`` is fed verbatim into `time.LoadLocation <https://pkg.go.dev/time#LoadLocation>`_.
|
||||||
|
|
||||||
|
``timestamp_format`` supports the following values (case-insensitive).
|
||||||
|
|
||||||
|
* ``dense`` => Go format string ``20060102_150405_000``
|
||||||
|
* ``human`` => Go format string ``2006-01-02_15:04:05``
|
||||||
|
* ``iso-8601`` => Go format string ``2006-01-02T15:04:05.000Z07:00``
|
||||||
|
* ``unix-seconds`` => Unix seconds since the epoch, formatted as a decimal string (`time.Time.Unix <https://pkg.go.dev/time#Time.Unix>`_)
|
||||||
|
* Any custom Go time format accepted by `time.Time#Format <https://go.dev/src/time/format.go>`_.
|
||||||
|
|
||||||
|
The ``dense``, ``human``, and ``unix-seconds`` formats require the ``timestamp_location`` to be ``UTC`` because they do not contain timezone information.
|
||||||
|
|
||||||
|
The ``+`` character is `not allowed in ZFS snapshot names <https://github.com/openzfs/zfs/blob/1d3ba0bf01020f5459b1c28db3979129088924c0/module/zcommon/zfs_namecheck.c#L334-L351>`_.
|
||||||
|
Format strings are disallowed to contain the ``+`` character (it's not a character that is interpreted by ``time.Time.Format``, so there's no reason to use it).
|
||||||
|
If a format _produces_ a ``+``, that ``+`` is replaced by an `_`` character.
|
||||||
|
|
||||||
|
.. _job-snapshotting-hooks:
|
||||||
|
|
||||||
|
Pre- and Post-Snapshot Hooks
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Jobs with `periodic snapshots <job-snapshotting-spec_>`_ can run hooks before and/or after taking the snapshot specified in ``snapshotting.hooks``:
|
||||||
|
Hooks are called per filesystem before and after the snapshot is taken (pre- and post-edge).
|
||||||
|
Pre-edge invocations are in configuration order, post-edge invocations in reverse order, i.e. like a stack.
|
||||||
|
If a pre-snapshot invocation fails, ``err_is_fatal=true`` cuts off subsequent hooks, does not take a snapshot, and only invokes post-edges corresponding to previous successful pre-edges.
|
||||||
|
``err_is_fatal=false`` logs the failed pre-edge invocation but does not affect subsequent hooks nor snapshotting itself.
|
||||||
|
Post-edges are only invoked for hooks whose pre-edges ran without error.
|
||||||
|
Note that hook failures for one filesystem never affect other filesystems.
|
||||||
|
|
||||||
|
The optional ``timeout`` parameter specifies a period after which zrepl will kill the hook process and report an error.
|
||||||
|
The default is 30 seconds and may be specified in any units understood by `time.ParseDuration <https://golang.org/pkg/time/#ParseDuration>`_.
|
||||||
|
|
||||||
|
The optional ``filesystems`` filter which limits the filesystems the hook runs for. This uses the same |filter-spec| as jobs.
|
||||||
|
|
||||||
|
Most hook types take additional parameters, please refer to the respective subsections below.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 10 70
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Hook ``type``
|
||||||
|
- Details
|
||||||
|
- Description
|
||||||
|
* - ``command``
|
||||||
|
- :ref:`Details <job-hook-type-command>`
|
||||||
|
- Arbitrary pre- and post snapshot scripts.
|
||||||
|
* - ``postgres-checkpoint``
|
||||||
|
- :ref:`Details <job-hook-type-postgres-checkpoint>`
|
||||||
|
- Execute Postgres ``CHECKPOINT`` SQL command before snapshot.
|
||||||
|
* - ``mysql-lock-tables``
|
||||||
|
- :ref:`Details <job-hook-type-mysql-lock-tables>`
|
||||||
|
- Flush and read-Lock MySQL tables while taking the snapshot.
|
||||||
|
|
||||||
|
.. _job-hook-type-command:
|
||||||
|
|
||||||
|
``command`` Hooks
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
"tmp": false
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
prefix: zrepl_
|
||||||
|
interval: 10m
|
||||||
|
hooks:
|
||||||
|
- type: command
|
||||||
|
path: /etc/zrepl/hooks/zrepl-notify.sh
|
||||||
|
timeout: 30s
|
||||||
|
err_is_fatal: false
|
||||||
|
- type: command
|
||||||
|
path: /etc/zrepl/hooks/special-snapshot.sh
|
||||||
|
filesystems: {
|
||||||
|
"tank/special": true
|
||||||
|
}
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
``command`` hooks take a ``path`` to an executable script or binary to be executed before and after the snapshot.
|
||||||
|
``path`` must be absolute (e.g. ``/etc/zrepl/hooks/zrepl-notify.sh``).
|
||||||
|
No arguments may be specified; create a wrapper script if zrepl must call an executable that requires arguments.
|
||||||
|
The process standard output is logged at level INFO. Standard error is logged at level WARN.
|
||||||
|
The following environment variables are set:
|
||||||
|
|
||||||
|
* ``ZREPL_HOOKTYPE``: either "pre_snapshot" or "post_snapshot"
|
||||||
|
* ``ZREPL_FS``: the ZFS filesystem name being snapshotted
|
||||||
|
* ``ZREPL_SNAPNAME``: the zrepl-generated snapshot name (e.g. ``zrepl_20380119_031407_000``)
|
||||||
|
* ``ZREPL_DRYRUN``: set to ``"true"`` if a dry run is in progress so scripts can print, but not run, their commands
|
||||||
|
|
||||||
|
An empty template hook can be found in :sampleconf:`/hooks/template.sh`.
|
||||||
|
|
||||||
|
.. _job-hook-type-postgres-checkpoint:
|
||||||
|
|
||||||
|
``postgres-checkpoint`` Hook
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Connects to a Postgres server and executes the ``CHECKPOINT`` statement pre-snapshot.
|
||||||
|
Checkpointing applies the WAL contents to all data files and syncs the data files to disk.
|
||||||
|
This is not required for a consistent database backup: it merely forward-pays the "cost" of WAL replay to the time of snapshotting instead of at restore.
|
||||||
|
However, the Postgres manual recommends against checkpointing during normal operation.
|
||||||
|
Further, the operation requires Postgres superuser privileges.
|
||||||
|
zrepl users must decide on their own whether this hook is useful for them (it likely isn't).
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
Note that WALs and Postgres data directory (with all database data files) must be on the same filesystem to guarantee a correct point-in-time backup with the ZFS snapshot.
|
||||||
|
|
||||||
|
DSN syntax documented here: `<https://godoc.org/github.com/lib/pq>`_
|
||||||
|
|
||||||
|
.. code-block:: sql
|
||||||
|
|
||||||
|
CREATE USER zrepl_checkpoint PASSWORD yourpasswordhere;
|
||||||
|
ALTER ROLE zrepl_checkpoint SUPERUSER;
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
- type: postgres-checkpoint
|
||||||
|
dsn: "host=localhost port=5432 user=postgres password=yourpasswordhere sslmode=disable"
|
||||||
|
filesystems: {
|
||||||
|
"p1/postgres/data11": true
|
||||||
|
}
|
||||||
|
|
||||||
|
.. _job-hook-type-mysql-lock-tables:
|
||||||
|
|
||||||
|
``mysql-lock-tables`` Hook
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Connects to MySQL and executes
|
||||||
|
|
||||||
|
* pre-snapshot ``FLUSH TABLES WITH READ LOCK`` to lock all tables in all databases in the MySQL server we connect to (`docs <https://dev.mysql.com/doc/refman/8.0/en/flush.html#flush-tables-with-read-lock>`_)
|
||||||
|
* post-snapshot ``UNLOCK TABLES`` reverse above operation.
|
||||||
|
|
||||||
|
Above procedure is documented in the `MySQL manual <https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html>`_
|
||||||
|
as a means to produce a consistent backup of a MySQL DBMS installation (i.e., all databases).
|
||||||
|
|
||||||
|
`DSN syntax <https://github.com/go-sql-driver/mysql#dsn-data-source-name>`_: ``[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]``
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
All MySQL databases must be on the same ZFS filesystem to guarantee a consistent point-in-time backup with the ZFS snapshot.
|
||||||
|
|
||||||
|
.. code-block:: sql
|
||||||
|
|
||||||
|
CREATE USER zrepl_lock_tables IDENTIFIED BY 'yourpasswordhere';
|
||||||
|
GRANT RELOAD ON *.* TO zrepl_lock_tables;
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
- type: mysql-lock-tables
|
||||||
|
dsn: "zrepl_lock_tables:yourpasswordhere@tcp(localhost)/"
|
||||||
|
filesystems: {
|
||||||
|
"tank/mysql": true
|
||||||
|
}
|
||||||
@@ -50,12 +50,25 @@ Serve
|
|||||||
serve:
|
serve:
|
||||||
type: tcp
|
type: tcp
|
||||||
listen: ":8888"
|
listen: ":8888"
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "mysql01"
|
"192.168.122.123" : "mysql01",
|
||||||
"192.168.122.123" : "mx01"
|
"192.168.122.42" : "mx01",
|
||||||
|
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||||
|
|
||||||
|
# CIDR masks require a '*' in the client identity string
|
||||||
|
# that is expanded to the client's IP address
|
||||||
|
|
||||||
|
"10.23.42.0/24": "cluster-*"
|
||||||
|
"fde4:8dba:82e1::/64": "san-*"
|
||||||
}
|
}
|
||||||
...
|
...
|
||||||
|
|
||||||
|
.. _listen-freebind-explanation:
|
||||||
|
|
||||||
|
``listen_freebind`` controls whether the socket is allowed to bind to non-local or unconfigured IP addresses (Linux ``IP_FREEBIND`` , FreeBSD ``IP_BINDANY``).
|
||||||
|
Enable this option if you want to ``listen`` on a specific IP address that might not yet be configured when the zrepl daemon starts.
|
||||||
|
|
||||||
Connect
|
Connect
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
@@ -78,7 +91,8 @@ The ``tls`` transport uses TCP + TLS with client authentication using client cer
|
|||||||
The client identity is the common name (CN) presented in the client certificate.
|
The client identity is the common name (CN) presented in the client certificate.
|
||||||
|
|
||||||
It is recommended to set up a dedicated CA infrastructure for this transport, e.g. using OpenVPN's `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_.
|
It is recommended to set up a dedicated CA infrastructure for this transport, e.g. using OpenVPN's `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_.
|
||||||
For a simple 2-machine setup, see the :ref:`instructions below<transport-tcp+tlsclientauth-2machineopenssl>`.
|
For a simple 2-machine setup, mutual TLS might also be sufficient.
|
||||||
|
We provide :ref:`copy-pastable instructions to generate the certificates below <transport-tcp+tlsclientauth-certgen>`.
|
||||||
|
|
||||||
The implementation uses `Go's TLS library <https://golang.org/pkg/crypto/tls/>`_.
|
The implementation uses `Go's TLS library <https://golang.org/pkg/crypto/tls/>`_.
|
||||||
Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
|
Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
|
||||||
@@ -90,6 +104,16 @@ If intermediate CAs are used, the **full chain** must be present in either in th
|
|||||||
Regardless, the client's certificate must be first in the ``cert`` file, with each following certificate directly certifying the one preceding it (see `TLS's specification <https://tools.ietf.org/html/rfc5246#section-7.4.2>`_).
|
Regardless, the client's certificate must be first in the ``cert`` file, with each following certificate directly certifying the one preceding it (see `TLS's specification <https://tools.ietf.org/html/rfc5246#section-7.4.2>`_).
|
||||||
This is the common default when using a CA management tool.
|
This is the common default when using a CA management tool.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
As of Go 1.15 (zrepl 0.3.0 and newer), the Go TLS / x509 library **requrires Subject Alternative Names**
|
||||||
|
be present in certificates. You might need to re-generate your certificates using one of the :ref:`two alternatives
|
||||||
|
provided below<transport-tcp+tlsclientauth-certgen>`.
|
||||||
|
|
||||||
|
Note further that zrepl continues to use the CommonName field to assign client identities.
|
||||||
|
Hence, we recommend to keep the Subject Alternative Name and the CommonName in sync.
|
||||||
|
|
||||||
|
|
||||||
Serve
|
Serve
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
@@ -101,6 +125,7 @@ Serve
|
|||||||
serve:
|
serve:
|
||||||
type: tls
|
type: tls
|
||||||
listen: ":8888"
|
listen: ":8888"
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
ca: /etc/zrepl/ca.crt
|
ca: /etc/zrepl/ca.crt
|
||||||
cert: /etc/zrepl/prod.fullchain
|
cert: /etc/zrepl/prod.fullchain
|
||||||
key: /etc/zrepl/prod.key
|
key: /etc/zrepl/prod.key
|
||||||
@@ -110,6 +135,7 @@ Serve
|
|||||||
|
|
||||||
The ``ca`` field specified the certificate authority used to validate client certificates.
|
The ``ca`` field specified the certificate authority used to validate client certificates.
|
||||||
The ``client_cns`` list specifies a list of accepted client common names (which are also the client identities for this transport).
|
The ``client_cns`` list specifies a list of accepted client common names (which are also the client identities for this transport).
|
||||||
|
The ``listen_freebind`` field is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
|
|
||||||
Connect
|
Connect
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@@ -132,45 +158,25 @@ The ``server_cn`` specifies the expected common name (CN) of the server's certif
|
|||||||
It overrides the hostname specified in ``address``.
|
It overrides the hostname specified in ``address``.
|
||||||
The connection fails if either do not match.
|
The connection fails if either do not match.
|
||||||
|
|
||||||
|
.. _transport-tcp+tlsclientauth-certgen:
|
||||||
|
|
||||||
.. _transport-tcp+tlsclientauth-2machineopenssl:
|
.. _transport-tcp+tlsclientauth-2machineopenssl:
|
||||||
|
|
||||||
Self-Signed Certificates
|
Mutual-TLS between Two Machines
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make it easy to manage CA infrastructure for multiple clients, e.g. a central zrepl backup server (in sink mode).
|
For a two-machine setup, self-signed certificates distributed using an out-of-band mechanism will also work just fine:
|
||||||
However, for a two-machine setup, self-signed certificates distributed using an out-of-band mechanism will also work just fine:
|
|
||||||
|
|
||||||
Suppose you have a push-mode setup, with `backups.example.com` running the :ref:`sink job <job-sink>`, and `prod.example.com` running the :ref:`push job <job-push>`.
|
Suppose you have a push-mode setup, with `backups.example.com` running the :ref:`sink job <job-sink>`, and `prod.example.com` running the :ref:`push job <job-push>`.
|
||||||
Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
|
Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
:emphasize-lines: 1-5,24
|
|
||||||
|
|
||||||
openssl req -x509 -sha256 -nodes \
|
(name=HOSTNAME; openssl req -x509 -sha256 -nodes \
|
||||||
-newkey rsa:4096 \
|
-newkey rsa:4096 \
|
||||||
-days 365 \
|
-days 365 \
|
||||||
-keyout HOSTNAME.key \
|
-keyout $name.key \
|
||||||
-out HOSTNAME.crt
|
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
||||||
|
|
||||||
#Generating a 4096 bit RSA private key
|
|
||||||
#................++++
|
|
||||||
#.++++
|
|
||||||
#writing new private key to 'backups.key'
|
|
||||||
#-----
|
|
||||||
#You are about to be asked to enter information that will be incorporated
|
|
||||||
#into your certificate request.
|
|
||||||
#What you are about to enter is what is called a Distinguished Name or a DN.
|
|
||||||
#There are quite a few fields but you can leave some blank
|
|
||||||
#For some fields there will be a default value,
|
|
||||||
#If you enter '.', the field will be left blank.
|
|
||||||
#-----
|
|
||||||
#Country Name (2 letter code) [XX]:
|
|
||||||
#State or Province Name (full name) []:
|
|
||||||
#Locality Name (eg, city) [Default City]:
|
|
||||||
#Organization Name (eg, company) [Default Company Ltd]:
|
|
||||||
#Organizational Unit Name (eg, section) []:
|
|
||||||
#Common Name (eg, your name or your server's hostname) []:HOSTNAME
|
|
||||||
#Email Address []:
|
|
||||||
|
|
||||||
Now copy each machine's ``HOSTNAME.crt`` to the other machine's ``/etc/zrepl/HOSTNAME.crt``, for example using `scp`.
|
Now copy each machine's ``HOSTNAME.crt`` to the other machine's ``/etc/zrepl/HOSTNAME.crt``, for example using `scp`.
|
||||||
The serve & connect configuration will thus look like the following:
|
The serve & connect configuration will thus look like the following:
|
||||||
@@ -201,6 +207,36 @@ The serve & connect configuration will thus look like the following:
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
Certificate Authority using EasyRSA
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
For more than two machines, it might make sense to set up a CA infrastructure.
|
||||||
|
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make this very easy:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOSTS=(backupserver prod1 prod2 prod3 10.23.42.1)
|
||||||
|
|
||||||
|
curl -L https://github.com/OpenVPN/easy-rsa/releases/download/v3.2.5/EasyRSA-3.2.5.tgz > EasyRSA-3.2.5.tgz
|
||||||
|
echo "662ee3b453155aeb1dff7096ec052cd83176c460cfa82ac130ef8568ec4df490 EasyRSA-3.2.5.tgz" | sha256sum -c
|
||||||
|
rm -rf EasyRSA-3.2.5
|
||||||
|
tar -xf EasyRSA-3.2.5.tgz
|
||||||
|
cd EasyRSA-3.2.5
|
||||||
|
./easyrsa --batch
|
||||||
|
./easyrsa --batch init-pki
|
||||||
|
./easyrsa --batch build-ca nopass
|
||||||
|
|
||||||
|
for host in "${HOSTS[@]}"; do
|
||||||
|
./easyrsa --batch --auto-san build-serverClient-full $host nopass
|
||||||
|
echo cert for host $host available at pki/issued/$host.crt
|
||||||
|
echo key for host $host available at pki/private/$host.key
|
||||||
|
done
|
||||||
|
echo ca cert available at pki/ca.crt
|
||||||
|
|
||||||
|
|
||||||
.. _transport-ssh+stdinserver:
|
.. _transport-ssh+stdinserver:
|
||||||
|
|
||||||
``ssh+stdinserver`` Transport
|
``ssh+stdinserver`` Transport
|
||||||
@@ -287,9 +323,9 @@ Connect
|
|||||||
user: root
|
user: root
|
||||||
port: 22
|
port: 22
|
||||||
identity_file: /etc/zrepl/ssh/identity
|
identity_file: /etc/zrepl/ssh/identity
|
||||||
options: # optional, default [], `-o` arguments passed to ssh
|
# options: # optional, default [], `-o` arguments passed to ssh
|
||||||
- "Compression=on"
|
# - "Compression=yes"
|
||||||
dial_timeout: # optional, default 10s, max time.Duration until initial handshake is completed
|
# dial_timeout: 10s # optional, default 10s, max time.Duration until initial handshake is completed
|
||||||
|
|
||||||
The connecting zrepl daemon
|
The connecting zrepl daemon
|
||||||
|
|
||||||
@@ -345,5 +381,6 @@ The ``client_identity`` is used by the sink as documented above.
|
|||||||
type: local
|
type: local
|
||||||
listener_name: localsink
|
listener_name: localsink
|
||||||
client_identity: local_backup
|
client_identity: local_backup
|
||||||
|
dial_timeout: 2s # optional, 0 for no timeout
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|
.. _future:
|
||||||
|
|
||||||
|
Future
|
||||||
|
======
|
||||||
|
|
||||||
|
This page contains some notes about future plans for zrepl.
|
||||||
|
Then again, Christian only has very limited time available for zrepl maintenance these days.
|
||||||
|
So, don't count on any of this happening in the near future.
|
||||||
|
|
||||||
|
One big development that has happened in recent years is this fork of zrepl: https://github.com/dsh2dsh/zrepl
|
||||||
|
We should figure out whether we can pull in features from there.
|
||||||
|
|
||||||
|
The next major step in terms of new feature development for zrepl would be to revise snapshot management:
|
||||||
|
|
||||||
|
- 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.
|
||||||
@@ -2,3 +2,36 @@
|
|||||||
|
|
||||||
..
|
..
|
||||||
.. |macroname| replace:: macro expansion
|
.. |macroname| replace:: macro expansion
|
||||||
|
|
||||||
|
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
||||||
|
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
||||||
|
.. |Language: Go| image:: https://img.shields.io/badge/lang-Go-6ad7e5.svg
|
||||||
|
:target: https://golang.org/
|
||||||
|
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
||||||
|
:target: https://zrepl.github.io
|
||||||
|
.. |Donate via PayPal| image:: https://img.shields.io/badge/donate-paypal-yellow.svg
|
||||||
|
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
||||||
|
.. |Donate via Liberapay| image:: https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay
|
||||||
|
:target: https://liberapay.com/zrepl/donate
|
||||||
|
.. |Donate via Patreon| image:: https://img.shields.io/badge/dynamic/json?color=yellow&label=Patreon&query=data.attributes.patron_count&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F3095079
|
||||||
|
:target: https://www.patreon.com/zrepl
|
||||||
|
.. |Donate via GitHub Sponsors| image:: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow
|
||||||
|
:target: https://github.com/sponsors/problame
|
||||||
|
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
||||||
|
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
||||||
|
.. |Matrix| image:: https://img.shields.io/badge/chat-matrix-blue.svg
|
||||||
|
:target: https://matrix.to/#/#zrepl:matrix.org
|
||||||
|
|
||||||
|
.. |serve-transport| replace:: :ref:`serve specification<transport>`
|
||||||
|
.. |connect-transport| replace:: :ref:`connect specification<transport>`
|
||||||
|
.. |send-options| replace:: :ref:`send options<job-send-options>`, e.g. for encrypted sends
|
||||||
|
.. |recv-options| replace:: :ref:`recv options<job-recv-options>`
|
||||||
|
.. |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>`
|
||||||
|
.. |pruning-spec| replace:: :ref:`pruning specification <prune>`
|
||||||
|
.. |filter-spec| replace:: :ref:`filter specification<pattern-filter>`
|
||||||
|
|
||||||
|
.. |br| raw:: html
|
||||||
|
|
||||||
|
<br/>
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
.. _implementation_toc:
|
|
||||||
|
|
||||||
Implementation Overview
|
|
||||||
=======================
|
|
||||||
|
|
||||||
.. WARNING::
|
|
||||||
|
|
||||||
Incomplete and possibly outdated.
|
|
||||||
Check out the :ref:`talks about zrepl <pr-talks>` at various conferences for up-to-date material.
|
|
||||||
Alternatively, have a `look at the source code <http://github.com/zrepl/zrepl>`_ ;)
|
|
||||||
|
|
||||||
The following design aspects may convince you that ``zrepl`` is superior to a hacked-together shell script solution.
|
|
||||||
Also check out the :ref:`talks about zrepl <pr-talks>` at various conferences.
|
|
||||||
|
|
||||||
Testability & Performance
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
zrepl is written in Go, a real programming language with type safety,
|
|
||||||
reasonable performance, testing infrastructure and an (opinionated) idea of
|
|
||||||
software engineering.
|
|
||||||
|
|
||||||
* key parts & algorithms of zrepl are covered by unit tests (work in progress)
|
|
||||||
* zrepl is noticably faster than comparable shell scripts
|
|
||||||
|
|
||||||
|
|
||||||
RPC protocol
|
|
||||||
------------
|
|
||||||
|
|
||||||
While it is tempting to just issue a few ``ssh remote 'zfs send ...' | zfs recv``, this has a number of drawbacks:
|
|
||||||
|
|
||||||
* The snapshot streams need to be compatible.
|
|
||||||
* Communication is still unidirectional. Thus, you will most likely
|
|
||||||
|
|
||||||
* either not take advantage of advanced replication features such as *compressed send & recv*
|
|
||||||
* or issue additional ``ssh`` commands in advance to figure out what features are supported on the other side.
|
|
||||||
|
|
||||||
* Advanced logic in shell scripts is ugly to read, poorly testable and a pain to maintain.
|
|
||||||
|
|
||||||
zrepl takes a different approach:
|
|
||||||
|
|
||||||
* Define an RPC protocol.
|
|
||||||
* Establish an encrypted, authenticated, bidirectional communication channel.
|
|
||||||
* Run daemons on both sides of the setup and let them talk to each other.
|
|
||||||
|
|
||||||
This has several obvious benefits:
|
|
||||||
|
|
||||||
* No blank root shell access is given to the other side.
|
|
||||||
* An *authenticated* peer *requests* filesystem lists, snapshot streams, etc.
|
|
||||||
* The server decides which filesystems it exposes to which peers.
|
|
||||||
* The :ref:`transport mechanism <transport>` is decoupled from the remaining logic, which allows us to painlessly offer multiple transport mechanisms.
|
|
||||||
|
|
||||||
Protocol Implementation
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
zrepl uses a custom RPC protol because, at the time of writing, existing solutions like gRPC do not provide efficient means to transport large amounts of data, whose size is unknown at send time (= zfs send streams).
|
|
||||||
The package used is `github.com/problame/go-streamrpc <https://github.com/problame/go-streamrpc/tree/master>`_.
|
|
||||||
|
|
||||||
Logging & Transparency
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
zrepl comes with :ref:`rich, structured and configurable logging <logging>`, allowing administators to understand what the software is actually doing.
|
|
||||||
+47
-26
@@ -5,21 +5,8 @@
|
|||||||
|
|
||||||
.. include:: global.rst.inc
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|GitHub license| |Language: Go| |User Docs| |Donate via PayPal| |Donate
|
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal| |Matrix|
|
||||||
via Liberapay| |Twitter|
|
|
||||||
|
|
||||||
.. |GitHub license| image:: https://img.shields.io/github/license/zrepl/zrepl.svg
|
|
||||||
:target: https://github.com/zrepl/zrepl/blob/master/LICENSE
|
|
||||||
.. |Language: Go| image:: https://img.shields.io/badge/language-Go-6ad7e5.svg
|
|
||||||
:target: https://golang.org/
|
|
||||||
.. |User Docs| image:: https://img.shields.io/badge/docs-web-blue.svg
|
|
||||||
:target: https://zrepl.github.io
|
|
||||||
.. |Donate via PayPal| image:: https://img.shields.io/badge/donate-paypal-yellow.svg
|
|
||||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
|
|
||||||
.. |Donate via Liberapay| image:: https://img.shields.io/badge/donate-liberapay-yellow.svg
|
|
||||||
:target: https://liberapay.com/zrepl/donate
|
|
||||||
.. |Twitter| image:: https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social
|
|
||||||
:target: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl
|
|
||||||
|
|
||||||
zrepl - ZFS replication
|
zrepl - ZFS replication
|
||||||
-----------------------
|
-----------------------
|
||||||
@@ -28,16 +15,36 @@ zrepl - ZFS replication
|
|||||||
|
|
||||||
.. raw:: html
|
.. raw:: html
|
||||||
|
|
||||||
<div style="margin-bottom: 1em; background: #2e3436; min-height: 6em; max-width: 100%">
|
<div style="margin-bottom: 1em; background: #2e3436; color: white; font-size: 0.8em; min-height: 6em; max-width: 100%; overflow: auto;">
|
||||||
<a href="https://raw.githubusercontent.com/wiki/zrepl/zrepl/zrepl_0.1_status.mp4" target="_new" >
|
<pre>
|
||||||
<video title="zrepl status subcommand" loop autoplay style="width: 100%; display: block;" src="https://raw.githubusercontent.com/wiki/zrepl/zrepl/zrepl_0.1_status.mp4"></video>
|
Job: prod_to_backups
|
||||||
</a>
|
Type: push
|
||||||
|
Replication:
|
||||||
|
Attempt #1
|
||||||
|
Status: fan-out-filesystems
|
||||||
|
Progress: [=========================\----] 246.7 MiB / 264.7 MiB @ 11.5 MiB/s
|
||||||
|
zroot STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
|
zroot/ROOT DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
|
* zroot/ROOT/default STEPPING (step 1/2, 123.4 MiB/129.3 MiB) next: @a => @b
|
||||||
|
zroot/tmp STEPPING (step 1/2, 29.9 KiB/44.2 KiB) next: @a => @b
|
||||||
|
zroot/usr STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
|
* zroot/usr/home STEPPING (step 1/2, 123.3 MiB/135.3 MiB) next: @a => @b
|
||||||
|
zroot/var STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
|
zroot/var/audit DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
|
zroot/var/crash DONE (step 2/2, 1.2 KiB/1.2 KiB)
|
||||||
|
zroot/var/log STEPPING (step 1/2, 22.0 KiB/29.2 KiB) next: @a => @b
|
||||||
|
zroot/var/mail STEPPING (step 1/2, 624 B/1.2 KiB) next: @a => @b
|
||||||
|
Pruning Sender:
|
||||||
|
...
|
||||||
|
Pruning Receiver:
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
Getting started
|
Getting started
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The :ref:`10 minutes tutorial setup <tutorial>` gives you a first impression.
|
The :ref:`10 minute quick-start guides <quickstart-toc>` give you a first impression.
|
||||||
|
|
||||||
Main Features
|
Main Features
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
@@ -45,18 +52,26 @@ Main Features
|
|||||||
* **Filesystem replication**
|
* **Filesystem replication**
|
||||||
|
|
||||||
* [x] Pull & Push mode
|
* [x] Pull & Push mode
|
||||||
* [x] Multiple transport :ref:`transports <transport>`: TCP, TCP + TLS client auth, SSH
|
* [x] Multiple :ref:`transport modes <transport>`: TCP, TCP + TLS client auth, SSH
|
||||||
|
|
||||||
* Advanced replication features
|
* Advanced replication features
|
||||||
|
|
||||||
* [x] Automatic retries for temporary network errors
|
* [x] Automatic retries for temporary network errors
|
||||||
* [ ] Resumable send & receive
|
* [x] Automatic resumable send & receive
|
||||||
* [ ] Compressed send & receive
|
* [x] Automatic ZFS holds during send & receive
|
||||||
* [ ] Raw encrypted send & receive
|
* [x] Automatic bookmark \& hold management for guaranteed incremental send & recv
|
||||||
|
* [x] Encrypted raw send & receive to untrusted receivers (OpenZFS native encryption)
|
||||||
|
* [x] Properties send & receive
|
||||||
|
* [x] Compressed send & receive
|
||||||
|
* [x] Large blocks send & receive
|
||||||
|
* [x] Embedded data send & receive
|
||||||
|
* [x] Resume state send & receive
|
||||||
|
* [x] Bandwidth limiting
|
||||||
|
|
||||||
* **Automatic snapshot management**
|
* **Automatic snapshot management**
|
||||||
|
|
||||||
* [x] Periodic filesystem snapshots
|
* [x] Periodic :ref:`filesystem snapshots <job-snapshotting-spec>`
|
||||||
|
* [x] Support for :ref:`pre- and post-snapshot hooks <job-snapshotting-hooks>` with builtins for MySQL & Postgres
|
||||||
* [x] Flexible :ref:`pruning rule system <prune>`
|
* [x] Flexible :ref:`pruning rule system <prune>`
|
||||||
|
|
||||||
* [x] Age-based fading (grandfathering scheme)
|
* [x] Age-based fading (grandfathering scheme)
|
||||||
@@ -75,6 +90,7 @@ Main Features
|
|||||||
* **Maintainable implementation in Go**
|
* **Maintainable implementation in Go**
|
||||||
|
|
||||||
* [x] Cross platform
|
* [x] Cross platform
|
||||||
|
* [x] Dynamic feature checking
|
||||||
* [x] Type safe & testable code
|
* [x] Type safe & testable code
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +105,7 @@ Contributing
|
|||||||
|
|
||||||
We are happy about any help we can get!
|
We are happy about any help we can get!
|
||||||
|
|
||||||
|
* :ref:`Financial Support <supporters>`
|
||||||
* Explore the codebase
|
* Explore the codebase
|
||||||
|
|
||||||
* These docs live in the ``docs/`` subdirectory
|
* These docs live in the ``docs/`` subdirectory
|
||||||
@@ -104,6 +121,8 @@ We are happy about any help we can get!
|
|||||||
Make sure to read the `Developer Documentation section <https://github.com/zrepl/zrepl>`_ and open new issues or pull requests there.
|
Make sure to read the `Developer Documentation section <https://github.com/zrepl/zrepl>`_ and open new issues or pull requests there.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Table of Contents
|
Table of Contents
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@@ -111,12 +130,14 @@ Table of Contents
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
tutorial
|
quickstart
|
||||||
installation
|
installation
|
||||||
configuration
|
configuration
|
||||||
usage
|
usage
|
||||||
implementation
|
|
||||||
pr
|
pr
|
||||||
changelog
|
changelog
|
||||||
|
future
|
||||||
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||||
|
Chat: Matrix <https://matrix.to/#/#zrepl:matrix.org>
|
||||||
|
supporters
|
||||||
|
|
||||||
|
|||||||
+11
-82
@@ -1,90 +1,19 @@
|
|||||||
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
|
||||||
|
|
||||||
.. _installation:
|
.. _installation_toc:
|
||||||
|
|
||||||
|
************
|
||||||
Installation
|
Installation
|
||||||
============
|
************
|
||||||
|
|
||||||
.. TIP::
|
.. TIP::
|
||||||
|
|
||||||
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
Note: check out the :ref:`quick-start guides <quickstart-toc>` if you want a first impression of zrepl.
|
||||||
|
|
||||||
User Privileges
|
.. toctree::
|
||||||
---------------
|
|
||||||
|
|
||||||
It is possible to run zrepl as an unprivileged user in combination with
|
|
||||||
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
|
||||||
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
|
||||||
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
|
|
||||||
|
|
||||||
Packages
|
|
||||||
--------
|
|
||||||
|
|
||||||
zrepl source releases are signed & tagged by the author in the git repository.
|
|
||||||
Your OS vendor may provide binary packages of zrepl through the package manager.
|
|
||||||
Additionally, `binary releases`_ are provided on GitHub.
|
|
||||||
The following list may be incomplete, feel free to submit a PR with an update:
|
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - OS / Distro
|
|
||||||
- Install Command
|
|
||||||
- Link
|
|
||||||
* - FreeBSD
|
|
||||||
- ``pkg install zrepl``
|
|
||||||
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
|
||||||
* - Others
|
|
||||||
-
|
|
||||||
- Use `binary releases`_ or build from source.
|
|
||||||
|
|
||||||
Compile From Source
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Producing a release requires **Go 1.9** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
|
||||||
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
|
||||||
Python and pip3 should probably be installed via your distro's package manager.
|
|
||||||
|
|
||||||
Alternatively, you can use the Docker build process:
|
|
||||||
it is used to produce the official zrepl `binary releases`_
|
|
||||||
and serves as a reference for build dependencies and procedure:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
git clone https://github.com/zrepl/zrepl.git
|
|
||||||
cd zrepl
|
|
||||||
sudo docker build -t zrepl_build -f build.Dockerfile .
|
|
||||||
sudo docker run -it --rm \
|
|
||||||
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
|
|
||||||
--user "$(id -u):$(id -g)" \
|
|
||||||
zrepl_build make vendordeps release
|
|
||||||
|
|
||||||
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
python3 -m venv3
|
|
||||||
source venv3/bin/activate
|
|
||||||
./lazy.sh devsetup
|
|
||||||
make vendordeps release
|
|
||||||
|
|
||||||
The Python venv is used for the documentation build dependencies.
|
|
||||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
|
||||||
Either way, all build results are located in the ``artifacts/`` directory.
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
It is your job to install the apropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
|
||||||
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
|
||||||
|
|
||||||
What next?
|
|
||||||
----------
|
|
||||||
|
|
||||||
Read the :ref:`configuration chapter<configuration_toc>` and optionally create the :ref:`runtime directories <configuration_preface>`.
|
|
||||||
Afterwards, continue with the :ref:`usage chapter<usage>`.
|
|
||||||
|
|
||||||
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
|
||||||
|
|
||||||
|
installation/packages
|
||||||
|
installation/apt-repos
|
||||||
|
installation/rpm-repos
|
||||||
|
installation/user-privileges
|
||||||
|
installation/freebsd-jail-with-iocage
|
||||||
|
installation/what-next
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
.. _installation-apt-repos:
|
||||||
|
|
||||||
|
Debian / Ubuntu APT repositories
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
||||||
|
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
||||||
|
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
||||||
|
Please open an issue in on GitHub if you encounter any issues with the repository.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
(
|
||||||
|
set -ex
|
||||||
|
zrepl_apt_key_url=https://zrepl.cschwarz.com/apt/apt-key.asc
|
||||||
|
zrepl_apt_key_dst=/usr/share/keyrings/zrepl.gpg
|
||||||
|
zrepl_apt_repo_file=/etc/apt/sources.list.d/zrepl.list
|
||||||
|
|
||||||
|
# Install dependencies for subsequent commands
|
||||||
|
sudo apt update && sudo apt install curl gnupg lsb-release
|
||||||
|
|
||||||
|
# Deploy the zrepl apt key.
|
||||||
|
curl -fsSL "$zrepl_apt_key_url" | tee | gpg --dearmor | sudo tee "$zrepl_apt_key_dst" > /dev/null
|
||||||
|
|
||||||
|
# Add the zrepl apt repo.
|
||||||
|
ARCH="$(dpkg --print-architecture)"
|
||||||
|
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "Using Distro and Codename: $CODENAME"
|
||||||
|
echo "deb [arch=$ARCH signed-by=$zrepl_apt_key_dst] https://zrepl.cschwarz.com/apt/$CODENAME main" | sudo tee "$zrepl_apt_repo_file" > /dev/null
|
||||||
|
|
||||||
|
# Update apt repos.
|
||||||
|
sudo apt update
|
||||||
|
)
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
Until zrepl reaches 1.0, the repositories will be updated to the latest zrepl release immediately.
|
||||||
|
This includes breaking changes between zrepl versions.
|
||||||
|
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _installation-freebsd-jail-with-iocage:
|
||||||
|
|
||||||
|
FreeBSD Jail With iocage
|
||||||
|
========================
|
||||||
|
|
||||||
|
|
||||||
|
This tutorial shows how zrepl can be installed on FreeBSD, or FreeNAS in a jail using iocage.
|
||||||
|
While this tutorial focuses on using iocage, much of the setup would be similar
|
||||||
|
using a different jail manager.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
From a security perspective, just keep in mind that ``zfs send``/``recv`` was never designed with
|
||||||
|
jails in mind, an attacker could probably crash the receive-side kernel or worse induce stateful
|
||||||
|
damage to the receive-side pool if they were able to get access to the jail.
|
||||||
|
|
||||||
|
The jail doesn't provide security benefits, but only management ones.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
A dataset that will be delegated to the jail needs to be created if one does not already exist.
|
||||||
|
For the tutorial ``tank/zrepl`` will be used.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
zfs create -o mountpoint=none tank/zrepl
|
||||||
|
|
||||||
|
The only software requirements on the host system are ``iocage``, which can be installed
|
||||||
|
from ports or packages.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pkg install py37-iocage
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
By default ``iocage`` will "activate" on first use which will set up some defaults such as
|
||||||
|
which pool will be used. To activate ``iocage`` manually the ``iocage activate`` command can be used.
|
||||||
|
|
||||||
|
Jail Creation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
There are two options for jail creation using FreeBSD.
|
||||||
|
|
||||||
|
1. Manually set up the jail from scratch
|
||||||
|
2. Create the jail using the ``zrepl`` plugin. On FreeNAS this is possible from the user interface using the community index.
|
||||||
|
|
||||||
|
Manual Jail
|
||||||
|
###########
|
||||||
|
|
||||||
|
Create a jail, using the same release as the host, called ``zrepl`` that will be automatically started at boot.
|
||||||
|
The jail will have ``tank/zrepl`` delegated into it.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage create --release "$(freebsd-version -k | cut -d '-' -f '1,2')" --name zrepl \
|
||||||
|
boot=on nat=1 \
|
||||||
|
jail_zfs=on \
|
||||||
|
jail_zfs_dataset=zrepl \
|
||||||
|
jail_zfs_mountpoint='none'
|
||||||
|
|
||||||
|
Enter the jail:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage console zrepl
|
||||||
|
|
||||||
|
Install ``zrepl``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pkg update && pkg upgrade
|
||||||
|
pkg install zrepl
|
||||||
|
|
||||||
|
Create the log file ``/var/log/zrepl.log``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
touch /var/log/zrepl.log && service newsyslog restart
|
||||||
|
|
||||||
|
Tell syslogd to redirect facility local0 to the ``zrepl.log`` file:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
service syslogd reload
|
||||||
|
|
||||||
|
Enable the zrepl daemon to start automatically at boot:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sysrc zrepl_enable="YES"
|
||||||
|
|
||||||
|
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
||||||
|
|
||||||
|
Plugin
|
||||||
|
######
|
||||||
|
|
||||||
|
When using the plugin, ``zrepl`` will be installed for you in a jail using the following ``iocage`` properties.
|
||||||
|
|
||||||
|
* ``nat=1``
|
||||||
|
* ``jail_zfs=on``
|
||||||
|
* ``jail_zfs_mountpoint=none``
|
||||||
|
|
||||||
|
Additionally the delegated dataset should be specified upon creation, and optionally start on boot can be set.
|
||||||
|
This can also be done from the FreeNAS webui.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
fetch https://raw.githubusercontent.com/ix-plugin-hub/iocage-plugin-index/master/zrepl.json -o /tmp/zrepl.json
|
||||||
|
iocage fetch -P /tmp/zrepl.json --name zrepl jail_zfs_dataset=zrepl boot=on
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Now ``zrepl`` can be configured.
|
||||||
|
|
||||||
|
Enter the jail.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage console zrepl
|
||||||
|
|
||||||
|
Modify the ``/usr/local/etc/zrepl/zrepl.yml`` configuration file.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`quick-start guides <quickstart-toc>` for examples of a ``sink`` job.
|
||||||
|
|
||||||
|
Now ``zrepl`` can be started.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
service zrepl start
|
||||||
|
|
||||||
|
Now jump to :ref:`the summary <installation-freebsd-jail-summary>` below.
|
||||||
|
|
||||||
|
.. _installation-freebsd-jail-summary:
|
||||||
|
|
||||||
|
Summary
|
||||||
|
-------
|
||||||
|
|
||||||
|
Congratulations, you have a working jail!
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
With FreeBSD 13's transition to OpenZFS 2.0, please ensure that your jail's FreeBSD version matches the one in the kernel module.
|
||||||
|
If you are getting cryptic errors such as
|
||||||
|
``cannot receive new filesystem stream: invalid backup stream``
|
||||||
|
the instructions posted `here <https://github.com/zrepl/zrepl/issues/500#issuecomment-966215205>`_ might help.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.. _installation-packages:
|
||||||
|
|
||||||
|
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||||
|
|
||||||
|
Packages
|
||||||
|
--------
|
||||||
|
|
||||||
|
zrepl source releases are signed & tagged by the author in the git repository.
|
||||||
|
Your OS vendor may provide binary packages of zrepl through the package manager.
|
||||||
|
Additionally, `binary releases`_ are provided on GitHub.
|
||||||
|
The following list may be incomplete, feel free to submit a PR with an update:
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - OS / Distro
|
||||||
|
- Install Command
|
||||||
|
- Link
|
||||||
|
* - any
|
||||||
|
- Statically linked binaries.
|
||||||
|
- `Official GitHub releases <binary releases_>`_
|
||||||
|
* - FreeBSD
|
||||||
|
- ``pkg install zrepl``
|
||||||
|
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
||||||
|
|
||||||
|
:ref:`installation-freebsd-jail-with-iocage`
|
||||||
|
* - FreeNAS
|
||||||
|
-
|
||||||
|
- :ref:`installation-freebsd-jail-with-iocage`
|
||||||
|
* - MacOS
|
||||||
|
- ``brew install zrepl``
|
||||||
|
- Available on `homebrew <https://brew.sh>`_
|
||||||
|
* - Arch Linux
|
||||||
|
- ``yay install zrepl``
|
||||||
|
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
||||||
|
* - Fedora / RHEL / OpenSUSE
|
||||||
|
- ``dnf install zrepl``
|
||||||
|
- :ref:`RPM repository config <installation-rpm-repos>`
|
||||||
|
* - Debian + Ubuntu
|
||||||
|
- ``apt install zrepl``
|
||||||
|
- :ref:`APT repository config <installation-apt-repos>`
|
||||||
|
* - OmniOS
|
||||||
|
- ``pkg install zrepl``
|
||||||
|
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
||||||
|
* - Void Linux
|
||||||
|
- ``xbps-install zrepl``
|
||||||
|
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
|
||||||
|
* - any
|
||||||
|
- Build from source
|
||||||
|
- :repomasterlink:`README.md`.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.. _installation-rpm-repos:
|
||||||
|
|
||||||
|
RPM repositories
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We provide a single RPM repository for all RPM-based Linux distros.
|
||||||
|
The zrepl binary in the repo is the same as the one published to GitHub.
|
||||||
|
Since Go binaries are statically linked, the RPM should work about everywhere.
|
||||||
|
Please open an issue on GitHub if you encounter any issues with the repository.
|
||||||
|
|
||||||
|
The fingerprint of the repo & package signing key is:
|
||||||
|
``F6F6 E8EA 6F2F 1462 2878 B5DE 50E3 4417 826E 2CE6``.
|
||||||
|
It is available at `<https://zrepl.cschwarz.com/rpm/rpm-key.asc>`_ .
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
Until zrepl reaches 1.0, the repository will be updated to the latest zrepl release immediately.
|
||||||
|
This includes breaking changes between zrepl versions.
|
||||||
|
If that bothers you, use the `dnf versionlock plugin <https://dnf-plugins-core.readthedocs.io/en/latest/versionlock.html>`_ or ``zypper addlock zrepl`` to pin the version of zrepl on your system.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Fedora / AlmaLinux / Rocky Linux / RHEL
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
For ``dnf``/``yum``-based distributions, copy-paste the following snippet:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
rpmkeys --import 'https://zrepl.cschwarz.com/rpm/rpm-key.asc'
|
||||||
|
cat > /etc/yum.repos.d/zrepl.repo <<EOF
|
||||||
|
[zrepl]
|
||||||
|
name = zrepl
|
||||||
|
baseurl = https://zrepl.cschwarz.com/rpm/repo
|
||||||
|
gpgkey = https://zrepl.cschwarz.com/rpm/rpm-key.asc
|
||||||
|
repo_gpgcheck = 1
|
||||||
|
EOF
|
||||||
|
dnf install zrepl
|
||||||
|
# Or if you're on an older system:
|
||||||
|
# yum install zrepl
|
||||||
|
|
||||||
|
You will be asked by dnf to verify repository metadata (``repo_gpgcheck``).
|
||||||
|
There is no way to automate that prompt.
|
||||||
|
|
||||||
|
openSUSE
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
For SUSE-based distributions that use ``zypper``, copy-paste the following snippet:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
rpmkeys --import 'https://zrepl.cschwarz.com/rpm/rpm-key.asc'
|
||||||
|
zypper ar --check --gpgcheck-strict --refresh https://zrepl.cschwarz.com/rpm/repo zrepl
|
||||||
|
zypper install zrepl
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.. _installation-user-privileges:
|
||||||
|
|
||||||
|
User Privileges
|
||||||
|
---------------
|
||||||
|
|
||||||
|
It is possible to run zrepl as an unprivileged user in combination with
|
||||||
|
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
||||||
|
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`installation-freebsd-jail-with-iocage` for FreeBSD jail setup instructions.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.. _installation-what-next:
|
||||||
|
|
||||||
|
What next?
|
||||||
|
----------
|
||||||
|
|
||||||
|
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||||
|
|
||||||
|
**Reminder**: If you want a quick introduction, please read the :ref:`quick-start guides <quickstart-toc>`.
|
||||||
@@ -15,3 +15,5 @@ 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.
|
||||||
|
|||||||
+71
-56
@@ -1,79 +1,94 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -eo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
GHPAGESREPO="git@github.com:zrepl/zrepl.github.io.git"
|
GHPAGESREPO="https://github.com/zrepl/zrepl.github.io.git"
|
||||||
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||||
PUBLICDIR="${SCRIPTDIR}/public_git"
|
PUBLICDIR="${SCRIPTDIR}/public_git"
|
||||||
|
ROOTDIR="${SCRIPTDIR}/.."
|
||||||
|
|
||||||
checkout_repo_msg() {
|
PUSH=false
|
||||||
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
DO_CLONE=false
|
||||||
echo " git clone ${GHPAGESREPO} ${PUBLICDIR}"
|
NON_INTERACTIVE=false
|
||||||
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
|
|
||||||
}
|
|
||||||
|
|
||||||
exit_msg() {
|
while getopts "caPh" arg; do
|
||||||
echo "error, exiting..."
|
case "$arg" in
|
||||||
}
|
"a") NON_INTERACTIVE=true ;;
|
||||||
trap exit_msg EXIT
|
"c") DO_CLONE=true ;;
|
||||||
|
"P") PUSH=true ;;
|
||||||
if ! type sphinx-versioning >/dev/null; then
|
"h") echo "Usage: $0 [-c clone] [-a auto/non-interactive] [-P push]"; exit 0 ;;
|
||||||
echo "install sphinx-versioning and come back"
|
*) echo "invalid option"; exit 1 ;;
|
||||||
exit 1
|
esac
|
||||||
fi
|
done
|
||||||
|
|
||||||
cd "$SCRIPTDIR"
|
cd "$SCRIPTDIR"
|
||||||
|
|
||||||
|
# Clone or verify repo
|
||||||
if [ ! -d "$PUBLICDIR" ]; then
|
if [ ! -d "$PUBLICDIR" ]; then
|
||||||
checkout_repo_msg
|
if $DO_CLONE; then
|
||||||
|
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
|
||||||
|
else
|
||||||
|
echo "Run with -c to clone ${GHPAGESREPO}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $NON_INTERACTIVE; then
|
||||||
|
echo -n "PRESS ENTER to confirm you committed and pushed docs changes to the zrepl repo"
|
||||||
|
read -r
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify we're in the right repo
|
||||||
|
cd "$PUBLICDIR"
|
||||||
|
REMOTE_URL=$(git remote get-url origin)
|
||||||
|
if [[ "$REMOTE_URL" != *"zrepl.github.io"* ]]; then
|
||||||
|
echo "ERROR: ${PUBLICDIR} remote is '${REMOTE_URL}', expected zrepl.github.io repo"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n "PRESS ENTER to confirm you commited and pushed docs changes and tags to the zrepl repo"
|
# Reset public repo to latest
|
||||||
read
|
echo "Resetting GitHub pages repo to latest commit..."
|
||||||
|
|
||||||
pushd "$PUBLICDIR"
|
|
||||||
|
|
||||||
echo "verify we're in the GitHub pages repo..."
|
|
||||||
git remote get-url origin | grep -E "^${GHPAGESREPO}\$"
|
|
||||||
if [ "$?" -ne "0" ] ;then
|
|
||||||
checkout_repo_msg
|
|
||||||
echo "finished checkout, please run again"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "resetting GitHub pages repo to latest commit"
|
|
||||||
git fetch origin
|
git fetch origin
|
||||||
git reset --hard origin/master
|
git reset --hard origin/master
|
||||||
|
|
||||||
echo "cleaning GitHub pages repo"
|
# Clean everything
|
||||||
git rm -rf .
|
echo "Cleaning GitHub pages repo..."
|
||||||
|
git rm -rf . || true
|
||||||
|
|
||||||
popd
|
# GitHub Pages https://github.blog/news-insights/the-library/bypassing-jekyll-on-github-pages/
|
||||||
|
touch .nojekyll
|
||||||
|
|
||||||
echo "building site"
|
# Build docs
|
||||||
set -e
|
echo "Building docs..."
|
||||||
sphinx-versioning build \
|
cd "$ROOTDIR"
|
||||||
--show-banner \
|
make docs
|
||||||
--whitelist-branches 'master' \
|
|
||||||
--whitelist-tags '(v)?\d+\.\d+\.\d+$' \
|
|
||||||
--whitelist-tags '0.1.0-rc(3|4)$' \
|
|
||||||
docs ./public_git \
|
|
||||||
-- -c sphinxconf # older conf.py throw errors because they used
|
|
||||||
# version = subprocess.show_output(["git", "describe"])
|
|
||||||
# which fails when building with sphinxcontrib-versioning
|
|
||||||
set +e
|
|
||||||
|
|
||||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
# Copy built docs to public repo
|
||||||
git status --porcelain
|
echo "Copying built docs..."
|
||||||
if [[ "$(git status --porcelain)" != "" ]]; then
|
cp -r artifacts/docs/html/* "$PUBLICDIR/"
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
cd "$PUBLICDIR"
|
||||||
|
cat > .gitignore <<EOF
|
||||||
|
**/.doctrees
|
||||||
|
EOF
|
||||||
|
git add .gitignore
|
||||||
|
git add -A
|
||||||
|
|
||||||
|
CURRENT_COMMIT=$(git -C "$ROOTDIR" rev-parse HEAD)
|
||||||
|
if [ "$(git -C "$ROOTDIR" status --porcelain)" != "" ]; then
|
||||||
CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)"
|
CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)"
|
||||||
fi
|
fi
|
||||||
COMMIT_MSG="sphinx-versioning render from publish.sh - `date -u` - ${CURRENT_COMMIT}"
|
COMMIT_MSG="docs: $(date -u) - ${CURRENT_COMMIT}"
|
||||||
|
|
||||||
pushd "$PUBLICDIR"
|
if [ "$(git status --porcelain)" != "" ]; then
|
||||||
|
git commit -m "$COMMIT_MSG"
|
||||||
echo "adding and commiting all changes in GitHub pages repo"
|
else
|
||||||
git add -A
|
echo "Nothing to commit"
|
||||||
git commit -m "$COMMIT_MSG"
|
fi
|
||||||
git push origin master
|
|
||||||
|
|
||||||
|
if $PUSH; then
|
||||||
|
echo "Pushing to GitHub pages repo..."
|
||||||
|
git push origin master
|
||||||
|
else
|
||||||
|
echo "Not pushing. Use -P to push."
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "zrepl-docs"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "zrepl documentation build dependencies"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = [
|
||||||
|
"Sphinx>=7.2,<8",
|
||||||
|
"sphinx-rtd-theme>=1.3,<2",
|
||||||
|
]
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|
.. _quickstart-toc:
|
||||||
|
|
||||||
|
***********************
|
||||||
|
Quick Start by Use Case
|
||||||
|
***********************
|
||||||
|
|
||||||
|
The goal of this quick-start guide is to give you an impression of how zrepl can accomodate your use case.
|
||||||
|
|
||||||
|
Install zrepl
|
||||||
|
=============
|
||||||
|
|
||||||
|
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here.
|
||||||
|
|
||||||
|
Overview Of How zrepl Works
|
||||||
|
============================
|
||||||
|
|
||||||
|
Check out the :ref:`overview section <job-overview>` to get a rough idea of what you are going to configure in the next step, then come back here.
|
||||||
|
|
||||||
|
Configuration Examples
|
||||||
|
======================
|
||||||
|
|
||||||
|
zrepl is configured through a YAML configuration file in ``/etc/zrepl/zrepl.yml``.
|
||||||
|
We have prepared example use cases that show-case typical deployments and different functionality of zrepl.
|
||||||
|
We encourage you to read through all of the examples to get an idea of what zrepl has to offer, and how you can mix-and-match configurations for your use case.
|
||||||
|
Keep the :ref:`full config documentation <configuration_toc>` handy if a config snippet is unclear.
|
||||||
|
|
||||||
|
**Example Use Cases**
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:titlesonly:
|
||||||
|
|
||||||
|
quickstart/continuous_server_backup
|
||||||
|
quickstart/backup_to_external_disk
|
||||||
|
quickstart/fan_out_replication
|
||||||
|
|
||||||
|
Use ``zrepl configcheck`` to validate your configuration.
|
||||||
|
No output indicates that everything is fine.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
Please open an issue on GitHub if your use case for zrepl is significantly different from those listed above.
|
||||||
|
Or even better, write it up in the same style as above and open a PR!
|
||||||
|
|
||||||
|
.. _quickstart-apply-config:
|
||||||
|
|
||||||
|
Apply Configuration Changes
|
||||||
|
===========================
|
||||||
|
|
||||||
|
We hope that you have found a configuration that fits your use case.
|
||||||
|
Use ``zrepl configcheck`` once again to make sure the config is correct (output indicates that everything is fine).
|
||||||
|
Then restart the zrepl daemon on all systems involved in the replication, likely using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
Please :ref:`read up carefully <prune>` on the pruning rules before applying the config.
|
||||||
|
In particular, note that most example configs apply to all snapshots, not just zrepl-created snapshots.
|
||||||
|
Use the following keep rule on sender and receiver to prevent this:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
- type: regex
|
||||||
|
negate: true
|
||||||
|
regex: "^zrepl_.*" # <- the 'prefix' specified in snapshotting.prefix
|
||||||
|
|
||||||
|
|
||||||
|
Watch it Work
|
||||||
|
=============
|
||||||
|
|
||||||
|
Run ``zrepl status`` on the active side of the replication setup to monitor snaphotting, replication and pruning activity.
|
||||||
|
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup JOBNAME``.
|
||||||
|
(refer to the example use case document if you are uncertain which job you want to wake up).
|
||||||
|
|
||||||
|
You can also use basic UNIX tools to inspect see what's going on.
|
||||||
|
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||||
|
|
||||||
|
pkg install gnu-watch tmux
|
||||||
|
tmux new -s zrepl -d
|
||||||
|
tmux split-window -t zrepl "tail -f /var/log/messages"
|
||||||
|
tmux split-window -t zrepl "gnu-watch 'zfs list -t snapshot -o name,creation -s creation'"
|
||||||
|
tmux split-window -t zrepl "zrepl status"
|
||||||
|
tmux select-layout -t zrepl tiled
|
||||||
|
tmux attach -t zrepl
|
||||||
|
|
||||||
|
The Linux equivalent might look like this: ::
|
||||||
|
|
||||||
|
# make sure tmux is installed & let's assume you use systemd + journald
|
||||||
|
tmux new -s zrepl -d
|
||||||
|
tmux split-window -t zrepl "journalctl -f -u zrepl.service"
|
||||||
|
tmux split-window -t zrepl "watch 'zfs list -t snapshot -o name,creation -s creation'"
|
||||||
|
tmux split-window -t zrepl "zrepl status"
|
||||||
|
tmux select-layout -t zrepl tiled
|
||||||
|
tmux attach -t zrepl
|
||||||
|
|
||||||
|
What Next?
|
||||||
|
==========
|
||||||
|
|
||||||
|
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
||||||
|
* Configure :ref:`logging <logging>` \& :ref:`monitoring <monitoring>`.
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _quickstart-backup-to-external-disk:
|
||||||
|
|
||||||
|
Local Snapshots + Offline Backup to an External Disk
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
This config example shows how we can use zrepl to make periodic snapshots of our local workstation and back it up to a zpool on an external disk which we occassionally connect.
|
||||||
|
|
||||||
|
The local snapshots should be taken every 15 minutes for pain-free recovery from CLI disasters (``rm -rf /`` and the like).
|
||||||
|
However, we do not want to keep the snapshots around for very long because our workstation is a little tight on disk space.
|
||||||
|
Thus, we only keep one hour worth of high-resolution snapshots, then fade them out to one per hour for a day (24 hours), then one per day for 14 days.
|
||||||
|
|
||||||
|
At the end of each work day, we connect our external disk that serves as our workstation's local offline backup.
|
||||||
|
We want zrepl to inspect the filesystems and snapshots on the external pool, figure out which snapshots were created since the last time we connected the external disk, and use incremental replication to efficiently mirror our workstation to our backup disk.
|
||||||
|
Afterwards, we want to clean up old snapshots on the backup pool: we want to keep all snapshots younger than one hour, 24 for each hour of the first day, then 360 daily backups.
|
||||||
|
|
||||||
|
A few additional requirements:
|
||||||
|
|
||||||
|
* Snapshot creation and pruning on our workstation should happen in the background, without interaction from our side.
|
||||||
|
* However, we want to explicitly trigger replication via the command line.
|
||||||
|
* We want to use OpenZFS native encryption to protect our data on the external disk.
|
||||||
|
It is absolutely critical that only encrypted data leaves our workstation.
|
||||||
|
**zrepl should provide an easy config knob for this and prevent replication of unencrypted datasets to the external disk.**
|
||||||
|
* We want to be able to put off the backups for more than three weeks, i.e., longer than the lifetime of the automatically created snapshots on our workstation.
|
||||||
|
**zrepl should use bookmarks and holds to achieve this goal**.
|
||||||
|
* When we yank out the drive during replication and go on a long vacation, we do *not* want the partially replicated snapshot to stick around as it would hold on to too much disk space over time.
|
||||||
|
Therefore, we want zrepl to deviate from its :ref:`default behavior <replication-option-protection>` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation.
|
||||||
|
**zrepl should provide an easy config knob to disable step holds for incremental replication**.
|
||||||
|
|
||||||
|
The following config snippet implements the setup described above.
|
||||||
|
You will likely want to customize some aspects mentioned in the top comment in the file.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../internal/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.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _quickstart-continuous-replication:
|
||||||
|
|
||||||
|
Continuous Backup of a Server
|
||||||
|
=============================
|
||||||
|
|
||||||
|
This config example shows how we can backup our ZFS-based server to another machine using a zrepl push job.
|
||||||
|
|
||||||
|
* Production server ``prod`` with filesystems to back up:
|
||||||
|
|
||||||
|
* The entire pool ``zroot``
|
||||||
|
* except ``zroot/var/tmp`` and all child datasets of it
|
||||||
|
* and except ``zroot/usr/home/paranoid`` which belongs to a user doing backups themselves.
|
||||||
|
|
||||||
|
* Backup server ``backups`` with a dataset sub-tree for use by zrepl:
|
||||||
|
|
||||||
|
* In our example, that will be ``storage/zrepl/sink/prod``.
|
||||||
|
|
||||||
|
Our backup solution should fulfill the following requirements:
|
||||||
|
|
||||||
|
* Periodically snapshot the filesystems on ``prod`` *every 10 minutes*
|
||||||
|
* Incrementally replicate these snapshots to ``storage/zrepl/sink/prod/*`` on ``backups``
|
||||||
|
* Keep only very few snapshots on ``prod`` to save disk space
|
||||||
|
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on ``backups``
|
||||||
|
* The network is untrusted - zrepl should use TLS to protect its communication and our data.
|
||||||
|
|
||||||
|
Analysis
|
||||||
|
--------
|
||||||
|
|
||||||
|
We can model this situation as two jobs:
|
||||||
|
|
||||||
|
* A **push job** on ``prod``
|
||||||
|
|
||||||
|
* Creates the snapshots
|
||||||
|
* Keeps a short history of local snapshots to enable incremental replication to ``backups``
|
||||||
|
* Connects to the ``zrepl daemon`` process on ``backups``
|
||||||
|
* Pushes snapshots ``backups``
|
||||||
|
* Prunes snapshots on ``backups`` after replication is complete
|
||||||
|
|
||||||
|
* A **sink job** on ``backups``
|
||||||
|
|
||||||
|
* Accepts connections & responds to requests from ``prod``
|
||||||
|
* Limits client ``prod`` access to filesystem sub-tree ``storage/zrepl/sink/prod``
|
||||||
|
|
||||||
|
Generate TLS Certificates
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
We use the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
||||||
|
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
|
||||||
|
For convenience, we generate the key pairs on our local machine and distribute them using ssh:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
(name=backups; openssl req -x509 -sha256 -nodes \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-days 365 \
|
||||||
|
-keyout $name.key \
|
||||||
|
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
||||||
|
|
||||||
|
(name=prod; openssl req -x509 -sha256 -nodes \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-days 365 \
|
||||||
|
-keyout $name.key \
|
||||||
|
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
||||||
|
|
||||||
|
ssh root@backups "mkdir /etc/zrepl"
|
||||||
|
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
||||||
|
|
||||||
|
ssh root@prod "mkdir /etc/zrepl"
|
||||||
|
scp prod.key prod.crt backups.crt root@prod:/etc/zrepl
|
||||||
|
|
||||||
|
Note that alternative transports exist, e.g. via :ref:`TCP without TLS <transport-tcp>` or :ref:`ssh <transport-ssh+stdinserver>`.
|
||||||
|
|
||||||
|
Configure server ``prod``
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
We define a **push job** named ``prod_to_backups`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` :
|
||||||
|
|
||||||
|
.. literalinclude:: ../../internal/config/samples/quickstart_continuous_server_backup_sender.yml
|
||||||
|
|
||||||
|
.. _tutorial-configure-prod:
|
||||||
|
|
||||||
|
Configure server ``backups``
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``backups`` :
|
||||||
|
|
||||||
|
.. literalinclude:: ../../internal/config/samples/quickstart_continuous_server_backup_receiver.yml
|
||||||
|
|
||||||
|
Go Back To Quickstart Guide
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _quickstart-fan-out-replication:
|
||||||
|
|
||||||
|
Fan-out replication
|
||||||
|
===================
|
||||||
|
|
||||||
|
This quick-start example demonstrates how to implement a fan-out replication setup where datasets on a server (A) are replicated to multiple targets (B, C, etc.).
|
||||||
|
|
||||||
|
This example uses multiple ``source`` jobs on server A and ``pull`` jobs on the target servers.
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
Before implementing this setup, please see the caveats listed in the :ref:`fan-out replication configuration overview <fan-out-replication>`.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
On the source server (A), there should be:
|
||||||
|
|
||||||
|
* A ``snap`` job
|
||||||
|
|
||||||
|
* Creates the snapshots
|
||||||
|
* Handles the pruning of snapshots
|
||||||
|
|
||||||
|
* A ``source`` job for target B
|
||||||
|
|
||||||
|
* Accepts connections from server B and B only
|
||||||
|
|
||||||
|
* Further ``source`` jobs for each additional target (C, D, etc.)
|
||||||
|
|
||||||
|
* Listens on a unique port
|
||||||
|
* Only accepts connections from the specific target
|
||||||
|
|
||||||
|
On each target server, there should be:
|
||||||
|
|
||||||
|
* A ``pull`` job that connects to the corresponding ``source`` job on A
|
||||||
|
|
||||||
|
* ``prune_sender`` should keep all snapshots since A's ``snap`` job handles the pruning
|
||||||
|
* ``prune_receiver`` can be configured as appropriate on each target server
|
||||||
|
|
||||||
|
Generate TLS Certificates
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Mutual TLS via the :ref:`TLS client authentication transport <transport-tcp+tlsclientauth>` can be used to secure the connections between the servers. In this example, a self-signed certificate is created for each server without setting up a CA.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
source=a.example.com
|
||||||
|
targets=(
|
||||||
|
b.example.com
|
||||||
|
c.example.com
|
||||||
|
# ...
|
||||||
|
)
|
||||||
|
|
||||||
|
for server in "${source}" "${targets[@]}"; do
|
||||||
|
openssl req -x509 -sha256 -nodes \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-days 365 \
|
||||||
|
-keyout "${server}.key" \
|
||||||
|
-out "${server}.crt" \
|
||||||
|
-addext "subjectAltName = DNS:${server}" \
|
||||||
|
-subj "/CN=${server}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Distribute each host's keypair
|
||||||
|
for server in "${source}" "${targets[@]}"; do
|
||||||
|
ssh root@"${server}" mkdir /etc/zrepl
|
||||||
|
scp "${server}".{crt,key} root@"${server}":/etc/zrepl/
|
||||||
|
done
|
||||||
|
|
||||||
|
# Distribute target certificates to the source
|
||||||
|
scp "${targets[@]/%/.crt}" root@"${source}":/etc/zrepl/
|
||||||
|
|
||||||
|
# Distribute source certificate to the targets
|
||||||
|
for server in "${targets[@]}"; do
|
||||||
|
scp "${source}.crt" root@"${server}":/etc/zrepl/
|
||||||
|
done
|
||||||
|
|
||||||
|
Configure source server A
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. literalinclude:: ../../internal/config/samples/quickstart_fan_out_replication_source.yml
|
||||||
|
|
||||||
|
Configure each target server
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. literalinclude:: ../../internal/config/samples/quickstart_fan_out_replication_target.yml
|
||||||
|
|
||||||
|
Go Back To Quickstart Guide
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
:ref:`Click here <quickstart-apply-config>` to go back to the quickstart guide.
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
alabaster==0.7.12
|
|
||||||
Babel==2.6.0
|
|
||||||
certifi==2018.10.15
|
|
||||||
chardet==3.0.4
|
|
||||||
docutils==0.14
|
|
||||||
idna==2.7
|
|
||||||
imagesize==1.1.0
|
|
||||||
Jinja2==2.10
|
|
||||||
MarkupSafe==1.1.0
|
|
||||||
packaging==18.0
|
|
||||||
Pygments==2.2.0
|
|
||||||
pyparsing==2.3.0
|
|
||||||
pytz==2018.7
|
|
||||||
requests==2.20.1
|
|
||||||
six==1.11.0
|
|
||||||
snowballstemmer==1.2.1
|
|
||||||
Sphinx==1.8.2
|
|
||||||
sphinx-rtd-theme==0.4.2
|
|
||||||
sphinxcontrib-websupport==1.1.0
|
|
||||||
urllib3==1.24.1
|
|
||||||
@@ -1,182 +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']
|
|
||||||
|
|
||||||
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-2018, 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 '),
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|
.. _supporters:
|
||||||
|
|
||||||
|
|Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||||
|
|
||||||
|
zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||||
|
You can support maintenance and feature development through one of the services listed above.
|
||||||
|
For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
|
**Thanks for your support!**
|
||||||
|
|
||||||
|
.. NOTE ::
|
||||||
|
|
||||||
|
PayPal takes a relatively high fixed processing fee plus percentage of the donation.
|
||||||
|
Larger less-frequent donations make more sense there.
|
||||||
|
|
||||||
|
Supporters
|
||||||
|
==========
|
||||||
|
|
||||||
|
We would like to thank the following people and organizations for supporting zrepl through monetary and other means:
|
||||||
|
|
||||||
|
.. |supporter-std| raw:: html
|
||||||
|
|
||||||
|
<div class="fa fa-star-o" style="width: 1em;"></div>
|
||||||
|
|
||||||
|
.. |supporter-gold| raw:: html
|
||||||
|
|
||||||
|
<div class="fa fa-star" style="width: 1em;"></div>
|
||||||
|
|
||||||
|
.. |supporter-code| raw:: html
|
||||||
|
|
||||||
|
<div class="fa fa-code" style="width: 1em;"></div>
|
||||||
|
|
||||||
|
..
|
||||||
|
The list below is (roughly) sorted by date of latest contribution, newest first.
|
||||||
|
|
||||||
|
..
|
||||||
|
↓ post v0.7.0
|
||||||
|
|
||||||
|
* |supporter-std| `drbawb <https://git.sr.ht/~hime>`_
|
||||||
|
|
||||||
|
..
|
||||||
|
↓ claude --permission-mode default /update-supporters v0.6.1..v0.7.0
|
||||||
|
|
||||||
|
* |supporter-code| `Bakhtiyar Neyman <https://github.com/bakhtiyarneyman>`_
|
||||||
|
* |supporter-code| `Zeyad Tamimi <https://github.com/ZeyadTamimi>`_
|
||||||
|
* |supporter-code| `Orsiris de Jong <https://github.com/deajan>`_
|
||||||
|
* |supporter-code| `Martin Glatzle <https://github.com/findesgh>`_
|
||||||
|
* |supporter-code| `Andrew Lorimer <https://github.com/alorimer>`_
|
||||||
|
* |supporter-code| `Adam Nielsen <https://github.com/Malvineous>`_
|
||||||
|
* |supporter-code| `wxiaoguang <https://github.com/wxiaoguang>`_
|
||||||
|
* |supporter-code| `Logan Pulley <https://github.com/lpulley>`_
|
||||||
|
* |supporter-code| `Florian <https://github.com/Raupinger>`_
|
||||||
|
* |supporter-code| `Fermín Olaiz <https://github.com/fermino>`_
|
||||||
|
* |supporter-code| `Denis Shaposhnikov <https://github.com/dsh2dsh>`_
|
||||||
|
..
|
||||||
|
↓ Before /update-supporters
|
||||||
|
* |supporter-gold| `Hostsharing eG – die Hosting-Genossenschaft <https://www.hostsharing.net/>`_
|
||||||
|
* |supporter-std| `Max Christian Pohle <https://coderonline.de>`_
|
||||||
|
* |supporter-gold| Prominic.NET, Inc.
|
||||||
|
* |supporter-std| Torsten Blum
|
||||||
|
* |supporter-gold| Cyberiada GmbH
|
||||||
|
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
||||||
|
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
||||||
|
* |supporter-std| `Michael D. Schmitt <https://waterbendingscroll.dancingdragons.org>`_
|
||||||
|
* |supporter-std| `Hans Schulz <https://github.com/schulzh>`_
|
||||||
|
* |supporter-std| Henning Kessler
|
||||||
|
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
|
||||||
|
* |supporter-std| `DrLuke <https://github.com/drluke>`_
|
||||||
|
* |supporter-gold| `Mateusz Kwiatkowski (runhyve.app) <https://runhyve.app>`_
|
||||||
|
* |supporter-std| `Gaelan D'costa <https://www.robot-disco.net>`_
|
||||||
|
* |supporter-std| `Tenzin Lhakhang <https://confluence.tenzin.io>`_
|
||||||
|
* |supporter-std| `Lapo Luchini <https://github.com/lapo-luchini>`_
|
||||||
|
* |supporter-std| `F. Schmid <https://github.com/tdschmidl>`_
|
||||||
|
* |supporter-gold| `MNX.io <https://mnx.io>`_
|
||||||
|
* |supporter-std| `Marshall Clyburn <https://github.com/mdclyburn>`_
|
||||||
|
* |supporter-code| `Ross Williams <https://github.com/overhacked>`_
|
||||||
|
* |supporter-gold| Mike T.
|
||||||
|
* |supporter-code| `Justin Scholz <https://github.com/JMoVS>`_
|
||||||
|
* |supporter-code| `InsanePrawn <https://github.com/InsanePrawn>`_
|
||||||
|
* |supporter-code| `Ben Woods <https://www.freshports.org/sysutils/zrepl/>`_
|
||||||
|
* |supporter-code| `Janis Streib <https://github.com/janisstreib>`_
|
||||||
|
* |supporter-code| `Anton Schirg <https://github.com/antonxy>`_
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
.. include:: global.rst.inc
|
|
||||||
|
|
||||||
.. _tutorial:
|
|
||||||
|
|
||||||
Tutorial
|
|
||||||
========
|
|
||||||
|
|
||||||
|
|
||||||
This tutorial shows how zrepl can be used to implement a ZFS-based push backup.
|
|
||||||
We assume the following scenario:
|
|
||||||
|
|
||||||
* Production server ``prod`` with filesystems to back up:
|
|
||||||
|
|
||||||
* ``zroot/var/db``
|
|
||||||
* ``zroot/usr/home`` and all its child filesystems
|
|
||||||
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
|
||||||
|
|
||||||
* Backup server ``backups`` with
|
|
||||||
|
|
||||||
* Filesystem ``storage/zrepl/sink/prod`` + children dedicated to backups of ``prod``
|
|
||||||
|
|
||||||
Our backup solution should fulfill the following requirements:
|
|
||||||
|
|
||||||
* Periodically snapshot the filesystems on ``prod`` *every 10 minutes*
|
|
||||||
* Incrementally replicate these snapshots to ``storage/zrepl/sink/prod/*`` on ``backups``
|
|
||||||
* Keep only very few snapshots on ``prod`` to save disk space
|
|
||||||
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on ``backups``
|
|
||||||
|
|
||||||
Analysis
|
|
||||||
--------
|
|
||||||
|
|
||||||
We can model this situation as two jobs:
|
|
||||||
|
|
||||||
* A **push job** on ``prod``
|
|
||||||
|
|
||||||
* Creates the snapshots
|
|
||||||
* Keeps a short history of local snapshots to enable incremental replication to ``backups``
|
|
||||||
* Connects to the ``zrepl daemon`` process on ``backups``
|
|
||||||
* Pushes snapshots ``backups``
|
|
||||||
* Prunes snapshots on ``backups`` after replication is complete
|
|
||||||
|
|
||||||
* A **sink job** on ``backups``
|
|
||||||
|
|
||||||
* Accepts connections & responds to requests from ``prod``
|
|
||||||
* Limits client ``prod`` access to filesystem sub-tree ``storage/zrepl/sink/prod``
|
|
||||||
|
|
||||||
Install zrepl
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
|
|
||||||
|
|
||||||
Generate TLS Certificates
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
We use the `TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
|
|
||||||
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
|
|
||||||
Again, for convenience, We generate the key pairs on our local machine and distribute them using ssh:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
:emphasize-lines: 6,13
|
|
||||||
|
|
||||||
openssl req -x509 -sha256 -nodes \
|
|
||||||
-newkey rsa:4096 \
|
|
||||||
-days 365 \
|
|
||||||
-keyout backups.key \
|
|
||||||
-out backups.crt
|
|
||||||
# ... and use "backups" as Common Name (CN)
|
|
||||||
|
|
||||||
openssl req -x509 -sha256 -nodes \
|
|
||||||
-newkey rsa:4096 \
|
|
||||||
-days 365 \
|
|
||||||
-keyout prod.key \
|
|
||||||
-out prod.crt
|
|
||||||
# ... and use "prod" as Common Name (CN)
|
|
||||||
|
|
||||||
ssh root@backups "mkdir /etc/zrepl"
|
|
||||||
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
|
|
||||||
|
|
||||||
ssh root@prod "mkdir /etc/zrepl"
|
|
||||||
scp prod.key prod.crt backups.crt root@prod:/etc/zrepl
|
|
||||||
|
|
||||||
|
|
||||||
Configure server ``prod``
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
We define a **push job** named ``prod_to_backups`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` : ::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- name: prod_to_backups
|
|
||||||
type: push
|
|
||||||
connect:
|
|
||||||
type: tls
|
|
||||||
address: "backups.example.com:8888"
|
|
||||||
ca: /etc/zrepl/backups.crt
|
|
||||||
cert: /etc/zrepl/prod.crt
|
|
||||||
key: /etc/zrepl/prod.key
|
|
||||||
server_cn: "backups"
|
|
||||||
filesystems: {
|
|
||||||
"zroot/var/db:": true,
|
|
||||||
"zroot/usr/home<": true,
|
|
||||||
"zroot/usr/home/paranoid": false
|
|
||||||
}
|
|
||||||
snapshotting:
|
|
||||||
type: periodic
|
|
||||||
prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: not_replicated
|
|
||||||
- type: last_n
|
|
||||||
count: 10
|
|
||||||
keep_receiver:
|
|
||||||
- type: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
|
|
||||||
regex: "^zrepl_"
|
|
||||||
|
|
||||||
.. _tutorial-configure-prod:
|
|
||||||
|
|
||||||
Configure server ``backups``
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` : ::
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- name: sink
|
|
||||||
type: sink
|
|
||||||
serve:
|
|
||||||
type: tls
|
|
||||||
listen: ":8888"
|
|
||||||
ca: "/etc/zrepl/prod.crt"
|
|
||||||
cert: "/etc/zrepl/backups.crt"
|
|
||||||
key: "/etc/zrepl/backups.key"
|
|
||||||
client_cns:
|
|
||||||
- "prod"
|
|
||||||
root_fs: "storage/zrepl/sink"
|
|
||||||
|
|
||||||
|
|
||||||
Apply Configuration Changes
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
We use ``zrepl configcheck`` before to catch any configuration errors: no output indicates that everything is fine.
|
|
||||||
If that is the case, restart the zrepl daemon on **both** ``prod`` and ``backups`` using ``service zrepl restart`` or ``systemctl restart zrepl``.
|
|
||||||
|
|
||||||
|
|
||||||
Watch it Work
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Run ``zrepl status`` on ``prod`` to monitor the replication and pruning activity.
|
|
||||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup prod_to_backups`` on ``prod``.
|
|
||||||
|
|
||||||
If you like tmux, here is a handy script that works on FreeBSD: ::
|
|
||||||
|
|
||||||
pkg install gnu-watch tmux
|
|
||||||
tmux new -s zrepl -d
|
|
||||||
tmux split-window -t zrepl "tail -f /var/log/messages"
|
|
||||||
tmux split-window -t zrepl "gnu-watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
|
|
||||||
tmux split-window -t zrepl "zrepl status"
|
|
||||||
tmux select-layout -t zrepl tiled
|
|
||||||
tmux attach -t zrepl
|
|
||||||
|
|
||||||
The Linux equivalent might look like this: ::
|
|
||||||
|
|
||||||
# make sure tmux is installed & let's assume you use systemd + journald
|
|
||||||
tmux new -s zrepl -d
|
|
||||||
tmux split-window -t zrepl "journalctl -f -u zrepl.service"
|
|
||||||
tmux split-window -t zrepl "watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
|
|
||||||
tmux split-window -t zrepl "zrepl status"
|
|
||||||
tmux select-layout -t zrepl tiled
|
|
||||||
tmux attach -t zrepl
|
|
||||||
|
|
||||||
Summary
|
|
||||||
-------
|
|
||||||
|
|
||||||
Congratulations, you have a working push backup. Where to go next?
|
|
||||||
|
|
||||||
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
|
||||||
* Configure :ref:`logging <logging>` \& :ref:`monitoring <monitoring>`.
|
|
||||||
* Learn about :ref:`implementation details <implementation_toc>` of zrepl.
|
|
||||||
|
|
||||||
|
|
||||||
+75
-3
@@ -26,7 +26,7 @@ CLI Overview
|
|||||||
* - ``zrepl daemon``
|
* - ``zrepl daemon``
|
||||||
- run the daemon, required for all zrepl functionality
|
- run the daemon, required for all zrepl functionality
|
||||||
* - ``zrepl status``
|
* - ``zrepl status``
|
||||||
- show job activity, or with ``--raw`` for JSON output
|
- show job activity, or with ``--mode raw`` for JSON output
|
||||||
* - ``zrepl stdinserver``
|
* - ``zrepl stdinserver``
|
||||||
- see :ref:`transport-ssh+stdinserver`
|
- see :ref:`transport-ssh+stdinserver`
|
||||||
* - ``zrepl signal wakeup JOB``
|
* - ``zrepl signal wakeup JOB``
|
||||||
@@ -38,6 +38,8 @@ CLI Overview
|
|||||||
* - ``zrepl migrate``
|
* - ``zrepl migrate``
|
||||||
- | perform on-disk state / ZFS property migrations
|
- | perform on-disk state / ZFS property migrations
|
||||||
| (see :ref:`changelog <changelog>` for details)
|
| (see :ref:`changelog <changelog>` for details)
|
||||||
|
* - ``zrepl zfs-abstraction``
|
||||||
|
- list and remove zrepl's abstractions on top of ZFS, e.g. holds and step bookmarks (see :ref:`overview <replication-cursor-and-last-received-hold>` )
|
||||||
|
|
||||||
.. _usage-zrepl-daemon:
|
.. _usage-zrepl-daemon:
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ zrepl daemon
|
|||||||
All actual work zrepl does is performed by a daemon process.
|
All actual work zrepl does is performed by a daemon process.
|
||||||
The daemon supports structured :ref:`logging <logging>` and provides :ref:`monitoring endpoints <monitoring>`.
|
The daemon supports structured :ref:`logging <logging>` and provides :ref:`monitoring endpoints <monitoring>`.
|
||||||
|
|
||||||
When installating from a package, the package maintainer should have provided an init script / systemd.service file.
|
When installing from a package, the package maintainer should have provided an init script / systemd.service file.
|
||||||
You should thus be able to start zrepl daemon using your init system.
|
You should thus be able to start zrepl daemon using your init system.
|
||||||
|
|
||||||
Alternatively, or for running zrepl in the foreground, simply execute ``zrepl daemon``.
|
Alternatively, or for running zrepl in the foreground, simply execute ``zrepl daemon``.
|
||||||
@@ -73,6 +75,76 @@ The daemon exits as soon as all jobs have reported shut down.
|
|||||||
Systemd Unit File
|
Systemd Unit File
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
A systemd service defintion template is available in :repomasterlink:`dist/systemd`.
|
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
|
||||||
Note that some of the options only work on recent versions of systemd.
|
Note that some of the options only work on recent versions of systemd.
|
||||||
Any help & improvements are very welcome, see :issue:`145`.
|
Any help & improvements are very welcome, see :issue:`145`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
============
|
||||||
|
Ops Runbooks
|
||||||
|
============
|
||||||
|
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
usage/runbooks/migrating_sending_side_to_new_zpool.rst
|
||||||
|
|
||||||
|
|
||||||
|
.. _usage-platform-tests:
|
||||||
|
|
||||||
|
==============
|
||||||
|
Platform Tests
|
||||||
|
==============
|
||||||
|
|
||||||
|
Along with the main ``zrepl`` binary, we release the ``platformtest`` binaries.
|
||||||
|
The zrepl platform tests are an integration test suite that is complementary to the pure Go unit tests.
|
||||||
|
Any test that needs to interact with ZFS is a platform test.
|
||||||
|
|
||||||
|
The platform need to run as root.
|
||||||
|
For each test, we create a fresh dummy zpool backed by a file-based vdev.
|
||||||
|
The file path, and a root mountpoint for the dummy zpool, must be specified on the command line:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p /tmp/zreplplatformtest
|
||||||
|
./platformtest \
|
||||||
|
-poolname 'zreplplatformtest' \ # <- name must contain zreplplatformtest
|
||||||
|
-imagepath /tmp/zreplplatformtest.img \ # <- zrepl will create the file
|
||||||
|
-mountpoint /tmp/zreplplatformtest # <- must exist
|
||||||
|
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
``platformtest`` will unconditionally overwrite the file at `imagepath`
|
||||||
|
and unconditionally ``zpool destroy $poolname``.
|
||||||
|
So, don't use a production poolname, and consider running the test in a VM.
|
||||||
|
It'll be a lot faster as well because the underlying operations, ``zfs list`` in particular, will be faster.
|
||||||
|
|
||||||
|
|
||||||
|
While the platformtests are running, there will be a log of log output.
|
||||||
|
After all tests have run, it prints a summary with a list of tests, grouped by result type (success, failure, skipped):
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
PASSING TESTS:
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.BatchDestroy
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.CreateReplicationCursor
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.GetNonexistent
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.HoldsWork
|
||||||
|
...
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.SendStreamNonEOFReadErrorHandling
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.UndestroyableSnapshotParsing
|
||||||
|
SKIPPED TESTS:
|
||||||
|
github.com/zrepl/zrepl/platformtest/tests.SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false
|
||||||
|
FAILED TESTS: []
|
||||||
|
|
||||||
|
|
||||||
|
If there is a failure, or a skipped test that you believe should be passing, re-run the test suite, capture stderr & stdout to a text file, and create an issue on GitHub.
|
||||||
|
|
||||||
|
To run a specific test case, or a subset of tests matched by regex, use the ``-run REGEX`` command line flag.
|
||||||
|
|
||||||
|
To stop test execution at the first failing test, and prevent cleanup of the dummy zpool, use the ``-failure.stop-and-keep-pool`` flag.
|
||||||
|
|
||||||
|
To build the platformtests yourself, use ``make test-platform-bin``.
|
||||||
|
There's also the ``make test-platform`` target to run the platform tests with a default command line.
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
Migrating Sending Side
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**Objective**:
|
||||||
|
Move sending-side zpool to new hardware.
|
||||||
|
Make the move fully transparent to the sending-side jobs.
|
||||||
|
After the move is done, all sending-side zrepl jobs should continue to work as if the move had not happened.
|
||||||
|
In particular, incremental replication should be able to pick up where it left before the move.
|
||||||
|
|
||||||
|
Suppose we want to migrate all data from one zpool ``oldpool`` to another zpool ``newpool``.
|
||||||
|
A possible reason might be that we want to change RAID levels, ``ashift``, or just migrate over to next-gen hardware.
|
||||||
|
|
||||||
|
If the pool names are different, zrepl's matching between sender and receiver dataset will break becase the receive-side dataset names contain ``oldpool``.
|
||||||
|
To avoid this, we will need the name of the new pool to match that of the old pool.
|
||||||
|
The following steps will accomplish this:
|
||||||
|
|
||||||
|
1. Stop zrepl.
|
||||||
|
2. Create the new pool: ``zpool create newpool ...``
|
||||||
|
3. Take a snapshot of the old pool so that you have something that you can ``zfs send``.
|
||||||
|
For example, run ``zfs snapshot -r oldpool@migration_oldpool_newpool``.
|
||||||
|
4. Send all of the oldpool's datasets to the new pool:
|
||||||
|
``zfs send -R oldpool@migration_oldpool_newpool | zfs recv -F newpool``
|
||||||
|
5. Export the old pool: ``zpool export oldpool``
|
||||||
|
6. Export the new pool: ``zpool export newpool``
|
||||||
|
7. (Optional) Change the name of the old pool to something that does not conflict with the new pool.
|
||||||
|
We are going to use the name ``oldoldpool`` in this example.
|
||||||
|
Use ``zpool import`` with no arguments to see the pool id.
|
||||||
|
Then ``zpool import <id> oldoldpool && zpool export oldoldpool``.
|
||||||
|
8. Import the new pool, while changing the name to match the old pool: ``zpool import newpool oldpool``
|
||||||
|
9. Start zrepl again and wake up the relevant jobs.
|
||||||
|
10. Use ``zrepl status`` or you monitoring to ensure that replication works.
|
||||||
|
The best test is an end-to-end test where you write some junk data on a sender dataset and wait until a snapshot with that data appears on the receiving side.
|
||||||
|
11. Once you are confident that replication is working, you may dispose of the old pool.
|
||||||
|
|
||||||
|
Note that, depending on pruning rules, it will not be possible to switch back to the old pool seamlessly, i.e., without a full re-replication.
|
||||||
Generated
+306
@@ -0,0 +1,306 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alabaster"
|
||||||
|
version = "0.7.16"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "babel"
|
||||||
|
version = "2.18.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.1.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docutils"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/b1/b880503681ea1b64df05106fc7e3c4e3801736cf63deffc6fa7fc5404cf5/docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06", size = 2043249, upload-time = "2021-11-23T17:49:42.043Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/14/69b4bad34e3f250afe29a854da03acb6747711f3df06c359fa053fae4e76/docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", size = 570050, upload-time = "2021-11-23T17:49:38.556Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imagesize"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snowballstemmer"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinx"
|
||||||
|
version = "7.3.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "alabaster" },
|
||||||
|
{ name = "babel" },
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "docutils" },
|
||||||
|
{ name = "imagesize" },
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "snowballstemmer" },
|
||||||
|
{ name = "sphinxcontrib-applehelp" },
|
||||||
|
{ name = "sphinxcontrib-devhelp" },
|
||||||
|
{ name = "sphinxcontrib-htmlhelp" },
|
||||||
|
{ name = "sphinxcontrib-jsmath" },
|
||||||
|
{ name = "sphinxcontrib-qthelp" },
|
||||||
|
{ name = "sphinxcontrib-serializinghtml" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b7/0a/b88033900b1582f5ed8f880263363daef968d1cd064175e32abfd9714410/sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc", size = 7094808, upload-time = "2024-04-19T04:44:48.297Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/fa/130c32ed94cf270e3d0b9ded16fb7b2c8fea86fa7263c29a696a30c1dde7/sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3", size = 3335650, upload-time = "2024-04-19T04:44:43.839Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinx-rtd-theme"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "docutils" },
|
||||||
|
{ name = "sphinx" },
|
||||||
|
{ name = "sphinxcontrib-jquery" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/db/3e/477c5b3ed78b6818d673f63512db12ace8c89e83eb9eecc913f9e2cc8416/sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931", size = 2785069, upload-time = "2023-08-21T18:28:35.63Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/01/76f40a18e9209bb098c1c1313c823dbbd001b23a2db71e7fd4eb5a48559c/sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0", size = 2824803, upload-time = "2023-08-21T18:28:32.926Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinxcontrib-applehelp"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinxcontrib-devhelp"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinxcontrib-htmlhelp"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinxcontrib-jquery"
|
||||||
|
version = "4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "sphinx" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinxcontrib-jsmath"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinxcontrib-qthelp"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinxcontrib-serializinghtml"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.6.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zrepl-docs"
|
||||||
|
version = "0.0.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "sphinx" },
|
||||||
|
{ name = "sphinx-rtd-theme" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "sphinx", specifier = ">=7.2,<8" },
|
||||||
|
{ name = "sphinx-rtd-theme", specifier = ">=1.3,<2" },
|
||||||
|
]
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package endpoint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextKeyLogger contextKey = iota
|
|
||||||
ClientIdentityKey
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger = logger.Logger
|
|
||||||
|
|
||||||
func WithLogger(ctx context.Context, log Logger) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKeyLogger, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLogger(ctx context.Context) Logger {
|
|
||||||
if l, ok := ctx.Value(contextKeyLogger).(Logger); ok {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
return logger.NewNullLogger()
|
|
||||||
}
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
// Package endpoint implements replication endpoints for use with package replication.
|
|
||||||
package endpoint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sender implements replication.ReplicationEndpoint for a sending side
|
|
||||||
type Sender struct {
|
|
||||||
FSFilter zfs.DatasetFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSender(fsf zfs.DatasetFilter) *Sender {
|
|
||||||
return &Sender{FSFilter: fsf}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sender) filterCheckFS(fs string) (*zfs.DatasetPath, error) {
|
|
||||||
dp, err := zfs.NewDatasetPath(fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if dp.Length() == 0 {
|
|
||||||
return nil, errors.New("empty filesystem not allowed")
|
|
||||||
}
|
|
||||||
pass, err := s.FSFilter.Filter(dp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !pass {
|
|
||||||
return nil, fmt.Errorf("endpoint does not allow access to filesystem %s", fs)
|
|
||||||
}
|
|
||||||
return dp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
|
||||||
fss, err := zfs.ZFSListMapping(ctx, s.FSFilter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rfss := make([]*pdu.Filesystem, len(fss))
|
|
||||||
for i := range fss {
|
|
||||||
rfss[i] = &pdu.Filesystem{
|
|
||||||
Path: fss[i].ToString(),
|
|
||||||
// FIXME: not supporting ResumeToken yet
|
|
||||||
IsPlaceholder: false, // sender FSs are never placeholders
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := &pdu.ListFilesystemRes{Filesystems: rfss}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error) {
|
|
||||||
lp, err := s.filterCheckFS(r.GetFilesystem())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rfsvs := make([]*pdu.FilesystemVersion, len(fsvs))
|
|
||||||
for i := range fsvs {
|
|
||||||
rfsvs[i] = pdu.FilesystemVersionFromZFS(&fsvs[i])
|
|
||||||
}
|
|
||||||
res := &pdu.ListFilesystemVersionsRes{Versions: rfsvs}
|
|
||||||
return res, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
|
||||||
_, err := s.filterCheckFS(r.Filesystem)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
si, err := zfs.ZFSSendDry(r.Filesystem, r.From, r.To, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
var expSize int64 = 0 // protocol says 0 means no estimate
|
|
||||||
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
|
|
||||||
expSize = si.SizeEstimate
|
|
||||||
}
|
|
||||||
res := &pdu.SendRes{ExpectedSize: expSize}
|
|
||||||
|
|
||||||
if r.DryRun {
|
|
||||||
return res, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
streamCopier, err := zfs.ZFSSend(ctx, r.Filesystem, r.From, r.To, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return res, streamCopier, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Sender) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
|
||||||
dp, err := p.filterCheckFS(req.Filesystem)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return doDestroySnapshots(ctx, dp, req.Snapshots)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Sender) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
|
||||||
res := pdu.PingRes{
|
|
||||||
Echo: req.GetMessage(),
|
|
||||||
}
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Sender) PingDataconn(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
|
||||||
return p.Ping(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Sender) WaitForConnectivity(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Sender) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
|
||||||
dp, err := p.filterCheckFS(req.Filesystem)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch op := req.Op.(type) {
|
|
||||||
case *pdu.ReplicationCursorReq_Get:
|
|
||||||
cursor, err := zfs.ZFSGetReplicationCursor(dp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if cursor == nil {
|
|
||||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Notexist{Notexist: true}}, nil
|
|
||||||
}
|
|
||||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil
|
|
||||||
case *pdu.ReplicationCursorReq_Set:
|
|
||||||
guid, err := zfs.ZFSSetReplicationCursor(dp, op.Set.Snapshot)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: guid}}, nil
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("unknown op %T", op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
|
||||||
return nil, fmt.Errorf("sender does not implement Receive()")
|
|
||||||
}
|
|
||||||
|
|
||||||
type FSFilter interface { // FIXME unused
|
|
||||||
Filter(path *zfs.DatasetPath) (pass bool, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: can we get away without error types here?
|
|
||||||
type FSMap interface { // FIXME unused
|
|
||||||
FSFilter
|
|
||||||
Map(path *zfs.DatasetPath) (*zfs.DatasetPath, error)
|
|
||||||
Invert() (FSMap, error)
|
|
||||||
AsFilter() FSFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver implements replication.ReplicationEndpoint for a receiving side
|
|
||||||
type Receiver struct {
|
|
||||||
rootWithoutClientComponent *zfs.DatasetPath
|
|
||||||
appendClientIdentity bool
|
|
||||||
|
|
||||||
recvParentCreationMtx *chainlock.L
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReceiver(rootDataset *zfs.DatasetPath, appendClientIdentity bool) *Receiver {
|
|
||||||
if rootDataset.Length() <= 0 {
|
|
||||||
panic(fmt.Sprintf("root dataset must not be an empty path: %v", rootDataset))
|
|
||||||
}
|
|
||||||
return &Receiver{
|
|
||||||
rootWithoutClientComponent: rootDataset.Copy(),
|
|
||||||
appendClientIdentity: appendClientIdentity,
|
|
||||||
recvParentCreationMtx: chainlock.New(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClientIdentity(rootFS *zfs.DatasetPath, clientIdentity string) error {
|
|
||||||
_, err := clientRoot(rootFS, clientIdentity)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func clientRoot(rootFS *zfs.DatasetPath, clientIdentity string) (*zfs.DatasetPath, error) {
|
|
||||||
rootFSLen := rootFS.Length()
|
|
||||||
clientRootStr := path.Join(rootFS.ToString(), clientIdentity)
|
|
||||||
clientRoot, err := zfs.NewDatasetPath(clientRootStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if rootFSLen+1 != clientRoot.Length() {
|
|
||||||
return nil, fmt.Errorf("client identity must be a single ZFS filesystem path component")
|
|
||||||
}
|
|
||||||
return clientRoot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) clientRootFromCtx(ctx context.Context) *zfs.DatasetPath {
|
|
||||||
if !s.appendClientIdentity {
|
|
||||||
return s.rootWithoutClientComponent.Copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
clientIdentity, ok := ctx.Value(ClientIdentityKey).(string)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("ClientIdentityKey context value must be set"))
|
|
||||||
}
|
|
||||||
|
|
||||||
clientRoot, err := clientRoot(s.rootWithoutClientComponent, clientIdentity)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("ClientIdentityContextKey must have been validated before invoking Receiver: %s", err))
|
|
||||||
}
|
|
||||||
return clientRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
type subroot struct {
|
|
||||||
localRoot *zfs.DatasetPath
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ zfs.DatasetFilter = subroot{}
|
|
||||||
|
|
||||||
// Filters local p
|
|
||||||
func (f subroot) Filter(p *zfs.DatasetPath) (pass bool, err error) {
|
|
||||||
return p.HasPrefix(f.localRoot) && !p.Equal(f.localRoot), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
|
|
||||||
p, err := zfs.NewDatasetPath(fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if p.Length() == 0 {
|
|
||||||
return nil, errors.Errorf("cannot map empty filesystem")
|
|
||||||
}
|
|
||||||
c := f.localRoot.Copy()
|
|
||||||
c.Extend(p)
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
|
||||||
filtered, err := zfs.ZFSListMapping(ctx, subroot{root})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// present filesystem without the root_fs prefix
|
|
||||||
fss := make([]*pdu.Filesystem, 0, len(filtered))
|
|
||||||
for _, a := range filtered {
|
|
||||||
l := getLogger(ctx).WithField("fs", a)
|
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(a)
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Error("error getting placeholder state")
|
|
||||||
return nil, errors.Wrapf(err, "cannot get placeholder state for fs %q", a)
|
|
||||||
}
|
|
||||||
l.WithField("placeholder_state", fmt.Sprintf("%#v", ph)).Debug("placeholder state")
|
|
||||||
if !ph.FSExists {
|
|
||||||
l.Error("inconsistent placeholder state: filesystem must exists")
|
|
||||||
err := errors.Errorf("inconsistent placeholder state: filesystem %q must exist in this context", a.ToString())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
a.TrimPrefix(root)
|
|
||||||
fss = append(fss, &pdu.Filesystem{Path: a.ToString(), IsPlaceholder: ph.IsPlaceholder})
|
|
||||||
}
|
|
||||||
if len(fss) == 0 {
|
|
||||||
getLogger(ctx).Debug("no filesystems found")
|
|
||||||
return &pdu.ListFilesystemRes{}, nil
|
|
||||||
}
|
|
||||||
return &pdu.ListFilesystemRes{Filesystems: fss}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error) {
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
|
||||||
lp, err := subroot{root}.MapToLocal(req.GetFilesystem())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rfsvs := make([]*pdu.FilesystemVersion, len(fsvs))
|
|
||||||
for i := range fsvs {
|
|
||||||
rfsvs[i] = pdu.FilesystemVersionFromZFS(&fsvs[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return &pdu.ListFilesystemVersionsRes{Versions: rfsvs}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
|
||||||
res := pdu.PingRes{
|
|
||||||
Echo: req.GetMessage(),
|
|
||||||
}
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) PingDataconn(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
|
|
||||||
return s.Ping(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) WaitForConnectivity(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) ReplicationCursor(context.Context, *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
|
|
||||||
return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
|
|
||||||
return nil, nil, fmt.Errorf("receiver does not implement Send()")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
|
|
||||||
getLogger(ctx).Debug("incoming Receive")
|
|
||||||
defer receive.Close()
|
|
||||||
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
|
||||||
lp, err := subroot{root}.MapToLocal(req.Filesystem)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// create placeholder parent filesystems as appropriate
|
|
||||||
//
|
|
||||||
// Manipulating the ZFS dataset hierarchy must happen exclusively.
|
|
||||||
// TODO: Use fine-grained locking to allow separate clients / requests to pass
|
|
||||||
// through the following section concurrently when operating on disjoint
|
|
||||||
// ZFS dataset hierarchy subtrees.
|
|
||||||
var visitErr error
|
|
||||||
func() {
|
|
||||||
getLogger(ctx).Debug("begin aquire recvParentCreationMtx")
|
|
||||||
defer s.recvParentCreationMtx.Lock().Unlock()
|
|
||||||
getLogger(ctx).Debug("end aquire recvParentCreationMtx")
|
|
||||||
defer getLogger(ctx).Debug("release recvParentCreationMtx")
|
|
||||||
|
|
||||||
f := zfs.NewDatasetPathForest()
|
|
||||||
f.Add(lp)
|
|
||||||
getLogger(ctx).Debug("begin tree-walk")
|
|
||||||
f.WalkTopDown(func(v zfs.DatasetPathVisit) (visitChildTree bool) {
|
|
||||||
if v.Path.Equal(lp) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(v.Path)
|
|
||||||
getLogger(ctx).
|
|
||||||
WithField("fs", v.Path.ToString()).
|
|
||||||
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
|
|
||||||
WithField("err", fmt.Sprintf("%s", err)).
|
|
||||||
WithField("errType", fmt.Sprintf("%T", err)).
|
|
||||||
Debug("placeholder state for filesystem")
|
|
||||||
if err != nil {
|
|
||||||
visitErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ph.FSExists {
|
|
||||||
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
|
|
||||||
l.Debug("create placeholder filesystem")
|
|
||||||
err := zfs.ZFSCreatePlaceholderFilesystem(v.Path)
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Error("cannot create placeholder filesystem")
|
|
||||||
visitErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
getLogger(ctx).WithField("filesystem", v.Path.ToString()).Debug("exists")
|
|
||||||
return true // leave this fs as is
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
getLogger(ctx).WithField("visitErr", visitErr).Debug("complete tree-walk")
|
|
||||||
if visitErr != nil {
|
|
||||||
return nil, visitErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine whether we need to rollback the filesystem / change its placeholder state
|
|
||||||
var clearPlaceholderProperty bool
|
|
||||||
var recvOpts zfs.RecvOptions
|
|
||||||
ph, err := zfs.ZFSGetFilesystemPlaceholderState(lp)
|
|
||||||
if err == nil && ph.FSExists && ph.IsPlaceholder {
|
|
||||||
recvOpts.RollbackAndForceRecv = true
|
|
||||||
clearPlaceholderProperty = true
|
|
||||||
}
|
|
||||||
if clearPlaceholderProperty {
|
|
||||||
if err := zfs.ZFSSetPlaceholder(lp, false); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogger(ctx).WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
|
|
||||||
|
|
||||||
if err := zfs.ZFSRecv(ctx, lp.ToString(), receive, recvOpts); err != nil {
|
|
||||||
getLogger(ctx).
|
|
||||||
WithError(err).
|
|
||||||
WithField("opts", recvOpts).
|
|
||||||
Error("zfs receive failed")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &pdu.ReceiveRes{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
|
|
||||||
root := s.clientRootFromCtx(ctx)
|
|
||||||
lp, err := subroot{root}.MapToLocal(req.Filesystem)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return doDestroySnapshots(ctx, lp, req.Snapshots)
|
|
||||||
}
|
|
||||||
|
|
||||||
func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.FilesystemVersion) (*pdu.DestroySnapshotsRes, error) {
|
|
||||||
fsvs := make([]*zfs.FilesystemVersion, len(snaps))
|
|
||||||
for i, fsv := range snaps {
|
|
||||||
if fsv.Type != pdu.FilesystemVersion_Snapshot {
|
|
||||||
return nil, fmt.Errorf("version %q is not a snapshot", fsv.Name)
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
fsvs[i], err = fsv.ZFSFilesystemVersion()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := &pdu.DestroySnapshotsRes{
|
|
||||||
Results: make([]*pdu.DestroySnapshotRes, len(fsvs)),
|
|
||||||
}
|
|
||||||
for i, fsv := range fsvs {
|
|
||||||
err := zfs.ZFSDestroyFilesystemVersion(lp, fsv)
|
|
||||||
errMsg := ""
|
|
||||||
if err != nil {
|
|
||||||
errMsg = err.Error()
|
|
||||||
}
|
|
||||||
res.Results[i] = &pdu.DestroySnapshotRes{
|
|
||||||
Snapshot: pdu.FilesystemVersionFromZFS(fsv),
|
|
||||||
Error: errMsg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
module github.com/zrepl/zrepl
|
||||||
|
|
||||||
|
go 1.24.13
|
||||||
|
|
||||||
|
toolchain go1.25.7
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bits-and-blooms/bitset v1.24.4
|
||||||
|
github.com/fatih/color v1.18.0
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.8
|
||||||
|
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909
|
||||||
|
github.com/go-logfmt/logfmt v0.6.1
|
||||||
|
github.com/go-playground/validator v9.31.0+incompatible
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jinzhu/copier v0.4.0
|
||||||
|
github.com/juju/ratelimit v1.0.2
|
||||||
|
github.com/kr/pretty v0.3.1
|
||||||
|
github.com/lib/pq v1.11.1
|
||||||
|
github.com/mattn/go-isatty v0.0.20
|
||||||
|
github.com/montanaflynn/stats v0.7.1
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/pkg/profile v1.7.0
|
||||||
|
github.com/problame/go-netssh v0.0.0-20201229122201-330ad6c9cc6f
|
||||||
|
github.com/prometheus/client_golang v1.23.2
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
github.com/spf13/pflag v1.0.10
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/yudai/gojsondiff v1.0.0
|
||||||
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd
|
||||||
|
golang.org/x/net v0.49.0
|
||||||
|
golang.org/x/sync v0.19.0
|
||||||
|
golang.org/x/sys v0.41.0
|
||||||
|
golang.org/x/tools v0.41.0
|
||||||
|
google.golang.org/grpc v1.78.0
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/felixge/fgprof v0.9.5 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef // indirect
|
||||||
|
github.com/hpcloud/tail v1.0.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/onsi/gomega v1.34.2 // indirect
|
||||||
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff // indirect
|
||||||
|
github.com/gdamore/encoding v1.0.1 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/onsi/ginkgo v1.10.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
|
github.com/rivo/tview v0.42.0
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sergi/go-diff v1.4.0 // indirect; go1.12 thinks it needs this
|
||||||
|
github.com/theckman/goconstraint v1.11.0 // indirect
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect; go1.12 thinks it needs this
|
||||||
|
github.com/yudai/pp v2.0.1+incompatible // indirect
|
||||||
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
|
golang.org/x/term v0.39.0 // indirect
|
||||||
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||||
|
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
|
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||||
|
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||||
|
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||||
|
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
||||||
|
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff h1:zk1wwii7uXmI0znwU+lqg+wFL9G5+vm5I+9rv2let60=
|
||||||
|
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff/go.mod h1:yUhRXHewUVJ1k89wHKP68xfzk7kwXUx/DV1nx4EBMbw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||||
|
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU=
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||||
|
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909 h1:9NC8seTx6/zRmMTAdsHj/uOMi0EGHGQtjyLafBjk77Q=
|
||||||
|
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909/go.mod h1:lP+DW8LR6Rw3ru9Vo2/y/3iiLaLWmofYql/va+7zJOk=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
|
||||||
|
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||||
|
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||||
|
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno=
|
||||||
|
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||||
|
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||||
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI=
|
||||||
|
github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94=
|
||||||
|
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
|
||||||
|
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||||
|
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/problame/go-netssh v0.0.0-20201229122201-330ad6c9cc6f h1:XlHuwwn1UROeb9RDS8Bjaa8xm0yIN2xtPXKxDwpKo2Y=
|
||||||
|
github.com/problame/go-netssh v0.0.0-20201229122201-330ad6c9cc6f/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
|
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
|
||||||
|
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
|
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0=
|
||||||
|
github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g=
|
||||||
|
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||||
|
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||||
|
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
|
||||||
|
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QESvbW8Meibz7zCrxshP71U9dH5KOCXM=
|
||||||
|
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/internal/daemon/logging/trace"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootArgs struct {
|
||||||
|
configPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "zrepl",
|
||||||
|
Short: "One-stop ZFS replication solution",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
var genCompletionCmd = &cobra.Command{
|
||||||
|
Use: "gencompletion",
|
||||||
|
Short: "generate shell auto-completions",
|
||||||
|
}
|
||||||
|
|
||||||
|
type completionCmdInfo struct {
|
||||||
|
genFunc func(outpath string) error
|
||||||
|
help string
|
||||||
|
}
|
||||||
|
|
||||||
|
var completionCmdMap = map[string]completionCmdInfo{
|
||||||
|
"zsh": {
|
||||||
|
rootCmd.GenZshCompletionFile,
|
||||||
|
" save to file `_zrepl` in your zsh's $fpath",
|
||||||
|
},
|
||||||
|
"bash": {
|
||||||
|
rootCmd.GenBashCompletionFile,
|
||||||
|
" save to a path and source that path in your .bashrc",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for sh, info := range completionCmdMap {
|
||||||
|
sh, info := sh, info
|
||||||
|
genCompletionCmd.AddCommand(&cobra.Command{
|
||||||
|
Use: fmt.Sprintf("%s path/to/out/file", sh),
|
||||||
|
Short: fmt.Sprintf("generate %s completions", sh),
|
||||||
|
Example: info.help,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||||
|
err := cmd.Usage()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := info.genFunc(args[0]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error generating %s completion: %s", sh, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rootCmd.AddCommand(genCompletionCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subcommand struct {
|
||||||
|
Use string
|
||||||
|
Short string
|
||||||
|
Example string
|
||||||
|
NoRequireConfig bool
|
||||||
|
Run func(ctx context.Context, subcommand *Subcommand, args []string) error
|
||||||
|
SetupFlags func(f *pflag.FlagSet)
|
||||||
|
SetupSubcommands func() []*Subcommand
|
||||||
|
|
||||||
|
config *config.Config
|
||||||
|
configErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subcommand) ConfigParsingError() error {
|
||||||
|
return s.configErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subcommand) Config() *config.Config {
|
||||||
|
if !s.NoRequireConfig && s.config == nil {
|
||||||
|
panic("command that requires config is running and has no config set")
|
||||||
|
}
|
||||||
|
return s.config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subcommand) run(cmd *cobra.Command, args []string) {
|
||||||
|
s.tryParseConfig()
|
||||||
|
ctx := context.Background()
|
||||||
|
endTask := trace.WithTaskFromStackUpdateCtx(&ctx)
|
||||||
|
defer endTask()
|
||||||
|
err := s.Run(ctx, s, args)
|
||||||
|
endTask()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subcommand) tryParseConfig() {
|
||||||
|
config, err := config.ParseConfig(rootArgs.configPath)
|
||||||
|
s.configErr = err
|
||||||
|
if err != nil {
|
||||||
|
if s.NoRequireConfig {
|
||||||
|
// doesn't matter
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "could not parse config: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddSubcommand(s *Subcommand) {
|
||||||
|
addSubcommandToCobraCmd(rootCmd, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) {
|
||||||
|
cmd := cobra.Command{
|
||||||
|
Use: s.Use,
|
||||||
|
Short: s.Short,
|
||||||
|
Example: s.Example,
|
||||||
|
}
|
||||||
|
if s.SetupSubcommands == nil {
|
||||||
|
cmd.Run = s.run
|
||||||
|
} else {
|
||||||
|
for _, sub := range s.SetupSubcommands() {
|
||||||
|
addSubcommandToCobraCmd(&cmd, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.SetupFlags != nil {
|
||||||
|
s.SetupFlags(cmd.Flags())
|
||||||
|
}
|
||||||
|
c.AddCommand(&cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,54 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/zrepl/yaml-config"
|
"github.com/zrepl/yaml-config"
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/internal/cli"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/internal/config"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/internal/daemon/job"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/internal/daemon/logging"
|
||||||
"os"
|
"github.com/zrepl/zrepl/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configcheckArgs struct {
|
var configcheckArgs struct {
|
||||||
format string
|
format string
|
||||||
what string
|
what string
|
||||||
|
skipCertCheck bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var ConfigcheckCmd = &cli.Subcommand{
|
var ConfigcheckCmd = &cli.Subcommand{
|
||||||
Use: "configcheck",
|
Use: "configcheck",
|
||||||
Short: "check if config can be parsed without errors",
|
Short: "check if config can be parsed without errors",
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
|
||||||
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
|
||||||
|
f.BoolVar(&configcheckArgs.skipCertCheck, "skip-cert-check", false, "skip checking cert files")
|
||||||
},
|
},
|
||||||
Run: func(subcommand *cli.Subcommand, args []string) error {
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
formatMap := map[string]func(interface{}) {
|
formatMap := map[string]func(interface{}){
|
||||||
"": func(i interface{}) {},
|
"": func(i interface{}) {},
|
||||||
"pretty": func(i interface{}) { pretty.Println(i) },
|
"pretty": func(i interface{}) {
|
||||||
|
if _, err := pretty.Println(i); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
"json": func(i interface{}) {
|
"json": func(i interface{}) {
|
||||||
json.NewEncoder(os.Stdout).Encode(subcommand.Config())
|
if err := json.NewEncoder(os.Stdout).Encode(subcommand.Config()); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"yaml": func(i interface{}) {
|
"yaml": func(i interface{}) {
|
||||||
yaml.NewEncoder(os.Stdout).Encode(subcommand.Config())
|
if err := yaml.NewEncoder(os.Stdout).Encode(subcommand.Config()); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +58,16 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hadErr bool
|
var hadErr bool
|
||||||
|
|
||||||
|
parseFlags := config.ParseFlagsNone
|
||||||
|
|
||||||
|
if configcheckArgs.skipCertCheck {
|
||||||
|
parseFlags |= config.ParseFlagsNoCertCheck
|
||||||
|
}
|
||||||
|
|
||||||
// further: try to build jobs
|
// further: try to build jobs
|
||||||
confJobs, err := job.JobsFromConfig(subcommand.Config())
|
confJobs, err := job.JobsFromConfig(subcommand.Config(), parseFlags)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := errors.Wrap(err, "cannot build jobs from config")
|
err := errors.Wrap(err, "cannot build jobs from config")
|
||||||
if configcheckArgs.what == "jobs" {
|
if configcheckArgs.what == "jobs" {
|
||||||
@@ -71,12 +92,11 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
whatMap := map[string]func(){
|
||||||
whatMap := map[string]func() {
|
|
||||||
"all": func() {
|
"all": func() {
|
||||||
o := struct {
|
o := struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
jobs []job.Job
|
jobs []job.Job
|
||||||
logging *logger.Outlets
|
logging *logger.Outlets
|
||||||
}{
|
}{
|
||||||
subcommand.Config(),
|
subcommand.Config(),
|
||||||
@@ -109,4 +129,3 @@ var ConfigcheckCmd = &cli.Subcommand{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func controlHttpClient(sockpath string) (client http.Client, err error) {
|
func controlHttpClient(sockpath string) (client http.Client, err error) {
|
||||||
@@ -35,7 +36,7 @@ func jsonRequestResponse(c http.Client, endpoint string, req interface{}, res in
|
|||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
var msg bytes.Buffer
|
var msg bytes.Buffer
|
||||||
io.CopyN(&msg, resp.Body, 4096)
|
_, _ = io.CopyN(&msg, resp.Body, 4096) // ignore error, just display what we got
|
||||||
return errors.Errorf("%s", msg.String())
|
return errors.Errorf("%s", msg.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/internal/daemon/job"
|
||||||
|
"github.com/zrepl/zrepl/internal/endpoint"
|
||||||
|
"github.com/zrepl/zrepl/internal/zfs"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/internal/cli"
|
||||||
|
"github.com/zrepl/zrepl/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
MigrateCmd = &cli.Subcommand{
|
||||||
|
Use: "migrate",
|
||||||
|
Short: "perform migration of the on-disk / zfs properties",
|
||||||
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
|
return migrations
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var migrations = []*cli.Subcommand{
|
||||||
|
&cli.Subcommand{
|
||||||
|
Use: "0.0.X:0.1:placeholder",
|
||||||
|
Run: doMigratePlaceholder0_1,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cli.Subcommand{
|
||||||
|
Use: "replication-cursor:v1-v2",
|
||||||
|
Run: doMigrateReplicationCursor,
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&migrateReplicationCursorArgs.dryRun, "dry-run", false, "dry run")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var migratePlaceholder0_1Args struct {
|
||||||
|
dryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func doMigratePlaceholder0_1(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := sc.Config()
|
||||||
|
|
||||||
|
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot list filesystems")
|
||||||
|
}
|
||||||
|
|
||||||
|
type workItem struct {
|
||||||
|
jobName string
|
||||||
|
rootFS *zfs.DatasetPath
|
||||||
|
fss []*zfs.DatasetPath
|
||||||
|
}
|
||||||
|
var wis []workItem
|
||||||
|
for i, j := range cfg.Jobs {
|
||||||
|
var rfsS string
|
||||||
|
switch job := j.Ret.(type) {
|
||||||
|
case *config.SinkJob:
|
||||||
|
rfsS = job.RootFS
|
||||||
|
case *config.PullJob:
|
||||||
|
rfsS = job.RootFS
|
||||||
|
default:
|
||||||
|
fmt.Printf("ignoring job %q (%d/%d, type %T)\n", j.Name(), i, len(cfg.Jobs), j.Ret)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rfs, err := zfs.NewDatasetPath(rfsS)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "root fs for job %q is not a valid dataset path", j.Name())
|
||||||
|
}
|
||||||
|
var fss []*zfs.DatasetPath
|
||||||
|
for _, fs := range allFSS {
|
||||||
|
if fs.HasPrefix(rfs) {
|
||||||
|
fss = append(fss, fs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wis = append(wis, workItem{j.Name(), rfs, fss})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, wi := range wis {
|
||||||
|
fmt.Printf("job %q => migrate filesystems below root_fs %q\n", wi.jobName, wi.rootFS.ToString())
|
||||||
|
if len(wi.fss) == 0 {
|
||||||
|
fmt.Printf("\tno filesystems\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, fs := range wi.fss {
|
||||||
|
fmt.Printf("\t%q ... ", fs.ToString())
|
||||||
|
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(ctx, fs, migratePlaceholder0_1Args.dryRun)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error: %s\n", err)
|
||||||
|
} else if !r.NeedsModification {
|
||||||
|
fmt.Printf("unchanged (placeholder=%v)\n", r.OriginalState.IsPlaceholder)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("migrate (placeholder=%v) (old value = %q)\n",
|
||||||
|
r.OriginalState.IsPlaceholder, r.OriginalState.RawLocalPropertyValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var migrateReplicationCursorArgs struct {
|
||||||
|
dryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var bold = color.New(color.Bold)
|
||||||
|
var succ = color.New(color.FgGreen)
|
||||||
|
var fail = color.New(color.FgRed)
|
||||||
|
|
||||||
|
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
|
||||||
|
|
||||||
|
func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return fmt.Errorf("migration does not take arguments, got %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := sc.Config()
|
||||||
|
jobs, err := job.JobsFromConfig(cfg, config.ParseFlagsNone)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("cannot parse config:\n%s\n\n", err)
|
||||||
|
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
|
||||||
|
return fmt.Errorf("exiting migration after error")
|
||||||
|
}
|
||||||
|
|
||||||
|
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
|
||||||
|
for i, j := range cfg.Jobs {
|
||||||
|
if jobs[i].Name() != j.Name() {
|
||||||
|
panic("implementation error")
|
||||||
|
}
|
||||||
|
switch j.Ret.(type) {
|
||||||
|
case *config.PushJob:
|
||||||
|
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||||
|
case *config.SourceJob:
|
||||||
|
v1cursorJobs = append(v1cursorJobs, jobs[i])
|
||||||
|
default:
|
||||||
|
fmt.Printf("ignoring job %q (%d/%d, type %T), not supposed to create v1 replication cursors\n", j.Name(), i, len(cfg.Jobs), j.Ret)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan all filesystems for v1 replication cursors
|
||||||
|
|
||||||
|
fss, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "list filesystems")
|
||||||
|
}
|
||||||
|
|
||||||
|
var hadError bool
|
||||||
|
for _, fs := range fss {
|
||||||
|
|
||||||
|
bold.Printf("INSPECT FILESYSTEM %q\n", fs.ToString())
|
||||||
|
|
||||||
|
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
|
||||||
|
if err == migrateReplicationCursorSkipSentinel {
|
||||||
|
bold.Printf("FILESYSTEM SKIPPED\n")
|
||||||
|
} else if err != nil {
|
||||||
|
hadError = true
|
||||||
|
fail.Printf("MIGRATION FAILED: %s\n", err)
|
||||||
|
} else {
|
||||||
|
succ.Printf("FILESYSTEM %q COMPLETE\n", fs.ToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hadError {
|
||||||
|
fail.Printf("\n\none or more filesystems could not be migrated, please inspect output and or re-run migration")
|
||||||
|
return errors.Errorf("")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, fs *zfs.DatasetPath) error {
|
||||||
|
|
||||||
|
var owningJob job.Job = nil
|
||||||
|
for _, job := range v1CursorJobs {
|
||||||
|
conf := job.SenderConfig()
|
||||||
|
if conf == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pass, err := conf.FSF.Filter(fs)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "filesystem filter error in job %q for fs %q", job.Name(), fs.ToString())
|
||||||
|
}
|
||||||
|
if !pass {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if owningJob != nil {
|
||||||
|
return errors.Errorf("jobs %q and %q both match %q\ncannot attribute replication cursor to either one", owningJob.Name(), job.Name(), fs)
|
||||||
|
}
|
||||||
|
owningJob = job
|
||||||
|
}
|
||||||
|
if owningJob == nil {
|
||||||
|
fmt.Printf("no job's Filesystems filter matches\n")
|
||||||
|
return migrateReplicationCursorSkipSentinel
|
||||||
|
}
|
||||||
|
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
||||||
|
|
||||||
|
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
|
||||||
|
Types: zfs.Bookmarks,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldCursor *zfs.FilesystemVersion
|
||||||
|
for i, fsv := range bookmarks {
|
||||||
|
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
|
||||||
|
if err != endpoint.ErrV1ReplicationCursor {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCursor != nil {
|
||||||
|
fmt.Printf("unexpected v1 replication cursor candidate: %q", fsv.ToAbsPath(fs))
|
||||||
|
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldCursor = &bookmarks[i]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldCursor == nil {
|
||||||
|
bold.Printf("no v1 replication cursor found for filesystem %q\n", fs.ToString())
|
||||||
|
return migrateReplicationCursorSkipSentinel
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("found v1 replication cursor:\n%s\n", pretty.Sprint(oldCursor))
|
||||||
|
|
||||||
|
mostRecentNew, err := endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs.ToString(), owningJob.SenderConfig().JobID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "get most recent v2 replication cursor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mostRecentNew == nil {
|
||||||
|
return errors.Errorf("no v2 replication cursor found for job %q on filesystem %q", owningJob.SenderConfig().JobID, fs.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("most recent v2 replication cursor:\n%#v", oldCursor)
|
||||||
|
|
||||||
|
if !(mostRecentNew.CreateTXG >= oldCursor.CreateTXG) {
|
||||||
|
return errors.Errorf("v1 replication cursor createtxg is higher than v2 cursor's, skipping this filesystem")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("determined that v2 cursor is bookmark of same or newer version than v1 cursor\n")
|
||||||
|
fmt.Printf("destroying v1 cursor %q\n", oldCursor.ToAbsPath(fs))
|
||||||
|
|
||||||
|
if migrateReplicationCursorArgs.dryRun {
|
||||||
|
succ.Printf("DRY RUN\n")
|
||||||
|
} else {
|
||||||
|
if err := zfs.ZFSDestroyFilesystemVersion(ctx, fs, oldCursor); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import "github.com/zrepl/zrepl/internal/cli"
|
||||||
|
|
||||||
|
var PprofCmd = &cli.Subcommand{
|
||||||
|
Use: "pprof",
|
||||||
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
|
return []*cli.Subcommand{PprofListenCmd, pprofActivityTraceCmd}
|
||||||
|
},
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user