Compare commits
947 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53a3d1df17 | |||
| 8153d399f0 | |||
| 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 | |||
| b96a287a2f | |||
| 0ab62f197a | |||
| 9a2d1b3074 | |||
| e809dbf45e | |||
| 644d6f8ffb | |||
| e37bb73b4e | |||
| 26ec29d8b2 | |||
| c0028c1c44 | |||
| ab38f24198 | |||
| 7d9a1b7eae | |||
| 2107483588 | |||
| c2b05954e9 | |||
| 7756c9a55c | |||
| 3e71542c78 | |||
| 2f2e6e6a00 | |||
| 6f7467e8d8 | |||
| f2a6193735 | |||
| 84eca86e14 | |||
| 86fdcfc437 | |||
| 25eeedbce8 | |||
| 2ba1ff15b7 | |||
| 5aefc47f71 | |||
| 7633c1cdf1 | |||
| 283c2821cd | |||
| fb999c8617 | |||
| 53dc0c3c5e | |||
| 84019238df | |||
| cef865f5ce | |||
| bdf99f6bb4 | |||
| 7424423d9a | |||
| c9b812570d | |||
| e62d157aac | |||
| 99b3337b1c | |||
| dd673bf923 | |||
| 158d1175e3 | |||
| b25da7b9b0 | |||
| 5cd2593f52 | |||
| d8d9e34914 | |||
| e8c0d206ea | |||
| 17818439a0 | |||
| 056be1185d | |||
| b0898ec8bc | |||
| 133b7013a0 | |||
| dabf7e3ec9 | |||
| da3ba50a2c | |||
| 5eababe0b0 | |||
| b2c5ffcaea | |||
| 4ee00091d6 | |||
| c655622bf7 | |||
| 71d331af16 | |||
| 34052d98d6 | |||
| 3543fbbb65 | |||
| aff639e87a | |||
| 5dfe24eeee | |||
| 78ec5aa716 | |||
| 457d5bad9a | |||
| a0f301d700 | |||
| fc311a9fd6 | |||
| a7993d18c6 | |||
| 5595cff6a6 | |||
| 2c3b3c093d | |||
| ab3e783168 | |||
| 7584c66bdb | |||
| d78d20e2d0 | |||
| b85ec52387 | |||
| edcd258cc9 | |||
| d5250bbf51 | |||
| d50e553ebb | |||
| 1eb0f12a61 | |||
| 8129ed91f1 | |||
| c87759affe | |||
| 07b43bffa4 | |||
| 0230c6321f | |||
| 796c5ad42d | |||
| d281fb00e3 | |||
| 76a6c623f3 | |||
| 25c974f0b5 | |||
| ea719f5b5a | |||
| 3105fa4ff8 | |||
| f724480c7b | |||
| 5c5e8c0baf | |||
| bb5278fe9b | |||
| 38b0bd76f5 | |||
| c1aab0bee9 | |||
| ef3283638a | |||
| 68b62a5c00 | |||
| 7a75a4d384 | |||
| 1aae7b222f | |||
| 3535b251ab | |||
| 707f070a3c | |||
| 160a3b6d32 | |||
| d977796f18 | |||
| 3cef76d463 | |||
| e9564a7e5c | |||
| b79ad3ddc3 | |||
| d0f898751f | |||
| 22d9830baa | |||
| c4e23862cd | |||
| e10dc129de | |||
| dd11fc96db | |||
| 7de3c0a09a | |||
| 141e49727c | |||
| 442d61918b | |||
| 58dcc07430 | |||
| 19d0916e34 | |||
| 1265cc7934 | |||
| 3d2688e959 | |||
| 7ab51fad0d | |||
| 3472145df6 | |||
| 5e1ea21f85 | |||
| 2db3977408 | |||
| ca6d5d3bb5 | |||
| 163c2bc533 | |||
| dd286aa12e | |||
| 80babe3ab4 | |||
| ca0cab0a15 | |||
| ad8be226fd | |||
| 9b3e5c38e2 | |||
| 7e1c5f5d1f | |||
| 98bc8d1717 | |||
| 2889a5d5ff | |||
| 0b8c19c620 | |||
| a62b475f46 | |||
| 1691839c6b | |||
| 36265ff349 | |||
| 94427d334b | |||
| b2844569c8 | |||
| ae5e60b1ae | |||
| fffda09f67 | |||
| 5ec7a5c078 | |||
| 190c7270d9 | |||
| f704b28cad | |||
| 5efeec1819 | |||
| 438f950be3 | |||
| 50c1549865 | |||
| 6e21a67473 | |||
| 17ab39d646 | |||
| 44d2057df8 | |||
| 3e359aaeda | |||
| 8cfeeee23a | |||
| f535b2327f | |||
| e63ac7d1bb | |||
| 359ab2ca0c | |||
| 45373168ad | |||
| 69bfcb7bed | |||
| 4ede99b08c | |||
| 814fec60f0 | |||
| ace4f3d892 | |||
| 82f0060eec | |||
| 53ac853cb4 | |||
| a5376913fd | |||
| 6fcf0635a5 | |||
| 1f072936c5 | |||
| 3c06235dca | |||
| f13749380d | |||
| eadb6f823d | |||
| e7497ab3d0 | |||
| 59a4e2db5f | |||
| 2c994e879c | |||
| de2768c91d | |||
| fb6f58b735 | |||
| be4e244f1f | |||
| 074f989547 | |||
| 87c8957889 | |||
| f6cf23779f | |||
| 92a1a6d2ca | |||
| 63169c51b7 | |||
| 5c3c83b2cb | |||
| aeb87ffbcf | |||
| a85abe8bae | |||
| d584e1ac54 | |||
| af3d96dab8 | |||
| 89e0103abd | |||
| 025fbda984 | |||
| 9bb7b19c93 | |||
| cb83a26c90 | |||
| d17ecc3b5c | |||
| f9d24d15ed | |||
| 1fb59c953a | |||
| be962998ba | |||
| a97684923a | |||
| 1643198713 | |||
| 125b561df3 | |||
| 0c3a694470 | |||
| 525a875825 | |||
| 4e16952ad9 | |||
| 14febbeb4c | |||
| 93c90cd705 | |||
| 01668a989e | |||
| 976c1f3929 | |||
| 75e42fd860 | |||
| 2990193512 | |||
| 75ba5874a5 | |||
| 9e941d5be5 | |||
| 328ac687f6 | |||
| 1ce0c69e4f | |||
| f3e8eda04d | |||
| cf5d63ee88 | |||
| 4e04f8d3d2 | |||
| d04b9713c4 | |||
| 6889f441b2 | |||
| 9c86e03384 | |||
| ffe33aff3d | |||
| e3be120d88 | |||
| 9446b51a1f | |||
| 4a6160baf3 | |||
| 9dd662df08 | |||
| 7f9eb62640 | |||
| 6c3f442f13 | |||
| fa47667f31 | |||
| ab9446137f | |||
| 0c2ac3a168 | |||
| bf5099baac | |||
| 7836ea36fc | |||
| 1edf020ce7 | |||
| c60ed78bc5 | |||
| 82d51cd0dc | |||
| 0f75677e59 | |||
| 2c25f28972 | |||
| 1323a30a0c | |||
| 975fdee217 | |||
| acd2418803 | |||
| 9eca269ad8 | |||
| c21222ef13 | |||
| 6c988d0ebb | |||
| 4b39a18178 | |||
| 6c31c66562 | |||
| adab06405b | |||
| 52f0c0c33b | |||
| bfc631f6a6 | |||
| 1e27720b99 | |||
| 8eade3d20a | |||
| 308e5e35fb | |||
| e161347e47 | |||
| 754b253043 | |||
| be57d6ce8e | |||
| 4336af295f | |||
| 0c4a3f8dc4 | |||
| 8799108b55 | |||
| 03f9f81cb5 | |||
| 2da0e51fda | |||
| f0860767f5 | |||
| ad28fd1ecb | |||
| 3d8e552c6a | |||
| d55a271ac7 | |||
| b95e983d0d | |||
| f387e23214 | |||
| 32391adf4f | |||
| c0a3e1f121 | |||
| 5442d8e7d5 | |||
| 48feaff054 | |||
| acd2a68cfb | |||
| 1690339440 | |||
| b5957aca37 | |||
| 47d8a5a7cd | |||
| 583773025f | |||
| 98f3f3dfd8 | |||
| 6ca11a7391 | |||
| 42056f7a32 | |||
| b2f01e454f | |||
| 6cedd0a2e8 | |||
| e495824834 | |||
| 7dd49b835a | |||
| 22ca80eb7e | |||
| 12dd240b5f | |||
| d684302864 | |||
| a2aa8e7bd7 | |||
| 0de17fd051 | |||
| fb0a8d8b40 | |||
| c69ebd3806 | |||
| df6e1bc64d | |||
| 89dc267780 | |||
| c7237cb09d | |||
| 4073c5dfb0 | |||
| c2b04d10c5 | |||
| b0d17803f0 | |||
| 16e1396261 | |||
| b955d308d9 | |||
| ecd9db4ac6 | |||
| b4ea5f56b2 | |||
| 5e51595d7f | |||
| 48a08e4f4d | |||
| e2bf557d17 | |||
| fbb8a25320 | |||
| add1b69809 | |||
| cd9a428841 | |||
| 4ec5e23457 | |||
| 13dc63bd23 | |||
| 38bb78b642 | |||
| 6425c26b1b | |||
| 428339e1ad | |||
| ee5445777d | |||
| a0f72b585b | |||
| 7ff72fb6d9 | |||
| f6be5b776b | |||
| 666ead2646 | |||
| cf01086df5 | |||
| ea0e3a29e4 | |||
| 71203ab325 | |||
| 88de8ba8bb | |||
| 861e5f8313 | |||
| e30ae972f4 | |||
| e082816de5 | |||
| b56e236874 | |||
| f46d1bc338 | |||
| 51cfcfe79b | |||
| d677cde6d0 | |||
| 873c64ecc3 | |||
| 54c9dcb7c1 | |||
| 9b537ec704 | |||
| 70aad0940f | |||
| 7b3a84e2a3 | |||
| 301c7b2dd5 | |||
| 2f205d205b | |||
| 38532abf45 | |||
| c7d28fee8f | |||
| bf1e626b9a | |||
| 93929b61e4 | |||
| 5479463783 | |||
| 094eced2c7 | |||
| 991f13a3da | |||
| 7303d91abf | |||
| c1f3076eb3 | |||
| 74445a0017 | |||
| a0b320bfeb | |||
| 1826535e6f | |||
| 1a8d2c5ebe | |||
| 8cca0a8547 | |||
| 302d77cf4a | |||
| c7f0f779b1 | |||
| e6426db8da | |||
| fa6426f803 | |||
| 0918ef6815 | |||
| 181875a89b | |||
| 67743d2a66 | |||
| 386d3b19b2 | |||
| 9d7110eaad | |||
| 82ea535692 | |||
| a4da029105 | |||
| aa3865d0a3 | |||
| 0895e02844 | |||
| 0764f8824e | |||
| 30057d4e59 | |||
| 75fd21e454 | |||
| 0d2f73d728 | |||
| 9b803aad2d | |||
| fb74addc1e | |||
| 7f89372cfa | |||
| 26b436463d | |||
| 61af396fdd | |||
| 792c1a23b2 | |||
| 7464e967c8 | |||
| 921deb43f5 | |||
| 4cf910874d | |||
| 3ba3648f0f | |||
| aa92261ea7 | |||
| 8e34843eb1 | |||
| bfaf6fdfbb | |||
| f992fed968 | |||
| 94967b596c | |||
| 759dae4552 | |||
| f3d3a7f5f8 | |||
| ccd062e238 | |||
| fc1c46ffd7 | |||
| 6b5bd0a43c | |||
| 921bccb960 | |||
| 24b29a0865 | |||
| 5f2c14adab | |||
| 787675aee8 | |||
| 6f68c98c16 | |||
| 01e0519b7b | |||
| 8742b7f763 | |||
| 710bf79f7e | |||
| a622ef1487 | |||
| 56f13741f9 | |||
| 746fb4ff88 | |||
| 8473462adf | |||
| 61842988b9 | |||
| be7176bee7 | |||
| c403e56835 | |||
| 839eccf513 | |||
| 9a19615fd4 | |||
| 03ba2bb7c8 | |||
| 7ac2821147 | |||
| e6554b77c0 | |||
| acd9aedb98 | |||
| 835cf6b12f | |||
| 4b3d83ec1f | |||
| d13c6e3fc3 | |||
| 63fa7a67e9 | |||
| 7d89d1fb00 | |||
| b69089a527 | |||
| 59e34942d1 | |||
| 91c4a97f72 | |||
| 13562b48ed | |||
| 58ee796394 | |||
| ce351146cf | |||
| 14b8d69a63 | |||
| 8c7e373049 | |||
| 2c87b15e83 | |||
| d7f3fb93ae | |||
| ebf209427a | |||
| 51377a8ffb | |||
| f14dc3107f | |||
| 261d095108 | |||
| 583a63a68f | |||
| 2716c75ad5 | |||
| e8facfe9fa | |||
| d424e800c8 | |||
| 896f31bbf3 | |||
| d59426a8cc | |||
| 903fbff710 | |||
| bc4b129536 | |||
| b4b1bebb5c | |||
| 445a280aa2 | |||
| b276787dd4 | |||
| bfbab9382e | |||
| 2bfcfa5be8 | |||
| a7f70a566d | |||
| f5ead68586 | |||
| bf6c58425a | |||
| 8249a5d1b7 | |||
| b576253ea8 | |||
| 476348689a | |||
| ed68bffea5 | |||
| 51af880701 | |||
| cef63ac176 | |||
| 9e48c70f58 | |||
| fe40352f8e | |||
| fd123fc6c4 | |||
| 961500cc2c | |||
| 47726ad877 | |||
| 3b6cede108 | |||
| 2cad13f27b | |||
| b5475921a8 | |||
| 88c684e1d0 | |||
| a4d28701d9 | |||
| 000c8b4186 | |||
| ae4b12c9ba | |||
| 8cc31bd76a | |||
| 43871a9211 | |||
| 77576164ae | |||
| 4c450a640c | |||
| 36d2cb115a | |||
| 7ba5c14679 | |||
| ab7eb47483 | |||
| 707a189144 | |||
| 4f37dccb76 | |||
| 69084fb08f | |||
| 0a77be0ff2 | |||
| 597302de3f | |||
| 828c2982f3 | |||
| 6f7b8ca1af | |||
| e0f40de69f | |||
| 2fe7f29d31 | |||
| df181108b4 | |||
| c3af267f48 | |||
| a15a73e884 | |||
| ff10a71f3a | |||
| 1f266d02ce | |||
| 4efff312ea | |||
| 0ed5c01473 | |||
| 63bc27e6e1 | |||
| e72d274e88 | |||
| 007da664ea | |||
| f3433df617 | |||
| 493a01c4fe | |||
| 161ce3b3c3 | |||
| 83bb97a845 | |||
| 40919d06c2 | |||
| c48069ce88 | |||
| 4b489ad2c7 | |||
| 72d288567e | |||
| b5d46e2ec3 | |||
| 83d450b1f2 | |||
| 3e647c14c0 | |||
| b95260f4b5 | |||
| 2764c95952 | |||
| 678b4a6f4b | |||
| 79ab43ebca | |||
| a4963cecb7 | |||
| e6d08149ef | |||
| ea6f02368b | |||
| d891b2b119 | |||
| 5c6c9485a8 | |||
| b4d8c93fae | |||
| 3f394a8960 | |||
| 164f77d80c |
@@ -0,0 +1,300 @@
|
|||||||
|
version: 2.1
|
||||||
|
orbs:
|
||||||
|
# NB: this is not the Go version, but the Orb version
|
||||||
|
# https://circleci.com/developer/orbs/orb/circleci/go#usage-go-modules-cache
|
||||||
|
go: circleci/go@1.11.0
|
||||||
|
commands:
|
||||||
|
setup-home-local-bin:
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
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
|
||||||
|
|
||||||
|
apt-update-and-install-common-deps:
|
||||||
|
steps:
|
||||||
|
- run: sudo apt-get update
|
||||||
|
- run: sudo apt-get install -y gawk make
|
||||||
|
# CircleCI doesn't update its cimg/go images.
|
||||||
|
# So, need to update manually to get up-to-date trust chains.
|
||||||
|
# The need for this was required for cimg/go:1.12, but let's future proof this here and now.
|
||||||
|
- run: sudo apt-get install -y git ca-certificates
|
||||||
|
|
||||||
|
|
||||||
|
install-docdep:
|
||||||
|
steps:
|
||||||
|
- apt-update-and-install-common-deps
|
||||||
|
- run: sudo apt install python3 python3-pip libgirepository1.0-dev
|
||||||
|
- run: pip3 install -r docs/requirements.txt
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
# if we're pushing, we need to add the deploy key
|
||||||
|
# which is stored as "Additional SSH Keys" in the CircleCI project settings.
|
||||||
|
# We can't use the CircleCI-manage deploy key because we're pushing
|
||||||
|
# to a different repo than the one we're building.
|
||||||
|
- when:
|
||||||
|
condition: << parameters.push >>
|
||||||
|
steps:
|
||||||
|
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
||||||
|
- run: ssh-add -D
|
||||||
|
# the default circleci ssh config only additional ssh keys for Host !github.com
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
cat > ~/.ssh/config \<<EOF
|
||||||
|
Host *
|
||||||
|
IdentityFile /home/circleci/.ssh/id_rsa_458e62c517f6c480e40452126ce47421
|
||||||
|
EOF
|
||||||
|
- add_ssh_keys:
|
||||||
|
fingerprints:
|
||||||
|
# deploy key for zrepl.github.io
|
||||||
|
- "45:8e:62:c5:17:f6:c4:80:e4:04:52:12:6c:e4:74:21"
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
- quickcheck-docs
|
||||||
|
- quickcheck-go:
|
||||||
|
name: quickcheck-go-amd64-linux-1.23.1
|
||||||
|
goversion: &latest-go-release "1.23.1"
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- test-go:
|
||||||
|
goversion: *latest-go-release
|
||||||
|
- quickcheck-go:
|
||||||
|
requires:
|
||||||
|
- quickcheck-go-amd64-linux-1.23.1 #quickcheck-go-smoketest.name
|
||||||
|
matrix:
|
||||||
|
alias: quickcheck-go-matrix
|
||||||
|
parameters:
|
||||||
|
goversion: [*latest-go-release, "1.22.7"]
|
||||||
|
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"]
|
||||||
|
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:
|
||||||
|
- publish-zrepl-github-io:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- stable
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quickcheck-docs:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:2024.09
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-docdep
|
||||||
|
# do the current docs build
|
||||||
|
- run: make docs
|
||||||
|
# does the publish.sh script still work?
|
||||||
|
- docs-publish-sh:
|
||||||
|
push: false
|
||||||
|
|
||||||
|
quickcheck-go:
|
||||||
|
parameters:
|
||||||
|
goversion:
|
||||||
|
type: string
|
||||||
|
goos:
|
||||||
|
type: string
|
||||||
|
goarch:
|
||||||
|
type: string
|
||||||
|
docker:
|
||||||
|
# any toolchain >= 1.22 will auto-download GOTOOLCHAIN
|
||||||
|
- image: &cimg_with_modern_go cimg/go:1.22
|
||||||
|
environment:
|
||||||
|
GOOS: <<parameters.goos>>
|
||||||
|
GOARCH: <<parameters.goarch>>
|
||||||
|
GOTOOLCHAIN: "go<<parameters.goversion>>"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- go/load-cache:
|
||||||
|
key: quickcheck-<<parameters.goversion>>
|
||||||
|
- run: make build/install
|
||||||
|
- run: go mod download
|
||||||
|
- run: cd build && go mod download
|
||||||
|
- go/save-cache:
|
||||||
|
key: quickcheck-<<parameters.goversion>>
|
||||||
|
|
||||||
|
# ensure all code has been generated
|
||||||
|
- run: make generate
|
||||||
|
- run: |
|
||||||
|
if output=$(git status --porcelain) && [ -z "$output" ]; then
|
||||||
|
echo "Working directory clean"
|
||||||
|
else
|
||||||
|
echo "Uncommitted changes"
|
||||||
|
echo ""
|
||||||
|
echo "$output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# other checks
|
||||||
|
- run: make zrepl-bin test-platform-bin
|
||||||
|
- run: make vet
|
||||||
|
- run: make lint
|
||||||
|
|
||||||
|
- store_artifacts:
|
||||||
|
path: artifacts
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths: [.]
|
||||||
|
|
||||||
|
platformtest:
|
||||||
|
parameters:
|
||||||
|
goversion:
|
||||||
|
type: string
|
||||||
|
goos:
|
||||||
|
type: string
|
||||||
|
goarch:
|
||||||
|
type: string
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2204:current
|
||||||
|
resource_class: medium
|
||||||
|
environment:
|
||||||
|
GOOS: <<parameters.goos>>
|
||||||
|
GOARCH: <<parameters.goarch>>
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: sudo apt-get update
|
||||||
|
- run: sudo apt-get install -y zfsutils-linux
|
||||||
|
- run: sudo zfs version
|
||||||
|
- run: sudo make test-platform GOOS="$GOOS" GOARCH="$GOARCH"
|
||||||
|
|
||||||
|
test-go:
|
||||||
|
parameters:
|
||||||
|
goversion:
|
||||||
|
type: string
|
||||||
|
docker:
|
||||||
|
- image: *cimg_with_modern_go
|
||||||
|
environment:
|
||||||
|
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:
|
||||||
|
- image: cimg/base:2024.09
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run: make wrapup-and-checksum
|
||||||
|
- store_artifacts:
|
||||||
|
path: artifacts/release
|
||||||
|
|
||||||
|
publish-zrepl-github-io:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:2024.09
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-docdep
|
||||||
|
- docs-publish-sh:
|
||||||
|
push: true
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
circle_token = os.environ.get('CIRCLE_TOKEN')
|
||||||
|
if not circle_token:
|
||||||
|
raise ValueError('CIRCLE_TOKEN environment variable not set')
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Download artifacts from CircleCI')
|
||||||
|
parser.add_argument('build_num', type=str, help='Build number')
|
||||||
|
parser.add_argument('dst', type=Path, help='Destination directory')
|
||||||
|
parser.add_argument('--prefix', type=str, default='', help='Filter for prefix')
|
||||||
|
parser.add_argument('--match', type=str, default='.*', help='Only include paths matching the given regex')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
res = requests.get(
|
||||||
|
f"https://circleci.com/api/v1.1/project/github/zrepl/zrepl/{args.build_num}/artifacts",
|
||||||
|
headers={
|
||||||
|
"Circle-Token": circle_token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
res.raise_for_status()
|
||||||
|
|
||||||
|
# https://circleci.com/docs/api/v1/index.html#artifacts-of-a-job
|
||||||
|
# [ {
|
||||||
|
# "path" : "raw-test-output/go-test-report.xml",
|
||||||
|
# "pretty_path" : "raw-test-output/go-test-report.xml",
|
||||||
|
# "node_index" : 0,
|
||||||
|
# "url" : "https://24-88881093-gh.circle-artifacts.com/0/raw-test-output/go-test-report.xml"
|
||||||
|
# }, {
|
||||||
|
# "path" : "raw-test-output/go-test.out",
|
||||||
|
# "pretty_path" : "raw-test-output/go-test.out",
|
||||||
|
# "node_index" : 0,
|
||||||
|
# "url" : "https://24-88881093-gh.circle-artifacts.com/0/raw-test-output/go-test.out"
|
||||||
|
# } ]
|
||||||
|
res = res.json()
|
||||||
|
|
||||||
|
for artifact in res:
|
||||||
|
if not artifact["pretty_path"].startswith(args.prefix):
|
||||||
|
continue
|
||||||
|
if not re.match(args.match, artifact["pretty_path"]):
|
||||||
|
continue
|
||||||
|
stripped = artifact["pretty_path"][len(args.prefix):]
|
||||||
|
print(f"Downloading {artifact['pretty_path']} to {args.dst / stripped}")
|
||||||
|
artifact_rel = Path(stripped)
|
||||||
|
artifact_dst = args.dst / artifact_rel
|
||||||
|
artifact_dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
res = requests.get(
|
||||||
|
artifact["url"],
|
||||||
|
headers={
|
||||||
|
"Circle-Token": circle_token,
|
||||||
|
},
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
res.raise_for_status()
|
||||||
|
|
||||||
|
total_size = int(res.headers.get("Content-Length", 0))
|
||||||
|
block_size = 128 * 1024
|
||||||
|
with open(artifact_dst, "wb") as f:
|
||||||
|
progress = 0
|
||||||
|
start_time = time.time()
|
||||||
|
for chunk in res.iter_content(chunk_size=block_size):
|
||||||
|
f.write(chunk)
|
||||||
|
progress += len(chunk)
|
||||||
|
percent = progress / total_size * 100
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if elapsed_time >= 5:
|
||||||
|
print(f"Downloaded {progress}/{total_size} bytes ({percent:.2f}%)", end="\r")
|
||||||
|
start_time = time.time()
|
||||||
|
print(f"Downloaded {progress}/{total_size} bytes ({percent:.2f}%)")
|
||||||
|
print("Download complete!")
|
||||||
|
|
||||||
|
print("All files downloaded")
|
||||||
@@ -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: "*"
|
||||||
@@ -32,3 +32,4 @@ _testmain.go
|
|||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
|
.venv/
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
[submodule "docs/themes/docdock"]
|
|
||||||
path = docs/themes/docdock
|
|
||||||
url = git@github.com:zrepl/hugo-theme-docdock.git
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- goimports
|
||||||
|
- revive
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
goimports:
|
||||||
|
local-prefixes: github.com/zrepl/zrepl
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: time-equal
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
# Disable staticcheck 'Empty body in an if or else branch' as it's useful
|
||||||
|
# to put a comment into an empty else-clause that explains why whatever
|
||||||
|
# is done in the if-caluse is not necessary if the condition is false.
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "SA9003:"
|
||||||
|
|
||||||
Generated
-177
@@ -1,177 +0,0 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/davecgh/go-spew"
|
|
||||||
packages = ["spew"]
|
|
||||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/fsnotify/fsnotify"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "629574ca2a5df945712d3079857300b5e4da0236"
|
|
||||||
version = "v1.4.2"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/ftrvxmtrx/fd"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "c6d800382fff6dc1412f34269f71b7f83bd059ad"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/go-logfmt/logfmt"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
|
|
||||||
version = "v0.3.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "v2"
|
|
||||||
name = "github.com/go-yaml/yaml"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/hashicorp/hcl"
|
|
||||||
packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"]
|
|
||||||
revision = "68e816d1c783414e79bc65b3994d9ab6b0a722ab"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/inconshreveable/mousetrap"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
|
||||||
version = "v1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/jinzhu/copier"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "db4671f3a9b8df855e993f7c94ec5ef1ffb0a23b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/kr/logfmt"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/kr/pretty"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/kr/text"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "7cafcd837844e784b526369c9bce262804aebc60"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/magiconair/properties"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "be5ece7dd465ab0765a9682137865547526d1dfb"
|
|
||||||
version = "v1.7.3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/go-homedir"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/mapstructure"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/pelletier/go-buffruneio"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
|
||||||
version = "v0.2.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/pelletier/go-toml"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "5ccdfb18c776b740aecaf085c4d9a2779199c279"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
|
||||||
version = "v0.8.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/pmezard/go-difflib"
|
|
||||||
packages = ["difflib"]
|
|
||||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/afero"
|
|
||||||
packages = [".","mem"]
|
|
||||||
revision = "ee1bd8ee15a1306d1f9201acc41ef39cd9f99a1b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/spf13/cast"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/cobra"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "b78744579491c1ceeaaa3b40205e56b0591b93a3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/jwalterweatherman"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/spf13/pflag"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/spf13/viper"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
packages = ["assert"]
|
|
||||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
|
||||||
version = "v1.1.4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
packages = ["unix"]
|
|
||||||
revision = "429f518978ab01db8bb6f44b66785088e7fba58b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/text"
|
|
||||||
packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"]
|
|
||||||
revision = "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "v2"
|
|
||||||
name = "gopkg.in/yaml.v2"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
|
||||||
|
|
||||||
[solve-meta]
|
|
||||||
analyzer-name = "dep"
|
|
||||||
analyzer-version = 1
|
|
||||||
inputs-digest = "7ce2ead5225e4bb72a34132538171a649b26de574596e909730658ddbc904cd5"
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
||||||
-66
@@ -1,66 +0,0 @@
|
|||||||
|
|
||||||
# Gopkg.toml example
|
|
||||||
#
|
|
||||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
|
||||||
# for detailed Gopkg.toml documentation.
|
|
||||||
#
|
|
||||||
# required = ["github.com/user/thing/cmd/thing"]
|
|
||||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project"
|
|
||||||
# version = "1.0.0"
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project2"
|
|
||||||
# branch = "dev"
|
|
||||||
# source = "github.com/myfork/project2"
|
|
||||||
#
|
|
||||||
# [[override]]
|
|
||||||
# name = "github.com/x/y"
|
|
||||||
# version = "2.4.0"
|
|
||||||
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/ftrvxmtrx/fd"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/jinzhu/copier"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/kr/pretty"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/go-homedir"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/mapstructure"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
version = "0.8.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/spf13/viper"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
version = "1.1.4"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "v2"
|
|
||||||
name = "github.com/go-yaml/yaml"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/go-logfmt/logfmt"
|
|
||||||
version = "*"
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017 Christian Schwarz
|
Copyright (c) 2017-2018 Christian Schwarz
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,47 +1,390 @@
|
|||||||
.PHONY: generate build test vet cover release clean
|
.PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
|
||||||
|
.PHONY: release release-noarch
|
||||||
ROOT := github.com/zrepl/zrepl
|
.DEFAULT_GOAL := zrepl-bin
|
||||||
SUBPKGS := cmd logger rpc sshbytestream util
|
|
||||||
|
|
||||||
_TESTPKGS := $(ROOT) $(foreach p,$(SUBPKGS),$(ROOT)/$(p))
|
|
||||||
|
|
||||||
ARTIFACTDIR := artifacts
|
ARTIFACTDIR := artifacts
|
||||||
|
|
||||||
build: generate
|
ifdef ZREPL_VERSION
|
||||||
go build -o $(ARTIFACTDIR)/zrepl
|
_ZREPL_VERSION := $(ZREPL_VERSION)
|
||||||
|
endif
|
||||||
|
ifndef _ZREPL_VERSION
|
||||||
|
_ZREPL_VERSION := $(shell git describe --always --dirty 2>/dev/null || echo "ZREPL_BUILD_INVALID_VERSION" )
|
||||||
|
ifeq ($(_ZREPL_VERSION),ZREPL_BUILD_INVALID_VERSION) # can't use .SHELLSTATUS because Debian Stretch is still on gmake 4.1
|
||||||
|
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
generate:
|
ZREPL_PACKAGE_RELEASE := 1
|
||||||
@for pkg in $(_TESTPKGS); do\
|
|
||||||
go generate "$$pkg" || exit 1; \
|
|
||||||
done;
|
|
||||||
|
|
||||||
test:
|
GO := go
|
||||||
@for pkg in $(_TESTPKGS); do \
|
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
||||||
echo "Testing $$pkg"; \
|
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
|
||||||
go test "$$pkg" || exit 1; \
|
GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"')
|
||||||
done;
|
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/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 := $(GO) tool gocovmerge
|
||||||
|
RELEASE_GOVERSION ?= go1.24.0
|
||||||
|
STRIPPED_GOVERSION := $(subst go,,$(RELEASE_GOVERSION))
|
||||||
|
RELEASE_DOCKER_BASEIMAGE ?= golang:$(STRIPPED_GOVERSION)
|
||||||
|
RELEASE_DOCKER_CACHEMOUNT :=
|
||||||
|
|
||||||
|
ifneq ($(GOARM),)
|
||||||
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
|
||||||
|
else
|
||||||
|
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
ifneq ($(RELEASE_DOCKER_CACHEMOUNT),)
|
||||||
|
_RELEASE_DOCKER_CACHEMOUNT := -v $(RELEASE_DOCKER_CACHEMOUNT)/mod:/go/pkg/mod -v $(RELEASE_DOCKER_CACHEMOUNT)/xdg-cache:/root/.cache/go-build
|
||||||
|
.PHONY: release-docker-mkcachemount
|
||||||
|
release-docker-mkcachemount:
|
||||||
|
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
|
||||||
|
|
||||||
|
##################### PRODUCING A RELEASE #############
|
||||||
|
.PHONY: release wrapup-and-checksum check-git-clean 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 -f $(ARTIFACTDIR)/build.Dockerfile .
|
||||||
|
docker run --rm -i \
|
||||||
|
$(_RELEASE_DOCKER_CACHEMOUNT) \
|
||||||
|
-v $(CURDIR):/src -u $$(id -u):$$(id -g) \
|
||||||
|
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 \
|
||||||
|
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
|
||||||
|
--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)/bash_completion \
|
||||||
|
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
||||||
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
|
dist \
|
||||||
|
internal/config/samples
|
||||||
|
rm -rf "$(ARTIFACTDIR)/release"
|
||||||
|
mkdir -p "$(ARTIFACTDIR)/release"
|
||||||
|
cp -l $(ARTIFACTDIR)/zrepl* \
|
||||||
|
$(ARTIFACTDIR)/platformtest-* \
|
||||||
|
"$(ARTIFACTDIR)/release"
|
||||||
|
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
|
||||||
|
@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'; \
|
||||||
|
if [ "$(ZREPL_VERSION)" = "" ]; then \
|
||||||
|
echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
|
||||||
|
git status; \
|
||||||
|
echo "git diff:"; \
|
||||||
|
git diff | cat; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
fi;
|
||||||
|
|
||||||
|
tag-release:
|
||||||
|
test -n "$(ZREPL_TAG_VERSION)" || exit 1
|
||||||
|
git tag -u '328A6627FA98061D!' -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
|
||||||
|
|
||||||
|
sign:
|
||||||
|
gpg -u '328A6627FA98061D!' \
|
||||||
|
--armor \
|
||||||
|
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
|
||||||
|
|
||||||
|
clean: docs-clean
|
||||||
|
rm -rf "$(ARTIFACTDIR)"
|
||||||
|
|
||||||
|
download-circleci-release:
|
||||||
|
rm -rf "$(ARTIFACTDIR)"
|
||||||
|
mkdir -p "$(ARTIFACTDIR)/release"
|
||||||
|
python3 .circleci/download_artifacts.py --prefix 'artifacts/release/' "$(BUILD_NUM)" "$(ARTIFACTDIR)/release"
|
||||||
|
|
||||||
|
##################### 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:
|
||||||
|
$(GO_ENV_VARS) $(GO) tool golangci-lint run ./...
|
||||||
|
|
||||||
vet:
|
vet:
|
||||||
@for pkg in $(_TESTPKGS); do \
|
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
|
||||||
echo "Vetting $$pkg"; \
|
|
||||||
go vet "$$pkg" || exit 1; \
|
|
||||||
done;
|
|
||||||
|
|
||||||
cover: artifacts
|
test-go: $(ARTIFACTDIR)
|
||||||
@for pkg in $(_TESTPKGS); do \
|
rm -f "$(ARTIFACTDIR)/gotest.cover"
|
||||||
profile="$(ARTIFACTDIR)/cover-$$(basename $$pkg).out"; \
|
ifeq ($(COVER),1)
|
||||||
go test -coverprofile "$$profile" $$pkg || exit 1; \
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
if [ -f "$$profile" ]; then \
|
-coverpkg github.com/zrepl/zrepl/... \
|
||||||
go tool cover -html="$$profile" -o "$${profile}.html" || exit 2; \
|
-covermode atomic \
|
||||||
fi; \
|
-coverprofile "$(ARTIFACTDIR)/gotest.cover" \
|
||||||
done;
|
./...
|
||||||
|
else
|
||||||
|
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
|
||||||
|
./...
|
||||||
|
endif
|
||||||
|
|
||||||
artifacts:
|
zrepl-bin:
|
||||||
mkdir artifacts
|
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
|
||||||
|
|
||||||
release: generate artifacts vet test
|
COVER_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-cover-$(ZREPL_TARGET_TUPLE)
|
||||||
GOOS=linux GOARCH=amd64 go build -o "$(ARTIFACTDIR)/zrepl-linux-amd64"
|
cover-platform-bin:
|
||||||
GOOS=freebsd GOARCH=amd64 go build -o "$(ARTIFACTDIR)/zrepl-freebsd-amd64"
|
$(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
|
||||||
|
|
||||||
clean:
|
TEST_PLATFORM_BIN_PATH := $(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)
|
||||||
rm -rf "$(ARTIFACTDIR)"
|
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 protoc plugins to GOBIN"
|
||||||
|
$(GO) install tool
|
||||||
|
|
||||||
|
mv build/install.tmp build/install
|
||||||
|
|
||||||
|
|
||||||
|
generate: build/install
|
||||||
|
# protoc plugins are installed via 'go install tool' in build/install target
|
||||||
|
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 ./...
|
||||||
|
|
||||||
|
GOIMPORTS := $(GO) tool goimports -srcdir . -local 'github.com/zrepl/zrepl'
|
||||||
|
|
||||||
|
format:
|
||||||
|
@ $(GO) tool 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
|
||||||
|
$(MAKE) -C docs \
|
||||||
|
html \
|
||||||
|
BUILDDIR=../artifacts/docs \
|
||||||
|
SPHINXOPTS="-W --keep-going -n"
|
||||||
|
|
||||||
|
docs-clean:
|
||||||
|
$(MAKE) -C docs \
|
||||||
|
clean \
|
||||||
|
BUILDDIR=../artifacts/docs
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||||
|
[](https://golang.org/)
|
||||||
|
[](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://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 ZFS filesystem backup & replication solution written in Go.
|
zrepl is a one-stop ZFS backup & replication solution.
|
||||||
|
|
||||||
## User Documentation
|
## User Documentation
|
||||||
|
|
||||||
**User Documentation** cab be found at [zrepl.github.io](https://zrepl.github.io).
|
**User Documentation** can be found at [zrepl.github.io](https://zrepl.github.io).
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
|
|
||||||
@@ -12,43 +22,117 @@ zrepl is a ZFS filesystem backup & replication solution written in Go.
|
|||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
## Developer Documentation
|
## Development
|
||||||
|
|
||||||
### Overall Architecture
|
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
||||||
|
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
|
||||||
|
|
||||||
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
|
### Building
|
||||||
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
|
|
||||||
|
|
||||||
### Project Structure
|
#### Go Code
|
||||||
|
|
||||||
```
|
Dependencies:
|
||||||
├── cmd
|
|
||||||
│ ├── sampleconf # example configuration
|
|
||||||
├── docs
|
|
||||||
│ ├── content # hugo-based documentation -> sources for ./public_git
|
|
||||||
│ ├── deploy.sh # shell script for automated rendering & deploy to zrepl.github.io repo
|
|
||||||
│ ├── public_git # used by above shell script
|
|
||||||
│ └── themes
|
|
||||||
│ └── docdock # submodule of our docdock theme fork
|
|
||||||
├── jobrun # OBSOLETE
|
|
||||||
├── logger # logger package used by zrepl
|
|
||||||
├── rpc # rpc protocol implementation
|
|
||||||
├── scratchpad # small example programs demoing some internal packages. probably OBSOLETE
|
|
||||||
├── sshbytestream # io.ReadWriteCloser over SSH
|
|
||||||
├── util
|
|
||||||
└── zfs # ZFS wrappers, filesystemm diffing
|
|
||||||
```
|
|
||||||
|
|
||||||
### Coding Workflow
|
* Go 1.22 or newer
|
||||||
|
* GNU Make
|
||||||
|
* Git
|
||||||
|
* wget (`make generate`)
|
||||||
|
* unzip (`make generate`)
|
||||||
|
|
||||||
* Open an issue when starting to hack on a new feature
|
Some Go code is **generated**, and generated code is committed to the source tree.
|
||||||
* Commits should reference the issue they are related to
|
Therefore, building does not require having code generation tools set up.
|
||||||
* Docs improvements not documenting new features do not require an issue.
|
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
|
||||||
|
|
||||||
|
Set up a Python environment that has `docs/requirements.txt` installed via `pip`.
|
||||||
|
Use a [venv](https://docs.python.org/3/library/venv.html) to avoid global state.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
* Issue the source release:
|
||||||
|
* Git tag the release on the `master` branch.
|
||||||
|
* Push the tag.
|
||||||
|
* Run `./docs/publish.sh` to re-build & push zrepl.github.io.
|
||||||
|
* Issue the official binary release:
|
||||||
|
* Run the `release` pipeline (triggered via CircleCI API)
|
||||||
|
* Download the artifacts to the release manager's machine.
|
||||||
|
* Create a GitHub release, edit the changelog, upload all the release artifacts, including .rpm and .deb files.
|
||||||
|
* Issue the GitHub release.
|
||||||
|
* Add the .rpm and .deb files to the official zrepl repos.
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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 service manager file and _please_ try to upstream it to this repository.
|
||||||
|
* `dist/systemd` contains a Systemd unit template.
|
||||||
|
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
|
||||||
|
* Have a look at the `Makefile`'s `ZREPL_VERSION` variable and how it passed to Go's `ldFlags`.
|
||||||
|
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.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
install/
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
FROM !SUBSTITUTED_BY_MAKEFILE
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
unzip \
|
||||||
|
gawk
|
||||||
|
|
||||||
|
# setup venv for docs
|
||||||
|
ENV VIRTUAL_ENV=/opt/venv
|
||||||
|
RUN python3 -m venv $VIRTUAL_ENV
|
||||||
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
ADD docs/requirements.txt /tmp/requirements.txt
|
||||||
|
RUN pip3 install -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
# Go toolchain uses xdg-cache
|
||||||
|
RUN mkdir -p /.cache && chmod -R 0777 /.cache
|
||||||
|
|
||||||
|
# Go devtools are managed by Makefile
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
cd "$1"
|
||||||
|
|
||||||
|
MACH=$(uname -m)
|
||||||
|
MACH="${MACH/aarch64/aarch_64}"
|
||||||
|
|
||||||
|
VERSION=28.0
|
||||||
|
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"
|
||||||
|
|
||||||
|
sha256sum -c --ignore-missing <<EOF
|
||||||
|
d622619dcbfb5ecb281cfb92c1a74d6a0f42e752d9a2774b197f475f7ab1c8c4 protoc-28.0-linux-aarch_64.zip
|
||||||
|
b2e187c8b9f2d97cd3ecae4926d1bb2cbebe3ab768e7c987cbc86bb17f319358 protoc-28.0-linux-x86_64.zip
|
||||||
|
EOF
|
||||||
|
|
||||||
|
unzip -d . "$FILENAME"
|
||||||
+580
@@ -0,0 +1,580 @@
|
|||||||
|
4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA=
|
||||||
|
4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs=
|
||||||
|
4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc=
|
||||||
|
4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU=
|
||||||
|
github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8=
|
||||||
|
github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0=
|
||||||
|
github.com/Abirdcfly/dupword v0.1.1 h1:Bsxe0fIw6OwBtXMIncaTxCLHYO5BB+3mcsR5E8VXloY=
|
||||||
|
github.com/Abirdcfly/dupword v0.1.1/go.mod h1:B49AcJdTYYkpd4HjgAcutNGG9HZ2JWwKunH9Y2BA6sM=
|
||||||
|
github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM=
|
||||||
|
github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns=
|
||||||
|
github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ=
|
||||||
|
github.com/Antonboom/nilnil v0.1.9/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ=
|
||||||
|
github.com/Antonboom/testifylint v1.4.3 h1:ohMt6AHuHgttaQ1xb6SSnxCeK4/rnK7KKzbvs7DmEck=
|
||||||
|
github.com/Antonboom/testifylint v1.4.3/go.mod h1:+8Q9+AOLsz5ZiQiiYujJKs9mNz398+M6UgslP4qgJLA=
|
||||||
|
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
|
||||||
|
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/Crocmagnon/fatcontext v0.5.2 h1:vhSEg8Gqng8awhPju2w7MKHqMlg4/NI+gSDHtR3xgwA=
|
||||||
|
github.com/Crocmagnon/fatcontext v0.5.2/go.mod h1:87XhRMaInHP44Q7Tlc7jkgKKB7kZAOPiDkFMdKCC+74=
|
||||||
|
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
|
||||||
|
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||||
|
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU=
|
||||||
|
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao=
|
||||||
|
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||||
|
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA=
|
||||||
|
github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ=
|
||||||
|
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
||||||
|
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||||
|
github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c=
|
||||||
|
github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ=
|
||||||
|
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||||
|
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
github.com/alexkohler/nakedret/v2 v2.0.4 h1:yZuKmjqGi0pSmjGpOC016LtPJysIL0WEUiaXW5SUnNg=
|
||||||
|
github.com/alexkohler/nakedret/v2 v2.0.4/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU=
|
||||||
|
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
|
||||||
|
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
|
||||||
|
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
|
||||||
|
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
|
||||||
|
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/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY=
|
||||||
|
github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU=
|
||||||
|
github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s=
|
||||||
|
github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI=
|
||||||
|
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/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY=
|
||||||
|
github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM=
|
||||||
|
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
||||||
|
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
||||||
|
github.com/bombsimon/wsl/v4 v4.4.1 h1:jfUaCkN+aUpobrMO24zwyAMwMAV5eSziCkOKEauOLdw=
|
||||||
|
github.com/bombsimon/wsl/v4 v4.4.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo=
|
||||||
|
github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY=
|
||||||
|
github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ=
|
||||||
|
github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA=
|
||||||
|
github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U=
|
||||||
|
github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0=
|
||||||
|
github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA=
|
||||||
|
github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs=
|
||||||
|
github.com/butuzov/mirror v1.2.0/go.mod h1:DqZZDtzm42wIAIyHXeN8W/qb1EPlb9Qn/if9icBOpdQ=
|
||||||
|
github.com/catenacyber/perfsprint v0.7.1 h1:PGW5G/Kxn+YrN04cRAZKC+ZuvlVwolYMrIyyTJ/rMmc=
|
||||||
|
github.com/catenacyber/perfsprint v0.7.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50=
|
||||||
|
github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
|
||||||
|
github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
|
||||||
|
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/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4=
|
||||||
|
github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ=
|
||||||
|
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
|
||||||
|
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
|
||||||
|
github.com/ckaznocha/intrange v0.1.2 h1:3Y4JAxcMntgb/wABQ6e8Q8leMd26JbX2790lIss9MTI=
|
||||||
|
github.com/ckaznocha/intrange v0.1.2/go.mod h1:RWffCw/vKBwHeOEwWdCikAtY0q4gGt8VhJZEEA5n+RE=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo=
|
||||||
|
github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc=
|
||||||
|
github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c=
|
||||||
|
github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
|
||||||
|
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
|
||||||
|
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
|
||||||
|
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
|
||||||
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
|
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||||
|
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||||
|
github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=
|
||||||
|
github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||||
|
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||||
|
github.com/ghostiam/protogetter v0.3.6 h1:R7qEWaSgFCsy20yYHNIJsU9ZOb8TziSRRxuAOTVKeOk=
|
||||||
|
github.com/ghostiam/protogetter v0.3.6/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw=
|
||||||
|
github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU=
|
||||||
|
github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
|
||||||
|
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
|
||||||
|
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
|
||||||
|
github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
|
||||||
|
github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
|
||||||
|
github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
|
||||||
|
github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
|
||||||
|
github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
|
||||||
|
github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
|
||||||
|
github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
|
||||||
|
github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
|
||||||
|
github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
|
||||||
|
github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=
|
||||||
|
github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=
|
||||||
|
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||||
|
github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
|
||||||
|
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
|
||||||
|
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||||
|
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U=
|
||||||
|
github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||||
|
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||||
|
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
|
||||||
|
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||||
|
github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME=
|
||||||
|
github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE=
|
||||||
|
github.com/golangci/golangci-lint v1.60.3 h1:l38A5de24ZeDlcFF+EB7m3W5joPD99/hS5SIHJPyZa0=
|
||||||
|
github.com/golangci/golangci-lint v1.60.3/go.mod h1:J4vOpcjzRI+lDL2DKNGBZVB3EQSBfCBCMpaydWLtJNo=
|
||||||
|
github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
|
||||||
|
github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
|
||||||
|
github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA=
|
||||||
|
github.com/golangci/modinfo v0.3.4/go.mod h1:wytF1M5xl9u0ij8YSvhkEVPP3M5Mc7XLl1pxH3B2aUM=
|
||||||
|
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
|
||||||
|
github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc=
|
||||||
|
github.com/golangci/revgrep v0.5.3 h1:3tL7c1XBMtWHHqVpS5ChmiAAoe4PF/d5+ULzV9sLAzs=
|
||||||
|
github.com/golangci/revgrep v0.5.3/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
|
||||||
|
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs=
|
||||||
|
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA=
|
||||||
|
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
|
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
|
||||||
|
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
|
||||||
|
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
|
||||||
|
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
|
||||||
|
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
|
||||||
|
github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q=
|
||||||
|
github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
|
||||||
|
github.com/gostaticanalysis/forcetypeassert v0.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70=
|
||||||
|
github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak=
|
||||||
|
github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk=
|
||||||
|
github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A=
|
||||||
|
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
|
||||||
|
github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY=
|
||||||
|
github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU=
|
||||||
|
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk=
|
||||||
|
github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=
|
||||||
|
github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs=
|
||||||
|
github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=
|
||||||
|
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48=
|
||||||
|
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
|
||||||
|
github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk=
|
||||||
|
github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA=
|
||||||
|
github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY=
|
||||||
|
github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0=
|
||||||
|
github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos=
|
||||||
|
github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k=
|
||||||
|
github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0=
|
||||||
|
github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ=
|
||||||
|
github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg=
|
||||||
|
github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=
|
||||||
|
github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=
|
||||||
|
github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs=
|
||||||
|
github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY=
|
||||||
|
github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ=
|
||||||
|
github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA=
|
||||||
|
github.com/lasiar/canonicalheader v1.1.1 h1:wC+dY9ZfiqiPwAexUApFush/csSPXeIi4QqyxXmng8I=
|
||||||
|
github.com/lasiar/canonicalheader v1.1.1/go.mod h1:cXkb3Dlk6XXy+8MVQnF23CYKWlyA7kfQhSw2CcZtZb0=
|
||||||
|
github.com/ldez/gomoddirectives v0.2.4 h1:j3YjBIjEBbqZ0NKtBNzr8rtMHTOrLPeiwTkfUJZ3alg=
|
||||||
|
github.com/ldez/gomoddirectives v0.2.4/go.mod h1:oWu9i62VcQDYp9EQ0ONTfqLNh+mDLWWDO+SO0qSQw5g=
|
||||||
|
github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo=
|
||||||
|
github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4=
|
||||||
|
github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
|
||||||
|
github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
|
||||||
|
github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM=
|
||||||
|
github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM=
|
||||||
|
github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk=
|
||||||
|
github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I=
|
||||||
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI=
|
||||||
|
github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE=
|
||||||
|
github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04=
|
||||||
|
github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc=
|
||||||
|
github.com/matoous/godox v0.0.0-20240105082147-c5b5e0e7c0c0 h1:Ny7cm4KSWceJLYyI1sm+aFIVDWSGXLcOJ0O0UaS5wdU=
|
||||||
|
github.com/matoous/godox v0.0.0-20240105082147-c5b5e0e7c0c0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
|
||||||
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
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/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mgechev/revive v1.3.9 h1:18Y3R4a2USSBF+QZKFQwVkBROUda7uoBlkEuBD+YD1A=
|
||||||
|
github.com/mgechev/revive v1.3.9/go.mod h1:+uxEIr5UH0TjXWHTno3xh4u7eg6jDpXKzQccA9UGhHU=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
|
||||||
|
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
|
||||||
|
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/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
|
||||||
|
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
|
||||||
|
github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=
|
||||||
|
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
|
||||||
|
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
|
||||||
|
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
|
||||||
|
github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk=
|
||||||
|
github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
|
||||||
|
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/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
|
||||||
|
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||||
|
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||||
|
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||||
|
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||||
|
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||||
|
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||||
|
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/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/polyfloyd/go-errorlint v1.6.0 h1:tftWV9DE7txiFzPpztTAwyoRLKNj9gpVm2cg8/OwcYY=
|
||||||
|
github.com/polyfloyd/go-errorlint v1.6.0/go.mod h1:HR7u8wuP1kb1NeN1zqTd1ZMlqUKPPHF+Id4vIPvDqVw=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
|
github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4=
|
||||||
|
github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
|
||||||
|
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
|
||||||
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs=
|
||||||
|
github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI=
|
||||||
|
github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
|
||||||
|
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||||
|
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
|
||||||
|
github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
|
||||||
|
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
|
||||||
|
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||||
|
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
|
||||||
|
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU=
|
||||||
|
github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE=
|
||||||
|
github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
|
||||||
|
github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
|
||||||
|
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||||
|
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc=
|
||||||
|
github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
|
||||||
|
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
|
||||||
|
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
|
||||||
|
github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
|
||||||
|
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
|
||||||
|
github.com/sashamelentyev/usestdlibvars v1.27.0 h1:t/3jZpSXtRPRf2xr0m63i32ZrusyurIGT9E5wAvXQnI=
|
||||||
|
github.com/sashamelentyev/usestdlibvars v1.27.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8=
|
||||||
|
github.com/securego/gosec/v2 v2.21.1 h1:wllLJ4nf+JviAHpEzsD52OSWCnzYSGQ4e/k3W4206NQ=
|
||||||
|
github.com/securego/gosec/v2 v2.21.1/go.mod h1:FQXMaDLzBbTMdxSDWIYQOfP+mHYNWPDZVH7p1GAFNVs=
|
||||||
|
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
|
||||||
|
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs=
|
||||||
|
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||||
|
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
|
||||||
|
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
|
||||||
|
github.com/sivchari/tenv v1.10.0 h1:g/hzMA+dBCKqGXgW8AV/1xIWhAvDrx0zFKNR48NFMg0=
|
||||||
|
github.com/sivchari/tenv v1.10.0/go.mod h1:tdY24masnVoZFxYrHv/nD6Tc8FbkEtAQEEziXpyMgqY=
|
||||||
|
github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00=
|
||||||
|
github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
|
||||||
|
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
|
||||||
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
|
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||||
|
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||||
|
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
|
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
|
||||||
|
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||||
|
github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc=
|
||||||
|
github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I=
|
||||||
|
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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM=
|
||||||
|
github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg=
|
||||||
|
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
|
||||||
|
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
|
||||||
|
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
|
||||||
|
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
|
||||||
|
github.com/tetafro/godot v1.4.17 h1:pGzu+Ye7ZUEFx7LHU0dAKmCOXWsPjl7qA6iMGndsjPs=
|
||||||
|
github.com/tetafro/godot v1.4.17/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio=
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20240125160201-f835fa56326a h1:A6uKudFIfAEpoPdaal3aSqGxBzLyU8TqyXImLwo6dIo=
|
||||||
|
github.com/timakin/bodyclose v0.0.0-20240125160201-f835fa56326a/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
|
||||||
|
github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4=
|
||||||
|
github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg=
|
||||||
|
github.com/tomarrell/wrapcheck/v2 v2.9.0 h1:801U2YCAjLhdN8zhZ/7tdjB3EnAoRlJHt/s+9hijLQ4=
|
||||||
|
github.com/tomarrell/wrapcheck/v2 v2.9.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=
|
||||||
|
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
|
||||||
|
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||||
|
github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI=
|
||||||
|
github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4=
|
||||||
|
github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/Gk8VQ=
|
||||||
|
github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
|
||||||
|
github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM=
|
||||||
|
github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U=
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0=
|
||||||
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM=
|
||||||
|
github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU=
|
||||||
|
github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg=
|
||||||
|
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
|
||||||
|
github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
|
||||||
|
github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
|
||||||
|
github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
|
||||||
|
github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
|
||||||
|
github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
|
||||||
|
gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
|
||||||
|
go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
|
||||||
|
go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=
|
||||||
|
go-simpler.org/musttag v0.12.2 h1:J7lRc2ysXOq7eM8rwaTYnNrHd5JwjppzB6mScysB2Cs=
|
||||||
|
go-simpler.org/musttag v0.12.2/go.mod h1:uN1DVIasMTQKk6XSik7yrJoEysGtR2GRqvWnI9S7TYM=
|
||||||
|
go-simpler.org/sloglint v0.7.2 h1:Wc9Em/Zeuu7JYpl+oKoYOsQSy2X560aVueCW/m6IijY=
|
||||||
|
go-simpler.org/sloglint v0.7.2/go.mod h1:US+9C80ppl7VsThQclkM7BkCHQAzuz8kHLsW3ppuluo=
|
||||||
|
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||||
|
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||||
|
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20240904232852-e7e105dedf7e h1:0DK6kA0DeobCIe4QIvfGMq0iDFRbeg0kN8E9Ln77SbY=
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
|
golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||||
|
golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||||
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
|
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||||
|
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||||
|
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||||
|
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c h1:Kqjm4WpoWvwhMPcrAczoTyMySQmYa9Wy2iL6Con4zn8=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
|
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||||
|
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||||
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||||
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||||
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
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=
|
||||||
|
honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
|
||||||
|
honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs=
|
||||||
|
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
|
||||||
|
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
|
||||||
|
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=
|
||||||
|
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ=
|
||||||
-148
@@ -1,148 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IntervalAutosnap struct {
|
|
||||||
DatasetFilter zfs.DatasetFilter
|
|
||||||
Prefix string
|
|
||||||
SnapshotInterval time.Duration
|
|
||||||
|
|
||||||
log Logger
|
|
||||||
snaptimes []snapTime
|
|
||||||
}
|
|
||||||
|
|
||||||
type snapTime struct {
|
|
||||||
ds *zfs.DatasetPath
|
|
||||||
time time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *IntervalAutosnap) Run(ctx context.Context, didSnaps chan struct{}) {
|
|
||||||
|
|
||||||
a.log = ctx.Value(contextKeyLog).(Logger)
|
|
||||||
|
|
||||||
const LOG_TIME_FMT string = time.ANSIC
|
|
||||||
|
|
||||||
ds, err := zfs.ZFSListMapping(a.DatasetFilter)
|
|
||||||
if err != nil {
|
|
||||||
a.log.WithError(err).Error("cannot list datasets")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(ds) == 0 {
|
|
||||||
a.log.WithError(err).Error("no datasets matching dataset filter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
a.snaptimes = make([]snapTime, len(ds))
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
a.log.Debug("examine filesystem state")
|
|
||||||
for i, d := range ds {
|
|
||||||
|
|
||||||
l := a.log.WithField(logFSField, d.ToString())
|
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(d, &PrefixSnapshotFilter{a.Prefix})
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Error("cannot list filesystem versions")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(fsvs) <= 0 {
|
|
||||||
l.WithField("prefix", a.Prefix).Info("no filesystem versions with prefix")
|
|
||||||
a.snaptimes[i] = snapTime{d, now}
|
|
||||||
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 < a.SnapshotInterval {
|
|
||||||
next = latest.Creation.Add(a.SnapshotInterval)
|
|
||||||
}
|
|
||||||
a.snaptimes[i] = snapTime{d, next}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(a.snaptimes, func(i, j int) bool {
|
|
||||||
return a.snaptimes[i].time.Before(a.snaptimes[j].time)
|
|
||||||
})
|
|
||||||
|
|
||||||
syncPoint := a.snaptimes[0]
|
|
||||||
a.log.WithField("sync_point", syncPoint.time.Format(LOG_TIME_FMT)).
|
|
||||||
Info("wait for sync point")
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
a.log.WithError(ctx.Err()).Info("context done")
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-time.After(syncPoint.time.Sub(now)):
|
|
||||||
a.log.Debug("snapshot all filesystems to enable further snaps in lockstep")
|
|
||||||
a.doSnapshots(didSnaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
ticker := time.NewTicker(a.SnapshotInterval)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
ticker.Stop()
|
|
||||||
a.log.WithError(ctx.Err()).Info("context done")
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-ticker.C:
|
|
||||||
a.doSnapshots(didSnaps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *IntervalAutosnap) doSnapshots(didSnaps chan struct{}) {
|
|
||||||
|
|
||||||
// fetch new dataset list in case user added new dataset
|
|
||||||
ds, err := zfs.ZFSListMapping(a.DatasetFilter)
|
|
||||||
if err != nil {
|
|
||||||
a.log.WithError(err).Error("cannot list datasets")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO channel programs -> allow a little jitter?
|
|
||||||
for _, d := range ds {
|
|
||||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
|
||||||
snapname := fmt.Sprintf("%s%s", a.Prefix, suffix)
|
|
||||||
|
|
||||||
a.log.WithField(logFSField, d.ToString()).
|
|
||||||
WithField("snapname", snapname).
|
|
||||||
Info("create snapshot")
|
|
||||||
|
|
||||||
err := zfs.ZFSSnapshot(d, snapname, false)
|
|
||||||
if err != nil {
|
|
||||||
a.log.WithError(err).Error("cannot create snapshot")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case didSnaps <- struct{}{}:
|
|
||||||
default:
|
|
||||||
a.log.Error("warning: callback channel is full, discarding")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-100
@@ -1,100 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Global Global
|
|
||||||
Jobs map[string]Job
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) LookupJob(name string) (j Job, err error) {
|
|
||||||
j, ok := c.Jobs[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.Errorf("job '%s' is not defined", name)
|
|
||||||
}
|
|
||||||
return j, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Global struct {
|
|
||||||
Serve struct {
|
|
||||||
Stdinserver struct {
|
|
||||||
SockDir string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Control struct {
|
|
||||||
Sockpath string
|
|
||||||
}
|
|
||||||
logging *LoggingConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobDebugSettings struct {
|
|
||||||
Conn struct {
|
|
||||||
ReadDump string `mapstructure:"read_dump"`
|
|
||||||
WriteDump string `mapstructure:"write_dump"`
|
|
||||||
}
|
|
||||||
RPC struct {
|
|
||||||
Log bool
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type RWCConnecter interface {
|
|
||||||
Connect() (io.ReadWriteCloser, error)
|
|
||||||
}
|
|
||||||
type AuthenticatedChannelListenerFactory interface {
|
|
||||||
Listen() (AuthenticatedChannelListener, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthenticatedChannelListener interface {
|
|
||||||
Accept() (ch io.ReadWriteCloser, err error)
|
|
||||||
Close() (err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SSHStdinServerConnectDescr struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrunePolicy interface {
|
|
||||||
Prune(fs *zfs.DatasetPath, versions []zfs.FilesystemVersion) (keep, remove []zfs.FilesystemVersion, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PruningJob interface {
|
|
||||||
Pruner(side PrunePolicySide, dryRun bool) (Pruner, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A type for constants describing different prune policies of a PruningJob
|
|
||||||
// This is mostly a special-case for LocalJob, which is the only job that has two prune policies
|
|
||||||
// instead of one.
|
|
||||||
// It implements github.com/spf13/pflag.Value to be used as CLI flag for the test subcommand
|
|
||||||
type PrunePolicySide string
|
|
||||||
|
|
||||||
const (
|
|
||||||
PrunePolicySideDefault PrunePolicySide = ""
|
|
||||||
PrunePolicySideLeft PrunePolicySide = "left"
|
|
||||||
PrunePolicySideRight PrunePolicySide = "right"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *PrunePolicySide) String() string {
|
|
||||||
return string(*s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrunePolicySide) Set(news string) error {
|
|
||||||
p := PrunePolicySide(news)
|
|
||||||
switch p {
|
|
||||||
case PrunePolicySideRight:
|
|
||||||
fallthrough
|
|
||||||
case PrunePolicySideLeft:
|
|
||||||
*s = p
|
|
||||||
default:
|
|
||||||
return errors.Errorf("must be either %s or %s", PrunePolicySideLeft, PrunePolicySideRight)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PrunePolicySide) Type() string {
|
|
||||||
return fmt.Sprintf("%s | %s", PrunePolicySideLeft, PrunePolicySideRight)
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/jinzhu/copier"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/sshbytestream"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SSHStdinserverConnecter struct {
|
|
||||||
Host string
|
|
||||||
User string
|
|
||||||
Port uint16
|
|
||||||
IdentityFile string `mapstructure:"identity_file"`
|
|
||||||
TransportOpenCommand []string `mapstructure:"transport_open_command"`
|
|
||||||
SSHCommand string `mapstructure:"ssh_command"`
|
|
||||||
Options []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSSHStdinserverConnecter(i map[string]interface{}) (c *SSHStdinserverConnecter, err error) {
|
|
||||||
|
|
||||||
c = &SSHStdinserverConnecter{}
|
|
||||||
if err = mapstructure.Decode(i, c); err != nil {
|
|
||||||
err = errors.New(fmt.Sprintf("could not parse ssh transport: %s", err))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO assert fields are filled
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SSHStdinserverConnecter) Connect() (rwc io.ReadWriteCloser, err error) {
|
|
||||||
var rpcTransport sshbytestream.SSHTransport
|
|
||||||
if err = copier.Copy(&rpcTransport, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if rwc, err = sshbytestream.Outgoing(rpcTransport); err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PrefixSnapshotFilter struct {
|
|
||||||
Prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSnapshotPrefix(i string) (p string, err error) {
|
|
||||||
if len(i) <= 0 {
|
|
||||||
err = errors.Errorf("snapshot prefix must not be empty string")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p = i
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePrefixSnapshotFilter(i string) (f *PrefixSnapshotFilter, err error) {
|
|
||||||
if !(len(i) > 0) {
|
|
||||||
err = errors.Errorf("snapshot prefix must be longer than 0 characters")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f = &PrefixSnapshotFilter{i}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *PrefixSnapshotFilter) Filter(fsv zfs.FilesystemVersion) (accept bool, err error) {
|
|
||||||
return fsv.Type == zfs.Snapshot && strings.HasPrefix(fsv.Name, f.Prefix), nil
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/pprof"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ControlJob struct {
|
|
||||||
Name string
|
|
||||||
sockaddr *net.UnixAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewControlJob(name, sockpath string) (j *ControlJob, err error) {
|
|
||||||
j = &ControlJob{Name: name}
|
|
||||||
|
|
||||||
j.sockaddr, err = net.ResolveUnixAddr("unix", sockpath)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot resolve unix address")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *ControlJob) JobName() string {
|
|
||||||
return j.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ControlJobEndpointProfile string = "/debug/pprof/profile"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (j *ControlJob) JobStart(ctx context.Context) {
|
|
||||||
|
|
||||||
log := ctx.Value(contextKeyLog).(Logger)
|
|
||||||
defer log.Info("control job finished")
|
|
||||||
|
|
||||||
l, err := ListenUnixPrivate(j.sockaddr)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error listening")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.Handle(ControlJobEndpointProfile, requestLogger{log, pprof.Profile})
|
|
||||||
server := http.Server{Handler: mux}
|
|
||||||
|
|
||||||
outer:
|
|
||||||
for {
|
|
||||||
|
|
||||||
served := make(chan error)
|
|
||||||
go func() {
|
|
||||||
served <- server.Serve(l)
|
|
||||||
close(served)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.WithError(err).Info("contex done")
|
|
||||||
server.Shutdown(context.Background())
|
|
||||||
break outer
|
|
||||||
case err = <-served:
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error serving")
|
|
||||||
break outer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestLogger struct {
|
|
||||||
log Logger
|
|
||||||
handlerFunc func(w http.ResponseWriter, r *http.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l requestLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := l.log.WithField("method", r.Method).WithField("url", r.URL)
|
|
||||||
log.Info("start")
|
|
||||||
l.handlerFunc(w, r)
|
|
||||||
log.Info("finish")
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"context"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/rpc"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LocalJob struct {
|
|
||||||
Name string
|
|
||||||
Mapping *DatasetMapFilter
|
|
||||||
SnapshotPrefix string
|
|
||||||
Interval time.Duration
|
|
||||||
InitialReplPolicy InitialReplPolicy
|
|
||||||
PruneLHS PrunePolicy
|
|
||||||
PruneRHS PrunePolicy
|
|
||||||
Debug JobDebugSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLocalJob(c JobParsingContext, name string, i map[string]interface{}) (j *LocalJob, err error) {
|
|
||||||
|
|
||||||
var asMap struct {
|
|
||||||
Mapping map[string]string
|
|
||||||
SnapshotPrefix string `mapstructure:"snapshot_prefix"`
|
|
||||||
Interval string
|
|
||||||
InitialReplPolicy string `mapstructure:"initial_repl_policy"`
|
|
||||||
PruneLHS map[string]interface{} `mapstructure:"prune_lhs"`
|
|
||||||
PruneRHS map[string]interface{} `mapstructure:"prune_rhs"`
|
|
||||||
Debug map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = mapstructure.Decode(i, &asMap); err != nil {
|
|
||||||
err = errors.Wrap(err, "mapstructure error")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j = &LocalJob{Name: name}
|
|
||||||
|
|
||||||
if j.Mapping, err = parseDatasetMapFilter(asMap.Mapping, false); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.SnapshotPrefix, err = parseSnapshotPrefix(asMap.SnapshotPrefix); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.Interval, err = time.ParseDuration(asMap.Interval); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse interval")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.InitialReplPolicy, err = parseInitialReplPolicy(asMap.InitialReplPolicy, DEFAULT_INITIAL_REPL_POLICY); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.PruneLHS, err = parsePrunePolicy(asMap.PruneLHS); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'prune_lhs'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if j.PruneRHS, err = parsePrunePolicy(asMap.PruneRHS); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'prune_rhs'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = mapstructure.Decode(asMap.Debug, &j.Debug); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'debug'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *LocalJob) JobName() string {
|
|
||||||
return j.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *LocalJob) JobStart(ctx context.Context) {
|
|
||||||
|
|
||||||
log := ctx.Value(contextKeyLog).(Logger)
|
|
||||||
defer log.Info("exiting")
|
|
||||||
|
|
||||||
local := rpc.NewLocalRPC()
|
|
||||||
// Allow access to any dataset since we control what mapping
|
|
||||||
// is passed to the pull routine.
|
|
||||||
// All local datasets will be passed to its Map() function,
|
|
||||||
// but only those for which a mapping exists will actually be pulled.
|
|
||||||
// We can pay this small performance penalty for now.
|
|
||||||
handler := NewHandler(log.WithField(logTaskField, "handler"), localPullACL{}, &PrefixSnapshotFilter{j.SnapshotPrefix})
|
|
||||||
|
|
||||||
registerEndpoints(local, handler)
|
|
||||||
|
|
||||||
snapper := IntervalAutosnap{
|
|
||||||
DatasetFilter: j.Mapping.AsFilter(),
|
|
||||||
Prefix: j.SnapshotPrefix,
|
|
||||||
SnapshotInterval: j.Interval,
|
|
||||||
}
|
|
||||||
|
|
||||||
plhs, err := j.Pruner(PrunePolicySideLeft, false)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error creating lhs pruner")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
prhs, err := j.Pruner(PrunePolicySideRight, false)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error creating rhs pruner")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
makeCtx := func(parent context.Context, taskName string) (ctx context.Context) {
|
|
||||||
return context.WithValue(parent, contextKeyLog, log.WithField(logTaskField, taskName))
|
|
||||||
}
|
|
||||||
var snapCtx, plCtx, prCtx, pullCtx context.Context
|
|
||||||
snapCtx = makeCtx(ctx, "autosnap")
|
|
||||||
plCtx = makeCtx(ctx, "prune_lhs")
|
|
||||||
prCtx = makeCtx(ctx, "prune_rhs")
|
|
||||||
pullCtx = makeCtx(ctx, "repl")
|
|
||||||
|
|
||||||
didSnaps := make(chan struct{})
|
|
||||||
go snapper.Run(snapCtx, didSnaps)
|
|
||||||
|
|
||||||
outer:
|
|
||||||
for {
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
break outer
|
|
||||||
case <-didSnaps:
|
|
||||||
log.Debug("finished taking snapshots")
|
|
||||||
log.Info("starting replication procedure")
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
log := pullCtx.Value(contextKeyLog).(Logger)
|
|
||||||
log.Debug("replicating from lhs to rhs")
|
|
||||||
err := doPull(PullContext{local, log, j.Mapping, j.InitialReplPolicy})
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error replicating lhs to rhs")
|
|
||||||
}
|
|
||||||
// use a ctx as soon as doPull gains ctx support
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
break outer
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
log.Info("pruning lhs")
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
plhs.Run(plCtx)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Info("pruning rhs")
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
prhs.Run(prCtx)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithError(ctx.Err()).Info("context")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *LocalJob) Pruner(side PrunePolicySide, dryRun bool) (p Pruner, err error) {
|
|
||||||
|
|
||||||
var dsfilter zfs.DatasetFilter
|
|
||||||
var pp PrunePolicy
|
|
||||||
switch side {
|
|
||||||
case PrunePolicySideLeft:
|
|
||||||
pp = j.PruneLHS
|
|
||||||
dsfilter = j.Mapping.AsFilter()
|
|
||||||
case PrunePolicySideRight:
|
|
||||||
pp = j.PruneRHS
|
|
||||||
dsfilter, err = j.Mapping.InvertedFilter()
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot invert mapping for prune_rhs")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
err = errors.Errorf("must be either left or right side")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p = Pruner{
|
|
||||||
time.Now(),
|
|
||||||
dryRun,
|
|
||||||
dsfilter,
|
|
||||||
j.SnapshotPrefix,
|
|
||||||
pp,
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"context"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/rpc"
|
|
||||||
"github.com/zrepl/zrepl/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PullJob struct {
|
|
||||||
Name string
|
|
||||||
Connect RWCConnecter
|
|
||||||
Interval time.Duration
|
|
||||||
Mapping *DatasetMapFilter
|
|
||||||
// constructed from mapping during parsing
|
|
||||||
pruneFilter *DatasetMapFilter
|
|
||||||
SnapshotPrefix string
|
|
||||||
InitialReplPolicy InitialReplPolicy
|
|
||||||
Prune PrunePolicy
|
|
||||||
Debug JobDebugSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePullJob(c JobParsingContext, name string, i map[string]interface{}) (j *PullJob, err error) {
|
|
||||||
|
|
||||||
var asMap struct {
|
|
||||||
Connect map[string]interface{}
|
|
||||||
Interval string
|
|
||||||
Mapping map[string]string
|
|
||||||
InitialReplPolicy string `mapstructure:"initial_repl_policy"`
|
|
||||||
Prune map[string]interface{}
|
|
||||||
SnapshotPrefix string `mapstructure:"snapshot_prefix"`
|
|
||||||
Debug map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = mapstructure.Decode(i, &asMap); err != nil {
|
|
||||||
err = errors.Wrap(err, "mapstructure error")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j = &PullJob{Name: name}
|
|
||||||
|
|
||||||
j.Connect, err = parseSSHStdinserverConnecter(asMap.Connect)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'connect'")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.Interval, err = time.ParseDuration(asMap.Interval); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'interval'")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j.Mapping, err = parseDatasetMapFilter(asMap.Mapping, false)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'mapping'")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.pruneFilter, err = j.Mapping.InvertedFilter(); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot automatically invert 'mapping' for prune job")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j.InitialReplPolicy, err = parseInitialReplPolicy(asMap.InitialReplPolicy, DEFAULT_INITIAL_REPL_POLICY)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'initial_repl_policy'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.SnapshotPrefix, err = parseSnapshotPrefix(asMap.SnapshotPrefix); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.Prune, err = parsePrunePolicy(asMap.Prune); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse prune policy")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = mapstructure.Decode(asMap.Debug, &j.Debug); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'debug'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *PullJob) JobName() string {
|
|
||||||
return j.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *PullJob) JobStart(ctx context.Context) {
|
|
||||||
|
|
||||||
log := ctx.Value(contextKeyLog).(Logger)
|
|
||||||
defer log.Info("exiting")
|
|
||||||
|
|
||||||
ticker := time.NewTicker(j.Interval)
|
|
||||||
|
|
||||||
start:
|
|
||||||
|
|
||||||
log.Info("connecting")
|
|
||||||
rwc, err := j.Connect.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error connecting")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rwc, err = util.NewReadWriteCloserLogger(rwc, j.Debug.Conn.ReadDump, j.Debug.Conn.WriteDump)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := rpc.NewClient(rwc)
|
|
||||||
if j.Debug.RPC.Log {
|
|
||||||
client.SetLogger(log, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("starting pull")
|
|
||||||
|
|
||||||
pullLog := log.WithField(logTaskField, "pull")
|
|
||||||
err = doPull(PullContext{client, pullLog, j.Mapping, j.InitialReplPolicy})
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error doing pull")
|
|
||||||
}
|
|
||||||
|
|
||||||
closeRPCWithTimeout(log, client, time.Second*10, "")
|
|
||||||
|
|
||||||
log.Info("starting prune")
|
|
||||||
prunectx := context.WithValue(ctx, contextKeyLog, log.WithField(logTaskField, "prune"))
|
|
||||||
pruner, err := j.Pruner(PrunePolicySideDefault, false)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error creating pruner")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pruner.Run(prunectx)
|
|
||||||
log.Info("finish prune")
|
|
||||||
|
|
||||||
log.Info("wait for next interval")
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.WithError(ctx.Err()).Info("context")
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
goto start
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *PullJob) Pruner(side PrunePolicySide, dryRun bool) (p Pruner, err error) {
|
|
||||||
p = Pruner{
|
|
||||||
time.Now(),
|
|
||||||
dryRun,
|
|
||||||
j.pruneFilter,
|
|
||||||
j.SnapshotPrefix,
|
|
||||||
j.Prune,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *PullJob) doRun(ctx context.Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
mapstructure "github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/rpc"
|
|
||||||
"github.com/zrepl/zrepl/util"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SourceJob struct {
|
|
||||||
Name string
|
|
||||||
Serve AuthenticatedChannelListenerFactory
|
|
||||||
Datasets *DatasetMapFilter
|
|
||||||
SnapshotPrefix string
|
|
||||||
Interval time.Duration
|
|
||||||
Prune PrunePolicy
|
|
||||||
Debug JobDebugSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSourceJob(c JobParsingContext, name string, i map[string]interface{}) (j *SourceJob, err error) {
|
|
||||||
|
|
||||||
var asMap struct {
|
|
||||||
Serve map[string]interface{}
|
|
||||||
Datasets map[string]string
|
|
||||||
SnapshotPrefix string `mapstructure:"snapshot_prefix"`
|
|
||||||
Interval string
|
|
||||||
Prune map[string]interface{}
|
|
||||||
Debug map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = mapstructure.Decode(i, &asMap); err != nil {
|
|
||||||
err = errors.Wrap(err, "mapstructure error")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j = &SourceJob{Name: name}
|
|
||||||
|
|
||||||
if j.Serve, err = parseAuthenticatedChannelListenerFactory(c, asMap.Serve); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.Datasets, err = parseDatasetMapFilter(asMap.Datasets, true); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.SnapshotPrefix, err = parseSnapshotPrefix(asMap.SnapshotPrefix); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.Interval, err = time.ParseDuration(asMap.Interval); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'interval'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.Prune, err = parsePrunePolicy(asMap.Prune); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'prune'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = mapstructure.Decode(asMap.Debug, &j.Debug); err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot parse 'debug'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *SourceJob) JobName() string {
|
|
||||||
return j.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *SourceJob) JobStart(ctx context.Context) {
|
|
||||||
|
|
||||||
log := ctx.Value(contextKeyLog).(Logger)
|
|
||||||
defer log.Info("exiting")
|
|
||||||
|
|
||||||
a := IntervalAutosnap{DatasetFilter: j.Datasets, Prefix: j.SnapshotPrefix, SnapshotInterval: j.Interval}
|
|
||||||
p, err := j.Pruner(PrunePolicySideDefault, false)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error creating pruner")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
snapContext := context.WithValue(ctx, contextKeyLog, log.WithField(logTaskField, "autosnap"))
|
|
||||||
prunerContext := context.WithValue(ctx, contextKeyLog, log.WithField(logTaskField, "prune"))
|
|
||||||
serveContext := context.WithValue(ctx, contextKeyLog, log.WithField(logTaskField, "serve"))
|
|
||||||
didSnaps := make(chan struct{})
|
|
||||||
|
|
||||||
go j.serve(serveContext)
|
|
||||||
go a.Run(snapContext, didSnaps)
|
|
||||||
|
|
||||||
outer:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
break outer
|
|
||||||
case <-didSnaps:
|
|
||||||
log.Info("starting pruner")
|
|
||||||
p.Run(prunerContext)
|
|
||||||
log.Info("pruner done")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.WithError(prunerContext.Err()).Info("context")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *SourceJob) Pruner(side PrunePolicySide, dryRun bool) (p Pruner, err error) {
|
|
||||||
p = Pruner{
|
|
||||||
time.Now(),
|
|
||||||
dryRun,
|
|
||||||
j.Datasets,
|
|
||||||
j.SnapshotPrefix,
|
|
||||||
j.Prune,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *SourceJob) serve(ctx context.Context) {
|
|
||||||
|
|
||||||
log := ctx.Value(contextKeyLog).(Logger)
|
|
||||||
|
|
||||||
listener, err := j.Serve.Listen()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error listening")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rwcChan := make(chan io.ReadWriteCloser)
|
|
||||||
|
|
||||||
// Serve connections until interrupted or error
|
|
||||||
outer:
|
|
||||||
for {
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
rwc, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error accepting connection")
|
|
||||||
close(rwcChan)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rwcChan <- rwc
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
|
|
||||||
case rwc, notClosed := <-rwcChan:
|
|
||||||
|
|
||||||
if !notClosed {
|
|
||||||
break outer // closed because of accept error
|
|
||||||
}
|
|
||||||
|
|
||||||
rwc, err := util.NewReadWriteCloserLogger(rwc, j.Debug.Conn.ReadDump, j.Debug.Conn.WriteDump)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct connection handler
|
|
||||||
handler := NewHandler(log, j.Datasets, &PrefixSnapshotFilter{j.SnapshotPrefix})
|
|
||||||
|
|
||||||
// handle connection
|
|
||||||
rpcServer := rpc.NewServer(rwc)
|
|
||||||
if j.Debug.RPC.Log {
|
|
||||||
rpclog := log.WithField("subsystem", "rpc")
|
|
||||||
rpcServer.SetLogger(rpclog, true)
|
|
||||||
}
|
|
||||||
registerEndpoints(rpcServer, handler)
|
|
||||||
if err = rpcServer.Serve(); err != nil {
|
|
||||||
log.WithError(err).Error("error serving connection")
|
|
||||||
}
|
|
||||||
rwc.Close()
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.WithError(ctx.Err()).Info("context")
|
|
||||||
break outer
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("closing listener")
|
|
||||||
err = listener.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error closing listener")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LoggingConfig struct {
|
|
||||||
Outlets logger.Outlets
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetNoMetadataFormatter interface {
|
|
||||||
SetNoMetadata(noMetadata bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLogging(i interface{}) (c *LoggingConfig, err error) {
|
|
||||||
|
|
||||||
c = &LoggingConfig{}
|
|
||||||
if i == nil {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var asMap struct {
|
|
||||||
Stdout struct {
|
|
||||||
Level string
|
|
||||||
Format string
|
|
||||||
}
|
|
||||||
TCP struct {
|
|
||||||
Level string
|
|
||||||
Format string
|
|
||||||
Net string
|
|
||||||
Address string
|
|
||||||
RetryInterval string `mapstructure:"retry_interval"`
|
|
||||||
TLS *struct {
|
|
||||||
CA string
|
|
||||||
Cert string
|
|
||||||
Key string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Syslog struct {
|
|
||||||
Enable bool
|
|
||||||
Format string
|
|
||||||
RetryInterval string `mapstructure:"retry_interval"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = mapstructure.Decode(i, &asMap); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "mapstructure error")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Outlets = logger.NewOutlets()
|
|
||||||
|
|
||||||
if asMap.Stdout.Level != "" {
|
|
||||||
|
|
||||||
out := WriterOutlet{
|
|
||||||
&HumanFormatter{},
|
|
||||||
os.Stdout,
|
|
||||||
}
|
|
||||||
|
|
||||||
level, err := logger.ParseLevel(asMap.Stdout.Level)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'level'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if asMap.Stdout.Format != "" {
|
|
||||||
out.Formatter, err = parseLogFormat(asMap.Stdout.Format)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'format'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Outlets.Add(out, level)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if asMap.TCP.Address != "" {
|
|
||||||
|
|
||||||
out := &TCPOutlet{}
|
|
||||||
|
|
||||||
out.Formatter, err = parseLogFormat(asMap.TCP.Format)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'format'")
|
|
||||||
}
|
|
||||||
|
|
||||||
lvl, err := logger.ParseLevel(asMap.TCP.Level)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'level'")
|
|
||||||
}
|
|
||||||
|
|
||||||
out.RetryInterval, err = time.ParseDuration(asMap.TCP.RetryInterval)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'retry_interval'")
|
|
||||||
}
|
|
||||||
|
|
||||||
out.Net, out.Address = asMap.TCP.Net, asMap.TCP.Address
|
|
||||||
|
|
||||||
if asMap.TCP.TLS != nil {
|
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(asMap.TCP.TLS.Cert, asMap.TCP.TLS.Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot load client cert")
|
|
||||||
}
|
|
||||||
|
|
||||||
var rootCAs *x509.CertPool
|
|
||||||
if asMap.TCP.TLS.CA == "" {
|
|
||||||
if rootCAs, err = x509.SystemCertPool(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot open system cert pool")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rootCAs = x509.NewCertPool()
|
|
||||||
rootCAPEM, err := ioutil.ReadFile(asMap.TCP.TLS.CA)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot load CA cert")
|
|
||||||
}
|
|
||||||
if !rootCAs.AppendCertsFromPEM(rootCAPEM) {
|
|
||||||
return nil, errors.New("cannot parse CA cert")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil && asMap.TCP.TLS.CA == "" {
|
|
||||||
return nil, errors.Wrap(err, "cannot load root ca pool")
|
|
||||||
}
|
|
||||||
|
|
||||||
out.TLS = &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
RootCAs: rootCAs,
|
|
||||||
}
|
|
||||||
|
|
||||||
out.TLS.BuildNameToCertificate()
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Outlets.Add(out, lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
if asMap.Syslog.Enable {
|
|
||||||
|
|
||||||
out := &SyslogOutlet{}
|
|
||||||
|
|
||||||
out.Formatter = &HumanFormatter{}
|
|
||||||
if asMap.Syslog.Format != "" {
|
|
||||||
out.Formatter, err = parseLogFormat(asMap.Syslog.Format)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'format'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if f, ok := out.Formatter.(SetNoMetadataFormatter); ok {
|
|
||||||
f.SetNoMetadata(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
out.RetryInterval = 0 // default to 0 as we assume local syslog will just work
|
|
||||||
if asMap.Syslog.RetryInterval != "" {
|
|
||||||
out.RetryInterval, err = time.ParseDuration(asMap.Syslog.RetryInterval)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'retry_interval'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Outlets.Add(out, logger.Debug)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
|
||||||
var is string
|
|
||||||
switch j := i.(type) {
|
|
||||||
case string:
|
|
||||||
is = j
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("invalid log format: wrong type: %T", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch is {
|
|
||||||
case "human":
|
|
||||||
return &HumanFormatter{}, nil
|
|
||||||
case "logfmt":
|
|
||||||
return &LogfmtFormatter{}, nil
|
|
||||||
case "json":
|
|
||||||
return &JSONFormatter{}, nil
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("invalid log format: '%s'", is)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"fmt"
|
|
||||||
yaml "github.com/go-yaml/yaml"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ConfigFileDefaultLocations []string = []string{
|
|
||||||
"/etc/zrepl/zrepl.yml",
|
|
||||||
"/usr/local/etc/zrepl/zrepl.yml",
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
JobNameControl string = "control"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ReservedJobNames []string = []string{
|
|
||||||
JobNameControl,
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigParsingContext struct {
|
|
||||||
Global *Global
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseConfig(path string) (config *Config, err error) {
|
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
// Try default locations
|
|
||||||
for _, l := range ConfigFileDefaultLocations {
|
|
||||||
stat, err := os.Stat(l)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !stat.Mode().IsRegular() {
|
|
||||||
err = errors.Errorf("file at default location is not a regular file: %s", l)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path = l
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var i interface{}
|
|
||||||
|
|
||||||
var bytes []byte
|
|
||||||
|
|
||||||
if bytes, err = ioutil.ReadFile(path); err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = yaml.Unmarshal(bytes, &i); err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseConfig(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseConfig(i interface{}) (c *Config, err error) {
|
|
||||||
|
|
||||||
var asMap struct {
|
|
||||||
Global map[string]interface{}
|
|
||||||
Jobs []map[string]interface{}
|
|
||||||
}
|
|
||||||
if err := mapstructure.Decode(i, &asMap); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "config root must be a dict")
|
|
||||||
}
|
|
||||||
|
|
||||||
c = &Config{}
|
|
||||||
|
|
||||||
// Parse global with defaults
|
|
||||||
c.Global.Serve.Stdinserver.SockDir = "/var/run/zrepl/stdinserver"
|
|
||||||
c.Global.Control.Sockpath = "/var/run/zrepl/control"
|
|
||||||
|
|
||||||
err = mapstructure.Decode(asMap.Global, &c.Global)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "mapstructure error on 'global' section: %s")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Global.logging, err = parseLogging(asMap.Global["logging"]); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse logging section")
|
|
||||||
}
|
|
||||||
|
|
||||||
cpc := ConfigParsingContext{&c.Global}
|
|
||||||
jpc := JobParsingContext{cpc}
|
|
||||||
|
|
||||||
// Jobs
|
|
||||||
c.Jobs = make(map[string]Job, len(asMap.Jobs))
|
|
||||||
for i := range asMap.Jobs {
|
|
||||||
job, err := parseJob(jpc, asMap.Jobs[i])
|
|
||||||
if err != nil {
|
|
||||||
// Try to find its name
|
|
||||||
namei, ok := asMap.Jobs[i]["name"]
|
|
||||||
if !ok {
|
|
||||||
namei = fmt.Sprintf("<no name, entry #%d in list>", i)
|
|
||||||
}
|
|
||||||
err = errors.Wrapf(err, "cannot parse job '%v'", namei)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
jn := job.JobName()
|
|
||||||
if _, ok := c.Jobs[jn]; ok {
|
|
||||||
err = errors.Errorf("duplicate job name: %s", jn)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.Jobs[job.JobName()] = job
|
|
||||||
}
|
|
||||||
|
|
||||||
cj, err := NewControlJob(JobNameControl, jpc.Global.Control.Sockpath)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "cannot create control job")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Jobs[JobNameControl] = cj
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractStringField(i map[string]interface{}, key string, notempty bool) (field string, err error) {
|
|
||||||
vi, ok := i[key]
|
|
||||||
if !ok {
|
|
||||||
err = errors.Errorf("must have field '%s'", key)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
field, ok = vi.(string)
|
|
||||||
if !ok {
|
|
||||||
err = errors.Errorf("'%s' field must have type string", key)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if notempty && len(field) <= 0 {
|
|
||||||
err = errors.Errorf("'%s' field must not be empty", key)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobParsingContext struct {
|
|
||||||
ConfigParsingContext
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseJob(c JobParsingContext, i map[string]interface{}) (j Job, err error) {
|
|
||||||
|
|
||||||
name, err := extractStringField(i, "name", true)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range ReservedJobNames {
|
|
||||||
if name == r {
|
|
||||||
err = errors.Errorf("job name '%s' is reserved", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jobtype, err := extractStringField(i, "type", true)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch jobtype {
|
|
||||||
case "pull":
|
|
||||||
return parsePullJob(c, name, i)
|
|
||||||
case "source":
|
|
||||||
return parseSourceJob(c, name, i)
|
|
||||||
case "local":
|
|
||||||
return parseLocalJob(c, name, i)
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("unknown job type '%s'", jobtype)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseConnect(i map[string]interface{}) (c RWCConnecter, err error) {
|
|
||||||
|
|
||||||
t, err := extractStringField(i, "type", true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch t {
|
|
||||||
case "ssh+stdinserver":
|
|
||||||
return parseSSHStdinserverConnecter(i)
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("unknown connection type '%s'", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInitialReplPolicy(v interface{}, defaultPolicy InitialReplPolicy) (p InitialReplPolicy, err error) {
|
|
||||||
s, ok := v.(string)
|
|
||||||
if !ok {
|
|
||||||
goto err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case s == "":
|
|
||||||
p = defaultPolicy
|
|
||||||
case s == "most_recent":
|
|
||||||
p = InitialReplPolicyMostRecent
|
|
||||||
case s == "all":
|
|
||||||
p = InitialReplPolicyAll
|
|
||||||
default:
|
|
||||||
goto err
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
err:
|
|
||||||
err = errors.New(fmt.Sprintf("expected InitialReplPolicy, got %#v", v))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePrunePolicy(v map[string]interface{}) (p PrunePolicy, err error) {
|
|
||||||
|
|
||||||
policyName, err := extractStringField(v, "policy", true)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch policyName {
|
|
||||||
case "grid":
|
|
||||||
return parseGridPrunePolicy(v)
|
|
||||||
case "noprune":
|
|
||||||
return NoPrunePolicy{}, nil
|
|
||||||
default:
|
|
||||||
err = errors.Errorf("unknown policy '%s'", policyName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAuthenticatedChannelListenerFactory(c JobParsingContext, v map[string]interface{}) (p AuthenticatedChannelListenerFactory, err error) {
|
|
||||||
|
|
||||||
t, err := extractStringField(v, "type", true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch t {
|
|
||||||
case "stdinserver":
|
|
||||||
return parseStdinserverListenerFactory(c, v)
|
|
||||||
default:
|
|
||||||
err = errors.Errorf("unknown type '%s'", t)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/util"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GridPrunePolicy struct {
|
|
||||||
RetentionGrid *util.RetentionGrid
|
|
||||||
}
|
|
||||||
|
|
||||||
type retentionGridAdaptor struct {
|
|
||||||
zfs.FilesystemVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a retentionGridAdaptor) Date() time.Time {
|
|
||||||
return a.Creation
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a retentionGridAdaptor) LessThan(b util.RetentionGridEntry) bool {
|
|
||||||
return a.CreateTXG < b.(retentionGridAdaptor).CreateTXG
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GridPrunePolicy) Prune(_ *zfs.DatasetPath, versions []zfs.FilesystemVersion) (keep, remove []zfs.FilesystemVersion, err error) {
|
|
||||||
|
|
||||||
// Build adaptors for retention grid
|
|
||||||
adaptors := make([]util.RetentionGridEntry, len(versions))
|
|
||||||
for fsv := range versions {
|
|
||||||
adaptors[fsv] = retentionGridAdaptor{versions[fsv]}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(adaptors, func(i, j int) bool {
|
|
||||||
return adaptors[i].LessThan(adaptors[j])
|
|
||||||
})
|
|
||||||
now := adaptors[len(adaptors)-1].Date()
|
|
||||||
|
|
||||||
// Evaluate retention grid
|
|
||||||
keepa, removea := p.RetentionGrid.FitEntries(now, adaptors)
|
|
||||||
|
|
||||||
// Revert adaptors
|
|
||||||
keep = make([]zfs.FilesystemVersion, len(keepa))
|
|
||||||
for i := range keepa {
|
|
||||||
keep[i] = keepa[i].(retentionGridAdaptor).FilesystemVersion
|
|
||||||
}
|
|
||||||
remove = make([]zfs.FilesystemVersion, len(removea))
|
|
||||||
for i := range removea {
|
|
||||||
remove[i] = removea[i].(retentionGridAdaptor).FilesystemVersion
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseGridPrunePolicy(e map[string]interface{}) (p *GridPrunePolicy, err error) {
|
|
||||||
|
|
||||||
var i struct {
|
|
||||||
Grid string
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = mapstructure.Decode(e, &i); err != nil {
|
|
||||||
err = errors.Wrapf(err, "mapstructure error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p = &GridPrunePolicy{}
|
|
||||||
|
|
||||||
// Parse grid policy
|
|
||||||
intervals, err := parseRetentionGridIntervalsString(i.Grid)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("cannot parse retention grid: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Assert intervals are of increasing length (not necessarily required, but indicates config mistake)
|
|
||||||
lastDuration := time.Duration(0)
|
|
||||||
for i := range intervals {
|
|
||||||
if intervals[i].Length < lastDuration {
|
|
||||||
err = fmt.Errorf("retention grid interval length must be monotonically increasing:"+
|
|
||||||
"interval %d is shorter than %d", i+1, i)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
lastDuration = intervals[i].Length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.RetentionGrid = util.NewRetentionGrid(intervals)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
|
||||||
|
|
||||||
func parseDuration(e string) (d time.Duration, err error) {
|
|
||||||
comps := durationStringRegex.FindStringSubmatch(e)
|
|
||||||
if 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var retentionStringIntervalRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*x\s*([^\(]+)\s*(\((.*)\))?\s*$`)
|
|
||||||
|
|
||||||
func parseRetentionGridIntervalString(e string) (intervals []util.RetentionInterval, err error) {
|
|
||||||
|
|
||||||
comps := retentionStringIntervalRegex.FindStringSubmatch(e)
|
|
||||||
if comps == nil {
|
|
||||||
err = fmt.Errorf("retention string does not match expected format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
times, err := strconv.Atoi(comps[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if times <= 0 {
|
|
||||||
return nil, fmt.Errorf("contains factor <= 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
duration, err := parseDuration(comps[2])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
keepCount := 1
|
|
||||||
if comps[3] != "" {
|
|
||||||
// Decompose key=value, comma separated
|
|
||||||
// For now, only keep_count is supported
|
|
||||||
re := regexp.MustCompile(`^\s*keep=(.+)\s*$`)
|
|
||||||
res := re.FindStringSubmatch(comps[4])
|
|
||||||
if res == nil || len(res) != 2 {
|
|
||||||
err = fmt.Errorf("interval parameter contains unknown parameters")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if res[1] == "all" {
|
|
||||||
keepCount = util.RetentionGridKeepCountAll
|
|
||||||
} else {
|
|
||||||
keepCount, err = strconv.Atoi(res[1])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("cannot parse keep_count value")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
intervals = make([]util.RetentionInterval, times)
|
|
||||||
for i := range intervals {
|
|
||||||
intervals[i] = util.RetentionInterval{
|
|
||||||
Length: duration,
|
|
||||||
KeepCount: keepCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRetentionGridIntervalsString(s string) (intervals []util.RetentionInterval, err error) {
|
|
||||||
|
|
||||||
ges := strings.Split(s, "|")
|
|
||||||
intervals = make([]util.RetentionInterval, 0, 7*len(ges))
|
|
||||||
|
|
||||||
for intervalIdx, e := range ges {
|
|
||||||
parsed, err := parseRetentionGridIntervalString(e)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot parse interval %d of %d: %s: %s", intervalIdx+1, len(ges), err, strings.TrimSpace(e))
|
|
||||||
}
|
|
||||||
intervals = append(intervals, parsed...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import "github.com/zrepl/zrepl/zfs"
|
|
||||||
|
|
||||||
type NoPrunePolicy struct{}
|
|
||||||
|
|
||||||
func (p NoPrunePolicy) Prune(fs *zfs.DatasetPath, versions []zfs.FilesystemVersion) (keep, remove []zfs.FilesystemVersion, err error) {
|
|
||||||
keep = versions
|
|
||||||
remove = []zfs.FilesystemVersion{}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ftrvxmtrx/fd"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StdinserverListenerFactory struct {
|
|
||||||
ClientIdentity string `mapstructure:"client_identity"`
|
|
||||||
sockaddr *net.UnixAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseStdinserverListenerFactory(c JobParsingContext, i map[string]interface{}) (f *StdinserverListenerFactory, err error) {
|
|
||||||
|
|
||||||
f = &StdinserverListenerFactory{}
|
|
||||||
|
|
||||||
if err = mapstructure.Decode(i, f); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "mapstructure error")
|
|
||||||
}
|
|
||||||
if !(len(f.ClientIdentity) > 0) {
|
|
||||||
err = errors.Errorf("must specify 'client_identity'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f.sockaddr, err = stdinserverListenerSocket(c.Global.Serve.Stdinserver.SockDir, f.ClientIdentity)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func stdinserverListenerSocket(sockdir, clientIdentity string) (addr *net.UnixAddr, err error) {
|
|
||||||
sockpath := path.Join(sockdir, clientIdentity)
|
|
||||||
addr, err = net.ResolveUnixAddr("unix", sockpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot resolve unix address")
|
|
||||||
}
|
|
||||||
return addr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *StdinserverListenerFactory) Listen() (al AuthenticatedChannelListener, err error) {
|
|
||||||
|
|
||||||
ul, err := ListenUnixPrivate(f.sockaddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "cannot listen on unix socket %s", f.sockaddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
l := &StdinserverListener{ul}
|
|
||||||
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type StdinserverListener struct {
|
|
||||||
l *net.UnixListener
|
|
||||||
}
|
|
||||||
|
|
||||||
type fdRWC struct {
|
|
||||||
stdin, stdout *os.File
|
|
||||||
control *net.UnixConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f fdRWC) Read(p []byte) (n int, err error) {
|
|
||||||
return f.stdin.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f fdRWC) Write(p []byte) (n int, err error) {
|
|
||||||
return f.stdout.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f fdRWC) Close() (err error) {
|
|
||||||
f.stdin.Close()
|
|
||||||
f.stdout.Close()
|
|
||||||
return f.control.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *StdinserverListener) Accept() (ch io.ReadWriteCloser, err error) {
|
|
||||||
c, err := l.l.Accept()
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "error accepting on unix listener")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the stdin and stdout of the stdinserver command
|
|
||||||
files, err := fd.Get(c.(*net.UnixConn), 2, []string{"stdin", "stdout"})
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "error receiving fds from stdinserver command")
|
|
||||||
c.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
rwc := fdRWC{files[0], files[1], c.(*net.UnixConn)}
|
|
||||||
|
|
||||||
return rwc, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *StdinserverListener) Close() (err error) {
|
|
||||||
return l.l.Close() // removes socket file automatically
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/zrepl/zrepl/util"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
|
||||||
|
|
||||||
paths := []string{
|
|
||||||
"./sampleconf/localbackup/host1.yml",
|
|
||||||
"./sampleconf/pullbackup/backuphost.yml",
|
|
||||||
"./sampleconf/pullbackup/productionhost.yml",
|
|
||||||
"./sampleconf/random/debugging.yml",
|
|
||||||
"./sampleconf/random/logging.yml",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range paths {
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseRetentionGridStringParsing(t *testing.T) {
|
|
||||||
|
|
||||||
intervals, err := parseRetentionGridIntervalsString("2x10m(keep=2) | 1x1h | 3x1w")
|
|
||||||
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Len(t, intervals, 6)
|
|
||||||
proto := util.RetentionInterval{
|
|
||||||
KeepCount: 2,
|
|
||||||
Length: 10 * time.Minute,
|
|
||||||
}
|
|
||||||
assert.EqualValues(t, proto, intervals[0])
|
|
||||||
assert.EqualValues(t, proto, intervals[1])
|
|
||||||
|
|
||||||
proto.KeepCount = 1
|
|
||||||
proto.Length = 1 * time.Hour
|
|
||||||
assert.EqualValues(t, proto, intervals[2])
|
|
||||||
|
|
||||||
proto.Length = 7 * 24 * time.Hour
|
|
||||||
assert.EqualValues(t, proto, intervals[3])
|
|
||||||
assert.EqualValues(t, proto, intervals[4])
|
|
||||||
assert.EqualValues(t, proto, intervals[5])
|
|
||||||
|
|
||||||
intervals, err = parseRetentionGridIntervalsString("|")
|
|
||||||
assert.Error(t, err)
|
|
||||||
intervals, err = parseRetentionGridIntervalsString("2x10m")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
intervals, err = parseRetentionGridIntervalsString("1x10m(keep=all)")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, intervals, 1)
|
|
||||||
assert.EqualValues(t, util.RetentionGridKeepCountAll, intervals[0].KeepCount)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDatasetMapFilter(t *testing.T) {
|
|
||||||
|
|
||||||
expectMapping := func(m map[string]string, from, to string) {
|
|
||||||
dmf, err := parseDatasetMapFilter(m, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("expect test map to be valid: %s", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
fromPath, err := zfs.NewDatasetPath(from)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("expect test from path to be valid: %s", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := dmf.Map(fromPath)
|
|
||||||
if to == "" {
|
|
||||||
assert.Nil(t, res)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
t.Logf("%s => NOT MAPPED", fromPath.ToString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Nil(t, err)
|
|
||||||
toPath, err := zfs.NewDatasetPath(to)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("expect test to path to be valid: %s", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
assert.True(t, res.Equal(toPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
expectFilter := func(m map[string]string, path string, pass bool) {
|
|
||||||
dmf, err := parseDatasetMapFilter(m, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("expect test filter to be valid: %s", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
p, err := zfs.NewDatasetPath(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("expect test path to be valid: %s", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
res, err := dmf.Filter(p)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, pass, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
map1 := map[string]string{
|
|
||||||
"a/b/c<": "root1",
|
|
||||||
"a/b<": "root2",
|
|
||||||
"<": "root3/b/c",
|
|
||||||
"b": "!",
|
|
||||||
"a/b/c/d/e<": "!",
|
|
||||||
"q<": "root4/1/2",
|
|
||||||
}
|
|
||||||
|
|
||||||
expectMapping(map1, "a/b/c", "root1")
|
|
||||||
expectMapping(map1, "a/b/c/d", "root1/d")
|
|
||||||
expectMapping(map1, "a/b/c/d/e", "")
|
|
||||||
expectMapping(map1, "a/b/e", "root2/e")
|
|
||||||
expectMapping(map1, "a/b", "root2")
|
|
||||||
expectMapping(map1, "x", "root3/b/c")
|
|
||||||
expectMapping(map1, "x/y", "root3/b/c/y")
|
|
||||||
expectMapping(map1, "q", "root4/1/2")
|
|
||||||
expectMapping(map1, "b", "")
|
|
||||||
expectMapping(map1, "q/r", "root4/1/2/r")
|
|
||||||
|
|
||||||
filter1 := map[string]string{
|
|
||||||
"<": "!",
|
|
||||||
"a<": "ok",
|
|
||||||
"a/b<": "!",
|
|
||||||
}
|
|
||||||
|
|
||||||
expectFilter(filter1, "b", false)
|
|
||||||
expectFilter(filter1, "a", true)
|
|
||||||
expectFilter(filter1, "a/d", true)
|
|
||||||
expectFilter(filter1, "a/b", false)
|
|
||||||
expectFilter(filter1, "a/b/c", false)
|
|
||||||
|
|
||||||
filter2 := map[string]string{}
|
|
||||||
expectFilter(filter2, "foo", false) // default to omit
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDatasetMapFilter_AsFilter(t *testing.T) {
|
|
||||||
|
|
||||||
mapspec := map[string]string{
|
|
||||||
"a/b/c<": "root1",
|
|
||||||
"a/b<": "root2",
|
|
||||||
"<": "root3/b/c",
|
|
||||||
"b": "!",
|
|
||||||
"a/b/c/d/e<": "!",
|
|
||||||
"q<": "root4/1/2",
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := parseDatasetMapFilter(mapspec, false)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
f := m.AsFilter()
|
|
||||||
|
|
||||||
t.Logf("Mapping:\n%s\nFilter:\n%s", pretty.Sprint(m), pretty.Sprint(f))
|
|
||||||
|
|
||||||
tf := func(f zfs.DatasetFilter, path string, pass bool) {
|
|
||||||
p, err := zfs.NewDatasetPath(path)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
r, err := f.Filter(p)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, pass, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
tf(f, "a/b/c", true)
|
|
||||||
tf(f, "a/b", true)
|
|
||||||
tf(f, "b", false)
|
|
||||||
tf(f, "a/b/c/d/e", false)
|
|
||||||
tf(f, "a/b/c/d/e/f", false)
|
|
||||||
tf(f, "a", true)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDatasetMapFilter_InvertedFilter(t *testing.T) {
|
|
||||||
mapspec := map[string]string{
|
|
||||||
"a/b": "1/2",
|
|
||||||
"a/b/c<": "3",
|
|
||||||
"a/b/c/d<": "1/2/a",
|
|
||||||
"a/b/d": "!",
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := parseDatasetMapFilter(mapspec, false)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
inv, err := m.InvertedFilter()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
t.Log(pretty.Sprint(inv))
|
|
||||||
|
|
||||||
expectMapping := func(m *DatasetMapFilter, ps string, expRes bool) {
|
|
||||||
p, err := zfs.NewDatasetPath(ps)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
r, err := m.Filter(p)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, expRes, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectMapping(inv, "4", false)
|
|
||||||
expectMapping(inv, "3", true)
|
|
||||||
expectMapping(inv, "3/x", true)
|
|
||||||
expectMapping(inv, "1", false)
|
|
||||||
expectMapping(inv, "1/2", true)
|
|
||||||
expectMapping(inv, "1/2/3", false)
|
|
||||||
expectMapping(inv, "1/2/a/b", true)
|
|
||||||
|
|
||||||
}
|
|
||||||
-100
@@ -1,100 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"io"
|
|
||||||
golog "log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var controlCmd = &cobra.Command{
|
|
||||||
Use: "control",
|
|
||||||
Short: "control zrepl daemon",
|
|
||||||
}
|
|
||||||
|
|
||||||
var pprofCmd = &cobra.Command{
|
|
||||||
Use: "pprof cpu OUTFILE",
|
|
||||||
Short: "pprof CPU of daemon to OUTFILE (- for stdout)",
|
|
||||||
Run: doControlPProf,
|
|
||||||
}
|
|
||||||
var pprofCmdArgs struct {
|
|
||||||
seconds int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
RootCmd.AddCommand(controlCmd)
|
|
||||||
controlCmd.AddCommand(pprofCmd)
|
|
||||||
pprofCmd.Flags().Int64Var(&pprofCmdArgs.seconds, "seconds", 30, "seconds to profile")
|
|
||||||
}
|
|
||||||
|
|
||||||
func doControlPProf(cmd *cobra.Command, args []string) {
|
|
||||||
|
|
||||||
log := golog.New(os.Stderr, "", 0)
|
|
||||||
|
|
||||||
die := func() {
|
|
||||||
log.Printf("exiting after error")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
conf, err := ParseConfig(rootArgs.configFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error parsing config: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flags().Arg(0) != "cpu" {
|
|
||||||
log.Printf("only CPU profiles are supported")
|
|
||||||
log.Printf("%s", cmd.UsageString())
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
outfn := cmd.Flags().Arg(1)
|
|
||||||
if outfn == "" {
|
|
||||||
log.Printf("must specify output filename")
|
|
||||||
log.Printf("%s", cmd.UsageString())
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
var out io.Writer
|
|
||||||
if outfn == "-" {
|
|
||||||
out = os.Stdout
|
|
||||||
} else {
|
|
||||||
out, err = os.Create(outfn)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error creating output file: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("connecting to daemon")
|
|
||||||
httpc := http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
return net.Dial("unix", conf.Global.Control.Sockpath)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("profiling...")
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("seconds", fmt.Sprintf("%d", pprofCmdArgs.seconds))
|
|
||||||
v.Encode()
|
|
||||||
resp, err := httpc.Get("http://unix" + ControlJobEndpointProfile + "?" + v.Encode())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(out, resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error writing profile: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("finished")
|
|
||||||
|
|
||||||
}
|
|
||||||
-110
@@ -1,110 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// daemonCmd represents the daemon command
|
|
||||||
var daemonCmd = &cobra.Command{
|
|
||||||
Use: "daemon",
|
|
||||||
Short: "start daemon",
|
|
||||||
Run: doDaemon,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
RootCmd.AddCommand(daemonCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Job interface {
|
|
||||||
JobName() string
|
|
||||||
JobStart(ctxt context.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
func doDaemon(cmd *cobra.Command, args []string) {
|
|
||||||
|
|
||||||
conf, err := ParseConfig(rootArgs.configFile)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error parsing config: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
log := logger.NewLogger(conf.Global.logging.Outlets, 1*time.Second)
|
|
||||||
|
|
||||||
log.Debug("starting daemon")
|
|
||||||
ctx := context.WithValue(context.Background(), contextKeyLog, log)
|
|
||||||
ctx = context.WithValue(ctx, contextKeyLog, log)
|
|
||||||
|
|
||||||
d := NewDaemon(conf)
|
|
||||||
d.Loop(ctx)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type contextKey string
|
|
||||||
|
|
||||||
const (
|
|
||||||
contextKeyLog contextKey = contextKey("log")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Daemon struct {
|
|
||||||
conf *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDaemon(initialConf *Config) *Daemon {
|
|
||||||
return &Daemon{initialConf}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) Loop(ctx context.Context) {
|
|
||||||
|
|
||||||
log := ctx.Value(contextKeyLog).(Logger)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
finishs := make(chan Job)
|
|
||||||
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
|
|
||||||
log.Info("starting jobs from config")
|
|
||||||
i := 0
|
|
||||||
for _, job := range d.conf.Jobs {
|
|
||||||
logger := log.WithField(logJobField, job.JobName())
|
|
||||||
logger.Info("starting")
|
|
||||||
i++
|
|
||||||
jobCtx := context.WithValue(ctx, contextKeyLog, logger)
|
|
||||||
go func(j Job) {
|
|
||||||
j.JobStart(jobCtx)
|
|
||||||
finishs <- j
|
|
||||||
}(job)
|
|
||||||
}
|
|
||||||
|
|
||||||
finishCount := 0
|
|
||||||
outer:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-finishs:
|
|
||||||
finishCount++
|
|
||||||
if finishCount == len(d.conf.Jobs) {
|
|
||||||
log.Info("all jobs finished")
|
|
||||||
break outer
|
|
||||||
}
|
|
||||||
|
|
||||||
case sig := <-sigChan:
|
|
||||||
log.WithField("signal", sig).Info("received signal")
|
|
||||||
log.Info("cancelling all jobs")
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signal.Stop(sigChan)
|
|
||||||
cancel() // make go vet happy
|
|
||||||
|
|
||||||
log.Info("exiting")
|
|
||||||
|
|
||||||
}
|
|
||||||
-181
@@ -1,181 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/rpc"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DatasetMapping interface {
|
|
||||||
Map(source *zfs.DatasetPath) (target *zfs.DatasetPath, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilesystemRequest struct {
|
|
||||||
Roots []string // may be nil, indicating interest in all filesystems
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilesystemVersionsRequest struct {
|
|
||||||
Filesystem *zfs.DatasetPath
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitialTransferRequest struct {
|
|
||||||
Filesystem *zfs.DatasetPath
|
|
||||||
FilesystemVersion zfs.FilesystemVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
type IncrementalTransferRequest struct {
|
|
||||||
Filesystem *zfs.DatasetPath
|
|
||||||
From zfs.FilesystemVersion
|
|
||||||
To zfs.FilesystemVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler struct {
|
|
||||||
logger Logger
|
|
||||||
dsf zfs.DatasetFilter
|
|
||||||
fsvf zfs.FilesystemVersionFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandler(logger Logger, dsfilter zfs.DatasetFilter, snapfilter zfs.FilesystemVersionFilter) (h Handler) {
|
|
||||||
return Handler{logger, dsfilter, snapfilter}
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerEndpoints(server rpc.RPCServer, handler Handler) (err error) {
|
|
||||||
err = server.RegisterEndpoint("FilesystemRequest", handler.HandleFilesystemRequest)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = server.RegisterEndpoint("FilesystemVersionsRequest", handler.HandleFilesystemVersionsRequest)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = server.RegisterEndpoint("InitialTransferRequest", handler.HandleInitialTransferRequest)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = server.RegisterEndpoint("IncrementalTransferRequest", handler.HandleIncrementalTransferRequest)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h Handler) HandleFilesystemRequest(r *FilesystemRequest, roots *[]*zfs.DatasetPath) (err error) {
|
|
||||||
|
|
||||||
log := h.logger.WithField("endpoint", "FilesystemRequest")
|
|
||||||
|
|
||||||
log.WithField("request", r).Debug("request")
|
|
||||||
log.WithField("dataset_filter", h.dsf).Debug("dsf")
|
|
||||||
|
|
||||||
allowed, err := zfs.ZFSListMapping(h.dsf)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error listing filesystems")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("response", allowed).Debug("response")
|
|
||||||
*roots = allowed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h Handler) HandleFilesystemVersionsRequest(r *FilesystemVersionsRequest, versions *[]zfs.FilesystemVersion) (err error) {
|
|
||||||
|
|
||||||
log := h.logger.WithField("endpoint", "FilesystemVersionsRequest")
|
|
||||||
|
|
||||||
log.WithField("request", r).Debug("request")
|
|
||||||
|
|
||||||
// allowed to request that?
|
|
||||||
if h.pullACLCheck(r.Filesystem, nil); err != nil {
|
|
||||||
log.WithError(err).Warn("pull ACL check failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// find our versions
|
|
||||||
vs, err := zfs.ZFSListFilesystemVersions(r.Filesystem, h.fsvf)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("cannot list filesystem versions")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("resposne", vs).Debug("response")
|
|
||||||
|
|
||||||
*versions = vs
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h Handler) HandleInitialTransferRequest(r *InitialTransferRequest, stream *io.Reader) (err error) {
|
|
||||||
|
|
||||||
log := h.logger.WithField("endpoint", "InitialTransferRequest")
|
|
||||||
|
|
||||||
log.WithField("request", r).Debug("request")
|
|
||||||
if err = h.pullACLCheck(r.Filesystem, &r.FilesystemVersion); err != nil {
|
|
||||||
log.WithError(err).Warn("pull ACL check failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("invoking zfs send")
|
|
||||||
|
|
||||||
s, err := zfs.ZFSSend(r.Filesystem, &r.FilesystemVersion, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("cannot send filesystem")
|
|
||||||
}
|
|
||||||
*stream = s
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h Handler) HandleIncrementalTransferRequest(r *IncrementalTransferRequest, stream *io.Reader) (err error) {
|
|
||||||
|
|
||||||
log := h.logger.WithField("endpoint", "IncrementalTransferRequest")
|
|
||||||
log.WithField("request", r).Debug("request")
|
|
||||||
if err = h.pullACLCheck(r.Filesystem, &r.From); err != nil {
|
|
||||||
log.WithError(err).Warn("pull ACL check failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = h.pullACLCheck(r.Filesystem, &r.To); err != nil {
|
|
||||||
log.WithError(err).Warn("pull ACL check failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("invoking zfs send")
|
|
||||||
|
|
||||||
s, err := zfs.ZFSSend(r.Filesystem, &r.From, &r.To)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("cannot send filesystem")
|
|
||||||
}
|
|
||||||
|
|
||||||
*stream = s
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h Handler) pullACLCheck(p *zfs.DatasetPath, v *zfs.FilesystemVersion) (err error) {
|
|
||||||
var fsAllowed, vAllowed bool
|
|
||||||
fsAllowed, err = h.dsf.Filter(p)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("error evaluating ACL: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !fsAllowed {
|
|
||||||
err = fmt.Errorf("ACL prohibits access to %s", p.ToString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if v == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vAllowed, err = h.fsvf.Filter(*v)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "error evaluating version filter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !vAllowed {
|
|
||||||
err = fmt.Errorf("ACL prohibits access to %s", v.ToAbsPath(p))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ListenUnixPrivate(sockaddr *net.UnixAddr) (*net.UnixListener, error) {
|
|
||||||
|
|
||||||
sockdir := filepath.Dir(sockaddr.Name)
|
|
||||||
sdstat, err := os.Stat(sockdir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "cannot stat(2) '%s'", sockdir)
|
|
||||||
}
|
|
||||||
if !sdstat.IsDir() {
|
|
||||||
return nil, errors.Errorf("not a directory: %s", sockdir)
|
|
||||||
}
|
|
||||||
p := sdstat.Mode().Perm()
|
|
||||||
if p&0007 != 0 {
|
|
||||||
return nil, errors.Errorf("socket directory not be world-accessible: %s (permissions are %#o)", sockdir, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maybe things have not been cleaned up before
|
|
||||||
s, err := os.Stat(sockaddr.Name)
|
|
||||||
if err == nil {
|
|
||||||
if s.Mode()&os.ModeSocket != 0 {
|
|
||||||
// opportunistically try to remove it, but if this fails, it is not an error
|
|
||||||
os.Remove(sockaddr.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return net.ListenUnix("unix", sockaddr)
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/go-logfmt/logfmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EntryFormatter interface {
|
|
||||||
Format(e *logger.Entry) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
FieldLevel = "level"
|
|
||||||
FieldMessage = "msg"
|
|
||||||
FieldTime = "time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
logJobField string = "job"
|
|
||||||
logTaskField string = "task"
|
|
||||||
logFSField string = "filesystem"
|
|
||||||
logMapFromField string = "map_from"
|
|
||||||
logMapToField string = "map_to"
|
|
||||||
logIncFromField string = "inc_from"
|
|
||||||
logIncToField string = "inc_to"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NoFormatter struct{}
|
|
||||||
|
|
||||||
func (f NoFormatter) Format(e *logger.Entry) ([]byte, error) {
|
|
||||||
return []byte(e.Message), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type HumanFormatter struct {
|
|
||||||
NoMetadata bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ SetNoMetadataFormatter = &HumanFormatter{}
|
|
||||||
|
|
||||||
func (f *HumanFormatter) SetNoMetadata(noMetadata bool) {
|
|
||||||
f.NoMetadata = noMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
|
|
||||||
|
|
||||||
var line bytes.Buffer
|
|
||||||
|
|
||||||
if !f.NoMetadata {
|
|
||||||
fmt.Fprintf(&line, "[%s]", e.Level.Short())
|
|
||||||
}
|
|
||||||
|
|
||||||
prefixFields := []string{logJobField, logTaskField, logFSField}
|
|
||||||
prefixed := make(map[string]bool, len(prefixFields)+2)
|
|
||||||
for _, field := range prefixFields {
|
|
||||||
val, ok := e.Fields[field].(string)
|
|
||||||
if ok {
|
|
||||||
fmt.Fprintf(&line, "[%s]", val)
|
|
||||||
prefixed[field] = true
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// even more prefix fields
|
|
||||||
mapFrom, mapFromOk := e.Fields[logMapFromField].(string)
|
|
||||||
mapTo, mapToOk := e.Fields[logMapToField].(string)
|
|
||||||
if mapFromOk && mapToOk {
|
|
||||||
fmt.Fprintf(&line, "[%s => %s]", mapFrom, mapTo)
|
|
||||||
prefixed[logMapFromField], prefixed[logMapToField] = true, true
|
|
||||||
}
|
|
||||||
incFrom, incFromOk := e.Fields[logIncFromField].(string)
|
|
||||||
incTo, incToOk := e.Fields[logIncToField].(string)
|
|
||||||
if incFromOk && incToOk {
|
|
||||||
fmt.Fprintf(&line, "[%s => %s]", incFrom, incTo)
|
|
||||||
prefixed[logIncFromField], prefixed[logIncToField] = true, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.Len() > 0 {
|
|
||||||
fmt.Fprint(&line, ": ")
|
|
||||||
}
|
|
||||||
fmt.Fprint(&line, e.Message)
|
|
||||||
|
|
||||||
for field, value := range e.Fields {
|
|
||||||
|
|
||||||
if prefixed[field] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.ContainsAny(field, " \t") {
|
|
||||||
return nil, errors.Errorf("field must not contain whitespace: '%s'", field)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(&line, " %s=\"%s\"", field, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return line.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type JSONFormatter struct{}
|
|
||||||
|
|
||||||
func (f *JSONFormatter) Format(e *logger.Entry) ([]byte, error) {
|
|
||||||
data := make(logger.Fields, len(e.Fields)+3)
|
|
||||||
for k, v := range e.Fields {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case error:
|
|
||||||
// Otherwise errors are ignored by `encoding/json`
|
|
||||||
// https://github.com/sirupsen/logrus/issues/137
|
|
||||||
data[k] = v.Error()
|
|
||||||
default:
|
|
||||||
_, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("field is not JSON encodable: %s", k)
|
|
||||||
}
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data[FieldMessage] = e.Message
|
|
||||||
data[FieldTime] = e.Time.Format(time.RFC3339)
|
|
||||||
data[FieldLevel] = e.Level
|
|
||||||
|
|
||||||
return json.Marshal(data)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogfmtFormatter struct {
|
|
||||||
NoMetadata bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ SetNoMetadataFormatter = &LogfmtFormatter{}
|
|
||||||
|
|
||||||
func (f *LogfmtFormatter) SetNoMetadata(noMetadata bool) {
|
|
||||||
f.NoMetadata = noMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
enc := logfmt.NewEncoder(&buf)
|
|
||||||
|
|
||||||
if !f.NoMetadata {
|
|
||||||
enc.EncodeKeyval(FieldTime, e.Time)
|
|
||||||
enc.EncodeKeyval(FieldLevel, e.Level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// at least try and put job and task in front
|
|
||||||
prefixed := make(map[string]bool, 2)
|
|
||||||
prefix := []string{logJobField, logTaskField}
|
|
||||||
for _, pf := range prefix {
|
|
||||||
v, ok := e.Fields[pf]
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
enc.EncodeKeyval(pf, v)
|
|
||||||
prefixed[pf] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
enc.EncodeKeyval(FieldMessage, e.Message)
|
|
||||||
|
|
||||||
for k, v := range e.Fields {
|
|
||||||
if !prefixed[k] {
|
|
||||||
enc.EncodeKeyval(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := enc.EndRecord(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"io"
|
|
||||||
"log/syslog"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WriterOutlet struct {
|
|
||||||
Formatter EntryFormatter
|
|
||||||
Writer io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h WriterOutlet) WriteEntry(ctx context.Context, entry logger.Entry) error {
|
|
||||||
bytes, err := h.Formatter.Format(&entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = h.Writer.Write(bytes)
|
|
||||||
h.Writer.Write([]byte("\n"))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPOutlet struct {
|
|
||||||
Formatter EntryFormatter
|
|
||||||
Net, Address string
|
|
||||||
Dialer net.Dialer
|
|
||||||
TLS *tls.Config
|
|
||||||
// Specifies how much time must pass between a connection error and a reconnection attempt
|
|
||||||
// Log entries written to the outlet during this time interval are silently dropped.
|
|
||||||
RetryInterval time.Duration
|
|
||||||
// nil if there was an error sending / connecting to remote server
|
|
||||||
conn net.Conn
|
|
||||||
// Last time an error occurred when sending / connecting to remote server
|
|
||||||
retry time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *TCPOutlet) WriteEntry(ctx context.Context, e logger.Entry) error {
|
|
||||||
|
|
||||||
b, err := h.Formatter.Format(&e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.conn == nil {
|
|
||||||
if time.Now().Sub(h.retry) < h.RetryInterval {
|
|
||||||
// cool-down phase, drop the log entry
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.TLS != nil {
|
|
||||||
h.conn, err = tls.DialWithDialer(&h.Dialer, h.Net, h.Address, h.TLS)
|
|
||||||
} else {
|
|
||||||
h.conn, err = h.Dialer.DialContext(ctx, h.Net, h.Address)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
h.conn = nil
|
|
||||||
h.retry = time.Now()
|
|
||||||
return errors.Wrap(err, "cannot dial")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = h.conn.Write(b)
|
|
||||||
if err == nil {
|
|
||||||
_, err = h.conn.Write([]byte("\n"))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
h.conn.Close()
|
|
||||||
h.conn = nil
|
|
||||||
h.retry = time.Now()
|
|
||||||
return errors.Wrap(err, "cannot write")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SyslogOutlet struct {
|
|
||||||
Formatter EntryFormatter
|
|
||||||
RetryInterval time.Duration
|
|
||||||
writer *syslog.Writer
|
|
||||||
lastConnectAttempt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *SyslogOutlet) WriteEntry(ctx context.Context, entry logger.Entry) error {
|
|
||||||
|
|
||||||
bytes, err := o.Formatter.Format(&entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := string(bytes)
|
|
||||||
|
|
||||||
if o.writer == nil {
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(o.lastConnectAttempt) < o.RetryInterval {
|
|
||||||
return nil // not an error toward logger
|
|
||||||
}
|
|
||||||
o.writer, err = syslog.New(syslog.LOG_LOCAL0, "zrepl")
|
|
||||||
o.lastConnectAttempt = time.Now()
|
|
||||||
if err != nil {
|
|
||||||
o.writer = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch entry.Level {
|
|
||||||
case logger.Debug:
|
|
||||||
return o.writer.Debug(s)
|
|
||||||
case logger.Info:
|
|
||||||
return o.writer.Info(s)
|
|
||||||
case logger.Warn:
|
|
||||||
return o.writer.Warning(s)
|
|
||||||
case logger.Error:
|
|
||||||
return o.writer.Err(s)
|
|
||||||
default:
|
|
||||||
return o.writer.Err(s) // write as error as reaching this case is in fact an error
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
// zrepl replicates ZFS filesystems & volumes between pools
|
|
||||||
//
|
|
||||||
// Code Organization
|
|
||||||
//
|
|
||||||
// The cmd package uses github.com/spf13/cobra for its CLI.
|
|
||||||
//
|
|
||||||
// It combines the other packages in the zrepl project to implement zrepl functionality.
|
|
||||||
//
|
|
||||||
// Each subcommand's code is in the corresponding *.go file.
|
|
||||||
// All other *.go files contain code shared by the subcommands.
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
//type Logger interface {
|
|
||||||
// Printf(format string, v ...interface{})
|
|
||||||
//}
|
|
||||||
|
|
||||||
type Logger = *logger.Logger
|
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
|
||||||
Use: "zrepl",
|
|
||||||
Short: "ZFS dataset replication",
|
|
||||||
Long: `Replicate ZFS filesystems & volumes between pools:
|
|
||||||
|
|
||||||
- push & pull mode
|
|
||||||
- automatic snapshot creation & pruning
|
|
||||||
- local / over the network
|
|
||||||
- ACLs instead of blank SSH access`,
|
|
||||||
}
|
|
||||||
|
|
||||||
var rootArgs struct {
|
|
||||||
configFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
//cobra.OnInitialize(initConfig)
|
|
||||||
RootCmd.PersistentFlags().StringVar(&rootArgs.configFile, "config", "", "config file path")
|
|
||||||
}
|
|
||||||
-103
@@ -1,103 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Pruner struct {
|
|
||||||
Now time.Time
|
|
||||||
DryRun bool
|
|
||||||
DatasetFilter zfs.DatasetFilter
|
|
||||||
SnapshotPrefix string
|
|
||||||
PrunePolicy PrunePolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
type PruneResult struct {
|
|
||||||
Filesystem *zfs.DatasetPath
|
|
||||||
All []zfs.FilesystemVersion
|
|
||||||
Keep []zfs.FilesystemVersion
|
|
||||||
Remove []zfs.FilesystemVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pruner) Run(ctx context.Context) (r []PruneResult, err error) {
|
|
||||||
|
|
||||||
log := ctx.Value(contextKeyLog).(Logger)
|
|
||||||
|
|
||||||
if p.DryRun {
|
|
||||||
log.Info("doing dry run")
|
|
||||||
}
|
|
||||||
|
|
||||||
filesystems, err := zfs.ZFSListMapping(p.DatasetFilter)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error applying filesystem filter")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(filesystems) <= 0 {
|
|
||||||
log.Info("no filesystems matching filter")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r = make([]PruneResult, 0, len(filesystems))
|
|
||||||
|
|
||||||
for _, fs := range filesystems {
|
|
||||||
|
|
||||||
log := log.WithField(logFSField, fs.ToString())
|
|
||||||
|
|
||||||
fsversions, err := zfs.ZFSListFilesystemVersions(fs, &PrefixSnapshotFilter{p.SnapshotPrefix})
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error listing filesytem versions")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(fsversions) == 0 {
|
|
||||||
log.WithField("prefix", p.SnapshotPrefix).Info("no filesystem versions matching prefix")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
keep, remove, err := p.PrunePolicy.Prune(fs, fsversions)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error evaluating prune policy")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("fsversions", fsversions).
|
|
||||||
WithField("keep", keep).
|
|
||||||
WithField("remove", remove).
|
|
||||||
Debug("prune policy debug dump")
|
|
||||||
|
|
||||||
r = append(r, PruneResult{fs, fsversions, keep, remove})
|
|
||||||
|
|
||||||
makeFields := func(v zfs.FilesystemVersion) (fields map[string]interface{}) {
|
|
||||||
fields = make(map[string]interface{})
|
|
||||||
fields["version"] = v.ToAbsPath(fs)
|
|
||||||
timeSince := v.Creation.Sub(p.Now)
|
|
||||||
fields["age_ns"] = timeSince
|
|
||||||
const day time.Duration = 24 * time.Hour
|
|
||||||
days := timeSince / day
|
|
||||||
remainder := timeSince % day
|
|
||||||
fields["age_str"] = fmt.Sprintf("%dd%s", days, remainder)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range remove {
|
|
||||||
fields := makeFields(v)
|
|
||||||
log.WithFields(fields).Info("destroying version")
|
|
||||||
// echo what we'll do and exec zfs destroy if not dry run
|
|
||||||
// TODO special handling for EBUSY (zfs hold)
|
|
||||||
// TODO error handling for clones? just echo to cli, skip over, and exit with non-zero status code (we're idempotent)
|
|
||||||
if !p.DryRun {
|
|
||||||
err := zfs.ZFSDestroyFilesystemVersion(fs, v)
|
|
||||||
if err != nil {
|
|
||||||
// handle
|
|
||||||
log.WithFields(fields).WithError(err).Error("error destroying version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/zrepl/zrepl/rpc"
|
|
||||||
"github.com/zrepl/zrepl/util"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type localPullACL struct{}
|
|
||||||
|
|
||||||
func (a localPullACL) Filter(p *zfs.DatasetPath) (pass bool, err error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOCAL_TRANSPORT_IDENTITY string = "local"
|
|
||||||
|
|
||||||
const DEFAULT_INITIAL_REPL_POLICY = InitialReplPolicyMostRecent
|
|
||||||
|
|
||||||
type InitialReplPolicy string
|
|
||||||
|
|
||||||
const (
|
|
||||||
InitialReplPolicyMostRecent InitialReplPolicy = "most_recent"
|
|
||||||
InitialReplPolicyAll InitialReplPolicy = "all"
|
|
||||||
)
|
|
||||||
|
|
||||||
func closeRPCWithTimeout(log Logger, remote rpc.RPCClient, timeout time.Duration, goodbye string) {
|
|
||||||
log.Info("closing rpc connection")
|
|
||||||
|
|
||||||
ch := make(chan error)
|
|
||||||
go func() {
|
|
||||||
ch <- remote.Close()
|
|
||||||
close(ch)
|
|
||||||
}()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
select {
|
|
||||||
case <-time.After(timeout):
|
|
||||||
err = fmt.Errorf("timeout exceeded (%s)", timeout)
|
|
||||||
case closeRequestErr := <-ch:
|
|
||||||
err = closeRequestErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("error closing connection")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type PullContext struct {
|
|
||||||
Remote rpc.RPCClient
|
|
||||||
Log Logger
|
|
||||||
Mapping DatasetMapping
|
|
||||||
InitialReplPolicy InitialReplPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
func doPull(pull PullContext) (err error) {
|
|
||||||
|
|
||||||
remote := pull.Remote
|
|
||||||
log := pull.Log
|
|
||||||
|
|
||||||
log.Info("request remote filesystem list")
|
|
||||||
fsr := FilesystemRequest{}
|
|
||||||
var remoteFilesystems []*zfs.DatasetPath
|
|
||||||
if err = remote.Call("FilesystemRequest", &fsr, &remoteFilesystems); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("map remote filesystems to local paths and determine order for per-filesystem sync")
|
|
||||||
type RemoteLocalMapping struct {
|
|
||||||
Remote *zfs.DatasetPath
|
|
||||||
Local *zfs.DatasetPath
|
|
||||||
}
|
|
||||||
replMapping := make(map[string]RemoteLocalMapping, len(remoteFilesystems))
|
|
||||||
localTraversal := zfs.NewDatasetPathForest()
|
|
||||||
for fs := range remoteFilesystems {
|
|
||||||
var err error
|
|
||||||
var localFs *zfs.DatasetPath
|
|
||||||
localFs, err = pull.Mapping.Map(remoteFilesystems[fs])
|
|
||||||
if err != nil {
|
|
||||||
err := fmt.Errorf("error mapping %s: %s", remoteFilesystems[fs], err)
|
|
||||||
log.WithError(err).WithField(logMapFromField, remoteFilesystems[fs]).Error("cannot map")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if localFs == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.WithField(logMapFromField, remoteFilesystems[fs].ToString()).
|
|
||||||
WithField(logMapToField, localFs.ToString()).Debug("mapping")
|
|
||||||
m := RemoteLocalMapping{remoteFilesystems[fs], localFs}
|
|
||||||
replMapping[m.Local.ToString()] = m
|
|
||||||
localTraversal.Add(m.Local)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("build cache for already present local filesystem state")
|
|
||||||
localFilesystemState, err := zfs.ZFSListFilesystemState()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("cannot request local filesystem state")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("start per-filesystem sync")
|
|
||||||
localTraversal.WalkTopDown(func(v zfs.DatasetPathVisit) bool {
|
|
||||||
|
|
||||||
log := log.WithField(logFSField, v.Path.ToString())
|
|
||||||
|
|
||||||
if v.FilledIn {
|
|
||||||
if _, exists := localFilesystemState[v.Path.ToString()]; exists {
|
|
||||||
// No need to verify if this is a placeholder or not. It is sufficient
|
|
||||||
// to know we can add child filesystems to it
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
log.Debug("create placeholder filesystem")
|
|
||||||
err = zfs.ZFSCreatePlaceholderFilesystem(v.Path)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("cannot create placeholder filesystem")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
m, ok := replMapping[v.Path.ToString()]
|
|
||||||
if !ok {
|
|
||||||
panic("internal inconsistency: replMapping should contain mapping for any path that was not filled in by WalkTopDown()")
|
|
||||||
}
|
|
||||||
|
|
||||||
log = log.WithField(logMapToField, m.Remote.ToString()).
|
|
||||||
WithField(logMapFromField, m.Local.ToString())
|
|
||||||
|
|
||||||
log.Debug("examing local filesystem state")
|
|
||||||
localState, localExists := localFilesystemState[m.Local.ToString()]
|
|
||||||
var versions []zfs.FilesystemVersion
|
|
||||||
switch {
|
|
||||||
case !localExists:
|
|
||||||
log.Info("local filesystem does not exist")
|
|
||||||
case localState.Placeholder:
|
|
||||||
log.Info("local filesystem is marked as placeholder")
|
|
||||||
default:
|
|
||||||
log.Debug("local filesystem exists")
|
|
||||||
log.Debug("requesting local filesystem versions")
|
|
||||||
if versions, err = zfs.ZFSListFilesystemVersions(m.Local, nil); err != nil {
|
|
||||||
log.WithError(err).Error("cannot get local filesystem versions")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("requesting remote filesystem versions")
|
|
||||||
r := FilesystemVersionsRequest{
|
|
||||||
Filesystem: m.Remote,
|
|
||||||
}
|
|
||||||
var theirVersions []zfs.FilesystemVersion
|
|
||||||
if err = remote.Call("FilesystemVersionsRequest", &r, &theirVersions); err != nil {
|
|
||||||
log.WithError(err).Error("cannot get remote filesystem versions")
|
|
||||||
log.Warn("stopping replication for all filesystems mapped as children of receiving filesystem")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("computing diff between remote and local filesystem versions")
|
|
||||||
diff := zfs.MakeFilesystemDiff(versions, theirVersions)
|
|
||||||
log.WithField("diff", diff).Debug("diff between local and remote filesystem")
|
|
||||||
|
|
||||||
if localState.Placeholder && diff.Conflict != zfs.ConflictAllRight {
|
|
||||||
panic("internal inconsistency: local placeholder implies ConflictAllRight")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch diff.Conflict {
|
|
||||||
case zfs.ConflictAllRight:
|
|
||||||
|
|
||||||
log.WithField("replication_policy", pull.InitialReplPolicy).Info("performing initial sync, following policy")
|
|
||||||
|
|
||||||
if pull.InitialReplPolicy != InitialReplPolicyMostRecent {
|
|
||||||
panic(fmt.Sprintf("policy '%s' not implemented", pull.InitialReplPolicy))
|
|
||||||
}
|
|
||||||
|
|
||||||
snapsOnly := make([]zfs.FilesystemVersion, 0, len(diff.MRCAPathRight))
|
|
||||||
for s := range diff.MRCAPathRight {
|
|
||||||
if diff.MRCAPathRight[s].Type == zfs.Snapshot {
|
|
||||||
snapsOnly = append(snapsOnly, diff.MRCAPathRight[s])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(snapsOnly) < 1 {
|
|
||||||
log.Warn("cannot perform initial sync: no remote snapshots")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
r := InitialTransferRequest{
|
|
||||||
Filesystem: m.Remote,
|
|
||||||
FilesystemVersion: snapsOnly[len(snapsOnly)-1],
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("version", r.FilesystemVersion).Debug("requesting snapshot stream")
|
|
||||||
|
|
||||||
var stream io.Reader
|
|
||||||
|
|
||||||
if err = remote.Call("InitialTransferRequest", &r, &stream); err != nil {
|
|
||||||
log.WithError(err).Error("cannot request initial transfer")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
log.Debug("received initial transfer request response")
|
|
||||||
|
|
||||||
log.Debug("invoke zfs receive")
|
|
||||||
watcher := util.IOProgressWatcher{Reader: stream}
|
|
||||||
watcher.KickOff(1*time.Second, func(p util.IOProgress) {
|
|
||||||
log.WithField("total_rx", p.TotalRX).Info("progress on receive operation")
|
|
||||||
})
|
|
||||||
|
|
||||||
recvArgs := []string{"-u"}
|
|
||||||
if localState.Placeholder {
|
|
||||||
log.Info("receive with forced rollback to replace placeholder filesystem")
|
|
||||||
recvArgs = append(recvArgs, "-F")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = zfs.ZFSRecv(m.Local, &watcher, recvArgs...); err != nil {
|
|
||||||
log.WithError(err).Error("canot receive stream")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
log.WithField("total_rx", watcher.Progress().TotalRX).
|
|
||||||
Info("finished receiving stream")
|
|
||||||
|
|
||||||
log.Debug("configuring properties of received filesystem")
|
|
||||||
if err = zfs.ZFSSet(m.Local, "readonly", "on"); err != nil {
|
|
||||||
log.WithError(err).Error("cannot set readonly property")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("finished initial transfer")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case zfs.ConflictIncremental:
|
|
||||||
|
|
||||||
if len(diff.IncrementalPath) < 2 {
|
|
||||||
log.Info("remote and local are in sync")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("following incremental path from diff")
|
|
||||||
var pathRx uint64
|
|
||||||
|
|
||||||
for i := 0; i < len(diff.IncrementalPath)-1; i++ {
|
|
||||||
|
|
||||||
from, to := diff.IncrementalPath[i], diff.IncrementalPath[i+1]
|
|
||||||
|
|
||||||
log, _ := log.WithField(logIncFromField, from.Name).WithField(logIncToField, to.Name), 0
|
|
||||||
|
|
||||||
log.Debug("requesting incremental snapshot stream")
|
|
||||||
r := IncrementalTransferRequest{
|
|
||||||
Filesystem: m.Remote,
|
|
||||||
From: from,
|
|
||||||
To: to,
|
|
||||||
}
|
|
||||||
var stream io.Reader
|
|
||||||
if err = remote.Call("IncrementalTransferRequest", &r, &stream); err != nil {
|
|
||||||
log.WithError(err).Error("cannot request incremental snapshot stream")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("invoking zfs receive")
|
|
||||||
watcher := util.IOProgressWatcher{Reader: stream}
|
|
||||||
watcher.KickOff(1*time.Second, func(p util.IOProgress) {
|
|
||||||
log.WithField("total_rx", p.TotalRX).Info("progress on receive operation")
|
|
||||||
})
|
|
||||||
|
|
||||||
if err = zfs.ZFSRecv(m.Local, &watcher); err != nil {
|
|
||||||
log.WithError(err).Error("cannot receive stream")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
totalRx := watcher.Progress().TotalRX
|
|
||||||
pathRx += totalRx
|
|
||||||
log.WithField("total_rx", totalRx).Info("finished incremental transfer")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("total_rx", pathRx).Info("finished following incremental path")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case zfs.ConflictNoCommonAncestor:
|
|
||||||
fallthrough
|
|
||||||
case zfs.ConflictDiverged:
|
|
||||||
|
|
||||||
var jsonDiff bytes.Buffer
|
|
||||||
if err := json.NewEncoder(&jsonDiff).Encode(diff); err != nil {
|
|
||||||
log.WithError(err).Error("cannot JSON-encode diff")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var problem, resolution string
|
|
||||||
|
|
||||||
switch diff.Conflict {
|
|
||||||
case zfs.ConflictNoCommonAncestor:
|
|
||||||
problem = "remote and local filesystem have snapshots, but no common one"
|
|
||||||
resolution = "perform manual establish a common snapshot history"
|
|
||||||
case zfs.ConflictDiverged:
|
|
||||||
problem = "remote and local filesystem share a history but have diverged"
|
|
||||||
resolution = "perform manual replication or delete snapshots on the receiving" +
|
|
||||||
"side to establish an incremental replication parse"
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("diff", jsonDiff.String()).
|
|
||||||
WithField("problem", problem).
|
|
||||||
WithField("resolution", resolution).
|
|
||||||
Error("manual conflict resolution required")
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
panic("should not be reached")
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
jobs:
|
|
||||||
- name: mirror_local
|
|
||||||
type: local
|
|
||||||
|
|
||||||
# snapshot the filesystems matched by the left-hand-side of the mapping
|
|
||||||
# every 10m with zrepl_ as prefix
|
|
||||||
mapping: {
|
|
||||||
"zroot/var/db<": "storage/backups/local/zroot/var/db",
|
|
||||||
"zroot/usr/home<": "storage/backups/local/zroot/usr/home",
|
|
||||||
"zroot/var/tmp": "!", #don't backup /tmp
|
|
||||||
}
|
|
||||||
snapshot_prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
initial_repl_policy: most_recent
|
|
||||||
|
|
||||||
# keep one hour of 10m interval snapshots of filesystems matched by
|
|
||||||
# the left-hand-side of the mapping
|
|
||||||
prune_lhs:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1h(keep=all)
|
|
||||||
|
|
||||||
# follow a grandfathering scheme for filesystems on the right-hand-side of the mapping
|
|
||||||
prune_rhs:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
jobs:
|
|
||||||
- name: fullbackup_prod1
|
|
||||||
type: pull
|
|
||||||
# connect to remote using ssh / stdinserver command
|
|
||||||
connect:
|
|
||||||
type: ssh+stdinserver
|
|
||||||
host: prod1.example.com
|
|
||||||
user: root
|
|
||||||
port: 22
|
|
||||||
identity_file: /root/.ssh/id_ed25519
|
|
||||||
|
|
||||||
# pull (=ask for new snapshots) every 10m, prune afterwards
|
|
||||||
# this will leave us at most 10m behind production
|
|
||||||
interval: 10m
|
|
||||||
|
|
||||||
# pull all offered filesystems to storage/backups/zrepl/pull/prod1.example.com
|
|
||||||
mapping: {
|
|
||||||
"<":"storage/backups/zrepl/pull/prod1.example.com"
|
|
||||||
}
|
|
||||||
initial_repl_policy: most_recent
|
|
||||||
|
|
||||||
# follow a grandfathering scheme for filesystems on the right-hand-side of the mapping
|
|
||||||
snapshot_prefix: zrepl_
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
global:
|
|
||||||
serve:
|
|
||||||
stdinserver:
|
|
||||||
# Directory where AF_UNIX sockets for stdinserver command are placed.
|
|
||||||
#
|
|
||||||
# `zrepl stdinserver CLIENT_IDENTITY`
|
|
||||||
# * connects to the socket in $sockdir/CLIENT_IDENTITY
|
|
||||||
# * sends its stdin / stdout file descriptors to the `zrepl daemon` process (see cmsg(3))
|
|
||||||
# * does nothing more
|
|
||||||
#
|
|
||||||
# This enables a setup where `zrepl daemon` is not directly exposed to the internet
|
|
||||||
# but instead all traffic is tunnelled through SSH.
|
|
||||||
# The server with the source job has an authorized_keys file entry for the public key
|
|
||||||
# used by the corresponding pull job
|
|
||||||
#
|
|
||||||
# command="/mnt/zrepl stdinserver CLIENT_IDENTITY" ssh-ed25519 AAAAC3NzaC1E... zrepl@pullingserver
|
|
||||||
#
|
|
||||||
# Below is the default value.
|
|
||||||
sockdir: /var/run/zrepl/stdinserver
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
- name: fullbackup_prod1
|
|
||||||
# expect remote to connect via ssh+stdinserver with fullbackup_prod1 as client_identity
|
|
||||||
type: source
|
|
||||||
serve:
|
|
||||||
type: stdinserver # see global.serve.stdinserver for explanation
|
|
||||||
client_identity: fullbackup_prod1
|
|
||||||
|
|
||||||
# snapshot these filesystems every 10m with zrepl_ as prefix
|
|
||||||
datasets: {
|
|
||||||
"zroot/var/db<": "ok",
|
|
||||||
"zroot/usr/home<": "ok",
|
|
||||||
"zroot/var/tmp": "!", #don't backup /tmp
|
|
||||||
}
|
|
||||||
snapshot_prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
|
|
||||||
|
|
||||||
# keep a one day window 10m interval snapshots in case pull doesn't work (link down, etc)
|
|
||||||
# (we cannot keep more than one day because this host will run out of disk space)
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1d(keep=all)
|
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
jobs:
|
|
||||||
- name: fullbackup_prod1
|
|
||||||
|
|
||||||
# expect remote to connect via ssh+stdinserver with fullbackup_prod1 as client_identity
|
|
||||||
type: push-sink
|
|
||||||
serve:
|
|
||||||
type: stdinserver
|
|
||||||
client_identity: fullbackup_prod1
|
|
||||||
|
|
||||||
# map all pushed datasets to storage/backups/zrepl/sink/prod1.example.com
|
|
||||||
mapping: {
|
|
||||||
"<":"storage/backups/zrepl/sink/prod1.example.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
# follow a grandfathering scheme for filesystems on the right-hand-side of the mapping
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
jobs:
|
|
||||||
- name: fullbackup_prod1
|
|
||||||
|
|
||||||
# connect to remote using ssh / stdinserver command
|
|
||||||
type: push
|
|
||||||
connect:
|
|
||||||
type: ssh+stdinserver
|
|
||||||
host: prod1.example.com
|
|
||||||
user: root
|
|
||||||
port: 22
|
|
||||||
identity_file: /root/.ssh/id_ed25519
|
|
||||||
|
|
||||||
# snapshot these datsets every 10m with zrepl_ as prefix
|
|
||||||
datasets: {
|
|
||||||
"zroot/var/db<": "ok",
|
|
||||||
"zroot/usr/home<": "!",
|
|
||||||
}
|
|
||||||
snapshot_prefix: zrepl_
|
|
||||||
interval: 10m
|
|
||||||
|
|
||||||
# keep a one day window 10m interval snapshots in case push doesn't work (link down, etc)
|
|
||||||
# (we cannot keep more than one day because this host will run out of disk space)
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1d(keep=all)
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
global:
|
|
||||||
serve:
|
|
||||||
stdinserver:
|
|
||||||
sockdir: /var/run/zrepl/stdinserver
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
- name: debian2_pull
|
|
||||||
# JOB DEBUGGING OPTIONS
|
|
||||||
# should be equal for all job types, but each job implements the debugging itself
|
|
||||||
# => consult job documentation for supported options
|
|
||||||
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
|
|
||||||
|
|
||||||
# ... just to make the unit tests pass.
|
|
||||||
# check other examples, e.g. localbackup or pullbackup for what the sutff below means
|
|
||||||
type: source
|
|
||||||
serve:
|
|
||||||
type: stdinserver
|
|
||||||
client_identity: debian2
|
|
||||||
datasets: {
|
|
||||||
"pool1/db<": ok
|
|
||||||
}
|
|
||||||
snapshot_prefix: zrepl_
|
|
||||||
interval: 1s
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x10s(keep=all)
|
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
global:
|
|
||||||
logging:
|
|
||||||
stdout:
|
|
||||||
level: warn
|
|
||||||
format: human
|
|
||||||
tcp:
|
|
||||||
level: debug
|
|
||||||
format: json
|
|
||||||
net: tcp
|
|
||||||
address: 127.0.0.1:8080
|
|
||||||
retry_interval: 1s
|
|
||||||
tls: # if not specified, use plain TCP
|
|
||||||
ca: sampleconf/random/logging/logserver.crt
|
|
||||||
cert: sampleconf/random/logging/client.crt
|
|
||||||
key: sampleconf/random/logging/client.key
|
|
||||||
syslog:
|
|
||||||
enable: true
|
|
||||||
format: logfmt
|
|
||||||
|
|
||||||
jobs: []
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDIzCCAgsCAQEwDQYJKoZIhvcNAQELBQAwWTELMAkGA1UEBhMCQVUxEzARBgNV
|
|
||||||
BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
|
|
||||||
ZDESMBAGA1UEAwwJbG9nc2VydmVyMB4XDTE3MDkyNDEyMzAzNloXDTE3MTAyNDEy
|
|
||||||
MzAzNlowVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV
|
|
||||||
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGY2xpZW50MIIB
|
|
||||||
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt/xJTUlqApeJGzRD+w2J8sZS
|
|
||||||
Bo+s+04T987L/M6gaCo8aDSTEb/ZH3XSoU5JEmO6kPpwNNapOsaEhTCjndZQdm5F
|
|
||||||
uqiUtAg1uW0HCkBEIDkGr9bFHDKzpewGmmMgfQ2+hfiBR/4ZCrc/vd9P0W9BiWQS
|
|
||||||
Dtc7p22XraWPVL8HlSz5K/Ih+V6i8O+kBltZkusiJh2bWPoRp/netiTZuc6du+Wp
|
|
||||||
kpWp1OBaTU4GXIAlLj5afF14BBphRQK983Yhaz53BkA7OQ76XxowynMjmuLQVGmK
|
|
||||||
f1R9zEJuohTX9XIr1tp/ueRHcS4Awk6LcNZUMCV6270FNSIw2f4hbOZvep+t2wID
|
|
||||||
AQABMA0GCSqGSIb3DQEBCwUAA4IBAQACK3OeNzScpiNwz/jpg/usQzvXbZ/wDvml
|
|
||||||
YLjtzn/A65ox8a8BhxvH1ydyoCM2YAGYX7+y7qXJnMgRO/v8565CQIVcznHhg9ST
|
|
||||||
3828/WqZ3bXf2DV5GxKKQf7hPmBnyVUUhn/Ny91MECED27lZucWiX/bczN8ffDeh
|
|
||||||
M3+ngezcJxsOBd4x0gLrqIJCoaFRSeepOaFEW6GHQ8loxE9GmA7FQd2phIpJHFSd
|
|
||||||
Z7nQl7X5C1iN2OboEApJHwtmNVC45UlOpg53vo2sDTLhSfdogstiWi8x1HmvhIGM
|
|
||||||
j3XHs0Illvo9OwVrmgUph8zQ7pvr/AFrTOIbhgzl/9uVUk5ApwFM
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE REQUEST-----
|
|
||||||
MIICmzCCAYMCAQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
|
|
||||||
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGY2xp
|
|
||||||
ZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt/xJTUlqApeJGzRD
|
|
||||||
+w2J8sZSBo+s+04T987L/M6gaCo8aDSTEb/ZH3XSoU5JEmO6kPpwNNapOsaEhTCj
|
|
||||||
ndZQdm5FuqiUtAg1uW0HCkBEIDkGr9bFHDKzpewGmmMgfQ2+hfiBR/4ZCrc/vd9P
|
|
||||||
0W9BiWQSDtc7p22XraWPVL8HlSz5K/Ih+V6i8O+kBltZkusiJh2bWPoRp/netiTZ
|
|
||||||
uc6du+WpkpWp1OBaTU4GXIAlLj5afF14BBphRQK983Yhaz53BkA7OQ76XxowynMj
|
|
||||||
muLQVGmKf1R9zEJuohTX9XIr1tp/ueRHcS4Awk6LcNZUMCV6270FNSIw2f4hbOZv
|
|
||||||
ep+t2wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAKnlr0Qs5KYF85u2YA7DJ5pL
|
|
||||||
HwAx+qNoNbox5CS1aynrDBpDTWLaErviUJ+4WxRlRyTMEscMOIOKajbYhqqFmtGZ
|
|
||||||
mu3SshZnFihErw8TOQMyU1LGGG+l6r+6ve5TciwJRLla2Y75z7izr6cyvQNRWdLr
|
|
||||||
PvxL1/Yqr8LKha12+7o28R4SLf6/GY0GcedqoebRmtuwA/jES0PuGauEUD5lH4cj
|
|
||||||
Me8sqRrB+IMHQ5j8hlJX4DbA8UQRUBL64sHkQzeQfWu+qkWmS5I19CFfLNrcH+OV
|
|
||||||
yhyjGfN0q0jHyHdpckBhgzS7IIdo6P66AIlm4qpHM7Scra3JaGM7oaZPamJ6f8U=
|
|
||||||
-----END CERTIFICATE REQUEST-----
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3/ElNSWoCl4kb
|
|
||||||
NEP7DYnyxlIGj6z7ThP3zsv8zqBoKjxoNJMRv9kfddKhTkkSY7qQ+nA01qk6xoSF
|
|
||||||
MKOd1lB2bkW6qJS0CDW5bQcKQEQgOQav1sUcMrOl7AaaYyB9Db6F+IFH/hkKtz+9
|
|
||||||
30/Rb0GJZBIO1zunbZetpY9UvweVLPkr8iH5XqLw76QGW1mS6yImHZtY+hGn+d62
|
|
||||||
JNm5zp275amSlanU4FpNTgZcgCUuPlp8XXgEGmFFAr3zdiFrPncGQDs5DvpfGjDK
|
|
||||||
cyOa4tBUaYp/VH3MQm6iFNf1civW2n+55EdxLgDCTotw1lQwJXrbvQU1IjDZ/iFs
|
|
||||||
5m96n63bAgMBAAECggEAF4om0sWe06ARwbJJNFjCGpa3LfG5/xk5Qs5pmPnS2iD1
|
|
||||||
Q5veaTnzjKvlfA/pF3o9B4mTS59fXY7Cq8vSU0J1XwGy2DPzeqlGPmgtq2kXjkvd
|
|
||||||
iCfhZj8ybvsoyR3/rSBSDRADcnOXPqC9fgyRSMmESBDOoql1D3HdIzF4ii46ySIU
|
|
||||||
/XQvExS6NWifbP+Ue6DETV8NhreO5PqjeXLITQhhndtc8MDL/8eCNOyN8XjYIWKX
|
|
||||||
smlBYtRQYOOY9BHOQgUn6yvPHrtKJNKci+qcQNvWir66mBhY1o40MH5wTIV+8yP2
|
|
||||||
Vbm/VzoNKIYgeROsilBW7QTwGvkDn3R11zeTqfUNSQKBgQD0eFzhJAEZi4uBw6Tg
|
|
||||||
NKmBC5Y1IHPOsb5gKPNz9Z9j4qYRDySgYl6ISk+2EdhgUCo1NmTk8EIPQjIerUVf
|
|
||||||
S+EogFnpsj8U9LR3OM79DaGkNULxrHqhd209/g8DtVgk7yjkxL4vmVOv8qpHMp/7
|
|
||||||
eWsylN7AOxj2RB/eXYQBPrw+jQKBgQDAqae9HasLmvpJ9ktTv30yZSKXC+LP4A0D
|
|
||||||
RBBmx410VpPd4CvcpCJxXmjer6B7+9L1xHYP2pvsnMBid5i0knuvyK28dYy7fldl
|
|
||||||
CzWvb+lqNA5YYPFXQED4oEdihlQczoI1Bm06SFizeAKD1Q9e2c+lgbR/51j8xuXi
|
|
||||||
twvhMj/YBwKBgQCZw97/iQrcC2Zq7yiUEOuQjD4lGk1c83U/vGIsTJC9XcCAOFsc
|
|
||||||
OeMlrD/oz96d7a4unBDn4qpaOJOXsfpRT0PGmrxy/jcpMiUUW/ntNpa11v5NTeQw
|
|
||||||
DRL8DAFbnsNbL8Yz5f+Nps35fBNYBuKTZLJlNTfKByHTO9QjpAQ0WEZEvQKBgQCi
|
|
||||||
Ovm83EuYVSKmvxcE6Tyx/8lVqTOO2Vn7wweQlD4/lVujvE0S2L8L+XSS9w5K+GzW
|
|
||||||
eFz10p3zarbw80YJ30L5bSEmjVE43BUZR4woMzM4M6dUsiTm1HshIE2b4ALZ0uZ/
|
|
||||||
Ye794ceXL9nmSrVLqFsaQZLNFPCwwYb4FiyRry9lZwKBgAO9VbWcN8SEeBDKo3z8
|
|
||||||
yRbRTc6sI+AdKY44Dfx0tqOPmTjO3mE4X1GU4sbfD2Bvg3DdjwTuxxC/jHaKu0GG
|
|
||||||
dTM0CbrZGbDAj7E87SOcN/PWEeBckSvuQq5H3DQfwIpTmlS1l5oZn9CxRGbLqC2G
|
|
||||||
ifnel8XWUG0ROybsr1tk4mzW
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDiDCCAnCgAwIBAgIJALhp/WvTQeg/MA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV
|
|
||||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
|
||||||
aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvZ3NlcnZlcjAeFw0xNzA5MjQxMjI3
|
|
||||||
MDRaFw0yNzA5MjIxMjI3MDRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l
|
|
||||||
LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
|
|
||||||
BAMMCWxvZ3NlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKs3
|
|
||||||
TLYfXhV3hap71tOkhPQlM+m0EKRAo8Nua50Cci5UhDo4JkVpyYok1h+NFkqmjU2b
|
|
||||||
IiIuGvsZZPOWYjbWWnSJE4+n5pBFBzcfNQ4d8xVxjANImFn6Tcehhj0WkbDIv/Ge
|
|
||||||
364XUgywS7u3EGQj/FO7vZ8KHlUxBHNuPIOPHftwIVRyleh5K32UyBaSpSmnqGos
|
|
||||||
rvI1byMuznavcZpOs4vlebZ+Jy6a20iKf9fj/0f0t0O+F5x3JIk07D3zSywhJ4RM
|
|
||||||
M0mGIUmYXbh2SMh+f61KDZLDANpz/pMAPbUJe0mxEtBf0tnwK1gEqc3SLwA0EwiM
|
|
||||||
8Hnn2iaH5Ln20UE3LOkCAwEAAaNTMFEwHQYDVR0OBBYEFDXoDcwx9SngzZcRYCeP
|
|
||||||
BplBecfiMB8GA1UdIwQYMBaAFDXoDcwx9SngzZcRYCePBplBecfiMA8GA1UdEwEB
|
|
||||||
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADyNvs4AA91x3gurQb1pcPVhK6nR
|
|
||||||
mkYSTN1AsDKSRi/X2iCUmR7G7FlF7XW8mntTpHvVzcs+gr94WckH5wqEOA5iZnaw
|
|
||||||
PXUWexmdXUge4hmC2q6kBQ5e2ykhSJMRVZXvOLZOZV9qitceamHESV1cKZSNMvZM
|
|
||||||
aCSVA1RK61/nUzs04pVp5PFPv9gFxJp9ki39FYFdsgZmM5RZ5I/FqxxvTJzu4RnH
|
|
||||||
VPjsMopzARYwJw6dV2bKdFSYOE8B/Vs3Yv0GxjrABw2ko4PkBPTjLIz22x6+Hd9r
|
|
||||||
K9BQi4pVmQfvppF5+SORSftlHSS+N47b0DD1rW1f5R6QGi71dFuJGikOwvY=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrN0y2H14Vd4Wq
|
|
||||||
e9bTpIT0JTPptBCkQKPDbmudAnIuVIQ6OCZFacmKJNYfjRZKpo1NmyIiLhr7GWTz
|
|
||||||
lmI21lp0iROPp+aQRQc3HzUOHfMVcYwDSJhZ+k3HoYY9FpGwyL/xnt+uF1IMsEu7
|
|
||||||
txBkI/xTu72fCh5VMQRzbjyDjx37cCFUcpXoeSt9lMgWkqUpp6hqLK7yNW8jLs52
|
|
||||||
r3GaTrOL5Xm2ficumttIin/X4/9H9LdDvhecdySJNOw980ssISeETDNJhiFJmF24
|
|
||||||
dkjIfn+tSg2SwwDac/6TAD21CXtJsRLQX9LZ8CtYBKnN0i8ANBMIjPB559omh+S5
|
|
||||||
9tFBNyzpAgMBAAECggEBAIY8ZwJq+WKvQLb3POjWFf8so9TY/ispGrwAeJKy9j5o
|
|
||||||
uPrERw0o8YBDfTVjclS43BQ6Srqtly3DLSjlgL8ps+WmCxYYN2ZpGE0ZRIl65bis
|
|
||||||
O2/fnML+wbiAZTTD2xnVatfPDeP6GLQmDFpyHoHEzPIBQZvNXRbBxZGSnhMvQ/x7
|
|
||||||
FhqSBQG4kf3b1XDCENIbFEVOBOCg7WtMiIgjEGS7QnW3I65/Zt+Ts1LXRZbz+6na
|
|
||||||
Gmi0PGHA/oLUh1NRzsF4zuZn6fFzja5zw4mkt+JvCWEoxg1QhRAxRp6QQwmZ6MIc
|
|
||||||
1rw1D4Z+c5UEKyqHeIwZj4M6UNPhCfTXVm47c9eSiGECgYEA4U8pB+7eRo2fqX0C
|
|
||||||
nWsWMcmsULJvwplQnUSFenUayPn3E8ammS/ZBHksoKhj82vwIdDbtS1hQZn8Bzsi
|
|
||||||
atc8au0wz0YRDcVDzHX4HknXVQayHtP/FTPeSr5hwpoY8vhEbySuxBTBkXCrp4dx
|
|
||||||
u5ErfOiYEP3Q1ZvPRywelrATu20CgYEAwonV5dgOcen/4oAirlnvufc2NfqhAQwJ
|
|
||||||
FJ/JSVMAcXxPYu3sZMv0dGWrX8mLc+P1+XMCuV/7eBM/vU2LbDzmpeUV8sJfB2jw
|
|
||||||
wyKqKXZwBgeq60btriA4f+0ElwRGgU2KSiniUuuTX2JmyftFQx4cVAQRCFk27NY0
|
|
||||||
09psSsYyre0CgYBo6unabdtH029EB5iOIW3GZXk+Yrk0TxyA/4WAjsOYTv5FUT4H
|
|
||||||
G4bdVGf5sDBLDDpYJOAKsEUXvVLlMx5FzlCuIiGWg7QxS2jU7yJJSG1jhKixPlsM
|
|
||||||
Toj3GUyAyC1SB1Ymw1g2qsuwpFzquGG3zFQJ6G3Xi7oRnmqZY+wik3+8yQKBgB11
|
|
||||||
SdKYOPe++2SNCrNkIw0CBk9+OEs0S1u4Jn7X9sU4kbzlUlqhF89YZe8HUfqmlmTD
|
|
||||||
qbHwet/f6lL8HxSw1Cxi2EP+cu1oUqz53tKQgL4pAxTFlNA9SND2Ty+fEh4aY8p/
|
|
||||||
NSphSduzxuTnC8HyGVAPnZSqDcsnVLCP7r4T7TCxAoGAbJygkkk/gZ9pT4fZoIaq
|
|
||||||
8CMR8FTfxtkwCuZsWccSMUOWtx9nqet3gbCpKHfyoYZiKB4ke+lnUz4uFS16Y3hG
|
|
||||||
kN0hFfvfoNa8eB2Ox7vs60cMMfWJac0H7KSaDDy+EvbhE2KtQADT0eWxMyhzGR8p
|
|
||||||
5CbIivB0QCjeQIA8dOQpE8E=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
remotes:
|
|
||||||
offsite_backups:
|
|
||||||
transport:
|
|
||||||
ssh:
|
|
||||||
host: 192.168.122.6
|
|
||||||
user: root
|
|
||||||
port: 22
|
|
||||||
identity_file: /etc/zrepl/identities/offsite_backups
|
|
||||||
|
|
||||||
push:
|
|
||||||
|
|
||||||
offsite:
|
|
||||||
to: offsite_backups
|
|
||||||
filter: {
|
|
||||||
# like in pull_acls
|
|
||||||
"tank/var/db<": ok,
|
|
||||||
"tank/usr/home<": ok,
|
|
||||||
}
|
|
||||||
|
|
||||||
pull:
|
|
||||||
|
|
||||||
offsite:
|
|
||||||
from: offsite_backups
|
|
||||||
mapping: {
|
|
||||||
# like in sinks
|
|
||||||
}
|
|
||||||
|
|
||||||
# local replication, only allowed in pull mode
|
|
||||||
# the from name 'local' is reserved for this purpose
|
|
||||||
homemirror:
|
|
||||||
from: local
|
|
||||||
repeat:
|
|
||||||
interval: 15m
|
|
||||||
mapping: {
|
|
||||||
"tank/usr/home":"mirrorpool/foo/bar"
|
|
||||||
}
|
|
||||||
|
|
||||||
sink:
|
|
||||||
|
|
||||||
db1:
|
|
||||||
mapping: {
|
|
||||||
# direct mapping
|
|
||||||
"ssdpool/var/db/postgresql9.6":"zroot/backups/db1/pg_data"
|
|
||||||
}
|
|
||||||
|
|
||||||
mirror1:
|
|
||||||
mapping: {
|
|
||||||
# "<" subtree wildcard matches the dataset left of < and all its children
|
|
||||||
"tank/foo/bar<":"zroot/backups/mirror1"
|
|
||||||
}
|
|
||||||
|
|
||||||
mirror2:
|
|
||||||
# more specific path patterns win over less specific ones
|
|
||||||
# direct mappings win over subtree wildcards
|
|
||||||
# detailed rule precedence: check unit tests & docs for exact behavior
|
|
||||||
# TODO subcommand to test a mapping & filter
|
|
||||||
mapping: {
|
|
||||||
"tank<": "zroot/backups/mirror1/tank1",
|
|
||||||
"tank/cdn/root<": "storage/cdn/root",
|
|
||||||
"tank/legacydb": "legacypool/backups/legacydb",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pull_acl:
|
|
||||||
|
|
||||||
office_backup:
|
|
||||||
filter: {
|
|
||||||
# valid filter results (right hand side): ok, omit
|
|
||||||
# default is to omit
|
|
||||||
# rule precedence is same as for mappings
|
|
||||||
"tank<": omit,
|
|
||||||
"tank/usr/home": ok,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
prune:
|
|
||||||
|
|
||||||
clean_backups:
|
|
||||||
policy: grid
|
|
||||||
grid: 6x10m | 24x1h | 7x1d | 5 x 1w | 4 x 5w
|
|
||||||
dataset_filter: {
|
|
||||||
"tank/backups/legacyscript<": omit,
|
|
||||||
"tank/backups<": ok,
|
|
||||||
}
|
|
||||||
snapshot_filter: {
|
|
||||||
prefix: zrepl_
|
|
||||||
}
|
|
||||||
|
|
||||||
hfbak_prune: # cleans up after hfbak autosnap job
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1m(keep=all)
|
|
||||||
dataset_filter: {
|
|
||||||
"pool1<": ok
|
|
||||||
}
|
|
||||||
snapshot_filter: {
|
|
||||||
prefix: zrepl_hfbak_
|
|
||||||
}
|
|
||||||
repeat:
|
|
||||||
interval: 10s
|
|
||||||
|
|
||||||
autosnap:
|
|
||||||
|
|
||||||
hfbak:
|
|
||||||
prefix: zrepl_hfbak_
|
|
||||||
interval: 1s
|
|
||||||
dataset_filter: {
|
|
||||||
"pool1<": ok
|
|
||||||
}
|
|
||||||
# prune: hfbak_prune
|
|
||||||
# future versions may inline the retention policy here, but for now,
|
|
||||||
# pruning has to be triggered manually (it's safe to run autosnap + prune in parallel)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ftrvxmtrx/fd"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
var StdinserverCmd = &cobra.Command{
|
|
||||||
Use: "stdinserver CLIENT_IDENTITY",
|
|
||||||
Short: "start in stdinserver mode (from authorized_keys file)",
|
|
||||||
Run: cmdStdinServer,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
RootCmd.AddCommand(StdinserverCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cmdStdinServer(cmd *cobra.Command, args []string) {
|
|
||||||
|
|
||||||
log := log.New(os.Stderr, "", log.LUTC|log.Ldate|log.Ltime)
|
|
||||||
|
|
||||||
die := func() {
|
|
||||||
log.Printf("stdinserver exiting after fatal error")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
conf, err := ParseConfig(rootArgs.configFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error parsing config: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) != 1 || args[0] == "" {
|
|
||||||
err = fmt.Errorf("must specify client_identity as positional argument")
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
identity := args[0]
|
|
||||||
|
|
||||||
unixaddr, err := stdinserverListenerSocket(conf.Global.Serve.Stdinserver.SockDir, identity)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("opening control connection to zrepld via %s", unixaddr)
|
|
||||||
conn, err := net.DialUnix("unix", nil, unixaddr)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error connecting to zrepld: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("sending stdin and stdout fds to zrepld")
|
|
||||||
err = fd.Put(conn, os.Stdin, os.Stdout)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error: %s", err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("waiting for zrepld to close control connection")
|
|
||||||
for {
|
|
||||||
|
|
||||||
var buf [64]byte
|
|
||||||
n, err := conn.Read(buf[:])
|
|
||||||
if err == nil && n != 0 {
|
|
||||||
log.Printf("protocol error: read expected to timeout or EOF returned bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
log.Printf("zrepld closed control connection, terminating")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
neterr, ok := err.(net.Error)
|
|
||||||
if !ok {
|
|
||||||
log.Printf("received unexpected error type: %T %s", err, err)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
if !neterr.Timeout() {
|
|
||||||
log.Printf("receivd unexpected net.Error (not a timeout): %s", neterr)
|
|
||||||
die()
|
|
||||||
}
|
|
||||||
// Read timed out, as expected
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
-214
@@ -1,214 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/zrepl/zrepl/logger"
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testCmd = &cobra.Command{
|
|
||||||
Use: "test",
|
|
||||||
Short: "test configuration",
|
|
||||||
PersistentPreRun: testCmdGlobalInit,
|
|
||||||
}
|
|
||||||
|
|
||||||
var testCmdGlobal struct {
|
|
||||||
log Logger
|
|
||||||
conf *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
var testConfigSyntaxCmd = &cobra.Command{
|
|
||||||
Use: "config",
|
|
||||||
Short: "parse config file and dump parsed datastructure",
|
|
||||||
Run: doTestConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
var testDatasetMapFilter = &cobra.Command{
|
|
||||||
Use: "pattern jobname test/zfs/dataset/path",
|
|
||||||
Short: "test dataset mapping / filter specified in config",
|
|
||||||
Example: ` zrepl test pattern prune.clean_backups tank/backups/legacyscript/foo`,
|
|
||||||
Run: doTestDatasetMapFilter,
|
|
||||||
}
|
|
||||||
|
|
||||||
var testPrunePolicyArgs struct {
|
|
||||||
side PrunePolicySide
|
|
||||||
showKept bool
|
|
||||||
showRemoved bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var testPrunePolicyCmd = &cobra.Command{
|
|
||||||
Use: "prune jobname",
|
|
||||||
Short: "do a dry-run of the pruning part of a job",
|
|
||||||
Run: doTestPrunePolicy,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
RootCmd.AddCommand(testCmd)
|
|
||||||
testCmd.AddCommand(testConfigSyntaxCmd)
|
|
||||||
testCmd.AddCommand(testDatasetMapFilter)
|
|
||||||
|
|
||||||
testPrunePolicyCmd.Flags().VarP(&testPrunePolicyArgs.side, "side", "s", "prune_lhs (left) or prune_rhs (right)")
|
|
||||||
testPrunePolicyCmd.Flags().BoolVar(&testPrunePolicyArgs.showKept, "kept", false, "show kept snapshots")
|
|
||||||
testPrunePolicyCmd.Flags().BoolVar(&testPrunePolicyArgs.showRemoved, "removed", true, "show removed snapshots")
|
|
||||||
testCmd.AddCommand(testPrunePolicyCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testCmdGlobalInit(cmd *cobra.Command, args []string) {
|
|
||||||
|
|
||||||
out := logger.NewOutlets()
|
|
||||||
out.Add(WriterOutlet{&NoFormatter{}, os.Stdout}, logger.Info)
|
|
||||||
log := logger.NewLogger(out, 1*time.Second)
|
|
||||||
testCmdGlobal.log = log
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if testCmdGlobal.conf, err = ParseConfig(rootArgs.configFile); err != nil {
|
|
||||||
testCmdGlobal.log.Printf("error parsing config file: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func doTestConfig(cmd *cobra.Command, args []string) {
|
|
||||||
|
|
||||||
log, conf := testCmdGlobal.log, testCmdGlobal.conf
|
|
||||||
|
|
||||||
log.Printf("config ok")
|
|
||||||
log.Printf("%# v", pretty.Formatter(conf))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func doTestDatasetMapFilter(cmd *cobra.Command, args []string) {
|
|
||||||
|
|
||||||
log, conf := testCmdGlobal.log, testCmdGlobal.conf
|
|
||||||
|
|
||||||
if len(args) != 2 {
|
|
||||||
log.Printf("specify job name as first postitional argument, test input as second")
|
|
||||||
log.Printf(cmd.UsageString())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
n, i := args[0], args[1]
|
|
||||||
|
|
||||||
jobi, err := conf.LookupJob(n)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var mf *DatasetMapFilter
|
|
||||||
switch j := jobi.(type) {
|
|
||||||
case *PullJob:
|
|
||||||
mf = j.Mapping
|
|
||||||
case *SourceJob:
|
|
||||||
mf = j.Datasets
|
|
||||||
case *LocalJob:
|
|
||||||
mf = j.Mapping
|
|
||||||
default:
|
|
||||||
panic("incomplete implementation")
|
|
||||||
}
|
|
||||||
|
|
||||||
ip, err := zfs.NewDatasetPath(i)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("cannot parse test input as ZFS dataset path: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mf.filterMode {
|
|
||||||
pass, err := mf.Filter(ip)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error evaluating filter: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Printf("filter result: %v", pass)
|
|
||||||
} else {
|
|
||||||
res, err := mf.Map(ip)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error evaluating mapping: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
toStr := "NO MAPPING"
|
|
||||||
if res != nil {
|
|
||||||
toStr = res.ToString()
|
|
||||||
}
|
|
||||||
log.Printf("%s => %s", ip.ToString(), toStr)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func doTestPrunePolicy(cmd *cobra.Command, args []string) {
|
|
||||||
|
|
||||||
log, conf := testCmdGlobal.log, testCmdGlobal.conf
|
|
||||||
|
|
||||||
if cmd.Flags().NArg() != 1 {
|
|
||||||
log.Printf("specify job name as first positional argument")
|
|
||||||
log.Printf(cmd.UsageString())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
jobname := cmd.Flags().Arg(0)
|
|
||||||
jobi, err := conf.LookupJob(jobname)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
jobp, ok := jobi.(PruningJob)
|
|
||||||
if !ok {
|
|
||||||
log.Printf("job doesn't do any prunes")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("job dump:\n%s", pretty.Sprint(jobp))
|
|
||||||
|
|
||||||
pruner, err := jobp.Pruner(testPrunePolicyArgs.side, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("cannot create test pruner: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("start pruning")
|
|
||||||
|
|
||||||
ctx := context.WithValue(context.Background(), contextKeyLog, log)
|
|
||||||
result, err := pruner.Run(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error running pruner: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(result, func(i, j int) bool {
|
|
||||||
return strings.Compare(result[i].Filesystem.ToString(), result[j].Filesystem.ToString()) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
for _, r := range result {
|
|
||||||
fmt.Fprintf(&b, "%s\n", r.Filesystem.ToString())
|
|
||||||
|
|
||||||
if testPrunePolicyArgs.showKept {
|
|
||||||
fmt.Fprintf(&b, "\tkept:\n")
|
|
||||||
for _, v := range r.Keep {
|
|
||||||
fmt.Fprintf(&b, "\t- %s\n", v.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if testPrunePolicyArgs.showRemoved {
|
|
||||||
fmt.Fprintf(&b, "\tremoved:\n")
|
|
||||||
for _, v := range r.Remove {
|
|
||||||
fmt.Fprintf(&b, "\t- %s\n", v.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("pruning result:\n%s", b.String())
|
|
||||||
|
|
||||||
}
|
|
||||||
+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
+16
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=zrepl daemon
|
||||||
|
Documentation=https://zrepl.github.io
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStartPre=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml configcheck
|
||||||
|
ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
|
||||||
|
RuntimeDirectory=zrepl zrepl/stdinserver
|
||||||
|
RuntimeDirectoryMode=0700
|
||||||
|
|
||||||
|
# Make Go produce coredumps
|
||||||
|
Environment=GOTRACEBACK='crash'
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
public_git
|
_build/
|
||||||
|
public_git/
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SPHINXPROJ = zrepl
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(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
+147
@@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 70.555558 24.694445"
|
||||||
|
height="70pt"
|
||||||
|
width="200pt"
|
||||||
|
sodipodi:docname="zrepl.svg"
|
||||||
|
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1405"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="3.1350001"
|
||||||
|
inkscape:cx="6.2268563"
|
||||||
|
inkscape:cy="-2.6953765"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="35"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
transform="translate(0,-272.30556)"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
aria-label="zrepl"
|
||||||
|
transform="matrix(0.26458334,0,0,0.26458334,0.52916655,273.36389)"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:25px;font-family:monospace;-inkscape-font-specification:'monospace, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="flowRoot829">
|
||||||
|
<path
|
||||||
|
d="m 74.504883,26.943359 11.523437,-13.125 H 74.973633 V 9.5410156 h 17.480469 v 4.4726564 l -11.503907,13.125 h 11.503907 v 4.277344 H 74.504883 Z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path873" />
|
||||||
|
<path
|
||||||
|
d="m 100.11035,9.5410156 h 5.15625 l 0.56641,3.3984374 q 0.83984,-1.875 2.57812,-2.910156 1.73828,-1.0156251 4.00391,-1.0156251 1.26953,0 2.36328,0.2734375 1.25,0.3515625 2.12891,0.8398436 v 5.449219 q -0.9375,-0.859375 -2.1875,-1.289063 -1.23047,-0.429687 -2.75391,-0.429687 -1.81641,0 -3.18359,0.625 -1.36719,0.605469 -2.10938,1.835937 -0.19531,0.3125 -0.37109,0.761719 -0.17578,0.449219 -0.27344,1.054688 -0.0977,0.566406 -0.15625,1.308593 -0.0391,0.742188 -0.0391,1.894532 v 10.078125 h -5.72266 z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path875" />
|
||||||
|
<path
|
||||||
|
d="m 132.4541,31.982422 q -2.85156,0 -4.9414,-0.722656 -2.07032,-0.742188 -3.53516,-2.226563 -2.91016,-2.96875 -2.91016,-8.4375 0,-5.371094 2.79297,-8.476562 2.79297,-3.1054691 7.65625,-3.1054691 3.10547,-0.097656 5.42969,1.2500001 2.34375,1.347656 3.59375,3.867187 1.26953,2.519532 1.17187,5.878907 v 2.324218 H 126.8877 q 0.0195,2.578125 1.52343,3.847657 1.50391,1.269531 4.47266,1.269531 1.93359,0 3.90625,-0.566406 0.89844,-0.273438 1.89453,-0.703125 0.99609,-0.429688 2.10938,-1.09375 v 5.253906 q -1.99219,0.820312 -4.0625,1.230469 -1.05469,0.214843 -2.14844,0.3125 -1.07422,0.09766 -2.12891,0.09766 z m 3.4961,-13.945313 q -0.0195,-2.285156 -1.15235,-3.4375 -1.11328,-1.152343 -3.28125,-1.152343 -2.01172,0 -3.18359,1.191406 -1.17188,1.210937 -1.38672,3.417969 z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path877" />
|
||||||
|
<path
|
||||||
|
d="m 146.45801,9.5410156 h 5.13672 l 0.5664,3.2812504 q 0.89844,-1.894532 2.36328,-2.8515629 1.42579,-0.9570312 3.45704,-0.9570312 3.71093,0 5.80078,2.9882811 1.05468,1.523438 1.5625,3.691406 0.50781,2.148438 0.50781,4.824219 0,2.851563 -0.54688,4.980469 -0.54687,2.109375 -1.54296,3.515625 -1.07422,1.523437 -2.59766,2.246094 -1.50391,0.722656 -3.28125,0.722656 -0.91797,0 -1.73828,-0.214844 -0.80078,-0.195312 -1.54297,-0.703125 -0.625,-0.410156 -1.28906,-1.09375 -0.64453,-0.703125 -1.15235,-1.640625 v 11.40625 h -5.70312 z m 9.70703,17.6757814 q 1.85547,0 2.91016,-1.796875 1.05468,-1.777344 1.05468,-4.960938 0,-3.164062 -1.05468,-4.960937 -1.05469,-1.796875 -2.91016,-1.796875 -1.83594,0 -2.92969,1.796875 -1.07422,1.777344 -1.07422,4.960937 0,3.183594 1.07422,4.960938 1.09375,1.796875 2.92969,1.796875 z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path879" />
|
||||||
|
<path
|
||||||
|
d="m 182.98145,31.572266 q -4.0625,0 -5.85938,-2.109375 -1.77734,-2.070313 -1.77734,-7.011719 V 5.4199219 h -5.80078 V 1.0253906 h 11.52343 V 22.451172 q 0,2.558594 0.80078,3.652344 0.78125,1.074218 2.71485,1.074218 h 4.57031 v 4.394532 z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path881" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path837"
|
||||||
|
d="M 26.170418,286.30982 H 43.35751"
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:medium;line-height:25px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#f8f8f8;fill-opacity:1;fill-rule:evenodd;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||||
|
<g
|
||||||
|
aria-label="ZFS replication"
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:10.58333397px;line-height:6.61458349px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="text820">
|
||||||
|
<path
|
||||||
|
d="m 14.758548,294.66467 h -3.493327 v -0.47887 l 2.652724,-3.99286 h -2.570041 v -0.565 h 3.338297 v 0.47887 l -2.652723,3.99287 h 2.72507 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path844" />
|
||||||
|
<path
|
||||||
|
d="M 16.353627,294.66467 H 15.71973 v -5.03673 h 2.814643 v 0.55811 h -2.180746 v 1.78456 h 2.046387 v 0.55466 h -2.046387 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path846" />
|
||||||
|
<path
|
||||||
|
d="m 22.241296,293.31764 q 0,0.65801 -0.475423,1.03697 -0.475424,0.37896 -1.322917,0.37896 -0.895725,0 -1.384929,-0.23771 v -0.60633 q 0.320394,0.1378 0.699355,0.21704 0.382406,0.0792 0.713135,0.0792 0.564995,0 0.850938,-0.21704 0.285943,-0.21704 0.285943,-0.596 0,-0.25149 -0.103353,-0.41341 -0.103352,-0.16192 -0.347954,-0.30317 -0.241157,-0.14125 -0.733806,-0.32039 -0.699354,-0.25494 -0.999077,-0.59945 -0.299724,-0.34795 -0.299724,-0.89572 0,-0.58911 0.440973,-0.93707 0.444417,-0.34795 1.167887,-0.34795 0.754476,0 1.388373,0.28249 l -0.19637,0.54433 q -0.647678,-0.26872 -1.205783,-0.26872 -0.447863,0 -0.7028,0.19293 -0.251492,0.19292 -0.251492,0.54088 0,0.24804 0.09646,0.41341 0.09991,0.16192 0.316949,0.29628 0.220486,0.13436 0.689019,0.30661 0.56155,0.20671 0.837158,0.40308 0.275608,0.19292 0.406521,0.44786 0.130914,0.25149 0.130914,0.60289 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path848" />
|
||||||
|
<path
|
||||||
|
d="m 26.768151,290.81306 q 0.251492,0 0.444418,0.0413 l -0.07579,0.57188 q -0.21015,-0.0482 -0.409966,-0.0482 -0.299723,0 -0.558105,0.16537 -0.254937,0.16536 -0.399631,0.45819 -0.144694,0.28939 -0.144694,0.64424 v 2.01882 h -0.620118 v -3.78271 h 0.509874 l 0.0689,0.69246 h 0.02756 q 0.217041,-0.37551 0.513319,-0.56844 0.296279,-0.19292 0.644233,-0.19292 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path850" />
|
||||||
|
<path
|
||||||
|
d="m 29.465661,294.73357 q -0.850939,0 -1.336697,-0.51331 -0.482313,-0.51677 -0.482313,-1.41938 0,-0.90951 0.451307,-1.44694 0.451308,-0.54088 1.219564,-0.54088 0.713135,0 1.136881,0.46164 0.423747,0.4582 0.423747,1.24368 v 0.37551 h -2.590712 q 0.01723,0.64424 0.327284,0.97841 0.310059,0.33418 0.8785,0.33418 0.303168,0 0.575331,-0.0517 0.272162,-0.0551 0.637342,-0.21015 v 0.54432 q -0.313503,0.13436 -0.589111,0.18948 -0.275608,0.0551 -0.651123,0.0551 z m -0.155029,-3.41408 q -0.444418,0 -0.7028,0.28594 -0.258382,0.28594 -0.306613,0.79582 h 1.925808 q -0.0069,-0.53055 -0.244602,-0.80616 -0.237711,-0.2756 -0.671793,-0.2756 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path852" />
|
||||||
|
<path
|
||||||
|
d="m 33.630782,294.73357 q -0.761366,0 -1.171333,-0.54777 h -0.04134 l 0.01378,0.14125 q 0.02756,0.26872 0.02756,0.47887 v 1.55374 h -0.620117 v -5.4777 h 0.509874 l 0.08268,0.51676 h 0.02756 q 0.220486,-0.31006 0.509874,-0.44786 0.289388,-0.1378 0.668349,-0.1378 0.73725,0 1.143772,0.51332 0.409966,0.50987 0.409966,1.44005 0,0.92673 -0.409966,1.44694 -0.409967,0.5202 -1.150662,0.5202 z m -0.103353,-3.4003 q -0.561551,0 -0.813043,0.31694 -0.248047,0.31695 -0.254937,0.99564 v 0.12058 q 0,0.76825 0.254937,1.11276 0.254937,0.34107 0.826823,0.34107 0.475423,0 0.740696,-0.38586 0.268717,-0.38585 0.268717,-1.07487 0,-0.69246 -0.268717,-1.05764 -0.265273,-0.36862 -0.754476,-0.36862 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path854" />
|
||||||
|
<path
|
||||||
|
d="m 36.800269,294.66467 h -0.620117 v -5.36057 h 0.620117 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path856" />
|
||||||
|
<path
|
||||||
|
d="m 38.619281,294.66467 h -0.620117 v -3.78271 h 0.620117 z m -0.668348,-4.80591 q 0,-0.2067 0.103352,-0.29972 0.106798,-0.093 0.261828,-0.093 0.144694,0 0.251492,0.093 0.110243,0.093 0.110243,0.29972 0,0.20327 -0.110243,0.29973 -0.106798,0.0965 -0.251492,0.0965 -0.15503,0 -0.261828,-0.0965 -0.103352,-0.0965 -0.103352,-0.29973 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path858" />
|
||||||
|
<path
|
||||||
|
d="m 41.334016,294.73357 q -0.816488,0 -1.27124,-0.49953 -0.454753,-0.50299 -0.454753,-1.44005 0,-0.95085 0.458198,-1.46417 0.461642,-0.51676 1.312581,-0.51676 0.275608,0 0.544325,0.0586 0.272163,0.0551 0.447863,0.1447 l -0.189481,0.51676 q -0.478868,-0.17914 -0.816487,-0.17914 -0.571886,0 -0.844049,0.36173 -0.272162,0.35829 -0.272162,1.07143 0,0.68557 0.272162,1.05075 0.272163,0.36173 0.806153,0.36173 0.499538,0 0.981852,-0.22048 v 0.55121 q -0.392741,0.20326 -0.974962,0.20326 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path860" />
|
||||||
|
<path
|
||||||
|
d="m 45.543923,294.66467 -0.120578,-0.53743 h -0.02756 q -0.282498,0.35484 -0.564996,0.48231 -0.279052,0.12402 -0.706244,0.12402 -0.558106,0 -0.875055,-0.29283 -0.316948,-0.29283 -0.316948,-0.82682 0,-1.15066 1.815565,-1.20578 l 0.640788,-0.0241 v -0.22393 q 0,-0.43064 -0.186035,-0.6339 -0.186035,-0.2067 -0.596002,-0.2067 -0.299723,0 -0.568441,0.0896 -0.265272,0.0896 -0.499538,0.19981 l -0.189481,-0.46508 q 0.285943,-0.15159 0.623563,-0.23772 0.337619,-0.0861 0.668348,-0.0861 0.685574,0 1.019748,0.30316 0.334175,0.30317 0.334175,0.96463 v 2.57693 z m -1.27813,-0.43063 q 0.520209,0 0.816487,-0.27906 0.299724,-0.28249 0.299724,-0.79926 v -0.34106 l -0.558106,0.0241 q -0.651123,0.0241 -0.950846,0.20671 -0.296278,0.18259 -0.296278,0.57533 0,0.29628 0.179145,0.45475 0.18259,0.15848 0.509874,0.15848 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path862" />
|
||||||
|
<path
|
||||||
|
d="m 48.427466,294.22715 q 0.120578,0 0.282498,-0.0241 0.161919,-0.0241 0.244602,-0.0551 v 0.47542 q -0.08613,0.0379 -0.268718,0.0723 -0.179145,0.0379 -0.36518,0.0379 -1.109321,0 -1.109321,-1.16788 v -2.20142 h -0.53399 v -0.29628 l 0.54088,-0.24804 0.248047,-0.80616 h 0.368625 v 0.86817 h 1.092096 v 0.48231 h -1.092096 v 2.18419 q 0,0.32729 0.15503,0.50299 0.158474,0.1757 0.437527,0.1757 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path864" />
|
||||||
|
<path
|
||||||
|
d="m 50.332607,294.66467 h -0.620118 v -3.78271 h 0.620118 z m -0.668349,-4.80591 q 0,-0.2067 0.103353,-0.29972 0.106798,-0.093 0.261827,-0.093 0.144694,0 0.251492,0.093 0.110243,0.093 0.110243,0.29972 0,0.20327 -0.110243,0.29973 -0.106798,0.0965 -0.251492,0.0965 -0.155029,0 -0.261827,-0.0965 -0.103353,-0.0965 -0.103353,-0.29973 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path866" />
|
||||||
|
<path
|
||||||
|
d="m 54.818119,292.76643 q 0,0.92673 -0.468533,1.44694 -0.468533,0.5202 -1.295356,0.5202 -0.51332,0 -0.909506,-0.23771 -0.396186,-0.24115 -0.609782,-0.68902 -0.213595,-0.44786 -0.213595,-1.04041 0,-0.92329 0.461642,-1.43661 0.461643,-0.51676 1.295356,-0.51676 0.799262,0 1.267795,0.5271 0.471979,0.52365 0.471979,1.42627 z m -2.855985,0 q 0,0.71313 0.279053,1.0852 0.282498,0.36863 0.830268,0.36863 0.54088,0 0.823378,-0.36863 0.282498,-0.37207 0.282498,-1.0852 0,-0.71314 -0.285943,-1.07143 -0.282498,-0.36173 -0.830268,-0.36173 -1.098986,0 -1.098986,1.43316 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path868" />
|
||||||
|
<path
|
||||||
|
d="m 58.380348,294.66467 v -2.41845 q 0,-0.46165 -0.206706,-0.68558 -0.20326,-0.22737 -0.640787,-0.22737 -0.582222,0 -0.847494,0.32383 -0.261827,0.3204 -0.261827,1.04731 v 1.96026 h -0.620117 v -3.78271 h 0.499538 l 0.09302,0.51676 h 0.03445 q 0.1757,-0.27905 0.492649,-0.43063 0.316948,-0.15503 0.699354,-0.15503 0.695909,0 1.033529,0.33762 0.337619,0.33761 0.337619,1.05075 v 2.46324 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path870" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 19 KiB |
Vendored
+17
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends "!page.html" %}
|
||||||
|
{% block body %}
|
||||||
|
{% if current_version and latest_version and current_version != latest_version %}
|
||||||
|
<p class="scv-banner scv-sphinx_rtd_theme">
|
||||||
|
<strong>
|
||||||
|
{% if current_version.is_released %}
|
||||||
|
You're reading an old version of this documentation.
|
||||||
|
If you want up-to-date information, please have a look at <a href="{{ vpathto(latest_version.name) }}">{{latest_version.name}}</a>.
|
||||||
|
{% else %}
|
||||||
|
You're reading the documentation for a development version.
|
||||||
|
For the latest released version, please have a look at <a href="{{ vpathto(latest_version.name) }}">{{latest_version.name}}</a>.
|
||||||
|
{% endif %}
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}%
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{%- if current_version %}
|
||||||
|
<div class="rst-versions" data-toggle="rst-versions" role="note" aria-label="versions">
|
||||||
|
<span class="rst-current-version" data-toggle="rst-current-version">
|
||||||
|
<span class="fa fa-book"> Other Versions</span>
|
||||||
|
v: {{ current_version.name }}
|
||||||
|
<span class="fa fa-caret-down"></span>
|
||||||
|
</span>
|
||||||
|
<div class="rst-other-versions">
|
||||||
|
{%- if versions.tags %}
|
||||||
|
<dl>
|
||||||
|
<dt>Tags</dt>
|
||||||
|
{%- for item in versions.tags %}
|
||||||
|
<dd><a href="{{ item.url }}">{{ item.name }}</a></dd>
|
||||||
|
{%- endfor %}
|
||||||
|
</dl>
|
||||||
|
{%- endif %}
|
||||||
|
{%- if versions.branches %}
|
||||||
|
<dl>
|
||||||
|
<dt>Branches</dt>
|
||||||
|
{%- for item in versions.branches %}
|
||||||
|
<dd><a href="{{ item.url }}">{{ item.name }}</a></dd>
|
||||||
|
{%- endfor %}
|
||||||
|
</dl>
|
||||||
|
{%- endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%- endif %}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
title: "{{ replace .TranslationBaseName "-" " " | title }}"
|
|
||||||
date: {{ .Date }}
|
|
||||||
draft: true
|
|
||||||
---
|
|
||||||
|
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|
.. |break_config| replace:: **[CONFIG]**
|
||||||
|
.. |break| replace:: **[BREAK]**
|
||||||
|
.. |bugfix| replace:: [BUG]
|
||||||
|
.. |docs| replace:: [DOCS]
|
||||||
|
.. |feature| replace:: [FEATURE]
|
||||||
|
.. |mig| replace:: **[MIGRATION]**
|
||||||
|
.. |maint| replace:: [MAINT]
|
||||||
|
|
||||||
|
.. _changelog:
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
||||||
|
Developers should consult the git commit log or GitHub issue tracker.
|
||||||
|
|
||||||
|
Next Release
|
||||||
|
------------
|
||||||
|
|
||||||
|
The plan for the next release is to revisit how zrepl does snapshot management.
|
||||||
|
High-level goals:
|
||||||
|
|
||||||
|
- Make it easy to decouple snapshot management (snapshotting, pruning) from replication.
|
||||||
|
- Ability to include/exclude snapshots from replication.
|
||||||
|
This is useful for aforementioned decoupling, e.g., separate snapshot prefixes for local & remote replication.
|
||||||
|
Also, it makes explicit that by default, zrepl replicates all snapshots, and that
|
||||||
|
replication has no concept of "zrepl-created snapshots", which is a common misconception.
|
||||||
|
- Use of ``zfs snapshot`` comma syntax or channel programs to take snapshots of multiple
|
||||||
|
datasets atomically.
|
||||||
|
- Provide an alternative to the ``grid`` pruning policy.
|
||||||
|
Most likely something based on hourly/daily/weekly/monthly "trains" plus a count.
|
||||||
|
- Ability to prune at the granularity of the **group** of snapshots created at a given
|
||||||
|
time, as opposed to the individual snapshots within a dataset.
|
||||||
|
Maybe this will be addressed by the alternative to the ``grid`` pruning policy,
|
||||||
|
as it will likely be more predictable.
|
||||||
|
|
||||||
|
Those changes will likely come with some breakage in the config.
|
||||||
|
However, I want to avoid breaking **use cases** that are satisfied by the current design.
|
||||||
|
There will be beta/RC releases to give users a chance to evaluate.
|
||||||
|
|
||||||
|
0.7.0 (unreleased)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
INCOMPLETE LIST OF CHANGES
|
||||||
|
|
||||||
|
* |maint| Update to Go 1.23, require Go 1.22. Adopt `go.mod` toolchain aka `GOTOOLCHAIN` env var.
|
||||||
|
* |maint| Fix deprecations exposed by the toolchain update.
|
||||||
|
* |maint| Long-overdue update of all our dependencies & address deprecations.
|
||||||
|
* |maint| The `make release-docker` in CircleCI produces executables that are bit-identical to my personal machine.
|
||||||
|
|
||||||
|
0.6.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* |feature| add metric to detect filesystems rules that don't match any local dataset (thanks, `@gmekicaxcient <https://github.com/gmekicaxcient>`_).
|
||||||
|
* |bugfix| ``zrepl status``: hide progress bar once all filesystems reach terminal state (thanks, `@0x3333 <https://github.com/0x3333>`_).
|
||||||
|
* |bugfix| handling of tenative cursor presence if protection strategy doesn't use it (:issue:`714`).
|
||||||
|
* |docs| address setup with two or more external disks (thanks, `@se-jaeger <https://github.com/se-jaeger>`_).
|
||||||
|
* |docs| document ``replication`` and ``conflict_resolution`` options (thanks, `@InsanePrawn <https://github.com/InsanePrawn>`_).
|
||||||
|
* |docs| docs: talks: add note on keep_bookmarks option (thanks, `@skirmess <https://github.com/skirmess>`_).
|
||||||
|
* |maint| dist: add openrc service file (thanks, `@gramosg <https://github.com/gramosg>`_).
|
||||||
|
* |maint| grafana: update dashboard to Grafana 9.3.6.
|
||||||
|
* |maint| run platform tests as part of CI.
|
||||||
|
* |maint| build: upgrade to Go 1.21 and update golangci-lint; minimum Go version for builds is now 1.20
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
|
||||||
|
| You can support maintenance and feature development through one of the following services:
|
||||||
|
| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal|
|
||||||
|
| Note that PayPal processing fees are relatively high for small donations.
|
||||||
|
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
|
||||||
|
|
||||||
|
0.6
|
||||||
|
---
|
||||||
|
|
||||||
|
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
|
||||||
|
* |feature| 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:`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:`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
|
||||||
|
---
|
||||||
|
|
||||||
|
This release is a milestone for zrepl and required significant refactoring if not rewrites of substantial parts of the application.
|
||||||
|
It breaks both configuration and transport format, and thus requires manual intervention and updates on both sides of a replication setup.
|
||||||
|
|
||||||
|
.. DANGER::
|
||||||
|
The changes in the pruning system for this release require you to explicitly define **keep rules**:
|
||||||
|
for any snapshot that you want to keep, at least one rule must match.
|
||||||
|
This is different from previous releases where pruning only affected snapshots with the configured snapshotting prefix.
|
||||||
|
Make sure that snapshots to be kept or ignored by zrepl are covered, e.g. by using the ``regex`` keep rule.
|
||||||
|
:ref:`Learn more in the config docs... <prune>`
|
||||||
|
|
||||||
|
|
||||||
|
Notes to Package Maintainers
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
This behavior is independent from the ``stdout`` outlet type.
|
||||||
|
Please make sure the stderr output of the daemon is captured somewhere.
|
||||||
|
To conserve precious stack traces, make sure that multiple service restarts do not directly discard previous stderr output.
|
||||||
|
* Make it obvious for users how to set the ``GOTRACEBACK`` environment variable to ``GOTRACEBACK=crash``.
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
* |break| |mig| Placeholder property representation changed
|
||||||
|
|
||||||
|
* The :ref:`placeholder property <replication-placeholder-property>` now uses ``on|off`` as values
|
||||||
|
instead of hashes of the dataset path. This permits renames of the sink filesystem without
|
||||||
|
updating all placeholder properties.
|
||||||
|
* Relevant for 0.0.X-0.1-rc* to 0.1 migrations
|
||||||
|
* Make sure your config is valid with ``zrepl configcheck``
|
||||||
|
* Run ``zrepl migrate 0.0.X:0.1:placeholder``
|
||||||
|
|
||||||
|
* |feature| :issue:`55` : Push replication (see :ref:`push job <job-push>` and :ref:`sink job <job-sink>`)
|
||||||
|
* |feature| :ref:`TCP Transport <transport-tcp>`
|
||||||
|
* |feature| :ref:`TCP + TLS client authentication transport <transport-tcp+tlsclientauth>`
|
||||||
|
* |feature| :issue:`111`: RPC protocol rewrite
|
||||||
|
|
||||||
|
* |break| Protocol breakage; Update and restart of all zrepl daemons is required.
|
||||||
|
* Use `gRPC <https://grpc.io/>`_ for control RPCs and a custom protocol for bulk data transfer.
|
||||||
|
* Automatic retries for network-temporary errors
|
||||||
|
|
||||||
|
* Limited to errors during replication for this release.
|
||||||
|
Addresses the common problem of ISP-forced reconnection at night, but will become
|
||||||
|
way more useful with resumable send & recv support.
|
||||||
|
Pruning errors are handled per FS, i.e., a prune RPC is attempted at least once per FS.
|
||||||
|
|
||||||
|
* |feature| Proper timeout handling for the :ref:`SSH transport <transport-ssh+stdinserver>`
|
||||||
|
|
||||||
|
* |break| Requires Go 1.11 or later.
|
||||||
|
|
||||||
|
* |break| |break_config|: mappings are no longer supported
|
||||||
|
|
||||||
|
* Receiving sides (``pull`` and ``sink`` job) specify a single ``root_fs``.
|
||||||
|
Received filesystems are then stored *per client* in ``${root_fs}/${client_identity}``.
|
||||||
|
See :ref:`job-overview` for details.
|
||||||
|
|
||||||
|
* |feature| |break| |break_config| Manual snapshotting + triggering of replication
|
||||||
|
|
||||||
|
* |feature| :issue:`69`: include manually created snapshots in replication
|
||||||
|
* |break_config| ``manual`` and ``periodic`` :ref:`snapshotting types <job-snapshotting-spec>`
|
||||||
|
* |feature| ``zrepl signal wakeup JOB`` subcommand to trigger replication + pruning
|
||||||
|
* |feature| ``zrepl signal reset JOB`` subcommand to abort current replication + pruning
|
||||||
|
|
||||||
|
* |feature| |break| |break_config| New pruning system
|
||||||
|
|
||||||
|
* The active side of a replication (pull or push) decides what to prune for both sender and receiver.
|
||||||
|
The RPC protocol is used to execute the destroy operations on the remote side.
|
||||||
|
* New pruning policies (see :ref:`configuration documentation <prune>` )
|
||||||
|
|
||||||
|
* The decision what snapshots shall be pruned is now made based on *keep rules*
|
||||||
|
* |feature| :issue:`68`: keep rule ``not_replicated`` prevents divergence of sender and receiver
|
||||||
|
|
||||||
|
* |feature| |break| Bookmark pruning is no longer necessary
|
||||||
|
|
||||||
|
* Per filesystem, zrepl creates a single bookmark (``#zrepl_replication_cursor``) and moves it forward with the most recent successfully replicated snapshot on the receiving side.
|
||||||
|
* Old bookmarks created by prior versions of zrepl (named like their corresponding snapshot) must be deleted manually.
|
||||||
|
* |break_config| ``keep_bookmarks`` parameter of the ``grid`` keep rule has been removed
|
||||||
|
|
||||||
|
* |feature| ``zrepl status`` for live-updating replication progress (it's really cool!)
|
||||||
|
* |feature| :ref:`Snapshot- & pruning-only job type <job-snap>` (for local snapshot management)
|
||||||
|
* |feature| :issue:`67`: Expose `Prometheus <https://prometheus.io>`_ metrics via HTTP (:ref:`config docs <monitoring-prometheus>`)
|
||||||
|
|
||||||
|
* Compatible Grafana dashboard shipping in ``dist/grafana``
|
||||||
|
|
||||||
|
* |break_config| Logging outlet types must be specified using the ``type`` instead of ``outlet`` key
|
||||||
|
* |break| :issue:`53`: CLI: ``zrepl control *`` subcommands have been made direct subcommands of ``zrepl *``
|
||||||
|
* |bugfix| Goroutine leak on ssh transport connection timeouts
|
||||||
|
* |bugfix| :issue:`81` :issue:`77` : handle failed accepts correctly (``source`` job)
|
||||||
|
* |bugfix| :issue:`100`: fix incompatibility with ZoL 0.8
|
||||||
|
* |feature| :issue:`115`: logging: configurable syslog facility
|
||||||
|
* |feature| Systemd unit file in ``dist/systemd``
|
||||||
|
|
||||||
|
.. |lastrelease| replace:: 0.0.3
|
||||||
|
|
||||||
|
Previous Releases
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
Due to limitations in our documentation system, we only show the changelog since the last release and the time this documentation is built.
|
||||||
|
For the changelog of previous releases, use the version selection in the hosted version of these docs at `zrepl.github.io <https://zrepl.github.io>`_.
|
||||||
+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',
|
||||||
|
"sphinx_multiversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
# suppress_warnings = ['image.nonlocal_uri']
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['./_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = 'zrepl'
|
||||||
|
copyright = '2017-2023, Christian Schwarz'
|
||||||
|
author = 'Christian Schwarz'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
#version = set by sphinxcontrib-versioning
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
#release = version
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = 'en'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
html_css_files = [
|
||||||
|
'banner.css',
|
||||||
|
]
|
||||||
|
|
||||||
|
html_logo = '_static/zrepl.svg'
|
||||||
|
|
||||||
|
html_context = {
|
||||||
|
# https://github.com/rtfd/sphinx_rtd_theme/issues/205
|
||||||
|
# Add 'Edit on Github' link instead of 'View page source'
|
||||||
|
"display_github": True,
|
||||||
|
"github_user": "zrepl",
|
||||||
|
"github_repo": "zrepl",
|
||||||
|
"github_version": "master",
|
||||||
|
"conf_py_path": "/docs/",
|
||||||
|
"source_suffix": source_suffix,
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'zrepldoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'zrepl.tex', 'zrepl Documentation',
|
||||||
|
'Christian Schwarz', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'zrepl', 'zrepl Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'zrepl', 'zrepl Documentation',
|
||||||
|
author, 'zrepl', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for the extlinks extension -----------------------------------
|
||||||
|
# http://www.sphinx-doc.org/en/stable/ext/extlinks.html
|
||||||
|
extlinks = {
|
||||||
|
'issue':('https://github.com/zrepl/zrepl/issues/%s', 'issue #%s'),
|
||||||
|
'repomasterlink':('https://github.com/zrepl/zrepl/blob/master/%s', '%s'),
|
||||||
|
'sampleconf':('https://github.com/zrepl/zrepl/blob/master/internal/config/samples%s', 'internal/config/samples%s'),
|
||||||
|
'commit':('https://github.com/zrepl/zrepl/commit/%s', 'commit %s'),
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
baseURL = "https://zrepl.github.io"
|
|
||||||
languageCode = "en-us"
|
|
||||||
title = "zrepl - ZFS replication"
|
|
||||||
theme = "docdock"
|
|
||||||
|
|
||||||
[[menu.shortcuts]]
|
|
||||||
pre = "<h3>More</h3>"
|
|
||||||
name = "<i class='fa fa-github'></i> GitHub repo"
|
|
||||||
identifier = "ds"
|
|
||||||
url = "https://github.com/zrepl/zrepl"
|
|
||||||
weight = 10
|
|
||||||
|
|
||||||
[[menu.shortcuts]]
|
|
||||||
name = "<i class='fa fa-twitter'></i> Primary Author on Twitter"
|
|
||||||
url = "https://twitter.com/problame"
|
|
||||||
weight = 11
|
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
.. _configuration_toc:
|
||||||
|
|
||||||
|
*************
|
||||||
|
Configuration
|
||||||
|
*************
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
configuration/overview
|
||||||
|
configuration/jobs
|
||||||
|
configuration/transports
|
||||||
|
configuration/filter_syntax
|
||||||
|
configuration/sendrecvoptions
|
||||||
|
configuration/replication
|
||||||
|
configuration/conflict_resolution
|
||||||
|
configuration/snapshotting
|
||||||
|
configuration/prune
|
||||||
|
configuration/logging
|
||||||
|
configuration/monitoring
|
||||||
|
configuration/misc
|
||||||
@@ -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``.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _pattern-filter:
|
||||||
|
|
||||||
|
Filter Syntax
|
||||||
|
=============
|
||||||
|
|
||||||
|
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 is specified as a **YAML dictionary** with patterns as keys and booleans as values.
|
||||||
|
The following rules determine which result is chosen for a given filesystem path:
|
||||||
|
|
||||||
|
* More specific path patterns win over less specific ones
|
||||||
|
* Non-wildcard patterns (full path patterns) win over *subtree wildcards* (`<` at end of pattern)
|
||||||
|
* If the path in question does not match any pattern, the result is ``false``.
|
||||||
|
|
||||||
|
The **subtree wildcard** ``<`` means "the dataset left of ``<`` and all its children".
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
You can try out patterns for a configured job using the ``zrepl test filesystems`` subcommand for push and source jobs.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
Full Access
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
The following configuration will allow access to all filesystems.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: source
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
Fine-grained
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The following configuration demonstrates all rules presented above.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: source
|
||||||
|
filesystems: {
|
||||||
|
"tank<": true, # rule 1
|
||||||
|
"tank/foo<": false, # rule 2
|
||||||
|
"tank/foo/bar": true, # rule 3
|
||||||
|
}
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
Which rule applies to given path, and what is the result?
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
tank/foo/bar/loo => 2 false
|
||||||
|
tank/bar => 1 true
|
||||||
|
tank/foo/bar => 3 true
|
||||||
|
zroot => NONE false
|
||||||
|
tank/var/log => 1 true
|
||||||
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _job:
|
||||||
|
|
||||||
|
Job Types in Detail
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. _job-push:
|
||||||
|
|
||||||
|
Job Type ``push``
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``push``
|
||||||
|
* - ``name``
|
||||||
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
|
* - ``connect``
|
||||||
|
- |connect-transport|
|
||||||
|
* - ``filesystems``
|
||||||
|
- |filter-spec| for filesystems to be snapshotted and pushed to the sink
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
|
* - ``snapshotting``
|
||||||
|
- |snapshotting-spec|
|
||||||
|
* - ``pruning``
|
||||||
|
- |pruning-spec|
|
||||||
|
* - ``replication``
|
||||||
|
- |replication-options|
|
||||||
|
* - ``conflict_resolution``
|
||||||
|
- |conflict-resolution-options|
|
||||||
|
|
||||||
|
Example config: :sampleconf:`/push.yml`
|
||||||
|
|
||||||
|
.. _job-sink:
|
||||||
|
|
||||||
|
Job Type ``sink``
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``sink``
|
||||||
|
* - ``name``
|
||||||
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
|
* - ``serve``
|
||||||
|
- |serve-transport|
|
||||||
|
* - ``root_fs``
|
||||||
|
- ZFS filesystems are received to
|
||||||
|
``$root_fs/$client_identity/$source_path``
|
||||||
|
|
||||||
|
Example config: :sampleconf:`/sink.yml`
|
||||||
|
|
||||||
|
.. _job-pull:
|
||||||
|
|
||||||
|
Job Type ``pull``
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``pull``
|
||||||
|
* - ``name``
|
||||||
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
|
* - ``connect``
|
||||||
|
- |connect-transport|
|
||||||
|
* - ``root_fs``
|
||||||
|
- ZFS filesystems are received to
|
||||||
|
``$root_fs/$source_path``
|
||||||
|
* - ``interval``
|
||||||
|
- | Interval at which to pull from the source job (e.g. ``10m``).
|
||||||
|
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
|
||||||
|
* - ``pruning``
|
||||||
|
- |pruning-spec|
|
||||||
|
* - ``replication``
|
||||||
|
- |replication-options|
|
||||||
|
* - ``conflict_resolution``
|
||||||
|
- |conflict-resolution-options|
|
||||||
|
|
||||||
|
Example config: :sampleconf:`/pull.yml`
|
||||||
|
|
||||||
|
.. _job-source:
|
||||||
|
|
||||||
|
Job Type ``source``
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``source``
|
||||||
|
* - ``name``
|
||||||
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
|
* - ``serve``
|
||||||
|
- |serve-transport|
|
||||||
|
* - ``filesystems``
|
||||||
|
- |filter-spec| for filesystems to be snapshotted and exposed to connecting clients
|
||||||
|
* - ``send``
|
||||||
|
- |send-options|
|
||||||
|
* - ``snapshotting``
|
||||||
|
- |snapshotting-spec|
|
||||||
|
|
||||||
|
Example config: :sampleconf:`/source.yml`
|
||||||
|
|
||||||
|
|
||||||
|
.. _replication-local:
|
||||||
|
|
||||||
|
Local replication
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
If you have the need for local replication (most likely between two local storage pools), you can use the :ref:`local transport type <transport-local>` to connect a local push job to a local sink job.
|
||||||
|
|
||||||
|
Example config: :sampleconf:`/local.yml`.
|
||||||
|
|
||||||
|
|
||||||
|
.. _job-snap:
|
||||||
|
|
||||||
|
Job Type ``snap`` (snapshot & prune only)
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
Job type that only takes snapshots and performs pruning on the local machine.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``snap``
|
||||||
|
* - ``name``
|
||||||
|
- unique name of the job :issue:`(must not change)<327>`
|
||||||
|
* - ``filesystems``
|
||||||
|
- |filter-spec| for filesystems to be snapshotted
|
||||||
|
* - ``snapshotting``
|
||||||
|
- |snapshotting-spec|
|
||||||
|
* - ``pruning``
|
||||||
|
- |pruning-spec|
|
||||||
|
|
||||||
|
Example config: :sampleconf:`/snap.yml`
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _logging:
|
||||||
|
|
||||||
|
Logging
|
||||||
|
=======
|
||||||
|
|
||||||
|
zrepl uses structured logging to provide users with easily processable log messages.
|
||||||
|
|
||||||
|
Logging outlets are configured in the ``global`` section of the config file.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
global:
|
||||||
|
logging:
|
||||||
|
|
||||||
|
- type: OUTLET_TYPE
|
||||||
|
level: MINIMUM_LEVEL
|
||||||
|
format: FORMAT
|
||||||
|
|
||||||
|
- type: OUTLET_TYPE
|
||||||
|
level: MINIMUM_LEVEL
|
||||||
|
format: FORMAT
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
jobs: ...
|
||||||
|
|
||||||
|
.. _logging-error-outlet:
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
The **first outlet is special**: if an error writing to any outlet occurs, the first outlet receives the error and can print it.
|
||||||
|
Thus, the first outlet must be the one that always works and does not block, e.g. ``stdout``, which is the default.
|
||||||
|
|
||||||
|
.. _logging-default-config:
|
||||||
|
|
||||||
|
Default Configuration
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
By default, the following logging configuration is used
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
global:
|
||||||
|
logging:
|
||||||
|
|
||||||
|
- type: "stdout"
|
||||||
|
level: "warn"
|
||||||
|
format: "human"
|
||||||
|
|
||||||
|
Building Blocks
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The following sections document the semantics of the different log levels, formats and outlet types.
|
||||||
|
|
||||||
|
.. _logging-levels:
|
||||||
|
|
||||||
|
Levels
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 10 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Level
|
||||||
|
- SHORT
|
||||||
|
- Description
|
||||||
|
* - ``error``
|
||||||
|
- ``ERRO``
|
||||||
|
- immediate action required
|
||||||
|
* - ``warn``
|
||||||
|
- ``WARN``
|
||||||
|
- symptoms for misconfiguration, soon expected failure, etc.
|
||||||
|
* - ``info``
|
||||||
|
- ``INFO``
|
||||||
|
- explains what happens without too much detail
|
||||||
|
* - ``debug``
|
||||||
|
- ``DEBG``
|
||||||
|
- tracing information, state dumps, etc. useful for debugging.
|
||||||
|
|
||||||
|
Incorrectly classified messages are considered a bug and should be reported.
|
||||||
|
|
||||||
|
.. _logging-formats:
|
||||||
|
|
||||||
|
Formats
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Format
|
||||||
|
- Description
|
||||||
|
* - ``human``
|
||||||
|
- prints job and subsystem into brackets before the actual message,
|
||||||
|
followed by remaining fields in logfmt style
|
||||||
|
* - ``logfmt``
|
||||||
|
- `logfmt <https://brandur.org/logfmt>`_ output. zrepl uses `this Go package <https://github.com/go-logfmt/logfmt>`_.
|
||||||
|
* - ``json``
|
||||||
|
- JSON formatted output. Each line is a valid JSON document. Fields are marshaled by
|
||||||
|
``encoding/json.Marshal()``, which is particularly useful for processing in
|
||||||
|
log aggregation or when processing state dumps.
|
||||||
|
|
||||||
|
Outlets
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
Outlets are the destination for log entries.
|
||||||
|
|
||||||
|
.. _logging-outlet-stdout:
|
||||||
|
|
||||||
|
``stdout`` Outlet
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- ``stdout``
|
||||||
|
* - ``level``
|
||||||
|
- minimum :ref:`log level <logging-levels>`
|
||||||
|
* - ``format``
|
||||||
|
- output :ref:`format <logging-formats>`
|
||||||
|
* - ``time``
|
||||||
|
- always include time in output (``true`` or ``false``)
|
||||||
|
* - ``color``
|
||||||
|
- colorize output according to log level (``true`` or ``false``)
|
||||||
|
|
||||||
|
Writes all log entries with minimum level ``level`` formatted by ``format`` to stdout.
|
||||||
|
If stdout is a tty, interactive usage is assumed and both ``time`` and ``color`` are set to ``true``.
|
||||||
|
|
||||||
|
Can only be specified once.
|
||||||
|
|
||||||
|
``syslog`` Outlet
|
||||||
|
-----------------
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- ``syslog``
|
||||||
|
* - ``level``
|
||||||
|
- minimum :ref:`log level <logging-levels>`
|
||||||
|
* - ``format``
|
||||||
|
- output :ref:`format <logging-formats>`
|
||||||
|
* - ``facility``
|
||||||
|
- Which syslog facility to use (default = ``local0``)
|
||||||
|
* - ``retry_interval``
|
||||||
|
- Interval between reconnection attempts to syslog (default = 0)
|
||||||
|
|
||||||
|
Writes all log entries formatted by ``format`` to syslog.
|
||||||
|
On normal setups, you should not need to change the ``retry_interval``.
|
||||||
|
|
||||||
|
Can only be specified once.
|
||||||
|
|
||||||
|
``tcp`` Outlet
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- ``tcp``
|
||||||
|
* - ``level``
|
||||||
|
- minimum :ref:`log level <logging-levels>`
|
||||||
|
* - ``format``
|
||||||
|
- output :ref:`format <logging-formats>`
|
||||||
|
* - ``net``
|
||||||
|
- ``tcp`` in most cases
|
||||||
|
* - ``address``
|
||||||
|
- remote network, e.g. ``logs.example.com:10202``
|
||||||
|
* - ``retry_interval``
|
||||||
|
- Interval between reconnection attempts to ``address``
|
||||||
|
* - ``tls``
|
||||||
|
- TLS config (see below)
|
||||||
|
|
||||||
|
Establishes a TCP connection to ``address`` and sends log messages with minimum level ``level`` formatted by ``format``.
|
||||||
|
If ``tls`` is not specified, an unencrypted connection is established.
|
||||||
|
If ``tls`` is specified, the TCP connection is secured with TLS + Client Authentication.
|
||||||
|
The latter is particularly useful in combination with log aggregation services.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Description
|
||||||
|
* - ``ca``
|
||||||
|
- PEM-encoded certificate authority that signed the remote server's TLS certificate
|
||||||
|
* - ``cert``
|
||||||
|
- PEM-encoded client certificate identifying this zrepl daemon toward the remote server
|
||||||
|
* - ``key``
|
||||||
|
- PEM-encoded, unencrypted client private key identifying this zrepl daemon toward the remote server
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
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 before messages are dropped.
|
||||||
|
|
||||||
|
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>` ).
|
||||||
|
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
zrepl uses Go's ``crypto/tls`` and ``crypto/x509`` packages and leaves all but the required fields in ``tls.Config`` at their default values.
|
||||||
|
In case of a security defect in these packages, zrepl has to be rebuilt because Go binaries are statically linked.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.. _miscellaneous:
|
||||||
|
|
||||||
|
Miscellaneous
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. _conf-runtime-directories:
|
||||||
|
|
||||||
|
Runtime Directories & UNIX Sockets
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
The zrepl daemon needs to open various UNIX sockets in a runtime directory:
|
||||||
|
|
||||||
|
* a ``control`` socket that the CLI commands use to interact with the daemon
|
||||||
|
* the :ref:`transport-ssh+stdinserver` listener opens one socket per configured client, named after ``client_identity`` parameter
|
||||||
|
|
||||||
|
There is no authentication on these sockets except the UNIX permissions.
|
||||||
|
The zrepl daemon will refuse to bind any of the above sockets in a directory that is world-accessible.
|
||||||
|
|
||||||
|
The following sections of the ``global`` config shows the default paths.
|
||||||
|
The shell script below shows how the default runtime directory can be created.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
global:
|
||||||
|
control:
|
||||||
|
sockpath: /var/run/zrepl/control
|
||||||
|
serve:
|
||||||
|
stdinserver:
|
||||||
|
sockdir: /var/run/zrepl/stdinserver
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p /var/run/zrepl/stdinserver
|
||||||
|
chmod -R 0700 /var/run/zrepl
|
||||||
|
|
||||||
|
|
||||||
|
Durations & Intervals
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Interval & duration fields in job definitions, pruning configurations, etc. must match the following regex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _monitoring:
|
||||||
|
|
||||||
|
Monitoring
|
||||||
|
==========
|
||||||
|
|
||||||
|
Monitoring endpoints are configured in the ``global.monitoring`` section of the config file.
|
||||||
|
|
||||||
|
.. _monitoring-prometheus:
|
||||||
|
|
||||||
|
Prometheus & Grafana
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
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. ``: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 ``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:
|
||||||
|
see :repomasterlink:`dist/grafana`.
|
||||||
|
The dashboard also contains some advice on which metrics are important to monitor.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
At the time of writing, there is no stability guarantee on the exported metrics.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
global:
|
||||||
|
monitoring:
|
||||||
|
- type: prometheus
|
||||||
|
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`.
|
||||||
|
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
.. _prune:
|
||||||
|
|
||||||
|
Pruning Policies
|
||||||
|
================
|
||||||
|
|
||||||
|
In zrepl, *pruning* means *destroying snapshots*.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
On the receiving side, you may have more disk space available, or need to comply with other backup retention policies.
|
||||||
|
|
||||||
|
zrepl uses a set of **keep rules** per sending and receiving side to determine which snapshots shall be kept per filesystem.
|
||||||
|
**A snapshot that is not kept by any rule is destroyed.**
|
||||||
|
The keep rules are **evaluated on the active side** (:ref:`push <job-push>` or :ref:`pull job <job-pull>`) of the replication setup, for both active and passive side, after replication completed or was determined to have failed permanently.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Configuration:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
name: ...
|
||||||
|
connect: ...
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
"tmp": false
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
prefix: zrepl_
|
||||||
|
interval: 10m
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: not_replicated
|
||||||
|
# make sure manually created snapshots by the administrator are kept
|
||||||
|
- type: regex
|
||||||
|
regex: "^manual_.*"
|
||||||
|
- type: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 14x1d
|
||||||
|
regex: "^zrepl_.*"
|
||||||
|
keep_receiver:
|
||||||
|
- type: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
|
regex: "^zrepl_.*"
|
||||||
|
# manually created snapshots will be kept forever on receiver
|
||||||
|
- type: regex
|
||||||
|
regex: "^manual_.*"
|
||||||
|
|
||||||
|
.. DANGER::
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. _prune-keep-not-replicated:
|
||||||
|
|
||||||
|
Policy ``not_replicated``
|
||||||
|
-------------------------
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
- type: not_replicated
|
||||||
|
...
|
||||||
|
|
||||||
|
``not_replicated`` keeps all snapshots that have not been replicated to the receiving side.
|
||||||
|
It only makes sense to specify this rule for the ``keep_sender``.
|
||||||
|
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:
|
||||||
|
|
||||||
|
Policy ``grid``
|
||||||
|
---------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: pull
|
||||||
|
pruning:
|
||||||
|
keep_receiver:
|
||||||
|
- type: grid
|
||||||
|
regex: "^zrepl_.*"
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
|
│ │ │
|
||||||
|
└─ 1 repetition of a one-hour interval with keep=all
|
||||||
|
│ │
|
||||||
|
└─ 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 that thins out snapshots as they get older.
|
||||||
|
|
||||||
|
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 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 will be pruned unless another rule keeps them.
|
||||||
|
#. The snapshots that match ``regex`` are placed onto a time axis according to their ``creation`` date.
|
||||||
|
The youngest snapshot is on the left, the oldest on the right.
|
||||||
|
#. The first buckets are placed "under" that axis so that the ``grid`` spec's first bucket's left edge aligns with youngest snapshot.
|
||||||
|
#. All subsequent buckets are placed adjacent to their predecessor 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 syntax to describe the bucket list is as follows:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
Policy ``last_n``
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
pruning:
|
||||||
|
keep_receiver:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
regex: ^zrepl_.*$ # optional
|
||||||
|
...
|
||||||
|
|
||||||
|
``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:
|
||||||
|
|
||||||
|
Policy ``regex``
|
||||||
|
----------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
pruning:
|
||||||
|
keep_receiver:
|
||||||
|
# keep all snapshots with prefix zrepl_ or manual_
|
||||||
|
- type: regex
|
||||||
|
regex: "^(zrepl|manual)_.*"
|
||||||
|
|
||||||
|
- type: push
|
||||||
|
snapshotting:
|
||||||
|
prefix: zrepl_
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
# keep all snapshots that were not created by zrepl
|
||||||
|
- type: regex
|
||||||
|
negate: true
|
||||||
|
regex: "^zrepl_.*"
|
||||||
|
|
||||||
|
``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>`_).
|
||||||
|
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
|
||||||
|
|
||||||
|
.. _prune-workaround-source-side-pruning:
|
||||||
|
|
||||||
|
Source-side snapshot pruning
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
A :ref:`source jobs<job-source>` takes snapshots on the system it runs on.
|
||||||
|
The corresponding :ref:`pull job <job-pull>` on the replication target connects to the source job and replicates the snapshots.
|
||||||
|
Afterwards, the pull job coordinates pruning on both sender (the source job side) and receiver (the pull job side).
|
||||||
|
|
||||||
|
There is no built-in way to define and execute pruning on the source side independently of the pull side.
|
||||||
|
The source job will continue taking snapshots which will not be pruned until the pull side connects.
|
||||||
|
This means that **extended replication downtime will fill up the source's zpool with snapshots**.
|
||||||
|
|
||||||
|
If the above is a conceivable situation for you, consider using :ref:`push mode <job-push>`, where pruning happens on the same side where snapshots are taken.
|
||||||
|
|
||||||
|
Workaround using ``snap`` job
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
As a workaround (see GitHub :issue:`102` for development progress), a pruning-only :ref:`snap job <job-snap>` can be defined on the source side:
|
||||||
|
The snap job is in charge of snapshot creation & destruction, whereas the source job's role is reduced to just serving snapshots.
|
||||||
|
However, since, jobs are run independently, it is possible that the snap job will prune snapshots that are queued for replication / destruction by the remote pull job that connects to the source job.
|
||||||
|
Symptoms of such race conditions are spurious replication and destroy errors.
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# source side
|
||||||
|
jobs:
|
||||||
|
- type: snap
|
||||||
|
snapshotting:
|
||||||
|
type: periodic
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
# source side pruning rules go here
|
||||||
|
...
|
||||||
|
|
||||||
|
- type: source
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
root_fs: ...
|
||||||
|
|
||||||
|
# pull side
|
||||||
|
jobs:
|
||||||
|
- type: pull
|
||||||
|
pruning:
|
||||||
|
keep_sender:
|
||||||
|
# let the source-side snap job do the pruning
|
||||||
|
- type: regex
|
||||||
|
regex: ".*"
|
||||||
|
...
|
||||||
|
keep_receiver:
|
||||||
|
# feel free to prune on the pull side as desired
|
||||||
|
...
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
.. highlight:: bash
|
||||||
|
|
||||||
|
.. _transport:
|
||||||
|
|
||||||
|
Transports
|
||||||
|
==========
|
||||||
|
|
||||||
|
The zrepl RPC layer uses **transports** to establish a single, bidirectional data stream between an active and passive job.
|
||||||
|
On the passive (serving) side, the transport also provides the **client identity** to the upper layers:
|
||||||
|
this string is used for access control and separation of filesystem sub-trees in :ref:`sink jobs <job-sink>`.
|
||||||
|
Transports are specified in the ``connect`` or ``serve`` section of a job definition.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
The **client identities must be valid ZFS dataset path components**
|
||||||
|
because the :ref:`sink job <job-sink>` uses ``${root_fs}/${client_identity}`` to determine the client's subtree.
|
||||||
|
|
||||||
|
.. _transport-tcp:
|
||||||
|
|
||||||
|
``tcp`` Transport
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
The ``tcp`` transport uses plain TCP, which means that the data is **not encrypted** on the wire.
|
||||||
|
Clients are identified by their IPv4 or IPv6 addresses, and the client identity is established through a mapping on the server.
|
||||||
|
|
||||||
|
This transport may also be used in conjunction with network-layer encryption and/or VPN tunnels to provide encryption on the wire.
|
||||||
|
To make the IP-based client authentication effective, such solutions should provide authenticated IP addresses.
|
||||||
|
Some options to consider:
|
||||||
|
|
||||||
|
.. _transport-tcp-tunneling:
|
||||||
|
|
||||||
|
* `WireGuard <https://www.wireguard.com/>`_: Linux-focussed, in-kernel TLS
|
||||||
|
* `OpenVPN <https://openvpn.net/>`_: Cross-platform VPN, uses tun on \*nix
|
||||||
|
* `IPSec <https://en.wikipedia.org/wiki/IPsec>`_: Properly standardized, in-kernel network-layer VPN
|
||||||
|
* `spiped <http://www.tarsnap.com/spiped.html>`_: think of it as an encrypted pipe between two servers
|
||||||
|
* SSH
|
||||||
|
|
||||||
|
* `sshuttle <https://sshuttle.readthedocs.io/en/stable/overview.html>`_: VPN-like solution, but using SSH
|
||||||
|
* `SSH port forwarding <https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding>`_: Systemd user unit & make it start before the zrepl service.
|
||||||
|
|
||||||
|
Serve
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: sink
|
||||||
|
serve:
|
||||||
|
type: tcp
|
||||||
|
listen: ":8888"
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
|
clients: {
|
||||||
|
"192.168.122.123" : "mysql01",
|
||||||
|
"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
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: push
|
||||||
|
connect:
|
||||||
|
type: tcp
|
||||||
|
address: "10.23.42.23:8888"
|
||||||
|
dial_timeout: # optional, default 10s
|
||||||
|
...
|
||||||
|
|
||||||
|
.. _transport-tcp+tlsclientauth:
|
||||||
|
|
||||||
|
``tls`` Transport
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
The ``tls`` transport uses TCP + TLS with client authentication using client certificates.
|
||||||
|
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>`_.
|
||||||
|
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/>`_.
|
||||||
|
Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
|
||||||
|
|
||||||
|
All file paths are resolved relative to the zrepl daemon's working directory.
|
||||||
|
Specify absolute paths if you are unsure what directory that is (or find out from your init system).
|
||||||
|
|
||||||
|
If intermediate CAs are used, the **full chain** must be present in either in the ``ca`` file or the individual ``cert`` files.
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. 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
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: sink
|
||||||
|
root_fs: "pool2/backup_laptops"
|
||||||
|
serve:
|
||||||
|
type: tls
|
||||||
|
listen: ":8888"
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
|
ca: /etc/zrepl/ca.crt
|
||||||
|
cert: /etc/zrepl/prod.fullchain
|
||||||
|
key: /etc/zrepl/prod.key
|
||||||
|
client_cns:
|
||||||
|
- "laptop1"
|
||||||
|
- "homeserver"
|
||||||
|
|
||||||
|
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 ``listen_freebind`` field is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
|
|
||||||
|
Connect
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: pull
|
||||||
|
connect:
|
||||||
|
type: tls
|
||||||
|
address: "server1.foo.bar:8888"
|
||||||
|
ca: /etc/zrepl/ca.crt
|
||||||
|
cert: /etc/zrepl/backupserver.fullchain
|
||||||
|
key: /etc/zrepl/backupserver.key
|
||||||
|
server_cn: "server1"
|
||||||
|
dial_timeout: # optional, default 10s
|
||||||
|
|
||||||
|
The ``ca`` field specifies the CA which signed the server's certificate (``serve.cert``).
|
||||||
|
The ``server_cn`` specifies the expected common name (CN) of the server's certificate.
|
||||||
|
It overrides the hostname specified in ``address``.
|
||||||
|
The connection fails if either do not match.
|
||||||
|
|
||||||
|
.. _transport-tcp+tlsclientauth-certgen:
|
||||||
|
|
||||||
|
.. _transport-tcp+tlsclientauth-2machineopenssl:
|
||||||
|
|
||||||
|
Mutual-TLS between Two Machines
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
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>`.
|
||||||
|
Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
(name=HOSTNAME; openssl req -x509 -sha256 -nodes \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-days 365 \
|
||||||
|
-keyout $name.key \
|
||||||
|
-out $name.crt -addext "subjectAltName = DNS:$name" -subj "/CN=$name")
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# on backups.example.com
|
||||||
|
- type: sink
|
||||||
|
serve:
|
||||||
|
type: tls
|
||||||
|
listen: ":8888"
|
||||||
|
ca: "/etc/zrepl/prod.example.com.crt"
|
||||||
|
cert: "/etc/zrepl/backups.example.com.crt"
|
||||||
|
key: "/etc/zrepl/backups.example.com.key"
|
||||||
|
client_cns:
|
||||||
|
- "prod.example.com"
|
||||||
|
...
|
||||||
|
|
||||||
|
# on prod.example.com
|
||||||
|
- type: push
|
||||||
|
connect:
|
||||||
|
type: tls
|
||||||
|
address:"backups.example.com:8888"
|
||||||
|
ca: /etc/zrepl/backups.example.com.crt
|
||||||
|
cert: /etc/zrepl/prod.example.com.crt
|
||||||
|
key: /etc/zrepl/prod.example.com.key
|
||||||
|
server_cn: "backups.example.com"
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
``ssh+stdinserver`` Transport
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
``ssh+stdinserver`` uses the ``ssh`` command and some features of the server-side SSH ``authorized_keys`` file.
|
||||||
|
It is less efficient than other transports because the data passes through two more pipes.
|
||||||
|
However, it is fairly convenient to set up and allows the zrepl daemon to not be directly exposed to the internet, because all traffic passes through the system's SSH server.
|
||||||
|
|
||||||
|
The concept is inspired by `git shell <https://git-scm.com/docs/git-shell>`_ and `Borg Backup <https://borgbackup.readthedocs.io/en/stable/deployment.html>`_.
|
||||||
|
The implementation is provided by the Go package ``github.com/problame/go-netssh``.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
``ssh+stdinserver`` generally provides inferior error detection and handling compared to the ``tcp`` and ``tls`` transports.
|
||||||
|
When encountering such problems, consider using ``tcp`` or ``tls`` transports, or help improve package go-netssh.
|
||||||
|
|
||||||
|
.. _transport-ssh+stdinserver-serve:
|
||||||
|
|
||||||
|
Serve
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: source
|
||||||
|
serve:
|
||||||
|
type: stdinserver
|
||||||
|
client_identities:
|
||||||
|
- "client1"
|
||||||
|
- "client2"
|
||||||
|
...
|
||||||
|
|
||||||
|
First of all, note that ``type=stdinserver`` in this case:
|
||||||
|
Currently, only ``connect.type=ssh+stdinserver`` can connect to a ``serve.type=stdinserver``, but we want to keep that option open for future extensions.
|
||||||
|
|
||||||
|
The serving job opens a UNIX socket named after ``client_identity`` in the runtime directory.
|
||||||
|
In our example above, that is ``/var/run/zrepl/stdinserver/client1`` and ``/var/run/zrepl/stdinserver/client2``.
|
||||||
|
|
||||||
|
On the same machine, the ``zrepl stdinserver $client_identity`` command connects to ``/var/run/zrepl/stdinserver/$client_identity``.
|
||||||
|
It then passes its stdin and stdout file descriptors to the zrepl daemon via *cmsg(3)*.
|
||||||
|
zrepl daemon in turn combines them into an object implementing ``net.Conn``:
|
||||||
|
a ``Write()`` turns into a write to stdout, a ``Read()`` turns into a read from stdin.
|
||||||
|
|
||||||
|
Interactive use of the ``stdinserver`` subcommand does not make much sense.
|
||||||
|
However, we can force its execution when a user with a particular SSH pubkey connects via SSH.
|
||||||
|
This can be achieved with an entry in the ``authorized_keys`` file of the serving zrepl daemon.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# for OpenSSH >= 7.2
|
||||||
|
command="zrepl stdinserver CLIENT_IDENTITY",restrict CLIENT_SSH_KEY
|
||||||
|
# for older OpenSSH versions
|
||||||
|
command="zrepl stdinserver CLIENT_IDENTITY",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc CLIENT_SSH_KEY
|
||||||
|
|
||||||
|
* CLIENT_IDENTITY is substituted with an entry from ``client_identities`` in our example
|
||||||
|
* CLIENT_SSH_KEY is substituted with the public part of the SSH keypair specified in the ``connect.identity_file`` directive on the connecting host.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
You may need to adjust the ``PermitRootLogin`` option in ``/etc/ssh/sshd_config`` to ``forced-commands-only`` or higher for this to work.
|
||||||
|
Refer to sshd_config(5) for details.
|
||||||
|
|
||||||
|
To recap, this is of how client authentication works with the ``ssh+stdinserver`` transport:
|
||||||
|
|
||||||
|
* Connections to the ``/var/run/zrepl/stdinserver/${client_identity}`` UNIX socket are blindly trusted by zrepl daemon.
|
||||||
|
The connection client identity is the name of the socket, i.e. ``${client_identity}``.
|
||||||
|
* Thus, the runtime directory must be private to the zrepl user (this is checked by zrepl daemon)
|
||||||
|
* The admin of the host with the serving zrepl daemon controls the ``authorized_keys`` file.
|
||||||
|
* Thus, the administrator controls the mapping ``PUBKEY -> CLIENT_IDENTITY``.
|
||||||
|
|
||||||
|
.. _transport-ssh+stdinserver-connect:
|
||||||
|
|
||||||
|
Connect
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: pull
|
||||||
|
connect:
|
||||||
|
type: ssh+stdinserver
|
||||||
|
host: prod.example.com
|
||||||
|
user: root
|
||||||
|
port: 22
|
||||||
|
identity_file: /etc/zrepl/ssh/identity
|
||||||
|
# options: # optional, default [], `-o` arguments passed to ssh
|
||||||
|
# - "Compression=yes"
|
||||||
|
# dial_timeout: 10s # optional, default 10s, max time.Duration until initial handshake is completed
|
||||||
|
|
||||||
|
The connecting zrepl daemon
|
||||||
|
|
||||||
|
#. Creates a pipe
|
||||||
|
#. Forks
|
||||||
|
#. In the forked process
|
||||||
|
|
||||||
|
#. Replaces forked stdin and stdout with the corresponding pipe ends
|
||||||
|
#. Executes the ``ssh`` binary found in ``$PATH``.
|
||||||
|
|
||||||
|
#. The identity file (``-i``) is set to ``$identity_file``.
|
||||||
|
#. The remote user, host and port correspond to those configured.
|
||||||
|
#. Further options can be specified using the ``options`` field, which appends each entry in the list to the command line using ``-o $entry``.
|
||||||
|
|
||||||
|
#. Wraps the pipe ends in a ``net.Conn`` and returns it to the RPC layer.
|
||||||
|
|
||||||
|
As discussed in the section above, the connecting zrepl daemon expects that ``zrepl stdinserver $client_identity`` is executed automatically via an ``authorized_keys`` file entry.
|
||||||
|
|
||||||
|
The ``known_hosts`` file used by the ssh command must contain an entry for ``connect.host`` prior to starting zrepl.
|
||||||
|
Thus, run the following on the pulling host's command line (substituting ``connect.host``):
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
ssh -i /etc/zrepl/ssh/identity root@prod.example.com
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
The environment variables of the underlying SSH process are cleared. ``$SSH_AUTH_SOCK`` will not be available.
|
||||||
|
It is suggested to create a separate, unencrypted SSH key solely for that purpose.
|
||||||
|
|
||||||
|
|
||||||
|
.. _transport-local:
|
||||||
|
|
||||||
|
``local`` Transport
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The local transport can be used to implement :ref:`local replication <replication-local>`, i.e., push replication between a push and sink job defined in the same configuration file.
|
||||||
|
|
||||||
|
The ``listener_name`` is analogous to a hostname and must match between ``serve`` and ``connect``.
|
||||||
|
The ``client_identity`` is used by the sink as documented above.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- type: sink
|
||||||
|
serve:
|
||||||
|
type: local
|
||||||
|
listener_name: localsink
|
||||||
|
...
|
||||||
|
|
||||||
|
- type: push
|
||||||
|
connect:
|
||||||
|
type: local
|
||||||
|
listener_name: localsink
|
||||||
|
client_identity: local_backup
|
||||||
|
dial_timeout: 2s # optional, 0 for no timeout
|
||||||
|
...
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
zrepl Documentation
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "zrepl - ZFS replication"
|
|
||||||
+++
|
|
||||||
|
|
||||||
# zrepl - ZFS replication
|
|
||||||
|
|
||||||
{{% notice info %}}
|
|
||||||
zrepl as well as this documentation is still under active development.
|
|
||||||
Use & test at your own risk ;)
|
|
||||||
{{% /notice %}}
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
The [5 minute tutorial setup](/tutorial/) gives you a first impression.
|
|
||||||
|
|
||||||
## Main Features
|
|
||||||
|
|
||||||
* Filesystem Replication
|
|
||||||
* [x] Local & Remote
|
|
||||||
* [x] Pull mode
|
|
||||||
* [ ] Push mode
|
|
||||||
* [x] Access control checks when pulling datasets
|
|
||||||
* [x] [Flexible mapping]({{< ref "configuration/map_filter_syntax.md" >}}) rules
|
|
||||||
* [x] Bookmarks support
|
|
||||||
* [ ] Feature-negotiation for
|
|
||||||
* Resumable `send & receive`
|
|
||||||
* Compressed `send & receive`
|
|
||||||
* Raw encrypted `send & receive` (as soon as it is available)
|
|
||||||
* Automatic snapshot creation
|
|
||||||
* [x] Ensure fixed time interval between snapshots
|
|
||||||
* Automatic snapshot pruning
|
|
||||||
* [x] <i class="fa fa-arrow-right" aria-hidden="true"></i> [Retention Grid]({{< ref "configuration/prune.md#retention-grid" >}})
|
|
||||||
* Maintainable implementation in Go
|
|
||||||
* [x] Cross platform
|
|
||||||
* [x] Type safe & testable code
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
zrepl is usable but nowhere near a stable release right now - we are happy about contributors!
|
|
||||||
|
|
||||||
* Explore the codebase
|
|
||||||
* These docs live in the `docs/` subdirectory
|
|
||||||
* Document any non-obvious / confusing / plain broken behavior you encounter when setting up zrepl for the first time
|
|
||||||
* Check the *Issues* and *Projects* sections for things to do
|
|
||||||
|
|
||||||
{{% panel header="<i class='fa fa-github'></i> Getting your code merged"%}}
|
|
||||||
[The <i class='fa fa-github'></i> GitHub repository](https://github.com/zrepl/zrepl) is where all development happens.
|
|
||||||
Open your issue / PR there.
|
|
||||||
{{% /panel %}}
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Configuration"
|
|
||||||
alwaysopen = true
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
{{% children description="true" %}}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Job Types"
|
|
||||||
weight = 10
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
See `sampleconf/*/*.yml` for examples.
|
|
||||||
|
|
||||||
## Source
|
|
||||||
|
|
||||||
## Pull
|
|
||||||
|
|
||||||
## Local
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Logging"
|
|
||||||
weight = 90
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
See `cmd/sampleconf/random/logging.yml`.
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Mapping & Filter Syntax"
|
|
||||||
weight = 20
|
|
||||||
description = "How to specify mappings & filters"
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
## Mapping & Filter Syntax
|
|
||||||
|
|
||||||
For various job types, a filesystem `mapping` or `filter` needs to be
|
|
||||||
specified.
|
|
||||||
|
|
||||||
Both have in common that they return a result for a given filesystem path (in
|
|
||||||
the ZFS filesystem hierarchy): mappings return a *target filesystem*, filters
|
|
||||||
return a *filter result* (`omit`, `ok`).
|
|
||||||
|
|
||||||
The pattern syntax is the same for mappings and filters and is documented in
|
|
||||||
the following section.
|
|
||||||
|
|
||||||
#### Pattern Syntax
|
|
||||||
|
|
||||||
A mapping / filter is specified as a **YAML dictionary** with patterns as keys and
|
|
||||||
results as values.<br />
|
|
||||||
The following rules determine which result is chosen for a given filesystem path:
|
|
||||||
|
|
||||||
* More specific path patterns win over less specific ones
|
|
||||||
* Non-wildcard patterns (full path patterns) win over *subtree wildcards* (`<` at end of pattern)
|
|
||||||
|
|
||||||
{{% panel %}}
|
|
||||||
The **subtree wildcard** `<` means "*the dataset left of `<` and all its children*".
|
|
||||||
{{% / panel %}}
|
|
||||||
|
|
||||||
##### Example
|
|
||||||
|
|
||||||
```
|
|
||||||
# Rule number and its pattern
|
|
||||||
1: tank< # tank and all its children
|
|
||||||
2: tank/foo/bar # full path pattern (no wildcard)
|
|
||||||
3: tank/foo< # tank/foo and all its children
|
|
||||||
|
|
||||||
# Which rule applies to given path?
|
|
||||||
tank/foo/bar/loo => 3
|
|
||||||
tank/bar => 1
|
|
||||||
tank/foo/bar => 2
|
|
||||||
zroot => NO MATCH
|
|
||||||
tank/var/log => 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Mappings
|
|
||||||
|
|
||||||
The example below shows a pull job that would pull remote datasets to the given local paths.<br />
|
|
||||||
If there exists no mapping (`NO MATCH`), the filesystem will not be pulled.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
pull:
|
|
||||||
app01.example.com: # client identity
|
|
||||||
from: app01.example.com
|
|
||||||
mapping: {
|
|
||||||
"tank/var/db<": "zroot/backups/app01/tank/var/db",
|
|
||||||
"tank/var/www<": "zroot/backups/app01/tank/var/www",
|
|
||||||
"tank/var/log": "zroot/logbackup/app01",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
tank/var/db` => zroot/backups/app01/tank/var/db
|
|
||||||
tank/var/db/a/child => zroot/backups/app01/tank/var/db/a/child
|
|
||||||
...
|
|
||||||
tank/var/www => zroot/backups/app01/tank/var/www
|
|
||||||
tank/var/www/a/child => zroot/backups/app01/tank/var/www/a/child
|
|
||||||
...
|
|
||||||
tank/var/log => zroot/logbackup/app01
|
|
||||||
tank/var/log/foo => NOT MAPPED
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Filters
|
|
||||||
|
|
||||||
Valid filter results: `ok` or `omit`.
|
|
||||||
|
|
||||||
The example below shows a pull ACL that allows access to the user homes but not
|
|
||||||
to the rest of the system's datasets.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Example for filter syntax
|
|
||||||
pull_acls:
|
|
||||||
backups.example.com: # client identity
|
|
||||||
filter: {
|
|
||||||
"<": omit,
|
|
||||||
"tank/usr/home<": ok,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Miscellaneous"
|
|
||||||
weight = 100
|
|
||||||
+++
|
|
||||||
|
|
||||||
## Runtime Directories & UNIX Sockets
|
|
||||||
|
|
||||||
zrepl daemon creates various UNIX sockets to allow communicating with it:
|
|
||||||
|
|
||||||
* the `stdinserver` transport connects to a socket named after `client_identity` parameter
|
|
||||||
* the `control` subcommand connects to a defined control socket
|
|
||||||
|
|
||||||
There is no further authentication on these sockets.
|
|
||||||
Therefore we have to make sure they can only be created and accessed by `zrepl daemon`.
|
|
||||||
|
|
||||||
In fact, `zrepl daemon` will not bind a socket to a path in a directory that is world-accessible.
|
|
||||||
|
|
||||||
The directories can be configured in the main configuration file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
global:
|
|
||||||
control:
|
|
||||||
sockpath: /var/run/zrepl/control
|
|
||||||
serve:
|
|
||||||
stdinserver:
|
|
||||||
sockdir: /var/run/zrepl/stdinserver
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 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 ;)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
{{% notice info %}}
|
|
||||||
Connection dumps will almost certainly contain your or other's private data. Do not share it in a bug report.
|
|
||||||
{{% /notice %}}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Pruning"
|
|
||||||
description = "Automated pruning of snapshots"
|
|
||||||
weight = 40
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
## Retention Grid
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Transports"
|
|
||||||
weight = 30
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
## Stdinserver
|
|
||||||
|
|
||||||
|
|
||||||
The SSH transport connects to the remote server using the SSH binary in
|
|
||||||
`$PATH` and the parameters specified in the `zrepl` config file.
|
|
||||||
|
|
||||||
However, instead of a traditional interactive SSH session, `zrepl` expects
|
|
||||||
another instance of `zrepl` on the other side of the connection; You may be
|
|
||||||
familiar with this concept from [git shell](https://git-scm.com/docs/git-shell)
|
|
||||||
or [Borg Backup](https://borgbackup.readthedocs.io/en/stable/deployment.html).
|
|
||||||
|
|
||||||
Check the examples for instructions on how to set this up on your machines!
|
|
||||||
|
|
||||||
{{% panel %}}
|
|
||||||
The environment variables of the underlying SSH process are cleared. `$SSH_AUTH_SOCK` will not be available. We suggest creating a separate, unencrypted SSH key.
|
|
||||||
{{% / panel %}}
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Implementation Overview"
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
The following design aspects may convince you that `zrepl` is superior to a hacked-together shell script solution.
|
|
||||||
|
|
||||||
## Language
|
|
||||||
|
|
||||||
`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
|
|
||||||
* 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 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...
|
|
||||||
* ... with `zrepl` running at both ends of it.
|
|
||||||
|
|
||||||
This has several obvious benefits:
|
|
||||||
|
|
||||||
* No blank root shell access is given to the other side.
|
|
||||||
* Instead, access control lists (ACLs) are used to grant permissions to *authenticated* peers.
|
|
||||||
* The transport mechanism is decoupled from the remaining logic, keeping it extensible (e.g. TCP+TLS)
|
|
||||||
|
|
||||||
{{% panel %}}
|
|
||||||
Currently, the bidirectional communication channel is multiplexed on top of a single SSH connection.
|
|
||||||
Local replication is of course handled efficiently via simple method calls
|
|
||||||
See TODO for details.
|
|
||||||
{{% / panel %}}
|
|
||||||
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Installation"
|
|
||||||
weight = 20
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% notice note %}}
|
|
||||||
Note: check out the [tutorial]({{< relref "tutorial/_index.md" >}}) if you want a first impression of zrepl.
|
|
||||||
{{% /notice %}}
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
zrepl is currently not packaged on any operating system. Signed & versioned releases are planned but not available yet.
|
|
||||||
|
|
||||||
Check out the sources yourself, fetch dependencies using dep, compile and install to the zrepl user's `$PATH`.<br />
|
|
||||||
**Note**: if the zrepl binary is not in `$PATH`, you will have to adjust the examples in the [tutorial]({{< relref "tutorial/_index.md" >}}).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# NOTE: you may want to checkout & build as an unprivileged user
|
|
||||||
cd /root
|
|
||||||
git clone https://github.com/zrepl/zrepl.git
|
|
||||||
cd zrepl
|
|
||||||
dep ensure
|
|
||||||
go build -o zrepl
|
|
||||||
cp zrepl /usr/local/bin/zrepl
|
|
||||||
rehash
|
|
||||||
# see if it worked
|
|
||||||
zrepl help
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
zrepl searches for its main configuration file in the following locations (in that order):
|
|
||||||
|
|
||||||
* `/etc/zrepl/zrepl.yml`
|
|
||||||
* `/usr/local/etc/zrepl/zrepl.yml`
|
|
||||||
|
|
||||||
Alternatively, use CLI flags to specify a config location.
|
|
||||||
|
|
||||||
Copy a config from the [tutorial](/tutorial) or the `cmd/sampleconf` directory to one of these locations and customize it to your setup.
|
|
||||||
|
|
||||||
## Runtime Directories
|
|
||||||
|
|
||||||
Check the the [configuration documentation]({{< relref "configuration/misc.md#runtime-directories-unix-sockets" >}}) for more information.
|
|
||||||
For default settings, the following should to the trick.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /var/run/zrepl/stdinserver
|
|
||||||
chmod -R 0700 /var/run/zrepl
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Running the Daemon
|
|
||||||
|
|
||||||
All actual work zrepl does is performed by a daemon process.
|
|
||||||
|
|
||||||
Logging is configurable via the config file. Please refer to the [logging documentation]({{< relref "configuration/logging.md" >}}).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
zrepl daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
There are no *rc(8)* or *systemd.service(5)* service definitions yet. Note the *daemon(8)* utility on FreeBSD.
|
|
||||||
|
|
||||||
{{% notice info %}}
|
|
||||||
Make sure to actually monitor the error level output of zrepl: some configuration errors will not make the daemon exit.<br />
|
|
||||||
Example: if the daemon cannot create the [stdinserver]({{< relref "configuration/transports.md#stdinserver" >}}) sockets
|
|
||||||
in the runtime directory, it will emit an error message but not exit because other tasks such as periodic snapshots & pruning are of equal importance.
|
|
||||||
{{% / notice %}}
|
|
||||||
|
|
||||||
### Restarting
|
|
||||||
|
|
||||||
The daemon handles SIGINT and SIGTERM for graceful shutdown.
|
|
||||||
|
|
||||||
Graceful shutdown means at worst that a job will not be rescheduled for the next interval.
|
|
||||||
|
|
||||||
The daemon exits as soon as all jobs have reported shut down.
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Tutorial"
|
|
||||||
weight: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
This tutorial shows how zrepl can be used to implement a ZFS-based pull backup.
|
|
||||||
|
|
||||||
We assume the following scenario
|
|
||||||
|
|
||||||
* Production server `prod1` 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/pull/prod1` + children dedicated to backups of `prod1`
|
|
||||||
|
|
||||||
Our backup solution should fulfill the following requirements:
|
|
||||||
|
|
||||||
* Periodically snapshot the filesystems on `prod1` *every 10 minutes*
|
|
||||||
* Incrementally replicate these snapshots to `storage/zrepl/pull/prod1/*` on `backups`
|
|
||||||
* Keep only very few snapshots on `prod1` 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 **source job** on `prod1`
|
|
||||||
* Creates the snapshots
|
|
||||||
* Keeps a few snapshots that are also on `prod1` to enable incremental replication
|
|
||||||
* A **pull job** on `prod1`
|
|
||||||
* Pulls the snapshots
|
|
||||||
* Fades out snapshots as they age
|
|
||||||
|
|
||||||
{{%expand "Side note: why doesn't `backups` take the snapshots right before replication?" %}}
|
|
||||||
After all, a little `ssh prod1 'zfs snapshot...'` wouldn't be so bad, right?
|
|
||||||
|
|
||||||
As is the case with all distributed systems, the link between `prod1` and `backups` might be down for an hour or two.
|
|
||||||
We do not want to sacrifice our required backup resolution of 10 minute intervals for a temporary connection outage.
|
|
||||||
|
|
||||||
When the link comes up again, `backups` will happily catch up the 12 snapshots taken by `prod1` in the meantime, without
|
|
||||||
a gap in our backup history.
|
|
||||||
{{%/expand%}}
|
|
||||||
|
|
||||||
## Install zrepl
|
|
||||||
|
|
||||||
Follow the [OS-specific installation instructions](/install/) and come back here.
|
|
||||||
|
|
||||||
## Configure `backups`
|
|
||||||
|
|
||||||
We define a **pull job** named `pull_prod1` in the [main configuration file](/install/#main-configuration-file):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
jobs:
|
|
||||||
- name: pull_prod1
|
|
||||||
type: pull
|
|
||||||
connect:
|
|
||||||
type: ssh+stdinserver
|
|
||||||
host: prod1.example.com
|
|
||||||
user: root
|
|
||||||
port: 22
|
|
||||||
identity_file: /etc/zrepl/ssh/prod1
|
|
||||||
interval: 10m
|
|
||||||
mapping: {
|
|
||||||
"<":"storage/zrepl/pull/prod1"
|
|
||||||
}
|
|
||||||
initial_repl_policy: most_recent
|
|
||||||
snapshot_prefix: zrepl_pull_backup_
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
|
||||||
```
|
|
||||||
|
|
||||||
The `connect` section instructs zrepl to use the `stdinserver` transport: instead of directly exposing zrepl on `prod1`
|
|
||||||
to the internet, `backups` starts the `zrepl stdinserver` on `prod1` via SSH.
|
|
||||||
(You can learn more about what happens [here]({{< relref "configuration/transports.md#stdinserver" >}}), or just continue following this tutorial.)
|
|
||||||
|
|
||||||
Thus, we need to create the SSH key pair `/etc/zrepl/ssh/prod1{,.pub}` and later pass the public part to `prod1`
|
|
||||||
which will use it to authenticate `backups`. Execute the following commands on `backups` as the root user:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /etc/zrepl
|
|
||||||
mkdir -p ssh
|
|
||||||
chmod 0700 ssh
|
|
||||||
ssh-keygen -t ed25519 -N '' -f /etc/zrepl/ssh/prod1
|
|
||||||
```
|
|
||||||
You can learn more about the [**pull job** format here]({{< relref "configuration/jobs.md#pull" >}}) but for now we are good to go.
|
|
||||||
|
|
||||||
## Configure `prod1`
|
|
||||||
|
|
||||||
We define a corresponding **source job** named `pull_backup` in the [main configuration file](/install/#main-configuration-file)
|
|
||||||
`zrepl.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
- name: pull_backup
|
|
||||||
type: source
|
|
||||||
serve:
|
|
||||||
type: stdinserver
|
|
||||||
client_identity: backups.example.com
|
|
||||||
datasets: {
|
|
||||||
"zroot/var/db": "ok",
|
|
||||||
"zroot/usr/home<": "ok",
|
|
||||||
"zroot/usr/home/paranoid": "!",
|
|
||||||
}
|
|
||||||
snapshot_prefix: zrepl_pull_backup_
|
|
||||||
interval: 10m
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1d(keep=all)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The `serve` section corresponds to the `connect` section in the configuration of `backups`.
|
|
||||||
|
|
||||||
We need to allow the SSH key on `backups` to execute `zrepl stdinserver backups.example.com` on
|
|
||||||
`prod1`. For good measure, we will in fact enforce that only this command can be executed.
|
|
||||||
|
|
||||||
Open `/root/.ssh/authorized_keys` and add either of the the following lines, replacing BACKUPS_SSH_PUBKEY at the end
|
|
||||||
of the line with the contents of `/etc/zrepl/ssh/prod1.pub` (note the **.pub** !) from `backups`.
|
|
||||||
|
|
||||||
```
|
|
||||||
# for OpenSSH >= 7.2
|
|
||||||
command="zrepl stdinserver backups.example.com",restrict BACKUPS_SSH_PUBKEY
|
|
||||||
# for older OpenSSH versions
|
|
||||||
command="zrepl stdinserver backups.example.com",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc BACKUPS_SSH_PUBKEY
|
|
||||||
```
|
|
||||||
|
|
||||||
{{% alert theme="info" %}}The entries **must** be on a single line, including the replaced BACKUPS_SSH_PUBKEY{{% /alert %}}
|
|
||||||
|
|
||||||
Again, you can learn more about the [**source job** format here]({{< ref "configuration/jobs.md#source" >}}).
|
|
||||||
|
|
||||||
## Apply Configuration Changes
|
|
||||||
|
|
||||||
We need to restart the zrepl daemon on **both** `prod1` and `backups`.
|
|
||||||
|
|
||||||
This is [OS-specific](/install/#restarting).
|
|
||||||
|
|
||||||
## Watch it Work
|
|
||||||
|
|
||||||
A common setup is to `watch` the log output and `zfs list` of snapshots on both machines.
|
|
||||||
|
|
||||||
If you like tmux, here is a handy script that works on FreeBSD:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pkg install gnu-watch tmux
|
|
||||||
tmux new-window
|
|
||||||
tmux split-window "tail -f /var/log/zrepl.log"
|
|
||||||
tmux split-window "gnu-watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_pull_backup_'"
|
|
||||||
tmux select-layout tiled
|
|
||||||
```
|
|
||||||
|
|
||||||
The Linux equivalent might look like this
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# make sure tmux is installed & let's assume you use systemd + journald
|
|
||||||
tmux new-window
|
|
||||||
tmux split-window "journalctl -f -u zrepl.service"
|
|
||||||
tmux split-window "watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_pull_backup_'"
|
|
||||||
tmux select-layout tiled
|
|
||||||
```
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Congratulations, you have a working pull backup. Where to go next?
|
|
||||||
|
|
||||||
* Read more about [configuration format, options & job types](/configuration/)
|
|
||||||
* Learn about [implementation details](/impl/) of zrepl.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.. global list of substitutions
|
||||||
|
|
||||||
|
..
|
||||||
|
.. |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/>
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
.. zrepl documentation master file, created by
|
||||||
|
sphinx-quickstart on Wed Nov 8 22:28:10 2017.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|
|GitHub license| |Language: Go| |Twitter| |Donate via Patreon| |Donate via GitHub Sponsors| |Donate via Liberapay| |Donate via PayPal| |Matrix|
|
||||||
|
|
||||||
|
|
||||||
|
zrepl - ZFS replication
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
**zrepl** is a one-stop, integrated solution for ZFS replication.
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1em; background: #2e3436; color: white; font-size: 0.8em; min-height: 6em; max-width: 100%; overflow: auto;">
|
||||||
|
<pre>
|
||||||
|
Job: prod_to_backups
|
||||||
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
Getting started
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The :ref:`10 minute quick-start guides <quickstart-toc>` give you a first impression.
|
||||||
|
|
||||||
|
Main Features
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* **Filesystem replication**
|
||||||
|
|
||||||
|
* [x] Pull & Push mode
|
||||||
|
* [x] Multiple :ref:`transport modes <transport>`: TCP, TCP + TLS client auth, SSH
|
||||||
|
|
||||||
|
* Advanced replication features
|
||||||
|
|
||||||
|
* [x] Automatic retries for temporary network errors
|
||||||
|
* [x] Automatic resumable send & receive
|
||||||
|
* [x] Automatic ZFS holds during send & receive
|
||||||
|
* [x] Automatic bookmark \& hold management for guaranteed incremental send & recv
|
||||||
|
* [x] Encrypted raw send & receive to untrusted receivers (OpenZFS native encryption)
|
||||||
|
* [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**
|
||||||
|
|
||||||
|
* [x] Periodic :ref:`filesystem snapshots <job-snapshotting-spec>`
|
||||||
|
* [x] Support for :ref:`pre- and post-snapshot hooks <job-snapshotting-hooks>` with builtins for MySQL & Postgres
|
||||||
|
* [x] Flexible :ref:`pruning rule system <prune>`
|
||||||
|
|
||||||
|
* [x] Age-based fading (grandfathering scheme)
|
||||||
|
* [x] Bookmarks to avoid divergence between sender and receiver
|
||||||
|
|
||||||
|
* **Sophisticated Monitoring & Logging**
|
||||||
|
|
||||||
|
* [x] Live progress reporting via `zrepl status` :ref:`subcommand <usage>`
|
||||||
|
* [x] Comprehensive, structured :ref:`logging <logging>`
|
||||||
|
|
||||||
|
* ``human``, ``logfmt`` and ``json`` formatting
|
||||||
|
* stdout, syslog and TCP (+TLS client auth) outlets
|
||||||
|
|
||||||
|
* [x] Prometheus :ref:`monitoring <monitoring>` endpoint
|
||||||
|
|
||||||
|
* **Maintainable implementation in Go**
|
||||||
|
|
||||||
|
* [x] Cross platform
|
||||||
|
* [x] Dynamic feature checking
|
||||||
|
* [x] Type safe & testable code
|
||||||
|
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
zrepl as well as this documentation is still under active development.
|
||||||
|
There is no stability guarantee on the RPC protocol or configuration format,
|
||||||
|
but we do our best to document breaking changes in the :ref:`changelog`.
|
||||||
|
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We are happy about any help we can get!
|
||||||
|
|
||||||
|
* :ref:`Financial Support <supporters>`
|
||||||
|
* Explore the codebase
|
||||||
|
|
||||||
|
* These docs live in the ``docs/`` subdirectory
|
||||||
|
|
||||||
|
* Document any non-obvious / confusing / plain broken behavior you encounter when setting up zrepl for the first time
|
||||||
|
* Check the *Issues* and *Projects* sections for things to do.
|
||||||
|
The `good first issues <https://github.com/zrepl/zrepl/labels/good%20first%20issue>`_ and `docs <https://github.com/zrepl/zrepl/labels/docs>`_ are suitable starting points.
|
||||||
|
|
||||||
|
.. admonition:: Development Workflow
|
||||||
|
:class: note
|
||||||
|
|
||||||
|
The `GitHub repository <https://github.com/zrepl/zrepl>`_ is where all development happens.
|
||||||
|
Make sure to read the `Developer Documentation section <https://github.com/zrepl/zrepl>`_ and open new issues or pull requests there.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Table of Contents
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
quickstart
|
||||||
|
installation
|
||||||
|
configuration
|
||||||
|
usage
|
||||||
|
pr
|
||||||
|
changelog
|
||||||
|
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||||
|
Chat: Matrix <https://matrix.to/#/#zrepl:matrix.org>
|
||||||
|
supporters
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
.. _installation_toc:
|
||||||
|
|
||||||
|
************
|
||||||
|
Installation
|
||||||
|
************
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`quick-start guides <quickstart-toc>` if you want a first impression of zrepl.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user