Compare commits
826 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86d9b8b2ed | |||
| bd639cd6cb | |||
| 75657d2423 | |||
| 4a4167e0b6 | |||
| c1d9f231b1 | |||
| e486aad38c | |||
| 8539f59302 | |||
| 8066efb923 | |||
| 463cf8783c | |||
| 5a8dc7e313 | |||
| 7f43efc857 | |||
| 08b8f03661 | |||
| fe5e340d6e | |||
| 979c7e1f9d | |||
| a1cdfb57a7 | |||
| 487fdffd91 | |||
| b1a1038dec | |||
| 2b873e4cb8 | |||
| a12edcd360 | |||
| 383b1925ef | |||
| 0603a8c7e6 | |||
| 5620c199a9 | |||
| 08d99bf714 | |||
| ebe76358ce | |||
| 47b69f0530 | |||
| f46bae2372 | |||
| 830b3ea114 | |||
| 926e249272 | |||
| bcb60def00 | |||
| 03654ef5af | |||
| 2d59c68004 | |||
| 7a51040ac0 | |||
| c2b177db49 | |||
| 7f0aeed88a | |||
| 8391afdac5 | |||
| d91b205a89 | |||
| 3311bfbd9f | |||
| 351ce246c5 | |||
| 9572ac822f | |||
| a59d33ec03 | |||
| a9e4013d86 | |||
| 19c1945110 | |||
| fb22a015e5 | |||
| e6312a2318 | |||
| 776654970e | |||
| 22f730d5b5 | |||
| dc614483b5 | |||
| 891e29a362 | |||
| 2667553cf2 | |||
| 8467803fdd | |||
| 084cf958a0 | |||
| b4cba98564 | |||
| 39d5fb8d16 | |||
| 0c74cfd5e9 | |||
| 841f523f73 | |||
| 6d38d04a1e | |||
| 504089427d | |||
| 60f29aab70 | |||
| ee94e30004 | |||
| 3469d98a43 | |||
| 5fd775d855 | |||
| 725d5292b2 | |||
| 9161a2501c | |||
| 9b3f856eb0 | |||
| 9621184bd8 | |||
| 1f2273d2ab | |||
| 0514fa0241 | |||
| 2f263476d3 | |||
| e65aa8fdab | |||
| 70b17657a1 | |||
| b8389352ec | |||
| 7586d4ff29 | |||
| bc656cdef4 | |||
| 278f6de6f5 | |||
| 2de9fed1fa | |||
| 3bcd2be520 | |||
| 7eac09e547 | |||
| 5fb1ee54b9 | |||
| ecfd60803f | |||
| 81b17b389f | |||
| 57675c08eb | |||
| 64f869121b | |||
| c41e6f8240 | |||
| 7483d0c012 | |||
| f1b26e5933 | |||
| f8ddcd7b7c | |||
| 962bd06a32 | |||
| 3d6d4d5503 | |||
| 4b22705ff7 | |||
| 983ad1b1ae | |||
| 849c305d7d | |||
| ff0d0d2e8b | |||
| c98b8c6f05 | |||
| 4136f819a5 | |||
| 78fe5440a8 | |||
| 012325e996 | |||
| 951fa63296 | |||
| 6f86abd997 | |||
| c1917d51a0 | |||
| 75017a99df | |||
| 980fdc8203 | |||
| 7df21873c1 | |||
| 9bbaeb67d3 | |||
| a6b557882d | |||
| 90c02e58bf | |||
| 8829902e0b | |||
| e7c5fe9213 | |||
| 5a1ce55086 | |||
| cca320e2f4 | |||
| e4e3c57f20 | |||
| 5274639ca3 | |||
| 3e5ed906bc | |||
| 9a519432b0 | |||
| 6a3424faf4 | |||
| 19a8d28a24 | |||
| a52d9b052f | |||
| db56385513 | |||
| 7ab96e6a47 | |||
| c37bca287e | |||
| d17f6da77a | |||
| 460f809403 | |||
| 0e6a705d3f | |||
| d54eff344f | |||
| 79a54578b8 | |||
| 1d8f20ff25 | |||
| d3b8e2e414 | |||
| 85daf26174 | |||
| 53933957a4 | |||
| 8d941ebef4 | |||
| 800bd90778 | |||
| df38fdb99e | |||
| 23947bd967 | |||
| 32ea52c8f4 | |||
| d755267dd9 | |||
| 53659b4364 | |||
| 0035dd1e6f | |||
| c8680b06ac | |||
| 3f82d0fc57 | |||
| 5d95a33c5a | |||
| aeb0a4fbe7 | |||
| 9e139fd422 | |||
| 9733a55942 | |||
| befdf5ad6e | |||
| 663116c778 | |||
| 187b0440c8 | |||
| bdb9fa064d | |||
| d3ba9db0c6 | |||
| 3dffc05c9d | |||
| 6616ae7417 | |||
| dc40295dde | |||
| 1d8361cc5f | |||
| 35243fdba6 | |||
| 43e7c1f3e4 | |||
| dcd2ebc49c | |||
| 555350eab7 | |||
| e117acac04 | |||
| 16313b9e40 | |||
| 033a1cf6e5 | |||
| 8befec9769 | |||
| d22add5bfd | |||
| 69fb93a664 | |||
| f4b59dc702 | |||
| 17aa3d7e48 | |||
| 8bb9dae45c | |||
| c244645020 | |||
| 64029d2147 | |||
| 8081f12315 | |||
| 4ec2d5192a | |||
| 0e78afea6a | |||
| f0d1cf9861 | |||
| e17b023503 | |||
| a3ba06bcb0 | |||
| 01bcfd8638 | |||
| c0944f9fa2 | |||
| dedbffa107 | |||
| 67d5a4bff8 | |||
| 6d64a5e52d | |||
| 07e9eb4d8f | |||
| 1f53ff63a9 | |||
| 0eaed67334 | |||
| fd5e4180fa | |||
| ab87fe6f96 | |||
| 95efe10ef6 | |||
| e47c709f39 | |||
| 24d346962a | |||
| 3e2cae42e6 | |||
| 6e410bfc25 | |||
| 8ebf4e0ec0 | |||
| 8e8f77e546 | |||
| c128b8a1ca | |||
| 53d2928de2 | |||
| 4996f98cd1 | |||
| 5b254b1b28 | |||
| 4348e6045e | |||
| 28e9d69571 | |||
| 32011c5b1f | |||
| 5c8e28ddb5 | |||
| d62e609863 | |||
| ff51b41c38 | |||
| 76cf14a9ef | |||
| 301889ab8b | |||
| 1a163ce9f0 | |||
| 15a78737cb | |||
| d90e0a18e8 | |||
| a55ec37d21 | |||
| ee23f3ef6e | |||
| de67571f5e | |||
| a04163b72f | |||
| fc7f7e2c23 | |||
| e18306058a | |||
| e982f1e076 | |||
| a2639bc370 | |||
| fd1d0ac976 | |||
| e3fe0eeb79 | |||
| 782b3fbe0b | |||
| 3d8a77f9e4 | |||
| 535ec252b5 | |||
| d1bd92e6cc | |||
| 4f990f8d6f | |||
| cd9a7e172e | |||
| 206e62e698 | |||
| 57aa3b8433 | |||
| 70091eca8c | |||
| fdd35e0a2c | |||
| ccc54b53a5 | |||
| 1222eb813d | |||
| 054087fa1c | |||
| b64470b160 | |||
| 0dabb39ca4 | |||
| d302a22d3e | |||
| 1f3740dd59 | |||
| 919f5f2c08 | |||
| a6f1695e4e | |||
| 8f45a39967 | |||
| 0eb37a909e | |||
| 2211571689 | |||
| 6cb4275e31 | |||
| 5373954567 | |||
| a5ec5eca7a | |||
| b459821a8d | |||
| 4415bc32f5 | |||
| 5cb5396817 | |||
| 85673abb29 | |||
| 29be9d9896 | |||
| c4da3ee013 | |||
| 9288836b3a | |||
| 66624141f8 | |||
| 9c639b4977 | |||
| 98e05fc151 | |||
| 402dca9b31 | |||
| 89d6b6d93c | |||
| 33a6e2a979 | |||
| 14715fdab7 | |||
| 13d91fa512 | |||
| 0e8afa29e5 | |||
| d300866bc8 | |||
| aede8d21c1 | |||
| 1fe2e0710f | |||
| fe884f446a | |||
| 637ab05590 | |||
| 843712d7bf | |||
| 4aa8a18b4f | |||
| 83cc936c82 | |||
| e1e1920ffb | |||
| 34d55f0849 | |||
| 594b7d3c86 | |||
| 49b05fe8b8 | |||
| 789897acf6 | |||
| 1233da8dd6 | |||
| fce2425c56 | |||
| 219bbf9983 | |||
| d3b90cfe89 | |||
| b5d48db4dd | |||
| b81b6472fd | |||
| d380701703 | |||
| b2aadeb98c | |||
| b8675adf99 | |||
| 0463637d9f | |||
| 9b7171864a | |||
| 964b248de3 | |||
| c756729cac | |||
| 49498c0ca9 | |||
| be26672b85 | |||
| 0f4b01f996 | |||
| bb0f123e02 | |||
| a4fd08a8cd | |||
| 4a5711a570 | |||
| 0cf83d0744 | |||
| 5e66318c38 | |||
| 53d22e8c67 | |||
| 3256329064 | |||
| d2f8df88bf | |||
| 5259e13eef | |||
| ab01562c85 | |||
| 4d440bcb5b | |||
| 0fb1899322 | |||
| cb463350b4 | |||
| 5dd6e56ca9 | |||
| e8a5379ccd | |||
| 226b152fa0 | |||
| 4e9c6bf67b | |||
| c0ccd78517 | |||
| 5b6d31742e | |||
| 04a271a1e5 | |||
| 0f74cc8c7e | |||
| a0dc65f568 | |||
| 5fa4969cfe | |||
| 11754a362f | |||
| fcb6c9bd8e | |||
| 534b7142a8 | |||
| fac893f34a | |||
| 8bdf675b47 | |||
| d451a70db8 | |||
| 6a90f605cc | |||
| d03a4fd554 | |||
| d8963141fc | |||
| 0667304dd7 | |||
| ff9acf9638 | |||
| 233760d7a8 | |||
| fc115345a0 | |||
| 7403f31ac5 | |||
| 66b0492343 | |||
| 73a5175a6d | |||
| 904a4d0e40 | |||
| c227c38875 | |||
| 84207ee82b | |||
| eae3b92eaf | |||
| f167643980 | |||
| e12e19d5ee | |||
| 8b6acf7791 | |||
| e897ef6898 | |||
| 047403c2a5 | |||
| b13d6980de | |||
| 3c996c63f8 | |||
| 44ec984552 | |||
| 8a54f64b18 | |||
| 367c134ecc | |||
| 7cb2bdb6a1 | |||
| edf7ab4236 | |||
| 71125e32e0 | |||
| 43bd31f5d5 | |||
| 2afc41a9f4 | |||
| 272ee4f5af | |||
| afef2d18d6 | |||
| d558d682e3 | |||
| 4965db78ea | |||
| 28a9b40fdd | |||
| b6fa63ad42 | |||
| 3ce2807d9f | |||
| 4663f7632b | |||
| 8fc701b40e | |||
| 6f4f2c4a63 | |||
| a0c588359d | |||
| be4c9ce6f4 | |||
| d489971990 | |||
| 1fe9ffea72 | |||
| 0d5d8500df | |||
| 407318445d | |||
| 801a8bcf5f | |||
| 9f8a64a653 | |||
| daf582d6d8 | |||
| ccb6dcd14f | |||
| 89dda7fb15 | |||
| f4c8e8e1ad | |||
| e7cf2b04e2 | |||
| 886a1c4655 | |||
| f2b984e238 | |||
| 096f9a845c | |||
| 6c84dfa678 | |||
| 7aeb3be86d | |||
| 3c053cf51b | |||
| e6685b6fcf | |||
| 5993a3413e | |||
| 50f4e7b7d5 | |||
| 5680b805b1 | |||
| 43aadda73f | |||
| e8f878884d | |||
| 2974d8e1ae | |||
| 3894e7dfe7 | |||
| 1588a11868 | |||
| fea2d96077 | |||
| b67e77ed6a | |||
| 139a46dce0 | |||
| 6b918e81bb | |||
| 702f83ed44 | |||
| a7d02ca428 | |||
| 31113dc9a9 | |||
| 3dfd09cc81 | |||
| 01735e4c7a | |||
| 2446f36375 | |||
| 0507a0e740 | |||
| 89e25b4ca3 | |||
| 2d77fa8d10 | |||
| c55b465c2f | |||
| bc13ff7711 | |||
| 97e5a2b921 | |||
| e490b67377 | |||
| 782b30d064 | |||
| 6b0e92447a | |||
| afe04ae6c8 | |||
| 5bae9ea885 | |||
| 4035ec7fac | |||
| addbae4b1d | |||
| 8669124c73 | |||
| 05eecb72e2 | |||
| 46e180ee96 | |||
| da2f3af643 | |||
| f48ea22a42 | |||
| 6c300f24f0 | |||
| 12621fa36b | |||
| 4248db53ac | |||
| ac919278aa | |||
| 6c0193520d | |||
| 7c1da59bb7 | |||
| ca15978a6c | |||
| 95c71b122d | |||
| 1abdfc4bcd | |||
| 5eb684e7ea | |||
| 7a60ab1599 | |||
| c8a916d5ac | |||
| 1ea39b8117 | |||
| bd118be239 | |||
| 21f871b2f8 | |||
| dacb7cfec3 | |||
| 3b1ef1eb41 | |||
| 50fed682eb | |||
| 5ead4ba105 | |||
| 8ce3217b16 | |||
| 779e3ff8d4 | |||
| 388edf0ea6 | |||
| 49a097246d | |||
| 5b66659ce2 | |||
| af274d0076 | |||
| c67b3b2393 | |||
| d2da2eb387 | |||
| dc9e38d4ba | |||
| 1fb71a0f25 | |||
| 2712d212b6 | |||
| 46b29ce4fb | |||
| 440f270b25 | |||
| 1797c784af | |||
| fef8adad20 | |||
| 283f2da099 | |||
| e18bb37670 | |||
| 32e1250d06 | |||
| f19d604213 | |||
| bc1d3bdec3 | |||
| c64aa70b49 | |||
| 936630322f | |||
|
|
f7cac0eedf | ||
|
|
023d45f2bb | ||
|
|
21af9c8b62 | ||
|
|
d4ccc3dce0 | ||
|
|
f3f624be1f | ||
|
|
78d2499b46 | ||
|
|
7582e8d9cc | ||
|
|
edebd1588f | ||
|
|
6abfd868db | ||
|
|
1e2e63405a | ||
|
|
d8f0d49a64 | ||
|
|
5414c5e0cb | ||
|
|
e65b18430e | ||
|
|
4e25dc000c | ||
|
|
0ff09f0cbd | ||
|
|
8c416dd047 | ||
|
|
eb3069359d | ||
|
|
8c8e4b8433 | ||
|
|
9afe4eb619 | ||
|
|
a545a74242 | ||
|
|
606a60b1c0 | ||
|
|
695f204ee4 | ||
|
|
3ddaead092 | ||
|
|
83149c197e | ||
|
|
6fef63655c | ||
|
|
63b68b8d3e | ||
|
|
3b257aadab | ||
|
|
88e80f4107 | ||
|
|
8dfadbf9c3 | ||
|
|
0cda286db1 | ||
|
|
d8de90fa5d | ||
|
|
e05d987036 | ||
|
|
c8b7e34732 | ||
|
|
bb6eeba6fb | ||
|
|
6c2d4ca69f | ||
|
|
706c4028f8 | ||
|
|
3cd41adeaf | ||
|
|
8a13421577 | ||
|
|
9ff8dce802 | ||
|
|
49081248ae | ||
|
|
116697af9f | ||
|
|
dc2dd9aa7a | ||
|
|
495f5537be | ||
|
|
0e28b18298 | ||
|
|
8d4abe1ec6 | ||
|
|
ee3625311b | ||
|
|
1343a85e0b | ||
|
|
fbe62c8127 | ||
|
|
1387b5f1ae | ||
|
|
e3a1438247 | ||
|
|
b8cbf4648a | ||
|
|
15f0317fbe | ||
|
|
e44fc3dc04 | ||
|
|
f72ee7c85b | ||
|
|
5eac0e8c85 | ||
|
|
fbee9a32df | ||
|
|
1bde29bb17 | ||
|
|
d10f2b1eb3 | ||
|
|
1397e9c9a3 | ||
|
|
1b84e84841 | ||
|
|
c37be21034 | ||
|
|
57079a0cbe | ||
|
|
cd5b854b00 | ||
|
|
04d55caef6 | ||
|
|
8285a12f00 | ||
|
|
9946edc6f3 | ||
|
|
a3c7acc399 | ||
|
|
4530a34175 | ||
|
|
b8100f472b | ||
|
|
9b104cac25 | ||
|
|
7243ad9e9b | ||
|
|
12268daad6 | ||
|
|
d4738d762b | ||
|
|
72f560809b | ||
|
|
f55d46281c | ||
|
|
59537a536f | ||
|
|
ab55ad1020 | ||
|
|
bb3b4b9bca | ||
|
|
f31f86aa21 | ||
|
|
30b3d570fb | ||
|
|
f6baeb328b | ||
|
|
31a4da75aa | ||
|
|
a51d62f5d2 | ||
|
|
bada714b10 | ||
|
|
6f9218c5a1 | ||
|
|
e6940b151c | ||
|
|
5647654135 | ||
|
|
5fd9452a6c | ||
|
|
2a84822cfe | ||
|
|
e62a7781b4 | ||
|
|
9f4e304aec | ||
|
|
8b2d8d974e | ||
|
|
3cddebee11 | ||
|
|
c8b9a46aad | ||
|
|
35d8d4828b | ||
|
|
b9ff7e5953 | ||
|
|
8063833950 | ||
|
|
34b42832ac | ||
|
|
52404fe7ce | ||
|
|
76a568e8b6 | ||
|
|
6706d04298 | ||
|
|
cf09493486 | ||
|
|
8cabb029b3 | ||
|
|
e81f28cf04 | ||
|
|
df4ffd2d77 | ||
|
|
b235ede36d | ||
|
|
e5a16b5506 | ||
|
|
5e5118215a | ||
|
|
452c983f63 | ||
|
|
c9e565bbde | ||
|
|
5ed8f08231 | ||
|
|
7588741b30 | ||
|
|
f416852225 | ||
|
|
f5ab497bff | ||
|
|
f2439dcf66 | ||
|
|
b27f07a867 | ||
|
|
824b10546d | ||
|
|
33d973927c | ||
|
|
b528e9b94b | ||
|
|
8a6c166f16 | ||
|
|
49346ba20b | ||
|
|
31bf80f771 | ||
|
|
1340aaf52e | ||
|
|
739c38d1b4 | ||
|
|
df9c038d87 | ||
|
|
5b4ad017e1 | ||
|
|
4ef6826837 | ||
|
|
4b9980a8c3 | ||
|
|
8532f914c3 | ||
|
|
33062c3ec6 | ||
|
|
be6903d3a6 | ||
|
|
8a9434a384 | ||
|
|
24bf39dda5 | ||
|
|
0dbda1c200 | ||
|
|
dab554473e | ||
|
|
8b3f9d7736 | ||
|
|
b2b6f08b86 | ||
|
|
a4e819317b | ||
|
|
085eb2b2d3 | ||
|
|
e9771f1b9f | ||
|
|
63863f69c0 | ||
|
|
1a552844da | ||
|
|
9f95e78277 | ||
|
|
041098ecde | ||
|
|
09ca6bddf6 | ||
|
|
b205bd7555 | ||
|
|
d82a066fb3 | ||
|
|
e85afeb656 | ||
|
|
5fd969ebb2 | ||
|
|
ca835a69df | ||
|
|
63076ec921 | ||
|
|
f075d4f3cd | ||
|
|
349b4e9d3f | ||
|
|
41067d1aa4 | ||
|
|
1d2bfa9df9 | ||
|
|
60bc44a946 | ||
|
|
8092b5faff | ||
|
|
faf78e3766 | ||
|
|
f3114bfcef | ||
|
|
f82fa22be8 | ||
|
|
e4084956a2 | ||
|
|
60d30e9df0 | ||
|
|
2549a298a4 | ||
|
|
b52030b830 | ||
|
|
5a9716b0ff | ||
|
|
ad145c3ace | ||
|
|
1b5b354cc9 | ||
|
|
1e4713cb3a | ||
|
|
a759bbf58c | ||
|
|
f19a8eb6a8 | ||
|
|
b1e5992f05 | ||
|
|
5ac4d3cc33 | ||
|
|
806b5e1880 | ||
|
|
69ce72aa7b | ||
|
|
f9790912a6 | ||
|
|
f1afe13fad | ||
|
|
9ea206318c | ||
|
|
2176403bcc | ||
|
|
4906b13a38 | ||
|
|
386d7bab9b | ||
|
|
b08fe2c749 | ||
|
|
a581dbfee9 | ||
|
|
dbca66326a | ||
|
|
fab4d0a476 | ||
|
|
cf6e716301 | ||
|
|
df607f0656 | ||
|
|
cfee1d74b0 | ||
|
|
4e56ba6da0 | ||
|
|
e406db30f9 | ||
|
|
6727fcf404 | ||
|
|
78b324903d | ||
|
|
f4a4f22d69 | ||
|
|
d12b446f34 | ||
|
|
ae929e4773 | ||
|
|
fe70776dfc | ||
|
|
98ba428bb7 | ||
|
|
b579dc4928 | ||
|
|
0d168cfb5f | ||
|
|
6d8450b270 | ||
|
|
254af0c72b | ||
|
|
c8565876db | ||
|
|
323ad5bc2c | ||
|
|
d5e2290f12 | ||
|
|
4a23393691 | ||
|
|
63741f271b | ||
|
|
f7de8e4d2e | ||
|
|
fbc82ef6b1 | ||
|
|
cff4371fef | ||
|
|
b8cfd06e12 | ||
|
|
d0c3030e7a | ||
|
|
89621c7cbb | ||
|
|
95e00d4d71 | ||
|
|
ec1b52aa2f | ||
|
|
9de1444668 | ||
|
|
36ca196f3a | ||
|
|
c9b76596da | ||
|
|
44c67ba003 | ||
|
|
7663d3fcce | ||
|
|
3c09b3a984 | ||
|
|
41430ebc2f | ||
|
|
0e97f9e596 | ||
|
|
1b2926a24d | ||
|
|
b02adbb7cb | ||
|
|
cdd379ba82 | ||
|
|
2eab0d2ca9 | ||
|
|
5c9dea327c | ||
|
|
87861aae98 | ||
|
|
604e01f16e | ||
|
|
384f7dbfa8 | ||
|
|
9295e1789c | ||
| 7309a20c47 | |||
|
|
8cfa3575f8 | ||
|
|
59a598448d | ||
|
|
8b84fe0f0e | ||
|
|
c66374c9db | ||
|
|
d23384d4d1 | ||
|
|
5849ecc9e4 | ||
|
|
c92704390d | ||
|
|
2300874637 | ||
|
|
6cceae2458 | ||
|
|
7ad5f62022 | ||
|
|
33f4660503 | ||
|
|
fc65db3de5 | ||
|
|
b70c8e8217 | ||
|
|
357c591b69 | ||
|
|
38e542c184 | ||
|
|
d6e4cdb45a | ||
|
|
6c2473e2da | ||
|
|
715e163514 | ||
|
|
2d4afe6b53 | ||
|
|
2ec0e5068a | ||
|
|
f4b3841793 | ||
|
|
5d22aaa1eb | ||
|
|
706af2e127 | ||
|
|
72561bdb52 | ||
|
|
b5489cd22f | ||
|
|
fa300ca547 | ||
|
|
f0ecf64938 | ||
|
|
5d37e2665e | ||
|
|
2becf72559 | ||
|
|
806feb9fdd | ||
|
|
44fe39b025 | ||
|
|
11150b4f69 | ||
|
|
8a65459d69 | ||
|
|
776e876876 | ||
|
|
487899ae3e | ||
|
|
d2916ef4f9 | ||
|
|
89d2f3ce7a | ||
|
|
9ef958d935 | ||
|
|
99c12b6106 | ||
|
|
93d9f1af39 | ||
| b9896960ff | |||
|
|
fdcfa8a82b | ||
|
|
34e9366c61 | ||
|
|
b022eabeb0 | ||
|
|
df9d5fb62f | ||
|
|
4abfd5fcbc | ||
|
|
03378ed638 | ||
|
|
cf4bf15db0 | ||
|
|
753954ebaf | ||
|
|
ec4be43b5e | ||
|
|
a1a0beb8cb | ||
|
|
bba63bac3d | ||
|
|
a7a79b48d7 | ||
|
|
c62fae4fc4 | ||
|
|
3d070abca7 | ||
|
|
d8f887a4eb | ||
|
|
1536b4aa2c | ||
|
|
86884f4cb7 | ||
|
|
946b3d0e9f | ||
|
|
79820a0c10 | ||
|
|
a564d2ed31 | ||
|
|
5a49533460 | ||
|
|
7f443cfdd4 | ||
|
|
21b530cd8d | ||
|
|
5390f3ac3c | ||
|
|
1f0e660a4d | ||
|
|
596db36e10 | ||
|
|
594ae6df66 | ||
|
|
1066ca50ab | ||
|
|
07e6a2d07e | ||
|
|
16a7bb915f | ||
|
|
5b7c67815b | ||
|
|
f6ac34dfd0 | ||
|
|
d2a802524d | ||
|
|
1825faabf6 | ||
|
|
9e4163d291 | ||
|
|
3652a521de | ||
|
|
bc898c8009 | ||
|
|
43cf6cabea | ||
|
|
bb862ed6ec | ||
|
|
6a6ffc8720 | ||
|
|
9d6b6777bf | ||
|
|
6b1674c93a | ||
|
|
fda17989d3 | ||
|
|
d7e5483d74 | ||
|
|
3243bb1890 | ||
|
|
da06f7cf06 | ||
|
|
ef461fec1c | ||
|
|
e5b1bc0921 | ||
|
|
bbef19a73d | ||
|
|
15155cd7a9 | ||
|
|
b94b95c5f9 | ||
|
|
5931ce16d9 | ||
|
|
d2798a91c1 | ||
|
|
e5ddb318bd | ||
|
|
1a1121e010 | ||
|
|
2ecef5446a | ||
|
|
effded149a | ||
| 421d3c10ba | |||
|
|
077c65d8b9 | ||
|
|
40cedbf20c | ||
|
|
75d0043578 | ||
|
|
4e6071183f | ||
|
|
d1bbfecbc9 | ||
|
|
ea494b10e3 | ||
|
|
e184259c37 | ||
|
|
d2d233deda | ||
|
|
1398dc247b | ||
|
|
effe20d323 | ||
|
|
bad7977ffc | ||
|
|
b2d93de057 | ||
|
|
c6c9dc37f7 | ||
|
|
b840462782 | ||
|
|
b4f1145b6a | ||
|
|
7b3c1ece8d | ||
|
|
162982ac00 | ||
|
|
602ef0cc96 | ||
|
|
fe24a79b6f | ||
|
|
d10c5a2743 | ||
|
|
973ce6673b | ||
|
|
213aeacbdc | ||
|
|
be6a793114 | ||
|
|
1010537699 | ||
|
|
c88102b0ca | ||
|
|
a2a588b171 | ||
|
|
1c330b626e | ||
|
|
e26b259009 | ||
|
|
968f5e8d7d | ||
|
|
0c69e6c478 | ||
|
|
0b8b769b12 | ||
|
|
f82eceae2e | ||
|
|
9b3693dc04 | ||
|
|
ab0e8a8ff5 | ||
|
|
747d10f509 | ||
|
|
21e01feffc | ||
|
|
dbaeeeaad7 | ||
|
|
0a3837db64 | ||
|
|
0e5c6f5401 | ||
|
|
84e2d2b0ee | ||
|
|
69e18014e3 | ||
|
|
ada09df208 | ||
|
|
3e9da36e2e | ||
|
|
4b9bbeffdd | ||
|
|
2ca19525c9 | ||
|
|
a8c6cc1337 | ||
|
|
a3a5d68078 |
519 changed files with 22868 additions and 2128 deletions
22
.editorconfig
Normal file
22
.editorconfig
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.toml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.yaml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
9
.envrc
9
.envrc
|
|
@ -1,8 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
python3 -m venv .venv
|
PATH_add bin
|
||||||
source ./.venv/bin/activate
|
|
||||||
|
|
||||||
export BW_GIT_DEPLOY_CACHE="$(realpath ~)/.cache/bw/git_deploy"
|
source_env ~/.local/share/direnv/pyenv
|
||||||
mkdir -p "$BW_GIT_DEPLOY_CACHE"
|
source_env ~/.local/share/direnv/venv
|
||||||
unset PS1
|
source_env ~/.local/share/direnv/bundlewrap
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,2 +1,5 @@
|
||||||
.secrets.cfg*
|
.secrets.cfg*
|
||||||
.venv
|
.venv
|
||||||
|
.cache
|
||||||
|
*.pyc
|
||||||
|
.bw_debug_history
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
3.9.0
|
|
||||||
48
README.md
Normal file
48
README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
- dont spamfilter forwarded mails
|
||||||
|
- gollum wiki
|
||||||
|
- blog?
|
||||||
|
- fix dkim not working sometimes
|
||||||
|
- LDAP
|
||||||
|
- oauth2/OpenID
|
||||||
|
- icinga
|
||||||
|
|
||||||
|
Raspberry pi as soundcard
|
||||||
|
- gadget mode
|
||||||
|
- OTG g_audio
|
||||||
|
- https://audiosciencereview.com/forum/index.php?threads/raspberry-pi-as-usb-to-i2s-adapter.8567/post-215824
|
||||||
|
|
||||||
|
# install bw fork
|
||||||
|
|
||||||
|
pip3 install --editable git+file:///Users/mwiegand/Projekte/bundlewrap-fork@main#egg=bundlewrap
|
||||||
|
|
||||||
|
# monitor timers
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Timer=backup
|
||||||
|
|
||||||
|
Triggers=$(systemctl show ${Timer}.timer --property=Triggers --value)
|
||||||
|
echo $Triggers
|
||||||
|
if systemctl is-failed "$Triggers"
|
||||||
|
then
|
||||||
|
InvocationID=$(systemctl show "$Triggers" --property=InvocationID --value)
|
||||||
|
echo $InvocationID
|
||||||
|
ExitCode=$(systemctl show "$Triggers" -p ExecStartEx --value | sed 's/^{//' | sed 's/}$//' | tr ';' '\n' | xargs -n 1 | grep '^status=' | cut -d '=' -f 2)
|
||||||
|
echo $ExitCode
|
||||||
|
journalctl INVOCATION_ID="$InvocationID" --output cat
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
telegraf: execd for daemons
|
||||||
|
|
||||||
|
TEST
|
||||||
|
|
||||||
|
# git signing
|
||||||
|
|
||||||
|
git config --global gpg.format ssh
|
||||||
|
git config --global commit.gpgsign true
|
||||||
|
|
||||||
|
git config user.name CroneKorkN
|
||||||
|
git config user.email i@ckn.li
|
||||||
|
git config user.signingkey "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILMVroYmswD4tLk6iH+2tvQiyaMe42yfONDsPDIdFv6I"
|
||||||
148
bin/mikrotik-firmware-updater
Executable file
148
bin/mikrotik-firmware-updater
Executable file
|
|
@ -0,0 +1,148 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from bundlewrap.exceptions import RemoteException
|
||||||
|
from bundlewrap.utils.cmdline import get_target_nodes
|
||||||
|
from bundlewrap.utils.ui import io
|
||||||
|
from bundlewrap.repo import Repository
|
||||||
|
from os.path import realpath, dirname
|
||||||
|
|
||||||
|
|
||||||
|
# parse args
|
||||||
|
parser = ArgumentParser()
|
||||||
|
parser.add_argument("targets", nargs="*", default=['bundle:routeros'], help="bw nodes selector")
|
||||||
|
parser.add_argument("--yes", action="store_true", default=False, help="skip confirmation prompts")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def wait_up(node):
|
||||||
|
sleep(5)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
node.run_routeros('/system/resource/print')
|
||||||
|
except RemoteException:
|
||||||
|
sleep(2)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
io.debug(f"{node.name}: is up")
|
||||||
|
sleep(10)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_switch_os(node):
|
||||||
|
# get versions for comparison
|
||||||
|
with io.job(f"{node.name}: checking OS version"):
|
||||||
|
response = node.run_routeros('/system/package/update/check-for-updates').raw[-1]
|
||||||
|
installed_os = bw.libs.version.Version(response['installed-version'])
|
||||||
|
latest_os = bw.libs.version.Version(response['latest-version'])
|
||||||
|
io.debug(f"{node.name}: installed: {installed_os} >= latest: {latest_os}")
|
||||||
|
|
||||||
|
# compare versions
|
||||||
|
if installed_os >= latest_os:
|
||||||
|
# os is up to date
|
||||||
|
io.stdout(f"{node.name}: os up to date ({installed_os})")
|
||||||
|
else:
|
||||||
|
# confirm os upgrade
|
||||||
|
if not args.yes and not io.ask(
|
||||||
|
f"{node.name}: upgrade os from {installed_os} to {latest_os}?", default=True
|
||||||
|
):
|
||||||
|
io.stdout(f"{node.name}: skipped by user")
|
||||||
|
return
|
||||||
|
|
||||||
|
# download os
|
||||||
|
with io.job(f"{node.name}: downloading OS"):
|
||||||
|
response = node.run_routeros('/system/package/update/download').raw[-1]
|
||||||
|
io.debug(f"{node.name}: OS upgrade download response: {response['status']}")
|
||||||
|
|
||||||
|
# install and wait for reboot
|
||||||
|
with io.job(f"{node.name}: upgrading OS"):
|
||||||
|
try:
|
||||||
|
response = node.run_routeros('/system/package/update/install').raw[-1]
|
||||||
|
except RemoteException:
|
||||||
|
pass
|
||||||
|
wait_up(node)
|
||||||
|
|
||||||
|
# verify new os version
|
||||||
|
with io.job(f"{node.name}: checking new OS version"):
|
||||||
|
new_os = bw.libs.version.Version(node.run_routeros('/system/package/update/check-for-updates').raw[-1]['installed-version'])
|
||||||
|
if new_os == latest_os:
|
||||||
|
io.stdout(f"{node.name}: OS successfully upgraded from {installed_os} to {new_os}")
|
||||||
|
else:
|
||||||
|
raise Exception(f"{node.name}: OS upgrade failed, expected {latest_os}, got {new_os}")
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_switch_firmware(node):
|
||||||
|
# get versions for comparison
|
||||||
|
with io.job(f"{node.name}: checking Firmware version"):
|
||||||
|
response = node.run_routeros('/system/routerboard/print').raw[-1]
|
||||||
|
current_firmware = bw.libs.version.Version(response['current-firmware'])
|
||||||
|
upgrade_firmware = bw.libs.version.Version(response['upgrade-firmware'])
|
||||||
|
io.debug(f"{node.name}: firmware installed: {current_firmware}, upgrade: {upgrade_firmware}")
|
||||||
|
|
||||||
|
# compare versions
|
||||||
|
if current_firmware >= upgrade_firmware:
|
||||||
|
# firmware is up to date
|
||||||
|
io.stdout(f"{node.name}: firmware is up to date ({current_firmware})")
|
||||||
|
else:
|
||||||
|
# confirm firmware upgrade
|
||||||
|
if not args.yes and not io.ask(
|
||||||
|
f"{node.name}: upgrade firmware from {current_firmware} to {upgrade_firmware}?", default=True
|
||||||
|
):
|
||||||
|
io.stdout(f"{node.name}: skipped by user")
|
||||||
|
return
|
||||||
|
|
||||||
|
# upgrade firmware
|
||||||
|
with io.job(f"{node.name}: upgrading Firmware"):
|
||||||
|
node.run_routeros('/system/routerboard/upgrade')
|
||||||
|
|
||||||
|
# reboot and wait
|
||||||
|
with io.job(f"{node.name}: rebooting"):
|
||||||
|
try:
|
||||||
|
node.run_routeros('/system/reboot')
|
||||||
|
except RemoteException:
|
||||||
|
pass
|
||||||
|
wait_up(node)
|
||||||
|
|
||||||
|
# verify firmware version
|
||||||
|
new_firmware = bw.libs.version.Version(node.run_routeros('/system/routerboard/print').raw[-1]['current-firmware'])
|
||||||
|
if new_firmware == upgrade_firmware:
|
||||||
|
io.stdout(f"{node.name}: firmware successfully upgraded from {current_firmware} to {new_firmware}")
|
||||||
|
else:
|
||||||
|
raise Exception(f"firmware upgrade failed, expected {upgrade_firmware}, got {new_firmware}")
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_switch(node):
|
||||||
|
with io.job(f"{node.name}: checking"):
|
||||||
|
# check if routeros
|
||||||
|
if node.os != 'routeros':
|
||||||
|
io.progress_advance(2)
|
||||||
|
io.stdout(f"{node.name}: skipped, unsupported os {node.os}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# check switch reachability
|
||||||
|
try:
|
||||||
|
node.run_routeros('/system/resource/print')
|
||||||
|
except RemoteException as error:
|
||||||
|
io.progress_advance(2)
|
||||||
|
io.stdout(f"{node.name}: skipped, error {error}")
|
||||||
|
return
|
||||||
|
|
||||||
|
upgrade_switch_os(node)
|
||||||
|
io.progress_advance(1)
|
||||||
|
|
||||||
|
upgrade_switch_firmware(node)
|
||||||
|
io.progress_advance(1)
|
||||||
|
|
||||||
|
|
||||||
|
with io:
|
||||||
|
bw = Repository(dirname(dirname(realpath(__file__))))
|
||||||
|
|
||||||
|
nodes = get_target_nodes(bw, args.targets)
|
||||||
|
|
||||||
|
io.progress_set_total(len(nodes) * 2)
|
||||||
|
io.stdout(f"upgrading {len(nodes)} switches: {', '.join([node.name for node in sorted(nodes)])}")
|
||||||
|
|
||||||
|
for node in sorted(nodes):
|
||||||
|
upgrade_switch(node)
|
||||||
22
bin/passwords-for
Executable file
22
bin/passwords-for
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from bundlewrap.repo import Repository
|
||||||
|
from os.path import realpath, dirname
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('node', help='Node to generate passwords for')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
bw = Repository(dirname(dirname(realpath(__file__))))
|
||||||
|
node = bw.get_node(args.node)
|
||||||
|
|
||||||
|
if node.password:
|
||||||
|
print(f"password: {node.password}")
|
||||||
|
|
||||||
|
for metadata_key in sorted([
|
||||||
|
'users/root/password',
|
||||||
|
]):
|
||||||
|
if value := node.metadata.get(metadata_key, None):
|
||||||
|
print(f"{metadata_key}: {value}")
|
||||||
32
bin/rcon
Executable file
32
bin/rcon
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from sys import argv
|
||||||
|
from os.path import realpath, dirname
|
||||||
|
from shlex import quote
|
||||||
|
from bundlewrap.repo import Repository
|
||||||
|
|
||||||
|
repo = Repository(dirname(dirname(realpath(__file__))))
|
||||||
|
|
||||||
|
if len(argv) == 1:
|
||||||
|
for node in repo.nodes:
|
||||||
|
for name in node.metadata.get('left4dead2/servers', {}):
|
||||||
|
print(name)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
server = argv[1]
|
||||||
|
command = argv[2]
|
||||||
|
|
||||||
|
remote_code = """
|
||||||
|
from rcon.source import Client
|
||||||
|
|
||||||
|
with Client('127.0.0.1', {port}, passwd='''{password}''') as client:
|
||||||
|
response = client.run('''{command}''')
|
||||||
|
|
||||||
|
print(response)
|
||||||
|
"""
|
||||||
|
|
||||||
|
for node in repo.nodes:
|
||||||
|
for name, conf in node.metadata.get('left4dead2/servers', {}).items():
|
||||||
|
if name == server:
|
||||||
|
response = node.run('python3 -c ' + quote(remote_code.format(port=conf['port'], password=conf['rcon_password'], command=command)))
|
||||||
|
print(response.stdout.decode())
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
from bundlewrap.repo import Repository
|
from bundlewrap.repo import Repository
|
||||||
from os.path import realpath, dirname
|
from os.path import realpath, dirname
|
||||||
|
|
||||||
repo = Repository(dirname(dirname(realpath(__file__))))
|
bw = Repository(dirname(dirname(realpath(__file__))))
|
||||||
|
|
|
||||||
132
bin/sync_1password
Executable file
132
bin/sync_1password
Executable file
|
|
@ -0,0 +1,132 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from bundlewrap.repo import Repository
|
||||||
|
from os.path import realpath, dirname
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
bw = Repository(dirname(dirname(realpath(__file__))))
|
||||||
|
|
||||||
|
VAULT=bw.vault.decrypt('encrypt$gAAAAABpLgX_xxb5NmNCl3cgHM0JL65GT6PHVXO5gwly7IkmWoEgkCDSuAcSAkNFB8Tb4RdnTdpzVQEUL1XppTKVto_O7_b11GjATiyQYiSfiQ8KZkTKLvk=').value
|
||||||
|
BW_TAG = "bw"
|
||||||
|
BUNDLEWRAP_FIELD_LABEL = "bundlewrap node id"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OpResult:
|
||||||
|
stdout: str
|
||||||
|
stderr: str
|
||||||
|
returncode: int
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for node in bw.nodes_in_group('routeros'):
|
||||||
|
upsert_node_item(
|
||||||
|
node_name=node.name,
|
||||||
|
node_uuid=node.metadata.get('id'),
|
||||||
|
username=node.username,
|
||||||
|
password=node.password,
|
||||||
|
url=f'http://{node.hostname}',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_op(args):
|
||||||
|
proc = subprocess.run(
|
||||||
|
["op", "--vault", VAULT] + args,
|
||||||
|
env=os.environ.copy(),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"op {' '.join(args)} failed with code {proc.returncode}:\n"
|
||||||
|
f"STDOUT:\n{proc.stdout}\n\nSTDERR:\n{proc.stderr}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return OpResult(stdout=proc.stdout, stderr=proc.stderr, returncode=proc.returncode)
|
||||||
|
|
||||||
|
|
||||||
|
def op_item_list_bw():
|
||||||
|
out = run_op([
|
||||||
|
"item", "list",
|
||||||
|
"--tags", BW_TAG,
|
||||||
|
"--format", "json",
|
||||||
|
])
|
||||||
|
stdout = out.stdout.strip()
|
||||||
|
return json.loads(stdout) if stdout else []
|
||||||
|
|
||||||
|
|
||||||
|
def op_item_get(item_id):
|
||||||
|
args = ["item", "get", item_id, "--format", "json"]
|
||||||
|
return json.loads(run_op(args).stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def op_item_create(title, node_uuid, username, password, url):
|
||||||
|
print(f"creating {title}")
|
||||||
|
return json.loads(run_op([
|
||||||
|
"item", "create",
|
||||||
|
"--category", "LOGIN",
|
||||||
|
"--title", title,
|
||||||
|
"--tags", BW_TAG,
|
||||||
|
"--url", url,
|
||||||
|
"--format", "json",
|
||||||
|
f"username={username}",
|
||||||
|
f"password={password}",
|
||||||
|
f"{BUNDLEWRAP_FIELD_LABEL}[text]={node_uuid}",
|
||||||
|
]).stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def op_item_edit(item_id, title, username, password, url):
|
||||||
|
print(f"updating {title}")
|
||||||
|
return json.loads(run_op([
|
||||||
|
"item", "edit",
|
||||||
|
item_id,
|
||||||
|
"--title", title,
|
||||||
|
"--url", url,
|
||||||
|
"--format", "json",
|
||||||
|
f"username={username}",
|
||||||
|
f"password={password}",
|
||||||
|
]).stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def find_node_item_id(node_uuid):
|
||||||
|
for summary in op_item_list_bw():
|
||||||
|
item_id = summary.get("id")
|
||||||
|
if not item_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item = op_item_get(item_id)
|
||||||
|
for field in item.get("fields") or []:
|
||||||
|
label = field.get("label")
|
||||||
|
value = field.get("value")
|
||||||
|
if label == BUNDLEWRAP_FIELD_LABEL and value == node_uuid:
|
||||||
|
return item_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_node_item(node_name, node_uuid, username, password, url):
|
||||||
|
if item_id := find_node_item_id(node_uuid):
|
||||||
|
return op_item_edit(
|
||||||
|
item_id=item_id,
|
||||||
|
title=node_name,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return op_item_create(
|
||||||
|
title=node_name,
|
||||||
|
node_uuid=node_uuid,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
70
bin/upgrade_and_restart_all
Executable file
70
bin/upgrade_and_restart_all
Executable file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from bundlewrap.repo import Repository
|
||||||
|
from os.path import realpath, dirname
|
||||||
|
from ipaddress import ip_interface
|
||||||
|
|
||||||
|
repo = Repository(dirname(dirname(realpath(__file__))))
|
||||||
|
nodes = [
|
||||||
|
node
|
||||||
|
for node in sorted(repo.nodes_in_group('debian'))
|
||||||
|
if not node.dummy
|
||||||
|
]
|
||||||
|
|
||||||
|
print('updating nodes:', sorted(node.name for node in nodes))
|
||||||
|
|
||||||
|
# UPDATE
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
print('--------------------------------------')
|
||||||
|
print('updating', node.name)
|
||||||
|
print('--------------------------------------')
|
||||||
|
repo.libs.wol.wake(node)
|
||||||
|
print(node.run('DEBIAN_FRONTEND=noninteractive apt update').stdout.decode())
|
||||||
|
print(node.run('DEBIAN_FRONTEND=noninteractive apt list --upgradable').stdout.decode())
|
||||||
|
if int(node.run('DEBIAN_FRONTEND=noninteractive apt list --upgradable 2> /dev/null | grep upgradable | wc -l').stdout.decode()):
|
||||||
|
print(node.run('DEBIAN_FRONTEND=noninteractive apt -qy full-upgrade').stdout.decode())
|
||||||
|
|
||||||
|
# REBOOT IN ORDER
|
||||||
|
|
||||||
|
wireguard_servers = [
|
||||||
|
node
|
||||||
|
for node in nodes
|
||||||
|
if node.has_bundle('wireguard')
|
||||||
|
and (
|
||||||
|
ip_interface(node.metadata.get('wireguard/my_ip')).network.prefixlen <
|
||||||
|
ip_interface(node.metadata.get('wireguard/my_ip')).network.max_prefixlen
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
wireguard_s2s = [
|
||||||
|
node
|
||||||
|
for node in nodes
|
||||||
|
if node.has_bundle('wireguard')
|
||||||
|
and (
|
||||||
|
ip_interface(node.metadata.get('wireguard/my_ip')).network.prefixlen ==
|
||||||
|
ip_interface(node.metadata.get('wireguard/my_ip')).network.max_prefixlen
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
everything_else = [
|
||||||
|
node
|
||||||
|
for node in nodes
|
||||||
|
if not node.has_bundle('wireguard')
|
||||||
|
]
|
||||||
|
|
||||||
|
print('======================================')
|
||||||
|
|
||||||
|
for node in [
|
||||||
|
*everything_else,
|
||||||
|
*wireguard_s2s,
|
||||||
|
*wireguard_servers,
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
if node.run('test -e /var/run/reboot-required', may_fail=True).return_code == 0:
|
||||||
|
print('rebooting', node.name)
|
||||||
|
print(node.run('systemctl reboot').stdout.decode())
|
||||||
|
else:
|
||||||
|
print('not rebooting', node.name)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
9
bin/wake
Executable file
9
bin/wake
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from bundlewrap.repo import Repository
|
||||||
|
from os.path import realpath, dirname
|
||||||
|
from sys import argv
|
||||||
|
|
||||||
|
repo = Repository(dirname(dirname(realpath(__file__))))
|
||||||
|
|
||||||
|
repo.libs.wol.wake(repo.get_node(argv[1]))
|
||||||
|
|
@ -5,10 +5,18 @@ from os.path import realpath, dirname
|
||||||
from sys import argv
|
from sys import argv
|
||||||
from ipaddress import ip_network, ip_interface
|
from ipaddress import ip_network, ip_interface
|
||||||
|
|
||||||
repo = Repository(dirname(dirname(realpath(__file__))))
|
if len(argv) != 3:
|
||||||
|
print(f'usage: {argv[0]} <node> <client>')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
server_node = repo.get_node('htz.mails')
|
repo = Repository(dirname(dirname(realpath(__file__))))
|
||||||
data = server_node.metadata.get(f'wireguard/clients/{argv[1]}')
|
server_node = repo.get_node(argv[1])
|
||||||
|
|
||||||
|
if argv[2] not in server_node.metadata.get('wireguard/clients'):
|
||||||
|
print(f'client {argv[2]} not found in: {server_node.metadata.get("wireguard/clients").keys()}')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
data = server_node.metadata.get(f'wireguard/clients/{argv[2]}')
|
||||||
|
|
||||||
vpn_network = ip_interface(server_node.metadata.get('wireguard/my_ip')).network
|
vpn_network = ip_interface(server_node.metadata.get('wireguard/my_ip')).network
|
||||||
allowed_ips = [
|
allowed_ips = [
|
||||||
|
|
@ -20,17 +28,25 @@ for peer in server_node.metadata.get('wireguard/s2s').values():
|
||||||
if not ip_network(network).subnet_of(vpn_network):
|
if not ip_network(network).subnet_of(vpn_network):
|
||||||
allowed_ips.append(ip_network(network))
|
allowed_ips.append(ip_network(network))
|
||||||
|
|
||||||
print(
|
conf = f'''
|
||||||
f'''[Interface]
|
[Interface]
|
||||||
PrivateKey = {repo.libs.wireguard.privkey(data['peer_id'])}
|
PrivateKey = {repo.libs.wireguard.privkey(data['peer_id'])}
|
||||||
ListenPort = 51820
|
ListenPort = 51820
|
||||||
Address = {data['peer_ip']}
|
Address = {data['peer_ip']}
|
||||||
DNS = 8.8.8.8
|
DNS = 172.30.0.1
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
PublicKey = {repo.libs.wireguard.pubkey(server_node.metadata.get('id'))}
|
PublicKey = {repo.libs.wireguard.pubkey(server_node.metadata.get('id'))}
|
||||||
PresharedKey = {repo.libs.wireguard.psk(data['peer_id'], server_node.metadata.get('id'))}
|
PresharedKey = {repo.libs.wireguard.psk(data['peer_id'], server_node.metadata.get('id'))}
|
||||||
AllowedIPs = {', '.join(str(client_route) for client_route in sorted(allowed_ips))}
|
AllowedIPs = {', '.join(str(client_route) for client_route in sorted(allowed_ips))}
|
||||||
Endpoint = {ip_interface(server_node.metadata.get('network/external/ipv4')).ip}:51820
|
Endpoint = {ip_interface(server_node.metadata.get('network/external/ipv4')).ip}:51820
|
||||||
PersistentKeepalive = 10'''
|
PersistentKeepalive = 10
|
||||||
)
|
'''
|
||||||
|
|
||||||
|
print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
|
||||||
|
print(conf)
|
||||||
|
print('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<')
|
||||||
|
|
||||||
|
if input("print qrcode? [Yn]: ").upper() in ['', 'Y']:
|
||||||
|
import pyqrcode
|
||||||
|
print(pyqrcode.create(conf).terminal(quiet_zone=1))
|
||||||
|
|
|
||||||
10
bundles/apcupsd/README.md
Normal file
10
bundles/apcupsd/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
http://www.apcupsd.org/manual/manual.html#power-down-during-shutdown
|
||||||
|
|
||||||
|
- onbattery: power lost
|
||||||
|
- battery drains
|
||||||
|
- when BATTERYLEVEL or MINUTES threshold is reached, server is shut down and
|
||||||
|
the ups is issued to cut the power
|
||||||
|
- when the mains power returns, the ups will reinstate power to the server
|
||||||
|
- the server will reboot
|
||||||
|
|
||||||
|
NOT IMPLEMENTED
|
||||||
343
bundles/apcupsd/files/apcupsd.conf
Normal file
343
bundles/apcupsd/files/apcupsd.conf
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
## apcupsd.conf v1.1 ##
|
||||||
|
#
|
||||||
|
# "apcupsd" POSIX config file
|
||||||
|
|
||||||
|
#
|
||||||
|
# Note that the apcupsd daemon must be restarted in order for changes to
|
||||||
|
# this configuration file to become active.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# ========= General configuration parameters ============
|
||||||
|
#
|
||||||
|
|
||||||
|
# UPSNAME xxx
|
||||||
|
# Use this to give your UPS a name in log files and such. This
|
||||||
|
# is particulary useful if you have multiple UPSes. This does not
|
||||||
|
# set the EEPROM. It should be 8 characters or less.
|
||||||
|
#UPSNAME
|
||||||
|
|
||||||
|
# UPSCABLE <cable>
|
||||||
|
# Defines the type of cable connecting the UPS to your computer.
|
||||||
|
#
|
||||||
|
# Possible generic choices for <cable> are:
|
||||||
|
# simple, smart, ether, usb
|
||||||
|
#
|
||||||
|
# Or a specific cable model number may be used:
|
||||||
|
# 940-0119A, 940-0127A, 940-0128A, 940-0020B,
|
||||||
|
# 940-0020C, 940-0023A, 940-0024B, 940-0024C,
|
||||||
|
# 940-1524C, 940-0024G, 940-0095A, 940-0095B,
|
||||||
|
# 940-0095C, 940-0625A, M-04-02-2000
|
||||||
|
#
|
||||||
|
UPSCABLE usb
|
||||||
|
|
||||||
|
# To get apcupsd to work, in addition to defining the cable
|
||||||
|
# above, you must also define a UPSTYPE, which corresponds to
|
||||||
|
# the type of UPS you have (see the Description for more details).
|
||||||
|
# You must also specify a DEVICE, sometimes referred to as a port.
|
||||||
|
# For USB UPSes, please leave the DEVICE directive blank. For
|
||||||
|
# other UPS types, you must specify an appropriate port or address.
|
||||||
|
#
|
||||||
|
# UPSTYPE DEVICE Description
|
||||||
|
# apcsmart /dev/tty** Newer serial character device, appropriate for
|
||||||
|
# SmartUPS models using a serial cable (not USB).
|
||||||
|
#
|
||||||
|
# usb <BLANK> Most new UPSes are USB. A blank DEVICE
|
||||||
|
# setting enables autodetection, which is
|
||||||
|
# the best choice for most installations.
|
||||||
|
#
|
||||||
|
# net hostname:port Network link to a master apcupsd through apcupsd's
|
||||||
|
# Network Information Server. This is used if the
|
||||||
|
# UPS powering your computer is connected to a
|
||||||
|
# different computer for monitoring.
|
||||||
|
#
|
||||||
|
# snmp hostname:port:vendor:community
|
||||||
|
# SNMP network link to an SNMP-enabled UPS device.
|
||||||
|
# Hostname is the ip address or hostname of the UPS
|
||||||
|
# on the network. Vendor can be can be "APC" or
|
||||||
|
# "APC_NOTRAP". "APC_NOTRAP" will disable SNMP trap
|
||||||
|
# catching; you usually want "APC". Port is usually
|
||||||
|
# 161. Community is usually "private".
|
||||||
|
#
|
||||||
|
# netsnmp hostname:port:vendor:community
|
||||||
|
# OBSOLETE
|
||||||
|
# Same as SNMP above but requires use of the
|
||||||
|
# net-snmp library. Unless you have a specific need
|
||||||
|
# for this old driver, you should use 'snmp' instead.
|
||||||
|
#
|
||||||
|
# dumb /dev/tty** Old serial character device for use with
|
||||||
|
# simple-signaling UPSes.
|
||||||
|
#
|
||||||
|
# pcnet ipaddr:username:passphrase:port
|
||||||
|
# PowerChute Network Shutdown protocol which can be
|
||||||
|
# used as an alternative to SNMP with the AP9617
|
||||||
|
# family of smart slot cards. ipaddr is the IP
|
||||||
|
# address of the UPS management card. username and
|
||||||
|
# passphrase are the credentials for which the card
|
||||||
|
# has been configured. port is the port number on
|
||||||
|
# which to listen for messages from the UPS, normally
|
||||||
|
# 3052. If this parameter is empty or missing, the
|
||||||
|
# default of 3052 will be used.
|
||||||
|
#
|
||||||
|
# modbus /dev/tty** Serial device for use with newest SmartUPS models
|
||||||
|
# supporting the MODBUS protocol.
|
||||||
|
# modbus <BLANK> Leave the DEVICE setting blank for MODBUS over USB
|
||||||
|
# or set to the serial number of the UPS to ensure
|
||||||
|
# that apcupsd binds to that particular unit
|
||||||
|
# (helpful if you have more than one USB UPS).
|
||||||
|
#
|
||||||
|
UPSTYPE usb
|
||||||
|
#DEVICE /dev/ttyS0
|
||||||
|
|
||||||
|
# POLLTIME <int>
|
||||||
|
# Interval (in seconds) at which apcupsd polls the UPS for status. This
|
||||||
|
# setting applies both to directly-attached UPSes (UPSTYPE apcsmart, usb,
|
||||||
|
# dumb) and networked UPSes (UPSTYPE net, snmp). Lowering this setting
|
||||||
|
# will improve apcupsd's responsiveness to certain events at the cost of
|
||||||
|
# higher CPU utilization. The default of 60 is appropriate for most
|
||||||
|
# situations.
|
||||||
|
#POLLTIME 60
|
||||||
|
|
||||||
|
# LOCKFILE <path to lockfile>
|
||||||
|
# Path for device lock file for UPSes connected via USB or
|
||||||
|
# serial port. This is the directory into which the lock file
|
||||||
|
# will be written. The directory must already exist; apcupsd will not create
|
||||||
|
# it. The actual name of the lock file is computed from DEVICE.
|
||||||
|
# Not used on Win32.
|
||||||
|
LOCKFILE /var/lock
|
||||||
|
|
||||||
|
# SCRIPTDIR <path to script directory>
|
||||||
|
# Directory in which apccontrol and event scripts are located.
|
||||||
|
SCRIPTDIR /etc/apcupsd
|
||||||
|
|
||||||
|
# PWRFAILDIR <path to powerfail directory>
|
||||||
|
# Directory in which to write the powerfail flag file. This file
|
||||||
|
# is created when apcupsd initiates a system shutdown and is
|
||||||
|
# checked in the OS halt scripts to determine if a killpower
|
||||||
|
# (turning off UPS output power) is required.
|
||||||
|
PWRFAILDIR /etc/apcupsd
|
||||||
|
|
||||||
|
# NOLOGINDIR <path to nologin directory>
|
||||||
|
# Directory in which to write the nologin file. The existence
|
||||||
|
# of this flag file tells the OS to disallow new logins.
|
||||||
|
NOLOGINDIR /etc
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# ======== Configuration parameters used during power failures ==========
|
||||||
|
#
|
||||||
|
|
||||||
|
# The ONBATTERYDELAY is the time in seconds from when a power failure
|
||||||
|
# is detected until we react to it with an onbattery event.
|
||||||
|
#
|
||||||
|
# This means that, apccontrol will be called with the powerout argument
|
||||||
|
# immediately when a power failure is detected. However, the
|
||||||
|
# onbattery argument is passed to apccontrol only after the
|
||||||
|
# ONBATTERYDELAY time. If you don't want to be annoyed by short
|
||||||
|
# powerfailures, make sure that apccontrol powerout does nothing
|
||||||
|
# i.e. comment out the wall.
|
||||||
|
ONBATTERYDELAY 6
|
||||||
|
|
||||||
|
#
|
||||||
|
# Note: BATTERYLEVEL, MINUTES, and TIMEOUT work in conjunction, so
|
||||||
|
# the first that occurs will cause the initation of a shutdown.
|
||||||
|
#
|
||||||
|
|
||||||
|
# If during a power failure, the remaining battery percentage
|
||||||
|
# (as reported by the UPS) is below or equal to BATTERYLEVEL,
|
||||||
|
# apcupsd will initiate a system shutdown.
|
||||||
|
BATTERYLEVEL 10
|
||||||
|
|
||||||
|
# If during a power failure, the remaining runtime in minutes
|
||||||
|
# (as calculated internally by the UPS) is below or equal to MINUTES,
|
||||||
|
# apcupsd, will initiate a system shutdown.
|
||||||
|
MINUTES 5
|
||||||
|
|
||||||
|
# If during a power failure, the UPS has run on batteries for TIMEOUT
|
||||||
|
# many seconds or longer, apcupsd will initiate a system shutdown.
|
||||||
|
# A value of 0 disables this timer.
|
||||||
|
#
|
||||||
|
# Note, if you have a Smart UPS, you will most likely want to disable
|
||||||
|
# this timer by setting it to zero. That way, you UPS will continue
|
||||||
|
# on batteries until either the % charge remaing drops to or below BATTERYLEVEL,
|
||||||
|
# or the remaining battery runtime drops to or below MINUTES. Of course,
|
||||||
|
# if you are testing, setting this to 60 causes a quick system shutdown
|
||||||
|
# if you pull the power plug.
|
||||||
|
# If you have an older dumb UPS, you will want to set this to less than
|
||||||
|
# the time you know you can run on batteries.
|
||||||
|
TIMEOUT 0
|
||||||
|
|
||||||
|
# Time in seconds between annoying users to signoff prior to
|
||||||
|
# system shutdown. 0 disables.
|
||||||
|
ANNOY 300
|
||||||
|
|
||||||
|
# Initial delay after power failure before warning users to get
|
||||||
|
# off the system.
|
||||||
|
ANNOYDELAY 60
|
||||||
|
|
||||||
|
# The condition which determines when users are prevented from
|
||||||
|
# logging in during a power failure.
|
||||||
|
# NOLOGON <string> [ disable | timeout | percent | minutes | always ]
|
||||||
|
NOLOGON disable
|
||||||
|
|
||||||
|
# If KILLDELAY is non-zero, apcupsd will continue running after a
|
||||||
|
# shutdown has been requested, and after the specified time in
|
||||||
|
# seconds attempt to kill the power. This is for use on systems
|
||||||
|
# where apcupsd cannot regain control after a shutdown.
|
||||||
|
# KILLDELAY <seconds> 0 disables
|
||||||
|
KILLDELAY 0
|
||||||
|
|
||||||
|
#
|
||||||
|
# ==== Configuration statements for Network Information Server ====
|
||||||
|
#
|
||||||
|
|
||||||
|
# NETSERVER [ on | off ] on enables, off disables the network
|
||||||
|
# information server. If netstatus is on, a network information
|
||||||
|
# server process will be started for serving the STATUS and
|
||||||
|
# EVENT data over the network (used by CGI programs).
|
||||||
|
NETSERVER on
|
||||||
|
|
||||||
|
# NISIP <dotted notation ip address>
|
||||||
|
# IP address on which NIS server will listen for incoming connections.
|
||||||
|
# This is useful if your server is multi-homed (has more than one
|
||||||
|
# network interface and IP address). Default value is 0.0.0.0 which
|
||||||
|
# means any incoming request will be serviced. Alternatively, you can
|
||||||
|
# configure this setting to any specific IP address of your server and
|
||||||
|
# NIS will listen for connections only on that interface. Use the
|
||||||
|
# loopback address (127.0.0.1) to accept connections only from the
|
||||||
|
# local machine.
|
||||||
|
NISIP 127.0.0.1
|
||||||
|
|
||||||
|
# NISPORT <port> default is 3551 as registered with the IANA
|
||||||
|
# port to use for sending STATUS and EVENTS data over the network.
|
||||||
|
# It is not used unless NETSERVER is on. If you change this port,
|
||||||
|
# you will need to change the corresponding value in the cgi directory
|
||||||
|
# and rebuild the cgi programs.
|
||||||
|
NISPORT 3551
|
||||||
|
|
||||||
|
# If you want the last few EVENTS to be available over the network
|
||||||
|
# by the network information server, you must define an EVENTSFILE.
|
||||||
|
EVENTSFILE /var/log/apcupsd.events
|
||||||
|
|
||||||
|
# EVENTSFILEMAX <kilobytes>
|
||||||
|
# By default, the size of the EVENTSFILE will be not be allowed to exceed
|
||||||
|
# 10 kilobytes. When the file grows beyond this limit, older EVENTS will
|
||||||
|
# be removed from the beginning of the file (first in first out). The
|
||||||
|
# parameter EVENTSFILEMAX can be set to a different kilobyte value, or set
|
||||||
|
# to zero to allow the EVENTSFILE to grow without limit.
|
||||||
|
EVENTSFILEMAX 10
|
||||||
|
|
||||||
|
#
|
||||||
|
# ========== Configuration statements used if sharing =============
|
||||||
|
# a UPS with more than one machine
|
||||||
|
|
||||||
|
#
|
||||||
|
# Remaining items are for ShareUPS (APC expansion card) ONLY
|
||||||
|
#
|
||||||
|
|
||||||
|
# UPSCLASS [ standalone | shareslave | sharemaster ]
|
||||||
|
# Normally standalone unless you share an UPS using an APC ShareUPS
|
||||||
|
# card.
|
||||||
|
UPSCLASS standalone
|
||||||
|
|
||||||
|
# UPSMODE [ disable | share ]
|
||||||
|
# Normally disable unless you share an UPS using an APC ShareUPS card.
|
||||||
|
UPSMODE disable
|
||||||
|
|
||||||
|
#
|
||||||
|
# ===== Configuration statements to control apcupsd system logging ========
|
||||||
|
#
|
||||||
|
|
||||||
|
# Time interval in seconds between writing the STATUS file; 0 disables
|
||||||
|
STATTIME 0
|
||||||
|
|
||||||
|
# Location of STATUS file (written to only if STATTIME is non-zero)
|
||||||
|
STATFILE /var/log/apcupsd.status
|
||||||
|
|
||||||
|
# LOGSTATS [ on | off ] on enables, off disables
|
||||||
|
# Note! This generates a lot of output, so if
|
||||||
|
# you turn this on, be sure that the
|
||||||
|
# file defined in syslog.conf for LOG_NOTICE is a named pipe.
|
||||||
|
# You probably do not want this on.
|
||||||
|
LOGSTATS off
|
||||||
|
|
||||||
|
# Time interval in seconds between writing the DATA records to
|
||||||
|
# the log file. 0 disables.
|
||||||
|
DATATIME 0
|
||||||
|
|
||||||
|
# FACILITY defines the logging facility (class) for logging to syslog.
|
||||||
|
# If not specified, it defaults to "daemon". This is useful
|
||||||
|
# if you want to separate the data logged by apcupsd from other
|
||||||
|
# programs.
|
||||||
|
#FACILITY DAEMON
|
||||||
|
|
||||||
|
#
|
||||||
|
# ========== Configuration statements used in updating the UPS EPROM =========
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# These statements are used only by apctest when choosing "Set EEPROM with conf
|
||||||
|
# file values" from the EEPROM menu. THESE STATEMENTS HAVE NO EFFECT ON APCUPSD.
|
||||||
|
#
|
||||||
|
|
||||||
|
# UPS name, max 8 characters
|
||||||
|
#UPSNAME UPS_IDEN
|
||||||
|
|
||||||
|
# Battery date - 8 characters
|
||||||
|
#BATTDATE mm/dd/yy
|
||||||
|
|
||||||
|
# Sensitivity to line voltage quality (H cause faster transfer to batteries)
|
||||||
|
# SENSITIVITY H M L (default = H)
|
||||||
|
#SENSITIVITY H
|
||||||
|
|
||||||
|
# UPS delay after power return (seconds)
|
||||||
|
# WAKEUP 000 060 180 300 (default = 0)
|
||||||
|
#WAKEUP 60
|
||||||
|
|
||||||
|
# UPS Grace period after request to power off (seconds)
|
||||||
|
# SLEEP 020 180 300 600 (default = 20)
|
||||||
|
#SLEEP 180
|
||||||
|
|
||||||
|
# Low line voltage causing transfer to batteries
|
||||||
|
# The permitted values depend on your model as defined by last letter
|
||||||
|
# of FIRMWARE or APCMODEL. Some representative values are:
|
||||||
|
# D 106 103 100 097
|
||||||
|
# M 177 172 168 182
|
||||||
|
# A 092 090 088 086
|
||||||
|
# I 208 204 200 196 (default = 0 => not valid)
|
||||||
|
#LOTRANSFER 208
|
||||||
|
|
||||||
|
# High line voltage causing transfer to batteries
|
||||||
|
# The permitted values depend on your model as defined by last letter
|
||||||
|
# of FIRMWARE or APCMODEL. Some representative values are:
|
||||||
|
# D 127 130 133 136
|
||||||
|
# M 229 234 239 224
|
||||||
|
# A 108 110 112 114
|
||||||
|
# I 253 257 261 265 (default = 0 => not valid)
|
||||||
|
#HITRANSFER 253
|
||||||
|
|
||||||
|
# Battery charge needed to restore power
|
||||||
|
# RETURNCHARGE 00 15 50 90 (default = 15)
|
||||||
|
#RETURNCHARGE 15
|
||||||
|
|
||||||
|
# Alarm delay
|
||||||
|
# 0 = zero delay after pwr fail, T = power fail + 30 sec, L = low battery, N = never
|
||||||
|
# BEEPSTATE 0 T L N (default = 0)
|
||||||
|
#BEEPSTATE T
|
||||||
|
|
||||||
|
# Low battery warning delay in minutes
|
||||||
|
# LOWBATT 02 05 07 10 (default = 02)
|
||||||
|
#LOWBATT 2
|
||||||
|
|
||||||
|
# UPS Output voltage when running on batteries
|
||||||
|
# The permitted values depend on your model as defined by last letter
|
||||||
|
# of FIRMWARE or APCMODEL. Some representative values are:
|
||||||
|
# D 115
|
||||||
|
# M 208
|
||||||
|
# A 100
|
||||||
|
# I 230 240 220 225 (default = 0 => not valid)
|
||||||
|
#OUTPUTVOLTS 230
|
||||||
|
|
||||||
|
# Self test interval in hours 336=2 weeks, 168=1 week, ON=at power on
|
||||||
|
# SELFTEST 336 168 ON OFF (default = 336)
|
||||||
|
#SELFTEST 336
|
||||||
10
bundles/apcupsd/files/telegraf_plugin
Normal file
10
bundles/apcupsd/files/telegraf_plugin
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
date=$(date --utc +%s%N)
|
||||||
|
|
||||||
|
METRICS=$(apcaccess)
|
||||||
|
|
||||||
|
for METRIC in TIMELEFT LOADPCT BCHARGE
|
||||||
|
do
|
||||||
|
echo "apcupsd $METRIC=$(grep $METRIC <<< $METRICS | cut -d ':' -f 2 | xargs | cut -d ' ' -f 1 ) $date"
|
||||||
|
done
|
||||||
20
bundles/apcupsd/items.py
Normal file
20
bundles/apcupsd/items.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
files = {
|
||||||
|
'/etc/apcupsd/apcupsd.conf': {
|
||||||
|
'needs': [
|
||||||
|
'pkg_apt:apcupsd',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'/usr/local/share/telegraf/apcupsd': {
|
||||||
|
'source': 'telegraf_plugin',
|
||||||
|
'mode': '755',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc_systemd = {
|
||||||
|
'apcupsd': {
|
||||||
|
'needs': [
|
||||||
|
'pkg_apt:apcupsd',
|
||||||
|
'file:/etc/apcupsd/apcupsd.conf',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
30
bundles/apcupsd/metadata.py
Normal file
30
bundles/apcupsd/metadata.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'apcupsd': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'grafana_rows': {
|
||||||
|
'ups',
|
||||||
|
},
|
||||||
|
'sudoers': {
|
||||||
|
'telegraf': {
|
||||||
|
'/usr/local/share/telegraf/apcupsd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'telegraf': {
|
||||||
|
'config': {
|
||||||
|
'inputs': {
|
||||||
|
'exec': {
|
||||||
|
repo.libs.hashable.hashable({
|
||||||
|
'commands': ["sudo /usr/local/share/telegraf/apcupsd"],
|
||||||
|
'name_override': "apcupsd",
|
||||||
|
'data_format': "influx",
|
||||||
|
'interval': '30s',
|
||||||
|
'flush_interval': '30s',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
# https://manpages.debian.org/latest/apt/sources.list.5.de.html
|
||||||
|
# https://repolib.readthedocs.io/en/latest/deb822-format.html
|
||||||
|
|
||||||
```python
|
```python
|
||||||
{
|
{
|
||||||
'apt': {
|
'apt': {
|
||||||
|
|
@ -5,8 +8,32 @@
|
||||||
'apt-transport-https': {},
|
'apt-transport-https': {},
|
||||||
},
|
},
|
||||||
'sources': {
|
'sources': {
|
||||||
# place key under data/apt/keys/packages.cloud.google.com.{asc|gpg}
|
'debian': {
|
||||||
'deb https://packages.cloud.google.com/apt cloud-sdk main',
|
'types': { # optional, defaults to `{'deb'}``
|
||||||
|
'deb',
|
||||||
|
'deb-src',
|
||||||
|
},
|
||||||
|
'options': { # optional
|
||||||
|
'aarch': 'amd64',
|
||||||
|
},
|
||||||
|
'urls': {
|
||||||
|
'https://deb.debian.org/debian',
|
||||||
|
},
|
||||||
|
'suites': { # at least one
|
||||||
|
'{codename}',
|
||||||
|
'{codename}-updates',
|
||||||
|
'{codename}-backports',
|
||||||
|
},
|
||||||
|
'components': { # optional
|
||||||
|
'main',
|
||||||
|
'contrib',
|
||||||
|
'non-frese',
|
||||||
|
},
|
||||||
|
# key:
|
||||||
|
# - optional, defaults to source name (`debian` in this example)
|
||||||
|
# - place key under data/apt/keys/debian-12.{asc|gpg}
|
||||||
|
'key': 'debian-{version}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
bundles/apt/files/check_apt_upgradable
Normal file
15
bundles/apt/files/check_apt_upgradable
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
apt update -qq --silent 2> /dev/null
|
||||||
|
|
||||||
|
UPGRADABLE=$(apt list --upgradable -qq 2> /dev/null | cut -d '/' -f 1)
|
||||||
|
|
||||||
|
if test "$UPGRADABLE" != ""
|
||||||
|
then
|
||||||
|
echo "$(wc -l <<< $UPGRADABLE) package(s) upgradable:"
|
||||||
|
echo
|
||||||
|
echo "$UPGRADABLE"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
@ -1,33 +1,68 @@
|
||||||
from os.path import join
|
# TODO pin repo: https://superuser.com/a/1595920
|
||||||
from urllib.parse import urlparse
|
|
||||||
from glob import glob
|
|
||||||
from os.path import join, basename
|
from os.path import join, basename
|
||||||
|
|
||||||
directories = {
|
directories = {
|
||||||
'/etc/apt/sources.list.d': {
|
'/etc/apt': {
|
||||||
'purge': True,
|
'purge': True,
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'action:apt_update',
|
'action:apt_update',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/etc/apt/trusted.gpg.d': {
|
'/etc/apt/apt.conf.d': {
|
||||||
|
# existance is expected
|
||||||
'purge': True,
|
'purge': True,
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'action:apt_update',
|
'action:apt_update',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'/etc/apt/keyrings': {
|
||||||
|
# https://askubuntu.com/a/1307181
|
||||||
|
'purge': True,
|
||||||
|
'triggers': {
|
||||||
|
'action:apt_update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
# '/etc/apt/listchanges.conf.d': {
|
||||||
|
# 'purge': True,
|
||||||
|
# 'triggers': {
|
||||||
|
# 'action:apt_update',
|
||||||
|
# },
|
||||||
|
# },
|
||||||
'/etc/apt/preferences.d': {
|
'/etc/apt/preferences.d': {
|
||||||
'purge': True,
|
'purge': True,
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'action:apt_update',
|
'action:apt_update',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'/etc/apt/sources.list.d': {
|
||||||
|
'purge': True,
|
||||||
|
'triggers': {
|
||||||
|
'action:apt_update',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
files = {
|
files = {
|
||||||
'/etc/apt/sources.list': {
|
'/etc/apt/apt.conf': {
|
||||||
'content': '# managed'
|
'content': repo.libs.apt.render_apt_conf(node.metadata.get('apt/config')),
|
||||||
|
'triggers': {
|
||||||
|
'action:apt_update',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
'/etc/apt/sources.list': {
|
||||||
|
'content': '# managed by bundlewrap\n',
|
||||||
|
'triggers': {
|
||||||
|
'action:apt_update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
# '/etc/apt/listchanges.conf': {
|
||||||
|
# 'content': repo.libs.ini.dumps(node.metadata.get('apt/list_changes')),
|
||||||
|
# },
|
||||||
|
'/usr/lib/nagios/plugins/check_apt_upgradable': {
|
||||||
|
'mode': '0755',
|
||||||
|
},
|
||||||
|
# /etc/kernel/postinst.d/apt-auto-removal
|
||||||
}
|
}
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
|
|
@ -41,39 +76,22 @@ actions = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# group sources by apt server hostname
|
# create sources.lists and respective keyfiles
|
||||||
|
|
||||||
hosts = {}
|
for name, config in node.metadata.get('apt/sources').items():
|
||||||
|
# place keyfile
|
||||||
for source_string in node.metadata.get('apt/sources'):
|
keyfile_destination_path = repo.libs.apt.format_variables(node, config['options']['Signed-By'])
|
||||||
source = repo.libs.apt.AptSource(source_string)
|
files[keyfile_destination_path] = {
|
||||||
hosts\
|
'source': join(repo.path, 'data', 'apt', 'keys', basename(keyfile_destination_path)),
|
||||||
.setdefault(source.url.hostname, set())\
|
'content_type': 'binary',
|
||||||
.add(source)
|
|
||||||
|
|
||||||
# create sources lists and keyfiles
|
|
||||||
|
|
||||||
for host, sources in hosts.items():
|
|
||||||
keyfile = basename(glob(join(repo.path, 'data', 'apt', 'keys', f'{host}.*'))[0])
|
|
||||||
destination_path = f'/etc/apt/trusted.gpg.d/{keyfile}'
|
|
||||||
|
|
||||||
for source in sources:
|
|
||||||
source.options['signed-by'] = [destination_path]
|
|
||||||
|
|
||||||
files[f'/etc/apt/sources.list.d/{host}.list'] = {
|
|
||||||
'content': '\n'.join(
|
|
||||||
str(source) for source in sorted(sources)
|
|
||||||
).format(
|
|
||||||
release=node.metadata.get('os_release')
|
|
||||||
),
|
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'action:apt_update',
|
'action:apt_update',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
files[destination_path] = {
|
# place sources.list
|
||||||
'source': join(repo.path, 'data', 'apt', 'keys', keyfile),
|
files[f'/etc/apt/sources.list.d/{name}.sources'] = {
|
||||||
'content_type': 'binary',
|
'content': repo.libs.apt.render_source(node, name),
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'action:apt_update',
|
'action:apt_update',
|
||||||
},
|
},
|
||||||
|
|
@ -88,7 +106,7 @@ for package, options in node.metadata.get('apt/packages', {}).items():
|
||||||
files[f'/etc/apt/preferences.d/{package}'] = {
|
files[f'/etc/apt/preferences.d/{package}'] = {
|
||||||
'content': '\n'.join([
|
'content': '\n'.join([
|
||||||
f"Package: {package}",
|
f"Package: {package}",
|
||||||
f"Pin: release a={node.metadata.get('os_release')}-backports",
|
f"Pin: release a={node.metadata.get('os_codename')}-backports",
|
||||||
f"Pin-Priority: 900",
|
f"Pin-Priority: 900",
|
||||||
]),
|
]),
|
||||||
'needed_by': [
|
'needed_by': [
|
||||||
|
|
@ -98,3 +116,25 @@ for package, options in node.metadata.get('apt/packages', {}).items():
|
||||||
'action:apt_update',
|
'action:apt_update',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# unattended upgrades
|
||||||
|
#
|
||||||
|
# unattended-upgrades.service: delays shutdown if necessary
|
||||||
|
# apt-daily.timer: performs apt update
|
||||||
|
# apt-daily-upgrade.timer: performs apt upgrade
|
||||||
|
|
||||||
|
svc_systemd['unattended-upgrades.service'] = {
|
||||||
|
'needs': [
|
||||||
|
'pkg_apt:unattended-upgrades',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
svc_systemd['apt-daily.timer'] = {
|
||||||
|
'needs': [
|
||||||
|
'pkg_apt:unattended-upgrades',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
svc_systemd['apt-daily-upgrade.timer'] = {
|
||||||
|
'needs': [
|
||||||
|
'pkg_apt:unattended-upgrades',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,177 @@
|
||||||
defaults = {
|
defaults = {
|
||||||
'apt': {
|
'apt': {
|
||||||
'packages': {},
|
'packages': {
|
||||||
'sources': set(),
|
'apt-listchanges': {
|
||||||
|
'installed': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'config': {
|
||||||
|
'DPkg': {
|
||||||
|
'Pre-Install-Pkgs': {
|
||||||
|
'/usr/sbin/dpkg-preconfigure --apt || true',
|
||||||
|
},
|
||||||
|
'Post-Invoke': {
|
||||||
|
# keep package cache empty
|
||||||
|
'/bin/rm -f /var/cache/apt/archives/*.deb || true',
|
||||||
|
},
|
||||||
|
'Options': {
|
||||||
|
# https://unix.stackexchange.com/a/642541/357916
|
||||||
|
'--force-confold',
|
||||||
|
'--force-confdef',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'APT': {
|
||||||
|
'NeverAutoRemove': {
|
||||||
|
'^firmware-linux.*',
|
||||||
|
'^linux-firmware$',
|
||||||
|
'^linux-image-[a-z0-9]*$',
|
||||||
|
'^linux-image-[a-z0-9]*-[a-z0-9]*$',
|
||||||
|
},
|
||||||
|
'VersionedKernelPackages': {
|
||||||
|
# kernels
|
||||||
|
'linux-.*',
|
||||||
|
'kfreebsd-.*',
|
||||||
|
'gnumach-.*',
|
||||||
|
# (out-of-tree) modules
|
||||||
|
'.*-modules',
|
||||||
|
'.*-kernel',
|
||||||
|
},
|
||||||
|
'Never-MarkAuto-Sections': {
|
||||||
|
'metapackages',
|
||||||
|
'tasks',
|
||||||
|
},
|
||||||
|
'Move-Autobit-Sections': {
|
||||||
|
'oldlibs',
|
||||||
|
},
|
||||||
|
'Update': {
|
||||||
|
# https://unix.stackexchange.com/a/653377/357916
|
||||||
|
'Error-Mode': 'any',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sources': {},
|
||||||
|
},
|
||||||
|
'monitoring': {
|
||||||
|
'services': {
|
||||||
|
'apt upgradable': {
|
||||||
|
'vars.command': '/usr/lib/nagios/plugins/check_apt_upgradable',
|
||||||
|
'vars.sudo': True,
|
||||||
|
'check_interval': '1h',
|
||||||
|
},
|
||||||
|
'current kernel': {
|
||||||
|
'vars.command': 'ls /boot/vmlinuz-* | sort -V | tail -n 1 | xargs -n1 basename | cut -d "-" -f 2- | grep -q "^$(uname -r)$"',
|
||||||
|
'check_interval': '1h',
|
||||||
|
},
|
||||||
|
'apt reboot-required': {
|
||||||
|
'vars.command': 'ls /var/run/reboot-required 2> /dev/null && exit 1 || exit 0',
|
||||||
|
'check_interval': '1h',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'apt/sources',
|
||||||
|
)
|
||||||
|
def key(metadata):
|
||||||
|
return {
|
||||||
|
'apt': {
|
||||||
|
'sources': {
|
||||||
|
source_name: {
|
||||||
|
'key': source_name,
|
||||||
|
}
|
||||||
|
for source_name, source_config in metadata.get('apt/sources').items()
|
||||||
|
if 'key' not in source_config
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'apt/sources',
|
||||||
|
)
|
||||||
|
def signed_by(metadata):
|
||||||
|
return {
|
||||||
|
'apt': {
|
||||||
|
'sources': {
|
||||||
|
source_name: {
|
||||||
|
'options': {
|
||||||
|
'Signed-By': '/etc/apt/keyrings/' + metadata.get(f'apt/sources/{source_name}/key') + '.' + repo.libs.apt.find_keyfile_extension(node, metadata.get(f'apt/sources/{source_name}/key')),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for source_name in metadata.get('apt/sources')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'apt/config',
|
||||||
|
'apt/packages',
|
||||||
|
)
|
||||||
|
def unattended_upgrades(metadata):
|
||||||
|
return {
|
||||||
|
'apt': {
|
||||||
|
'config': {
|
||||||
|
'APT': {
|
||||||
|
'Periodic': {
|
||||||
|
'Update-Package-Lists': '1',
|
||||||
|
'Unattended-Upgrade': '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Unattended-Upgrade': {
|
||||||
|
'Origins-Pattern': {
|
||||||
|
"origin=*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'packages': {
|
||||||
|
'unattended-upgrades': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# @metadata_reactor.provides(
|
||||||
|
# 'apt/config',
|
||||||
|
# 'apt/list_changes',
|
||||||
|
# )
|
||||||
|
# def listchanges(metadata):
|
||||||
|
# return {
|
||||||
|
# 'apt': {
|
||||||
|
# 'config': {
|
||||||
|
# 'DPkg': {
|
||||||
|
# 'Pre-Install-Pkgs': {
|
||||||
|
# '/usr/bin/apt-listchanges --apt || test $? -lt 10',
|
||||||
|
# },
|
||||||
|
# 'Tools': {
|
||||||
|
# 'Options': {
|
||||||
|
# '/usr/bin/apt-listchanges': {
|
||||||
|
# 'Version': '2',
|
||||||
|
# 'InfoFD': '20',
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# 'Dir': {
|
||||||
|
# 'Etc': {
|
||||||
|
# 'apt-listchanges-main': 'listchanges.conf',
|
||||||
|
# 'apt-listchanges-parts': 'listchanges.conf.d',
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# 'list_changes': {
|
||||||
|
# 'apt': {
|
||||||
|
# 'frontend': 'pager',
|
||||||
|
# 'which': 'news',
|
||||||
|
# 'email_address': 'root',
|
||||||
|
# 'email_format': 'text',
|
||||||
|
# 'confirm': 'false',
|
||||||
|
# 'headers': 'false',
|
||||||
|
# 'reverse': 'false',
|
||||||
|
# 'save_seen': '/var/lib/apt/listchanges.db',
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
|
|
||||||
47
bundles/backup-freshness-check/files/check_backup_freshness
Normal file
47
bundles/backup-freshness-check/files/check_backup_freshness
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
from subprocess import check_output
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
two_days_ago = now - timedelta(days=2)
|
||||||
|
|
||||||
|
with open('/etc/backup-freshness-check.json', 'r') as file:
|
||||||
|
config = json.load(file)
|
||||||
|
|
||||||
|
local_datasets = check_output(['zfs', 'list', '-H', '-o', 'name']).decode().splitlines()
|
||||||
|
errors = set()
|
||||||
|
|
||||||
|
for dataset in config['datasets']:
|
||||||
|
if f'tank/{dataset}' not in local_datasets:
|
||||||
|
errors.add(f'dataset "{dataset}" not present at all')
|
||||||
|
continue
|
||||||
|
|
||||||
|
snapshots = [
|
||||||
|
snapshot
|
||||||
|
for snapshot in check_output(['zfs', 'list', '-H', '-o', 'name', '-t', 'snapshot', f'tank/{dataset}', '-s', 'creation']).decode().splitlines()
|
||||||
|
if f"@{config['prefix']}" in snapshot
|
||||||
|
]
|
||||||
|
|
||||||
|
if not snapshots:
|
||||||
|
errors.add(f'dataset "{dataset}" has no backup snapshots')
|
||||||
|
continue
|
||||||
|
|
||||||
|
newest_backup_snapshot = snapshots[-1]
|
||||||
|
snapshot_datetime = datetime.utcfromtimestamp(
|
||||||
|
int(check_output(['zfs', 'list', '-p', '-H', '-o', 'creation', '-t', 'snapshot', newest_backup_snapshot]).decode())
|
||||||
|
)
|
||||||
|
|
||||||
|
if snapshot_datetime < two_days_ago:
|
||||||
|
days_ago = (now - snapshot_datetime).days
|
||||||
|
errors.add(f'dataset "{dataset}" has not been backed up for {days_ago} days')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for error in errors:
|
||||||
|
print(error)
|
||||||
|
exit(2)
|
||||||
|
else:
|
||||||
|
print(f"all {len(config['datasets'])} datasets have fresh backups.")
|
||||||
15
bundles/backup-freshness-check/items.py
Normal file
15
bundles/backup-freshness-check/items.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from json import dumps
|
||||||
|
from bundlewrap.metadata import MetadataJSONEncoder
|
||||||
|
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'/etc/backup-freshness-check.json': {
|
||||||
|
'content': dumps({
|
||||||
|
'prefix': node.metadata.get('backup-freshness-check/prefix'),
|
||||||
|
'datasets': node.metadata.get('backup-freshness-check/datasets'),
|
||||||
|
}, indent=4, sort_keys=True, cls=MetadataJSONEncoder),
|
||||||
|
},
|
||||||
|
'/usr/lib/nagios/plugins/check_backup_freshness': {
|
||||||
|
'mode': '0755',
|
||||||
|
},
|
||||||
|
}
|
||||||
37
bundles/backup-freshness-check/metadata.py
Normal file
37
bundles/backup-freshness-check/metadata.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
defaults = {
|
||||||
|
'backup-freshness-check': {
|
||||||
|
'server': node.name,
|
||||||
|
'prefix': 'auto-backup_',
|
||||||
|
'datasets': {},
|
||||||
|
},
|
||||||
|
'monitoring': {
|
||||||
|
'services': {
|
||||||
|
'backup freshness': {
|
||||||
|
'vars.command': '/usr/lib/nagios/plugins/check_backup_freshness',
|
||||||
|
'check_interval': '6h',
|
||||||
|
'vars.sudo': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'backup-freshness-check/datasets'
|
||||||
|
)
|
||||||
|
def backup_freshness_check(metadata):
|
||||||
|
return {
|
||||||
|
'backup-freshness-check': {
|
||||||
|
'datasets': {
|
||||||
|
f"{other_node.metadata.get('id')}/{dataset}"
|
||||||
|
for other_node in repo.nodes
|
||||||
|
if not other_node.dummy
|
||||||
|
and other_node.has_bundle('backup')
|
||||||
|
and other_node.has_bundle('zfs')
|
||||||
|
and other_node.metadata.get('backup/server') == metadata.get('backup-freshness-check/server')
|
||||||
|
for dataset, options in other_node.metadata.get('zfs/datasets').items()
|
||||||
|
if options.get('backup', True)
|
||||||
|
and not options.get('mountpoint', None) in [None, 'none']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,18 @@ defaults = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'sudoers': {
|
'sudoers': {
|
||||||
'backup-receiver': ['ALL'],
|
'backup-receiver': {
|
||||||
}
|
'/usr/bin/rsync',
|
||||||
|
'/sbin/zfs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'zfs': {
|
||||||
|
'datasets': {
|
||||||
|
'tank': {
|
||||||
|
'recordsize': "1048576",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,30 +35,53 @@ def zfs(metadata):
|
||||||
|
|
||||||
for other_node in repo.nodes:
|
for other_node in repo.nodes:
|
||||||
if (
|
if (
|
||||||
|
not other_node.dummy and
|
||||||
other_node.has_bundle('backup') and
|
other_node.has_bundle('backup') and
|
||||||
other_node.metadata.get('backup/server') == node.name
|
other_node.metadata.get('backup/server') == node.name
|
||||||
):
|
):
|
||||||
|
id = other_node.metadata.get('id')
|
||||||
|
base_dataset = f'tank/{id}'
|
||||||
|
|
||||||
# container
|
# container
|
||||||
datasets[f"tank/{other_node.metadata.get('id')}"] = {
|
datasets[base_dataset] = {
|
||||||
'mountpoint': None,
|
'mountpoint': None,
|
||||||
'readonly': 'on',
|
'readonly': 'on',
|
||||||
|
'compression': 'lz4',
|
||||||
|
'com.sun:auto-snapshot': 'false',
|
||||||
'backup': False,
|
'backup': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# for rsync backups
|
# for rsync backups
|
||||||
datasets[f"tank/{other_node.metadata.get('id')}/fs"] = {
|
datasets[f'{base_dataset}/fs'] = {
|
||||||
'mountpoint': f"/mnt/backups/{other_node.metadata.get('id')}",
|
'mountpoint': f"/mnt/backups/{id}",
|
||||||
'readonly': 'off',
|
'readonly': 'off',
|
||||||
|
'compression': 'lz4',
|
||||||
|
'com.sun:auto-snapshot': 'true',
|
||||||
'backup': False,
|
'backup': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# for zfs send/recv
|
# for zfs send/recv
|
||||||
if other_node.has_bundle('zfs'):
|
if other_node.has_bundle('zfs'):
|
||||||
|
|
||||||
|
# base datasets for each tank
|
||||||
|
for pool in other_node.metadata.get('zfs/pools'):
|
||||||
|
datasets[f'{base_dataset}/{pool}'] = {
|
||||||
|
'mountpoint': None,
|
||||||
|
'readonly': 'on',
|
||||||
|
'compression': 'lz4',
|
||||||
|
'com.sun:auto-snapshot': 'false',
|
||||||
|
'backup': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# actual datasets
|
||||||
for path in other_node.metadata.get('backup/paths'):
|
for path in other_node.metadata.get('backup/paths'):
|
||||||
for dataset, config in other_node.metadata.get('zfs/datasets').items():
|
for dataset, config in other_node.metadata.get('zfs/datasets').items():
|
||||||
if path == config.get('mountpoint'):
|
if path == config.get('mountpoint'):
|
||||||
datasets[f"tank/{other_node.metadata.get('id')}/{dataset}"] = {
|
datasets[f'{base_dataset}/{dataset}'] = {
|
||||||
'mountpoint': None,
|
'mountpoint': None,
|
||||||
'readonly': 'on',
|
'readonly': 'on',
|
||||||
|
'compression': 'lz4',
|
||||||
|
'com.sun:auto-snapshot': 'false',
|
||||||
'backup': False,
|
'backup': False,
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -66,7 +99,7 @@ def zfs(metadata):
|
||||||
def dns(metadata):
|
def dns(metadata):
|
||||||
return {
|
return {
|
||||||
'dns': {
|
'dns': {
|
||||||
metadata.get('backup-server/hostname'): repo.libs.dns.get_a_records(metadata),
|
metadata.get('backup-server/hostname'): repo.libs.ip.get_a_records(metadata),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,31 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# FIXME: inelegant
|
||||||
|
% if wol_command:
|
||||||
|
${wol_command}
|
||||||
|
% endif
|
||||||
|
|
||||||
|
exit=0
|
||||||
|
failed_paths=""
|
||||||
|
|
||||||
for path in $(jq -r '.paths | .[]' < /etc/backup/config.json)
|
for path in $(jq -r '.paths | .[]' < /etc/backup/config.json)
|
||||||
do
|
do
|
||||||
|
echo backing up $path
|
||||||
/opt/backup/backup_path "$path"
|
/opt/backup/backup_path "$path"
|
||||||
|
# set exit to 1 if any backup fails
|
||||||
|
if [ $? -ne 0 ]
|
||||||
|
then
|
||||||
|
echo ERROR: backing up $path failed >&2
|
||||||
|
exit=5
|
||||||
|
failed_paths="$failed_paths $path"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [ $exit -ne 0 ]
|
||||||
|
then
|
||||||
|
echo "ERROR: failed to backup paths: $failed_paths" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exit
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -exu
|
||||||
|
|
||||||
path=$1
|
path=$1
|
||||||
|
|
||||||
if zfs list -H -o mountpoint | grep -q "$path"
|
if zfs list -H -o mountpoint | grep -q "^$path$"
|
||||||
then
|
then
|
||||||
/opt/backup/backup_path_via_zfs "$path"
|
/opt/backup/backup_path_via_zfs "$path"
|
||||||
elif test -d "$path"
|
elif test -e "$path"
|
||||||
then
|
then
|
||||||
/opt/backuo/backup_path_via_rsync "$path"
|
/opt/backup/backup_path_via_rsync "$path"
|
||||||
else
|
else
|
||||||
echo "UNKNOWN PATH: $path"
|
echo "UNKNOWN PATH: $path"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,16 @@ set -exu
|
||||||
path=$1
|
path=$1
|
||||||
uuid=$(jq -r .client_uuid < /etc/backup/config.json)
|
uuid=$(jq -r .client_uuid < /etc/backup/config.json)
|
||||||
server=$(jq -r .server_hostname < /etc/backup/config.json)
|
server=$(jq -r .server_hostname < /etc/backup/config.json)
|
||||||
ssh="ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 backup-receiver@$server"
|
ssh="ssh -o ConnectTimeout=5 backup-receiver@$server"
|
||||||
|
|
||||||
rsync -av --rsync-path="sudo rsync" "$path/" "backup-receiver@$server:/mnt/backups/$uuid$path/"
|
if test -d "$path"
|
||||||
$ssh sudo zfs snap "tank/$uuid/fs@auto-backup_$(date +"%Y-%m-%d_%H:%M:%S")"
|
then
|
||||||
|
postfix="/"
|
||||||
|
elif test -f "$path"
|
||||||
|
then
|
||||||
|
postfix=""
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rsync -av --rsync-path="sudo rsync" "$path$postfix" "backup-receiver@$server:/mnt/backups/$uuid$path$postfix"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -exu
|
set -eu
|
||||||
|
|
||||||
path=$1
|
path=$1
|
||||||
uuid=$(jq -r .client_uuid < /etc/backup/config.json)
|
uuid=$(jq -r .client_uuid < /etc/backup/config.json)
|
||||||
server=$(jq -r .server_hostname < /etc/backup/config.json)
|
server=$(jq -r .server_hostname < /etc/backup/config.json)
|
||||||
ssh="ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 backup-receiver@$server"
|
ssh="ssh -o ConnectTimeout=5 backup-receiver@$server"
|
||||||
|
|
||||||
source_dataset=$(zfs list -H -o mountpoint,name | grep -P "^$path\t" | cut -d $'\t' -f 2)
|
source_dataset=$(zfs list -H -o mountpoint,name | grep -P "^$path\t" | cut -d $'\t' -f 2)
|
||||||
target_dataset="tank/$uuid/$source_dataset"
|
target_dataset="tank/$uuid/$source_dataset"
|
||||||
|
|
@ -39,13 +39,26 @@ else
|
||||||
echo "INCREMENTAL BACKUP"
|
echo "INCREMENTAL BACKUP"
|
||||||
last_bookmark=$(zfs list -t bookmark -H -o name | grep "^$source_dataset#$bookmark_prefix" | sort | tail -1 | cut -d '#' -f 2)
|
last_bookmark=$(zfs list -t bookmark -H -o name | grep "^$source_dataset#$bookmark_prefix" | sort | tail -1 | cut -d '#' -f 2)
|
||||||
[[ -z "$last_bookmark" ]] && echo "ERROR - last_bookmark is empty" && exit 98
|
[[ -z "$last_bookmark" ]] && echo "ERROR - last_bookmark is empty" && exit 98
|
||||||
$(zfs send -v -i "#$last_bookmark" "$source_dataset@$new_bookmark" | $ssh sudo zfs recv "$target_dataset")
|
$(zfs send -v -L -i "#$last_bookmark" "$source_dataset@$new_bookmark" | $ssh sudo zfs recv "$target_dataset")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$?" == "0" ]]
|
if [[ "$?" == "0" ]]
|
||||||
then
|
then
|
||||||
|
|
||||||
|
# delete old local bookmarks
|
||||||
|
for destroyable_bookmark in $(zfs list -t bookmark -H -o name "$source_dataset" | grep "^$source_dataset#$bookmark_prefix")
|
||||||
|
do
|
||||||
|
zfs destroy "$destroyable_bookmark"
|
||||||
|
done
|
||||||
|
|
||||||
|
# delete remote snapshots from bookmarks (except newest, even of not necessary; maybe for resuming tho)
|
||||||
|
for destroyable_snapshot in $($ssh sudo zfs list -t snapshot -H -o name "$target_dataset" | grep "^$target_dataset@$bookmark_prefix" | grep -v "$new_bookmark")
|
||||||
|
do
|
||||||
|
$ssh sudo zfs destroy "$destroyable_snapshot"
|
||||||
|
done
|
||||||
|
|
||||||
zfs bookmark "$source_dataset@$new_bookmark" "$source_dataset#$new_bookmark"
|
zfs bookmark "$source_dataset@$new_bookmark" "$source_dataset#$new_bookmark"
|
||||||
zfs destroy "$source_dataset@$new_bookmark"
|
zfs destroy "$source_dataset@$new_bookmark" # keep snapshots?
|
||||||
echo "SUCCESS"
|
echo "SUCCESS"
|
||||||
else
|
else
|
||||||
zfs destroy "$source_dataset@$new_bookmark"
|
zfs destroy "$source_dataset@$new_bookmark"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
|
||||||
|
backup_node = repo.get_node(node.metadata.get('backup/server'))
|
||||||
|
|
||||||
directories['/opt/backup'] = {}
|
directories['/opt/backup'] = {}
|
||||||
|
|
||||||
files['/opt/backup/backup_all'] = {
|
files['/opt/backup/backup_all'] = {
|
||||||
'mode': '700',
|
'mode': '700',
|
||||||
|
'content_type': 'mako',
|
||||||
|
'context': {
|
||||||
|
'wol_command': backup_node.metadata.get('wol-sleeper/wake_command', False),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
files['/opt/backup/backup_path'] = {
|
files['/opt/backup/backup_path'] = {
|
||||||
'mode': '700',
|
'mode': '700',
|
||||||
|
|
@ -20,7 +27,7 @@ directories['/etc/backup'] = {}
|
||||||
files['/etc/backup/config.json'] = {
|
files['/etc/backup/config.json'] = {
|
||||||
'content': dumps(
|
'content': dumps(
|
||||||
{
|
{
|
||||||
'server_hostname': repo.get_node(node.metadata.get('backup/server')).metadata.get('backup-server/hostname'),
|
'server_hostname': backup_node.metadata.get('backup-server/hostname'),
|
||||||
'client_uuid': node.metadata.get('id'),
|
'client_uuid': node.metadata.get('id'),
|
||||||
'paths': sorted(set(node.metadata.get('backup/paths'))),
|
'paths': sorted(set(node.metadata.get('backup/paths'))),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
defaults = {
|
defaults = {
|
||||||
'apt': {
|
'apt': {
|
||||||
'packages': {
|
'packages': {
|
||||||
'jq': {},
|
'jq': {
|
||||||
'rsync': {},
|
'needed_by': {
|
||||||
|
'svc_systemd:backup.timer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'rsync': {
|
||||||
|
'needed_by': {
|
||||||
|
'svc_systemd:backup.timer',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'backup': {
|
'backup': {
|
||||||
|
|
@ -12,7 +20,11 @@ defaults = {
|
||||||
'systemd-timers': {
|
'systemd-timers': {
|
||||||
f'backup': {
|
f'backup': {
|
||||||
'command': '/opt/backup/backup_all',
|
'command': '/opt/backup/backup_all',
|
||||||
'when': 'daily',
|
'when': '1:00',
|
||||||
|
'persistent': True,
|
||||||
|
'after': {
|
||||||
|
'network-online.target',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
69
bundles/bind-acme/metadata.py
Normal file
69
bundles/bind-acme/metadata.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
from ipaddress import ip_interface
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'dns',
|
||||||
|
)
|
||||||
|
def acme_records(metadata):
|
||||||
|
domains = set()
|
||||||
|
|
||||||
|
for other_node in repo.nodes:
|
||||||
|
for domain, conf in other_node.metadata.get('letsencrypt/domains', {}).items():
|
||||||
|
domains.add(domain)
|
||||||
|
domains.update(conf.get('aliases', []))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'dns': {
|
||||||
|
f'_acme-challenge.{domain}': {
|
||||||
|
'CNAME': {f"{domain}.{metadata.get('bind/acme_zone')}."},
|
||||||
|
}
|
||||||
|
for domain in domains
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'bind/acls/acme',
|
||||||
|
'bind/views/external/keys/acme',
|
||||||
|
'bind/views/external/zones',
|
||||||
|
)
|
||||||
|
def acme_zone(metadata):
|
||||||
|
allowed_ips = {
|
||||||
|
*{
|
||||||
|
str(ip_interface(other_node.metadata.get('network/internal/ipv4')).ip)
|
||||||
|
for other_node in repo.nodes
|
||||||
|
if other_node.metadata.get('letsencrypt/domains', {})
|
||||||
|
},
|
||||||
|
*{
|
||||||
|
str(ip_interface(other_node.metadata.get('wireguard/my_ip')).ip)
|
||||||
|
for other_node in repo.nodes
|
||||||
|
if other_node.has_bundle('wireguard')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bind': {
|
||||||
|
'acls': {
|
||||||
|
'acme': {
|
||||||
|
'key acme',
|
||||||
|
'!{ !{' + ' '.join(f'{ip};' for ip in sorted(allowed_ips)) + '}; any;}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'views': {
|
||||||
|
'external': {
|
||||||
|
'keys': {
|
||||||
|
'acme': {},
|
||||||
|
},
|
||||||
|
'zones': {
|
||||||
|
metadata.get('bind/acme_zone'): {
|
||||||
|
'allow_update': {
|
||||||
|
'acme',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#https://lists.isc.org/pipermail/bind-users/2006-January/061051.html
|
||||||
|
|
@ -4,15 +4,15 @@ def column_width(column, table):
|
||||||
%>\
|
%>\
|
||||||
$TTL 600
|
$TTL 600
|
||||||
@ IN SOA ${hostname}. admin.${hostname}. (
|
@ IN SOA ${hostname}. admin.${hostname}. (
|
||||||
2021070821 ;Serial
|
2021111709 ;Serial
|
||||||
3600 ;Refresh
|
3600 ;Refresh
|
||||||
200 ;Retry
|
200 ;Retry
|
||||||
1209600 ;Expire
|
1209600 ;Expire
|
||||||
900 ;Negative response caching TTL
|
900 ;Negative response caching TTL
|
||||||
)
|
)
|
||||||
|
|
||||||
% for record in sorted(records, key=lambda r: (r['name'], r['type'], r['value'])):
|
% for record in sorted(records, key=lambda r: (tuple(reversed(r['name'].split('.'))), r['type'], r['value'])):
|
||||||
${(record['name'] or '@').ljust(column_width('name', records))} \
|
(${(record['name'] or '@').rjust(column_width('name', records))}) \
|
||||||
IN \
|
IN \
|
||||||
${record['type'].ljust(column_width('type', records))} \
|
${record['type'].ljust(column_width('type', records))} \
|
||||||
% if record['type'] == 'TXT':
|
% if record['type'] == 'TXT':
|
||||||
|
|
|
||||||
8
bundles/bind/files/db.empty
Normal file
8
bundles/bind/files/db.empty
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
$TTL 86400
|
||||||
|
@ IN SOA localhost. root.localhost. (
|
||||||
|
1 ; Serial
|
||||||
|
604800 ; Refresh
|
||||||
|
86400 ; Retry
|
||||||
|
2419200 ; Expire
|
||||||
|
86400 ) ; Negative Cache TTL
|
||||||
|
IN NS localhost.
|
||||||
|
|
@ -1,15 +1,35 @@
|
||||||
% for view in views:
|
# KEYS
|
||||||
acl "${view['name']}" {
|
|
||||||
${' '.join(f'{e};' for e in view['acl'])}
|
% for view_name, view_conf in views.items():
|
||||||
|
% for key_name, key_conf in sorted(view_conf['keys'].items()):
|
||||||
|
key "${key_name}" {
|
||||||
|
algorithm hmac-sha512;
|
||||||
|
secret "${key_conf['token']}";
|
||||||
|
};
|
||||||
|
% endfor
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
# ACLS
|
||||||
|
|
||||||
|
% for acl_name, acl_content in acls.items():
|
||||||
|
acl "${acl_name}" {
|
||||||
|
% for ac in sorted(acl_content, key=lambda e: (not e.startswith('!'), not e.startswith('key'), e)):
|
||||||
|
${ac};
|
||||||
|
% endfor
|
||||||
};
|
};
|
||||||
% endfor
|
% endfor
|
||||||
|
|
||||||
% for view in views:
|
# VIEWS
|
||||||
view "${view['name']}" {
|
|
||||||
match-clients { ${view['name']}; };
|
|
||||||
|
|
||||||
% if view['is_internal']:
|
% for view_name, view_conf in views.items():
|
||||||
|
view "${view_name}" {
|
||||||
|
match-clients {
|
||||||
|
${view_name};
|
||||||
|
};
|
||||||
|
|
||||||
|
% if view_conf['is_internal']:
|
||||||
recursion yes;
|
recursion yes;
|
||||||
|
include "/etc/bind/zones.rfc1918";
|
||||||
% else:
|
% else:
|
||||||
recursion no;
|
recursion no;
|
||||||
rate-limit {
|
rate-limit {
|
||||||
|
|
@ -25,18 +45,24 @@ view "${view['name']}" {
|
||||||
8.8.8.8;
|
8.8.8.8;
|
||||||
};
|
};
|
||||||
|
|
||||||
% for zone in zones:
|
% for zone_name, zone_conf in sorted(view_conf['zones'].items()):
|
||||||
zone "${zone}" {
|
zone "${zone_name}" {
|
||||||
type ${type};
|
% if type == 'slave' and zone_conf.get('allow_update', []):
|
||||||
% if type == 'slave':
|
type slave;
|
||||||
masters { ${master_ip}; };
|
masters { ${master_ip}; };
|
||||||
|
% else:
|
||||||
|
type master;
|
||||||
|
% if zone_conf.get('allow_update', []):
|
||||||
|
allow-update {
|
||||||
|
% for allow_update in zone_conf['allow_update']:
|
||||||
|
${allow_update};
|
||||||
|
% endfor
|
||||||
|
};
|
||||||
% endif
|
% endif
|
||||||
file "/var/lib/bind/${view['name']}/db.${zone}";
|
% endif
|
||||||
|
file "/var/lib/bind/${view_name}/${zone_name}";
|
||||||
};
|
};
|
||||||
% endfor
|
% endfor
|
||||||
|
|
||||||
include "/etc/bind/named.conf.default-zones";
|
|
||||||
include "/etc/bind/zones.rfc1918";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
% endfor
|
% endfor
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ options {
|
||||||
|
|
||||||
% if type == 'master':
|
% if type == 'master':
|
||||||
notify yes;
|
notify yes;
|
||||||
also-notify { ${' '.join([f'{ip};' for ip in slave_ips])} };
|
also-notify { ${' '.join(sorted(f'{ip};' for ip in slave_ips))} };
|
||||||
allow-transfer { ${' '.join([f'{ip};' for ip in slave_ips])} };
|
allow-transfer { ${' '.join(sorted(f'{ip};' for ip in slave_ips))} };
|
||||||
% endif
|
% endif
|
||||||
};
|
};
|
||||||
|
|
|
||||||
19
bundles/bind/files/zones.rfc1918
Normal file
19
bundles/bind/files/zones.rfc1918
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
zone "10.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "16.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "17.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "18.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "19.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "20.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "21.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "22.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "23.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "24.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "25.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "26.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "27.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "28.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "29.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "30.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "31.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "168.192.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
zone "254.169.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
from ipaddress import ip_address, ip_interface
|
from ipaddress import ip_address, ip_interface
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from hashlib import sha3_512
|
||||||
|
|
||||||
|
|
||||||
if node.metadata.get('bind/type') == 'master':
|
if node.metadata.get('bind/type') == 'master':
|
||||||
zones = node.metadata.get('bind/zones')
|
master_node = node
|
||||||
master_ip = None
|
|
||||||
slave_ips = [
|
|
||||||
ip_interface(repo.get_node(slave).metadata.get('network/external/ipv4')).ip
|
|
||||||
for slave in node.metadata.get('bind/slaves')
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
zones = repo.get_node(node.metadata.get('bind/master_node')).metadata.get('bind/zones')
|
master_node = repo.get_node(node.metadata.get('bind/master_node'))
|
||||||
master_ip = ip_interface(repo.get_node(node.metadata.get('bind/master_node')).metadata.get('network/external/ipv4')).ip
|
|
||||||
slave_ips = []
|
|
||||||
|
|
||||||
directories[f'/var/lib/bind'] = {
|
directories[f'/var/lib/bind'] = {
|
||||||
|
'owner': 'bind',
|
||||||
|
'group': 'bind',
|
||||||
'purge': True,
|
'purge': True,
|
||||||
|
'needs': [
|
||||||
|
'pkg_apt:bind9',
|
||||||
|
],
|
||||||
'needed_by': [
|
'needed_by': [
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
],
|
],
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:bind9:restart',
|
'svc_systemd:bind9:reload',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ files['/etc/default/bind9'] = {
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
],
|
],
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:bind9:restart',
|
'svc_systemd:bind9:reload',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,14 +43,16 @@ files['/etc/bind/named.conf'] = {
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
],
|
],
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:bind9:restart',
|
'svc_systemd:bind9:reload',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
files['/etc/bind/named.conf.options'] = {
|
files['/etc/bind/named.conf.options'] = {
|
||||||
'content_type': 'mako',
|
'content_type': 'mako',
|
||||||
'context': {
|
'context': {
|
||||||
'type': node.metadata.get('bind/type'),
|
'type': node.metadata.get('bind/type'),
|
||||||
'slave_ips': sorted(slave_ips),
|
'slave_ips': node.metadata.get('bind/slave_ips', []),
|
||||||
|
'master_ip': node.metadata.get('bind/master_ip', None),
|
||||||
},
|
},
|
||||||
'owner': 'root',
|
'owner': 'root',
|
||||||
'group': 'bind',
|
'group': 'bind',
|
||||||
|
|
@ -61,38 +63,26 @@ files['/etc/bind/named.conf.options'] = {
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
],
|
],
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:bind9:restart',
|
'svc_systemd:bind9:reload',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
views = [
|
|
||||||
{
|
|
||||||
'name': 'internal',
|
|
||||||
'is_internal': True,
|
|
||||||
'acl': [
|
|
||||||
'127.0.0.1',
|
|
||||||
'10.0.0.0/8',
|
|
||||||
'169.254.0.0/16',
|
|
||||||
'172.16.0.0/12',
|
|
||||||
'192.168.0.0/16',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'external',
|
|
||||||
'is_internal': False,
|
|
||||||
'acl': [
|
|
||||||
'any',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
files['/etc/bind/named.conf.local'] = {
|
files['/etc/bind/named.conf.local'] = {
|
||||||
'content_type': 'mako',
|
'content_type': 'mako',
|
||||||
'context': {
|
'context': {
|
||||||
'type': node.metadata.get('bind/type'),
|
'type': node.metadata.get('bind/type'),
|
||||||
'master_ip': master_ip,
|
'master_ip': node.metadata.get('bind/master_ip', None),
|
||||||
'views': views,
|
'acls': {
|
||||||
'zones': sorted(zones),
|
**master_node.metadata.get('bind/acls'),
|
||||||
|
**{
|
||||||
|
view_name: view_conf['match_clients']
|
||||||
|
for view_name, view_conf in master_node.metadata.get('bind/views').items()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'views': dict(sorted(
|
||||||
|
master_node.metadata.get('bind/views').items(),
|
||||||
|
key=lambda e: (e[1].get('default', False), e[0]),
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
'owner': 'root',
|
'owner': 'root',
|
||||||
'group': 'bind',
|
'group': 'bind',
|
||||||
|
|
@ -103,72 +93,45 @@ files['/etc/bind/named.conf.local'] = {
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
],
|
],
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:bind9:restart',
|
'svc_systemd:bind9:reload',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def record_matches_view(record, records, view):
|
for view_name, view_conf in master_node.metadata.get('bind/views').items():
|
||||||
if record['type'] in ['A', 'AAAA']:
|
directories[f"/var/lib/bind/{view_name}"] = {
|
||||||
if view == 'external':
|
'owner': 'bind',
|
||||||
# no internal addresses in external view
|
'group': 'bind',
|
||||||
if ip_address(record['value']).is_private:
|
|
||||||
return False
|
|
||||||
elif view == 'internal':
|
|
||||||
# external addresses in internal view only, if no internal exists
|
|
||||||
if ip_address(record['value']).is_global:
|
|
||||||
for other_record in records:
|
|
||||||
if (
|
|
||||||
record['name'] == other_record['name'] and
|
|
||||||
record['type'] == other_record['type'] and
|
|
||||||
ip_address(other_record['value']).is_private
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
for view in views:
|
|
||||||
directories[f"/var/lib/bind/{view['name']}"] = {
|
|
||||||
'purge': True,
|
'purge': True,
|
||||||
'needed_by': [
|
'needed_by': [
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
],
|
],
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:bind9:restart',
|
'svc_systemd:bind9:reload',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
for zone, record_dicts in zones.items():
|
for zone_name, zone_conf in view_conf['zones'].items():
|
||||||
records = record_dicts.values()
|
files[f"/var/lib/bind/{view_name}/{zone_name}"] = {
|
||||||
unique_records = [
|
|
||||||
dict(record_tuple)
|
|
||||||
for record_tuple in set(
|
|
||||||
tuple(record.items()) for record in records
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
files[f"/var/lib/bind/{view['name']}/db.{zone}"] = {
|
|
||||||
'group': 'bind',
|
|
||||||
'source': 'db',
|
'source': 'db',
|
||||||
'content_type': 'mako',
|
'content_type': 'mako',
|
||||||
|
'unless': f"test -f /var/lib/bind/{view_name}/{zone_name}" if zone_conf.get('allow_update', False) else 'false',
|
||||||
'context': {
|
'context': {
|
||||||
'view': view['name'],
|
|
||||||
'serial': datetime.now().strftime('%Y%m%d%H'),
|
'serial': datetime.now().strftime('%Y%m%d%H'),
|
||||||
'records': list(filter(
|
'records': zone_conf['records'],
|
||||||
lambda record: record_matches_view(record, records, view['name']),
|
|
||||||
unique_records
|
|
||||||
)),
|
|
||||||
'hostname': node.metadata.get('bind/hostname'),
|
'hostname': node.metadata.get('bind/hostname'),
|
||||||
|
'type': node.metadata.get('bind/type'),
|
||||||
},
|
},
|
||||||
'needs': [
|
'owner': 'bind',
|
||||||
f"directory:/var/lib/bind/{view['name']}",
|
'group': 'bind',
|
||||||
],
|
|
||||||
'needed_by': [
|
'needed_by': [
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
],
|
],
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:bind9:restart',
|
'svc_systemd:bind9:reload',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
svc_systemd['bind9'] = {}
|
svc_systemd['bind9'] = {}
|
||||||
|
|
||||||
actions['named-checkconf'] = {
|
actions['named-checkconf'] = {
|
||||||
|
|
@ -176,5 +139,24 @@ actions['named-checkconf'] = {
|
||||||
'unless': 'named-checkconf -z',
|
'unless': 'named-checkconf -z',
|
||||||
'needs': [
|
'needs': [
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
|
'svc_systemd:bind9:reload',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# beantwortet Anfragen nach privaten IP-Adressen mit NXDOMAIN, statt sie ins Internet weiterzuleiten
|
||||||
|
files['/etc/bind/zones.rfc1918'] = {
|
||||||
|
'needed_by': [
|
||||||
|
'svc_systemd:bind9',
|
||||||
|
],
|
||||||
|
'triggers': [
|
||||||
|
'svc_systemd:bind9:reload',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
files['/etc/bind/db.empty'] = {
|
||||||
|
'needed_by': [
|
||||||
|
'svc_systemd:bind9',
|
||||||
|
],
|
||||||
|
'triggers': [
|
||||||
|
'svc_systemd:bind9:reload',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from ipaddress import ip_interface
|
from ipaddress import ip_interface
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
h = repo.libs.hashable.hashable
|
||||||
|
repo.libs.bind.repo = repo
|
||||||
|
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
|
|
@ -9,8 +11,42 @@ defaults = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bind': {
|
'bind': {
|
||||||
'zones': {},
|
|
||||||
'slaves': {},
|
'slaves': {},
|
||||||
|
'acls': {
|
||||||
|
'our-nets': {
|
||||||
|
'127.0.0.1',
|
||||||
|
'10.0.0.0/8',
|
||||||
|
'169.254.0.0/16',
|
||||||
|
'172.16.0.0/12',
|
||||||
|
'192.168.0.0/16',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'views': {
|
||||||
|
'internal': {
|
||||||
|
'is_internal': True,
|
||||||
|
'keys': {},
|
||||||
|
'match_clients': {
|
||||||
|
'our-nets',
|
||||||
|
},
|
||||||
|
'zones': {},
|
||||||
|
},
|
||||||
|
'external': {
|
||||||
|
'default': True,
|
||||||
|
'is_internal': False,
|
||||||
|
'keys': {},
|
||||||
|
'match_clients': {
|
||||||
|
'any',
|
||||||
|
},
|
||||||
|
'zones': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'zones': set(),
|
||||||
|
},
|
||||||
|
'nftables': {
|
||||||
|
'input': {
|
||||||
|
'tcp dport 53 accept',
|
||||||
|
'udp dport 53 accept',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'telegraf': {
|
'telegraf': {
|
||||||
'config': {
|
'config': {
|
||||||
|
|
@ -28,11 +64,25 @@ defaults = {
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'bind/type',
|
'bind/type',
|
||||||
|
'bind/master_ip',
|
||||||
|
'bind/slave_ips',
|
||||||
)
|
)
|
||||||
def type(metadata):
|
def master_slave(metadata):
|
||||||
|
if metadata.get('bind/master_node', None):
|
||||||
return {
|
return {
|
||||||
'bind': {
|
'bind': {
|
||||||
'type': 'slave' if metadata.get('bind/master_node', None) else 'master',
|
'type': 'slave',
|
||||||
|
'master_ip': str(ip_interface(repo.get_node(metadata.get('bind/master_node')).metadata.get('network/external/ipv4')).ip),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'bind': {
|
||||||
|
'type': 'master',
|
||||||
|
'slave_ips': {
|
||||||
|
str(ip_interface(repo.get_node(slave).metadata.get('network/external/ipv4')).ip)
|
||||||
|
for slave in metadata.get('bind/slaves')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,26 +93,27 @@ def type(metadata):
|
||||||
def dns(metadata):
|
def dns(metadata):
|
||||||
return {
|
return {
|
||||||
'dns': {
|
'dns': {
|
||||||
metadata.get('bind/hostname'): repo.libs.dns.get_a_records(metadata),
|
metadata.get('bind/hostname'): repo.libs.ip.get_a_records(metadata),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'bind/zones',
|
'bind/views',
|
||||||
)
|
)
|
||||||
def collect_records(metadata):
|
def collect_records(metadata):
|
||||||
if metadata.get('bind/type') == 'slave':
|
if metadata.get('bind/type') == 'slave':
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
zones = {}
|
views = {}
|
||||||
|
|
||||||
|
for view_name, view_conf in metadata.get('bind/views').items():
|
||||||
for other_node in repo.nodes:
|
for other_node in repo.nodes:
|
||||||
for fqdn, records in other_node.metadata.get('dns', {}).items():
|
for fqdn, records in other_node.metadata.get('dns', {}).items():
|
||||||
matching_zones = sorted(
|
matching_zones = sorted(
|
||||||
filter(
|
filter(
|
||||||
lambda potential_zone: fqdn.endswith(potential_zone),
|
lambda potential_zone: fqdn.endswith(potential_zone),
|
||||||
metadata.get('bind/zones').keys()
|
metadata.get('bind/zones')
|
||||||
),
|
),
|
||||||
key=len,
|
key=len,
|
||||||
)
|
)
|
||||||
|
|
@ -75,22 +126,25 @@ def collect_records(metadata):
|
||||||
|
|
||||||
for type, values in records.items():
|
for type, values in records.items():
|
||||||
for value in values:
|
for value in values:
|
||||||
entry = {'name': name, 'type': type, 'value': value}
|
if repo.libs.bind.record_matches_view(value, type, name, zone, view_name, metadata):
|
||||||
zones\
|
views\
|
||||||
|
.setdefault(view_name, {})\
|
||||||
|
.setdefault('zones', {})\
|
||||||
.setdefault(zone, {})\
|
.setdefault(zone, {})\
|
||||||
.update({
|
.setdefault('records', set())\
|
||||||
str(hash(dumps(entry))): entry,
|
.add(
|
||||||
})
|
h({'name': name, 'type': type, 'value': value})
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'bind': {
|
'bind': {
|
||||||
'zones': zones,
|
'views': views,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'bind/zones',
|
'bind/views',
|
||||||
)
|
)
|
||||||
def ns_records(metadata):
|
def ns_records(metadata):
|
||||||
if metadata.get('bind/type') == 'slave':
|
if metadata.get('bind/type') == 'slave':
|
||||||
|
|
@ -105,12 +159,20 @@ def ns_records(metadata):
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
'bind': {
|
'bind': {
|
||||||
|
'views': {
|
||||||
|
view_name: {
|
||||||
'zones': {
|
'zones': {
|
||||||
zone: {
|
zone_name: {
|
||||||
|
'records': {
|
||||||
# FIXME: bw currently cant handle lists of dicts :(
|
# FIXME: bw currently cant handle lists of dicts :(
|
||||||
str(hash(dumps({'name': '@', 'type': 'NS', 'value': f"{nameserver}."}))): {'name': '@', 'type': 'NS', 'value': f"{nameserver}."}
|
h({'name': '@', 'type': 'NS', 'value': f"{nameserver}."})
|
||||||
for nameserver in nameservers
|
for nameserver in nameservers
|
||||||
} for zone in metadata.get('bind/zones').keys()
|
}
|
||||||
|
}
|
||||||
|
for zone_name, zone_conf in view_conf['zones'].items()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for view_name, view_conf in metadata.get('bind/views').items()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -132,3 +194,65 @@ def slaves(metadata):
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'bind/views',
|
||||||
|
)
|
||||||
|
def generate_keys(metadata):
|
||||||
|
if metadata.get('bind/type') == 'slave':
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bind': {
|
||||||
|
'views': {
|
||||||
|
view_name: {
|
||||||
|
'keys': {
|
||||||
|
key: {
|
||||||
|
'token':repo.libs.hmac.hmac_sha512(
|
||||||
|
key,
|
||||||
|
str(repo.vault.random_bytes_as_base64_for(
|
||||||
|
f"{metadata.get('id')} bind key {key} 20250713",
|
||||||
|
length=32,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for key in view_conf['keys']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for view_name, view_conf in metadata.get('bind/views').items()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'bind/views',
|
||||||
|
)
|
||||||
|
def generate_acl_entries_for_keys(metadata):
|
||||||
|
if metadata.get('bind/type') == 'slave':
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bind': {
|
||||||
|
'views': {
|
||||||
|
view_name: {
|
||||||
|
'match_clients': {
|
||||||
|
# allow keys from this view
|
||||||
|
*{
|
||||||
|
f'key {key}'
|
||||||
|
for key in view_conf['keys']
|
||||||
|
},
|
||||||
|
# reject keys from other views
|
||||||
|
*{
|
||||||
|
f'! key {key}'
|
||||||
|
for other_view_name, other_view_conf in metadata.get('bind/views').items()
|
||||||
|
if other_view_name != view_name
|
||||||
|
for key in other_view_conf.get('keys', [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for view_name, view_conf in metadata.get('bind/views').items()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
||||||
165
bundles/bootshorn/files/process
Executable file
165
bundles/bootshorn/files/process
Executable file
|
|
@ -0,0 +1,165 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import soundfile as sf
|
||||||
|
from scipy.fft import rfft, rfftfreq
|
||||||
|
import shutil
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
RECORDINGS_DIR = "recordings"
|
||||||
|
PROCESSED_RECORDINGS_DIR = "recordings/processed"
|
||||||
|
DETECTIONS_DIR = "events"
|
||||||
|
|
||||||
|
DETECT_FREQUENCY = 211 # Hz
|
||||||
|
DETECT_FREQUENCY_TOLERANCE = 2 # Hz
|
||||||
|
ADJACENCY_FACTOR = 2 # area to look for the frequency (e.g. 2 means 100Hz to 400Hz for 200Hz detection)
|
||||||
|
BLOCK_SECONDS = 3 # seconds (longer means more frequency resolution, but less time resolution)
|
||||||
|
DETECTION_DISTANCE_SECONDS = 30 # seconds (minimum time between detections)
|
||||||
|
BLOCK_OVERLAP_FACTOR = 0.9 # overlap between blocks (0.2 means 20% overlap)
|
||||||
|
MIN_SIGNAL_QUALITY = 1000.0 # maximum noise level (relative DB) to consider a detection valid
|
||||||
|
PLOT_PADDING_START_SECONDS = 2 # seconds (padding before and after the event in the plot)
|
||||||
|
PLOT_PADDING_END_SECONDS = 3 # seconds (padding before and after the event in the plot)
|
||||||
|
|
||||||
|
DETECTION_DISTANCE_BLOCKS = DETECTION_DISTANCE_SECONDS // BLOCK_SECONDS # number of blocks to skip after a detection
|
||||||
|
DETECT_FREQUENCY_FROM = DETECT_FREQUENCY - DETECT_FREQUENCY_TOLERANCE # Hz
|
||||||
|
DETECT_FREQUENCY_TO = DETECT_FREQUENCY + DETECT_FREQUENCY_TOLERANCE # Hz
|
||||||
|
|
||||||
|
|
||||||
|
def process_recording(filename):
|
||||||
|
print('processing', filename)
|
||||||
|
|
||||||
|
# get ISO 8601 nanosecond recording date from filename
|
||||||
|
date_string_from_filename = os.path.splitext(filename)[0]
|
||||||
|
recording_date = datetime.datetime.strptime(date_string_from_filename, "%Y-%m-%d_%H-%M-%S.%f%z")
|
||||||
|
|
||||||
|
# get data and metadata from recording
|
||||||
|
path = os.path.join(RECORDINGS_DIR, filename)
|
||||||
|
soundfile = sf.SoundFile(path)
|
||||||
|
samplerate = soundfile.samplerate
|
||||||
|
samples_per_block = int(BLOCK_SECONDS * samplerate)
|
||||||
|
overlapping_samples = int(samples_per_block * BLOCK_OVERLAP_FACTOR)
|
||||||
|
|
||||||
|
sample_num = 0
|
||||||
|
current_event = None
|
||||||
|
|
||||||
|
while sample_num < len(soundfile):
|
||||||
|
soundfile.seek(sample_num)
|
||||||
|
block = soundfile.read(frames=samples_per_block, dtype='float32', always_2d=False)
|
||||||
|
|
||||||
|
if len(block) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# calculate FFT
|
||||||
|
labels = rfftfreq(len(block), d=1/samplerate)
|
||||||
|
complex_amplitudes = rfft(block)
|
||||||
|
amplitudes = np.abs(complex_amplitudes)
|
||||||
|
|
||||||
|
# get the frequency with the highest amplitude within the search range
|
||||||
|
search_amplitudes = amplitudes[(labels >= DETECT_FREQUENCY_FROM/ADJACENCY_FACTOR) & (labels <= DETECT_FREQUENCY_TO*ADJACENCY_FACTOR)]
|
||||||
|
search_labels = labels[(labels >= DETECT_FREQUENCY_FROM/ADJACENCY_FACTOR) & (labels <= DETECT_FREQUENCY_TO*ADJACENCY_FACTOR)]
|
||||||
|
max_amplitude = max(search_amplitudes)
|
||||||
|
max_amplitude_index = np.argmax(search_amplitudes)
|
||||||
|
max_freq = search_labels[max_amplitude_index]
|
||||||
|
max_freq_detected = DETECT_FREQUENCY_FROM <= max_freq <= DETECT_FREQUENCY_TO
|
||||||
|
|
||||||
|
# calculate signal quality
|
||||||
|
adjacent_amplitudes = amplitudes[(labels < DETECT_FREQUENCY_FROM) | (labels > DETECT_FREQUENCY_TO)]
|
||||||
|
signal_quality = max_amplitude/np.mean(adjacent_amplitudes)
|
||||||
|
good_signal_quality = signal_quality > MIN_SIGNAL_QUALITY
|
||||||
|
|
||||||
|
# conclude detection
|
||||||
|
if (
|
||||||
|
max_freq_detected and
|
||||||
|
good_signal_quality
|
||||||
|
):
|
||||||
|
block_date = recording_date + datetime.timedelta(seconds=sample_num / samplerate)
|
||||||
|
|
||||||
|
# detecting an event
|
||||||
|
if not current_event:
|
||||||
|
current_event = {
|
||||||
|
'start_at': block_date,
|
||||||
|
'end_at': block_date,
|
||||||
|
'start_sample': sample_num,
|
||||||
|
'end_sample': sample_num + samples_per_block,
|
||||||
|
'start_freq': max_freq,
|
||||||
|
'end_freq': max_freq,
|
||||||
|
'max_amplitude': max_amplitude,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
current_event.update({
|
||||||
|
'end_at': block_date,
|
||||||
|
'end_freq': max_freq,
|
||||||
|
'end_sample': sample_num + samples_per_block,
|
||||||
|
'max_amplitude': max(max_amplitude, current_event['max_amplitude']),
|
||||||
|
})
|
||||||
|
print(f'- {block_date.strftime('%Y-%m-%d %H:%M:%S')}: {max_amplitude:.1f}rDB @ {max_freq:.1f}Hz (signal {signal_quality:.3f}x)')
|
||||||
|
else:
|
||||||
|
# not detecting an event
|
||||||
|
if current_event:
|
||||||
|
duration = (current_event['end_at'] - current_event['start_at']).total_seconds()
|
||||||
|
current_event['duration'] = duration
|
||||||
|
print(f'🔊 {current_event['start_at'].strftime('%Y-%m-%d %H:%M:%S')} ({duration:.1f}s): {current_event['start_freq']:.1f}Hz->{current_event['end_freq']:.1f}Hz @{current_event['max_amplitude']:.0f}rDB')
|
||||||
|
|
||||||
|
# read full audio clip again for writing
|
||||||
|
write_event(current_event=current_event, soundfile=soundfile, samplerate=samplerate)
|
||||||
|
|
||||||
|
current_event = None
|
||||||
|
sample_num += DETECTION_DISTANCE_BLOCKS * samples_per_block
|
||||||
|
|
||||||
|
sample_num += samples_per_block - overlapping_samples
|
||||||
|
|
||||||
|
# move to PROCESSED_RECORDINGS_DIR
|
||||||
|
|
||||||
|
os.makedirs(PROCESSED_RECORDINGS_DIR, exist_ok=True)
|
||||||
|
shutil.move(os.path.join(RECORDINGS_DIR, filename), os.path.join(PROCESSED_RECORDINGS_DIR, filename))
|
||||||
|
|
||||||
|
|
||||||
|
# write a spectrogram using the sound from start to end of the event
|
||||||
|
def write_event(current_event, soundfile, samplerate):
|
||||||
|
# date and filename
|
||||||
|
event_date = current_event['start_at'] - datetime.timedelta(seconds=PLOT_PADDING_START_SECONDS)
|
||||||
|
filename_prefix = event_date.strftime('%Y-%m-%d_%H-%M-%S.%f%z')
|
||||||
|
|
||||||
|
# event clip
|
||||||
|
event_start_sample = current_event['start_sample'] - samplerate * PLOT_PADDING_START_SECONDS
|
||||||
|
event_end_sample = current_event['end_sample'] + samplerate * PLOT_PADDING_END_SECONDS
|
||||||
|
total_samples = event_end_sample - event_start_sample
|
||||||
|
soundfile.seek(event_start_sample)
|
||||||
|
event_clip = soundfile.read(frames=total_samples, dtype='float32', always_2d=False)
|
||||||
|
|
||||||
|
# write flac
|
||||||
|
flac_path = os.path.join(DETECTIONS_DIR, f"{filename_prefix}.flac")
|
||||||
|
sf.write(flac_path, event_clip, samplerate, format='FLAC')
|
||||||
|
|
||||||
|
# write spectrogram
|
||||||
|
plt.figure(figsize=(8, 6))
|
||||||
|
plt.specgram(event_clip, Fs=samplerate, NFFT=samplerate, noverlap=samplerate//2, cmap='inferno', vmin=-100, vmax=-10)
|
||||||
|
plt.title(f"Bootshorn @{event_date.strftime('%Y-%m-%d %H:%M:%S%z')}")
|
||||||
|
plt.xlabel(f"Time {current_event['duration']:.1f}s")
|
||||||
|
plt.ylabel(f"Frequency {current_event['start_freq']:.1f}Hz -> {current_event['end_freq']:.1f}Hz")
|
||||||
|
plt.colorbar(label="Intensity (rDB)")
|
||||||
|
plt.ylim(50, 1000)
|
||||||
|
plt.savefig(os.path.join(DETECTIONS_DIR, f"{filename_prefix}.png"))
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(RECORDINGS_DIR, exist_ok=True)
|
||||||
|
os.makedirs(PROCESSED_RECORDINGS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
for filename in sorted(os.listdir(RECORDINGS_DIR)):
|
||||||
|
if filename.endswith(".flac"):
|
||||||
|
try:
|
||||||
|
process_recording(filename)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {filename}: {e}")
|
||||||
|
# print stacktrace
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
25
bundles/bootshorn/files/record
Executable file
25
bundles/bootshorn/files/record
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
mkdir -p recordings
|
||||||
|
|
||||||
|
while true
|
||||||
|
do
|
||||||
|
# get date in ISO 8601 format with nanoseconds
|
||||||
|
PROGRAMM=$(test $(uname) = "Darwin" && echo "gdate" || echo "date")
|
||||||
|
DATE=$($PROGRAMM "+%Y-%m-%d_%H-%M-%S.%6N%z")
|
||||||
|
|
||||||
|
# record audio using ffmpeg
|
||||||
|
ffmpeg \
|
||||||
|
-y \
|
||||||
|
-f pulse \
|
||||||
|
-i "alsa_input.usb-HANMUS_USB_AUDIO_24BIT_2I2O_1612310-00.analog-stereo" \
|
||||||
|
-ac 1 \
|
||||||
|
-ar 96000 \
|
||||||
|
-sample_fmt s32 \
|
||||||
|
-t "3600" \
|
||||||
|
-c:a flac \
|
||||||
|
-compression_level 12 \
|
||||||
|
"recordings/current/$DATE.flac"
|
||||||
|
|
||||||
|
mv "recordings/current/$DATE.flac" "recordings/$DATE.flac"
|
||||||
|
done
|
||||||
43
bundles/bootshorn/files/temperature
Executable file
43
bundles/bootshorn/files/temperature
Executable file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
import datetime
|
||||||
|
import csv
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
HUE_IP = "${hue_ip}" # replace with your bridge IP
|
||||||
|
HUE_APP_KEY = "${hue_app_key}" # local only
|
||||||
|
HUE_DEVICE_ID = "31f58786-3242-4e88-b9ce-23f44ba27bbe"
|
||||||
|
TEMPERATURE_LOG_DIR = "/opt/bootshorn/temperatures"
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
f"https://{HUE_IP}/clip/v2/resource/temperature",
|
||||||
|
headers={"hue-application-key": HUE_APP_KEY},
|
||||||
|
verify=False,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
for item in data["data"]:
|
||||||
|
if item["id"] == HUE_DEVICE_ID:
|
||||||
|
temperature = item["temperature"]["temperature"]
|
||||||
|
temperature_date_string = item["temperature"]["temperature_report"]["changed"]
|
||||||
|
temperature_date = datetime.datetime.fromisoformat(temperature_date_string).astimezone(datetime.timezone.utc)
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"@{temperature_date}: {temperature}°C")
|
||||||
|
|
||||||
|
filename = temperature_date.strftime("%Y-%m-%d_00-00-00.000000%z") + ".log"
|
||||||
|
logpath = os.path.join(TEMPERATURE_LOG_DIR, filename)
|
||||||
|
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
|
with open(logpath, "a+", newline="") as logfile:
|
||||||
|
writer = csv.writer(logfile)
|
||||||
|
writer.writerow([
|
||||||
|
now_utc.strftime('%Y-%m-%d_%H-%M-%S.%f%z'), # current UTC time
|
||||||
|
temperature_date.strftime('%Y-%m-%d_%H-%M-%S.%f%z'), # date of temperature reading
|
||||||
|
temperature,
|
||||||
|
])
|
||||||
61
bundles/bootshorn/items.py
Normal file
61
bundles/bootshorn/items.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# nano /etc/selinux/config
|
||||||
|
# SELINUX=disabled
|
||||||
|
# reboot
|
||||||
|
|
||||||
|
directories = {
|
||||||
|
'/opt/bootshorn': {
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
},
|
||||||
|
'/opt/bootshorn/temperatures': {
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
},
|
||||||
|
'/opt/bootshorn/recordings': {
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
},
|
||||||
|
'/opt/bootshorn/recordings/current': {
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
},
|
||||||
|
'/opt/bootshorn/recordings/processed': {
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
},
|
||||||
|
'/opt/bootshorn/events': {
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'/opt/bootshorn/record': {
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
'mode': '755',
|
||||||
|
},
|
||||||
|
'/opt/bootshorn/temperature': {
|
||||||
|
'content_type': 'mako',
|
||||||
|
'context': {
|
||||||
|
'hue_ip': repo.get_node('home.hue').hostname,
|
||||||
|
'hue_app_key': repo.vault.decrypt('encrypt$gAAAAABoc2WxZCLbxl-Z4IrSC97CdOeFgBplr9Fp5ujpd0WCCCPNBUY_WquHN86z8hKLq5Y04dwq8TdJW0PMSOSgTFbGgdp_P1q0jOBLEKaW9IIT1YM88h-JYwLf9QGDV_5oEfvnBCtO'),
|
||||||
|
},
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
'mode': '755',
|
||||||
|
},
|
||||||
|
'/opt/bootshorn/process': {
|
||||||
|
'owner': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
'mode': '755',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc_systemd = {
|
||||||
|
'bootshorn-record.service': {
|
||||||
|
'needs': {
|
||||||
|
'file:/opt/bootshorn/record',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
44
bundles/bootshorn/metadata.py
Normal file
44
bundles/bootshorn/metadata.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
defaults = {
|
||||||
|
'systemd': {
|
||||||
|
'units': {
|
||||||
|
'bootshorn-record.service': {
|
||||||
|
'Unit': {
|
||||||
|
'Description': 'Bootshorn Recorder',
|
||||||
|
'After': 'network.target',
|
||||||
|
},
|
||||||
|
'Service': {
|
||||||
|
'User': 'ckn',
|
||||||
|
'Group': 'ckn',
|
||||||
|
'Type': 'simple',
|
||||||
|
'WorkingDirectory': '/opt/bootshorn',
|
||||||
|
'ExecStart': '/opt/bootshorn/record',
|
||||||
|
'Restart': 'always',
|
||||||
|
'RestartSec': 5,
|
||||||
|
'Environment': {
|
||||||
|
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||||
|
"PULSE_SERVER": "unix:/run/user/1000/pulse/native",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'systemd-timers': {
|
||||||
|
'bootshorn-temperature': {
|
||||||
|
'command': '/opt/bootshorn/temperature',
|
||||||
|
'when': '*:0/10',
|
||||||
|
'working_dir': '/opt/bootshorn',
|
||||||
|
'user': 'ckn',
|
||||||
|
'group': 'ckn',
|
||||||
|
},
|
||||||
|
# 'bootshorn-process': {
|
||||||
|
# 'command': '/opt/bootshorn/process',
|
||||||
|
# 'when': 'hourly',
|
||||||
|
# 'working_dir': '/opt/bootshorn',
|
||||||
|
# 'user': 'ckn',
|
||||||
|
# 'group': 'ckn',
|
||||||
|
# 'after': {
|
||||||
|
# 'bootshorn-process.service',
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
},
|
||||||
|
}
|
||||||
0
bundles/build-agent/items.py
Normal file
0
bundles/build-agent/items.py
Normal file
38
bundles/build-agent/metadata.py
Normal file
38
bundles/build-agent/metadata.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'build-essential': {},
|
||||||
|
# crystal
|
||||||
|
'clang': {},
|
||||||
|
'libssl-dev': {},
|
||||||
|
'libpcre3-dev': {},
|
||||||
|
'libgc-dev': {},
|
||||||
|
'libevent-dev': {},
|
||||||
|
'zlib1g-dev': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'users': {
|
||||||
|
'build-agent': {
|
||||||
|
'home': '/var/lib/build-agent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'users/build-agent/authorized_users',
|
||||||
|
)
|
||||||
|
def ssh_keys(metadata):
|
||||||
|
return {
|
||||||
|
'users': {
|
||||||
|
'build-agent': {
|
||||||
|
'authorized_users': {
|
||||||
|
f'build-server@{other_node.name}'
|
||||||
|
for other_node in repo.nodes
|
||||||
|
if other_node.has_bundle('build-server')
|
||||||
|
for architecture in other_node.metadata.get('build-server/architectures').values()
|
||||||
|
if architecture['node'] == node.name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
9
bundles/build-ci/items.py
Normal file
9
bundles/build-ci/items.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
for project, options in node.metadata.get('build-ci').items():
|
||||||
|
directories[options['path']] = {
|
||||||
|
'owner': 'build-ci',
|
||||||
|
'group': options['group'],
|
||||||
|
'mode': '770',
|
||||||
|
'needs': [
|
||||||
|
'user:build-ci',
|
||||||
|
],
|
||||||
|
}
|
||||||
29
bundles/build-ci/metadata.py
Normal file
29
bundles/build-ci/metadata.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from shlex import quote
|
||||||
|
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
'build-ci': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'users/build-ci/authorized_users',
|
||||||
|
'sudoers/build-ci',
|
||||||
|
)
|
||||||
|
def ssh_keys(metadata):
|
||||||
|
return {
|
||||||
|
'users': {
|
||||||
|
'build-ci': {
|
||||||
|
'authorized_users': {
|
||||||
|
f'build-server@{other_node.name}'
|
||||||
|
for other_node in repo.nodes
|
||||||
|
if other_node.has_bundle('build-server')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sudoers': {
|
||||||
|
'build-ci': {
|
||||||
|
f"/usr/bin/chown -R build-ci\\:{quote(ci['group'])} {quote(ci['path'])}"
|
||||||
|
for ci in metadata.get('build-ci').values()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
2
bundles/build-server/README.md
Normal file
2
bundles/build-server/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
JSON=$(cat bundles/build-server/example.json)
|
||||||
|
curl -X POST 'https://build.sublimity.de/crystal?file=procio.cr' -H "Content-Type: application/json" --data-binary @- <<< $JSON
|
||||||
169
bundles/build-server/example.json
Normal file
169
bundles/build-server/example.json
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
{
|
||||||
|
"after": "122d7843c7814079e8df4919b0208c95ec7c75e3",
|
||||||
|
"before": "7a358255247926363ef0ef34111f0bc786a8c6f4",
|
||||||
|
"commits": [
|
||||||
|
{
|
||||||
|
"added": [],
|
||||||
|
"author": {
|
||||||
|
"email": "mwiegand@seibert-media.net",
|
||||||
|
"name": "mwiegand",
|
||||||
|
"username": ""
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"email": "mwiegand@seibert-media.net",
|
||||||
|
"name": "mwiegand",
|
||||||
|
"username": ""
|
||||||
|
},
|
||||||
|
"id": "122d7843c7814079e8df4919b0208c95ec7c75e3",
|
||||||
|
"message": "wip\n",
|
||||||
|
"modified": [
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"removed": [],
|
||||||
|
"timestamp": "2021-11-16T22:10:05+01:00",
|
||||||
|
"url": "https://git.sublimity.de/cronekorkn/telegraf-procio/commit/122d7843c7814079e8df4919b0208c95ec7c75e3",
|
||||||
|
"verification": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compare_url": "https://git.sublimity.de/cronekorkn/telegraf-procio/compare/7a358255247926363ef0ef34111f0bc786a8c6f4...122d7843c7814079e8df4919b0208c95ec7c75e3",
|
||||||
|
"head_commit": {
|
||||||
|
"added": [],
|
||||||
|
"author": {
|
||||||
|
"email": "mwiegand@seibert-media.net",
|
||||||
|
"name": "mwiegand",
|
||||||
|
"username": ""
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"email": "mwiegand@seibert-media.net",
|
||||||
|
"name": "mwiegand",
|
||||||
|
"username": ""
|
||||||
|
},
|
||||||
|
"id": "122d7843c7814079e8df4919b0208c95ec7c75e3",
|
||||||
|
"message": "wip\n",
|
||||||
|
"modified": [
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"removed": [],
|
||||||
|
"timestamp": "2021-11-16T22:10:05+01:00",
|
||||||
|
"url": "https://git.sublimity.de/cronekorkn/telegraf-procio/commit/122d7843c7814079e8df4919b0208c95ec7c75e3",
|
||||||
|
"verification": null
|
||||||
|
},
|
||||||
|
"pusher": {
|
||||||
|
"active": false,
|
||||||
|
"avatar_url": "https://git.sublimity.de/user/avatar/cronekorkn/-1",
|
||||||
|
"created": "2021-06-13T19:19:25+02:00",
|
||||||
|
"description": "",
|
||||||
|
"email": "i@ckn.li",
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"full_name": "",
|
||||||
|
"id": 1,
|
||||||
|
"is_admin": false,
|
||||||
|
"language": "",
|
||||||
|
"last_login": "0001-01-01T00:00:00Z",
|
||||||
|
"location": "",
|
||||||
|
"login": "cronekorkn",
|
||||||
|
"prohibit_login": false,
|
||||||
|
"restricted": false,
|
||||||
|
"starred_repos_count": 0,
|
||||||
|
"username": "cronekorkn",
|
||||||
|
"visibility": "public",
|
||||||
|
"website": ""
|
||||||
|
},
|
||||||
|
"ref": "refs/heads/master",
|
||||||
|
"repository": {
|
||||||
|
"allow_merge_commits": true,
|
||||||
|
"allow_rebase": true,
|
||||||
|
"allow_rebase_explicit": true,
|
||||||
|
"allow_squash_merge": true,
|
||||||
|
"archived": false,
|
||||||
|
"avatar_url": "",
|
||||||
|
"clone_url": "https://git.sublimity.de/cronekorkn/telegraf-procio.git",
|
||||||
|
"created_at": "2021-11-05T18:46:04+01:00",
|
||||||
|
"default_branch": "master",
|
||||||
|
"default_merge_style": "merge",
|
||||||
|
"description": "",
|
||||||
|
"empty": false,
|
||||||
|
"fork": false,
|
||||||
|
"forks_count": 0,
|
||||||
|
"full_name": "cronekorkn/telegraf-procio",
|
||||||
|
"has_issues": true,
|
||||||
|
"has_projects": true,
|
||||||
|
"has_pull_requests": true,
|
||||||
|
"has_wiki": true,
|
||||||
|
"html_url": "https://git.sublimity.de/cronekorkn/telegraf-procio",
|
||||||
|
"id": 5,
|
||||||
|
"ignore_whitespace_conflicts": false,
|
||||||
|
"internal": false,
|
||||||
|
"internal_tracker": {
|
||||||
|
"allow_only_contributors_to_track_time": true,
|
||||||
|
"enable_issue_dependencies": true,
|
||||||
|
"enable_time_tracker": true
|
||||||
|
},
|
||||||
|
"mirror": false,
|
||||||
|
"mirror_interval": "",
|
||||||
|
"name": "telegraf-procio",
|
||||||
|
"open_issues_count": 0,
|
||||||
|
"open_pr_counter": 0,
|
||||||
|
"original_url": "",
|
||||||
|
"owner": {
|
||||||
|
"active": false,
|
||||||
|
"avatar_url": "https://git.sublimity.de/user/avatar/cronekorkn/-1",
|
||||||
|
"created": "2021-06-13T19:19:25+02:00",
|
||||||
|
"description": "",
|
||||||
|
"email": "i@ckn.li",
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"full_name": "",
|
||||||
|
"id": 1,
|
||||||
|
"is_admin": false,
|
||||||
|
"language": "",
|
||||||
|
"last_login": "0001-01-01T00:00:00Z",
|
||||||
|
"location": "",
|
||||||
|
"login": "cronekorkn",
|
||||||
|
"prohibit_login": false,
|
||||||
|
"restricted": false,
|
||||||
|
"starred_repos_count": 0,
|
||||||
|
"username": "cronekorkn",
|
||||||
|
"visibility": "public",
|
||||||
|
"website": ""
|
||||||
|
},
|
||||||
|
"parent": null,
|
||||||
|
"permissions": {
|
||||||
|
"admin": true,
|
||||||
|
"pull": true,
|
||||||
|
"push": true
|
||||||
|
},
|
||||||
|
"private": false,
|
||||||
|
"release_counter": 0,
|
||||||
|
"size": 28,
|
||||||
|
"ssh_url": "git@git.sublimity.de:cronekorkn/telegraf-procio.git",
|
||||||
|
"stars_count": 0,
|
||||||
|
"template": false,
|
||||||
|
"updated_at": "2021-11-16T21:41:40+01:00",
|
||||||
|
"watchers_count": 1,
|
||||||
|
"website": ""
|
||||||
|
},
|
||||||
|
"sender": {
|
||||||
|
"active": false,
|
||||||
|
"avatar_url": "https://git.sublimity.de/user/avatar/cronekorkn/-1",
|
||||||
|
"created": "2021-06-13T19:19:25+02:00",
|
||||||
|
"description": "",
|
||||||
|
"email": "i@ckn.li",
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"full_name": "",
|
||||||
|
"id": 1,
|
||||||
|
"is_admin": false,
|
||||||
|
"language": "",
|
||||||
|
"last_login": "0001-01-01T00:00:00Z",
|
||||||
|
"location": "",
|
||||||
|
"login": "cronekorkn",
|
||||||
|
"prohibit_login": false,
|
||||||
|
"restricted": false,
|
||||||
|
"starred_repos_count": 0,
|
||||||
|
"username": "cronekorkn",
|
||||||
|
"visibility": "public",
|
||||||
|
"website": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
31
bundles/build-server/files/ci
Normal file
31
bundles/build-server/files/ci
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -xu
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_PATH=${config_path}
|
||||||
|
JSON="$1"
|
||||||
|
REPO_NAME=$(jq -r .repository.name <<< $JSON)
|
||||||
|
CLONE_URL=$(jq -r .repository.clone_url <<< $JSON)
|
||||||
|
REPO_BRANCH=$(jq -r .ref <<< $JSON | cut -d'/' -f3)
|
||||||
|
SSH_OPTIONS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
|
||||||
|
|
||||||
|
for INTEGRATION in "$(cat $CONFIG_PATH | jq -r '.ci | values[]')"
|
||||||
|
do
|
||||||
|
[[ $(jq -r '.repo' <<< $INTEGRATION) = "$REPO_NAME" ]] || continue
|
||||||
|
[[ $(jq -r '.branch' <<< $INTEGRATION) = "$REPO_BRANCH" ]] || continue
|
||||||
|
|
||||||
|
HOSTNAME=$(jq -r '.hostname' <<< $INTEGRATION)
|
||||||
|
DEST_PATH=$(jq -r '.path' <<< $INTEGRATION)
|
||||||
|
DEST_GROUP=$(jq -r '.group' <<< $INTEGRATION)
|
||||||
|
|
||||||
|
[[ -z "$HOSTNAME" ]] || [[ -z "$DEST_PATH" ]] || [[ -z "$DEST_GROUP" ]] && exit 5
|
||||||
|
|
||||||
|
cd ~
|
||||||
|
rm -rf "$REPO_NAME"
|
||||||
|
git clone "$CLONE_URL" "$REPO_NAME"
|
||||||
|
|
||||||
|
ssh $SSH_OPTIONS "build-ci@$HOSTNAME" "find \"$DEST_PATH\" -mindepth 1 -delete"
|
||||||
|
scp -r $SSH_OPTIONS "$REPO_NAME"/* "build-ci@$HOSTNAME:$DEST_PATH"
|
||||||
|
ssh $SSH_OPTIONS "build-ci@$HOSTNAME" "sudo chown -R build-ci:$DEST_GROUP $(printf "%q" "$DEST_PATH")"
|
||||||
|
done
|
||||||
32
bundles/build-server/files/crystal
Normal file
32
bundles/build-server/files/crystal
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -exu
|
||||||
|
|
||||||
|
DOWNLOAD_SERVER="${download_server}"
|
||||||
|
CONFIG=$(cat ${config_path})
|
||||||
|
JSON="$1"
|
||||||
|
ARGS="$2"
|
||||||
|
REPO_NAME=$(jq -r .repository.name <<< $JSON)
|
||||||
|
CLONE_URL=$(jq -r .repository.clone_url <<< $JSON)
|
||||||
|
BUILD_FILE=$(jq -r .file <<< $ARGS)
|
||||||
|
DATE=$(date --utc +%s)
|
||||||
|
|
||||||
|
cd ~
|
||||||
|
rm -rf "$REPO_NAME"
|
||||||
|
git clone "$CLONE_URL"
|
||||||
|
cd "$REPO_NAME"
|
||||||
|
shards install
|
||||||
|
|
||||||
|
for ARCH in $(jq -r '.architectures | keys[]' <<< $CONFIG)
|
||||||
|
do
|
||||||
|
TARGET=$(jq -r .architectures.$ARCH.target <<< $CONFIG)
|
||||||
|
IP=$(jq -r .architectures.$ARCH.ip <<< $CONFIG)
|
||||||
|
BUILD_CMD=$(crystal build "$BUILD_FILE" --cross-compile --target="$TARGET" --release -o "$REPO_NAME")
|
||||||
|
|
||||||
|
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$REPO_NAME.o" "build-agent@$IP:~"
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "build-agent@$IP" $BUILD_CMD
|
||||||
|
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "build-agent@$IP:~/$REPO_NAME" .
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "downloads@$DOWNLOAD_SERVER" mkdir -p "~/$REPO_NAME"
|
||||||
|
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$REPO_NAME" "downloads@$DOWNLOAD_SERVER:~/$REPO_NAME/$REPO_NAME-$ARCH-$DATE"
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "downloads@$DOWNLOAD_SERVER" ln -sf "$REPO_NAME-$ARCH-$DATE" "~/$REPO_NAME/$REPO_NAME-$ARCH-latest"
|
||||||
|
done
|
||||||
32
bundles/build-server/items.py
Normal file
32
bundles/build-server/items.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import json
|
||||||
|
from bundlewrap.metadata import MetadataJSONEncoder
|
||||||
|
|
||||||
|
directories = {
|
||||||
|
'/opt/build-server/strategies': {
|
||||||
|
'owner': 'build-server',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'/etc/build-server.json': {
|
||||||
|
'owner': 'build-server',
|
||||||
|
'content': json.dumps(node.metadata.get('build-server'), indent=4, sort_keys=True, cls=MetadataJSONEncoder)
|
||||||
|
},
|
||||||
|
'/opt/build-server/strategies/crystal': {
|
||||||
|
'content_type': 'mako',
|
||||||
|
'owner': 'build-server',
|
||||||
|
'mode': '0777', # FIXME
|
||||||
|
'context': {
|
||||||
|
'config_path': '/etc/build-server.json',
|
||||||
|
'download_server': node.metadata.get('build-server/download_server_ip'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/opt/build-server/strategies/ci': {
|
||||||
|
'content_type': 'mako',
|
||||||
|
'owner': 'build-server',
|
||||||
|
'mode': '0777', # FIXME
|
||||||
|
'context': {
|
||||||
|
'config_path': '/etc/build-server.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
78
bundles/build-server/metadata.py
Normal file
78
bundles/build-server/metadata.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
from ipaddress import ip_interface
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
'flask': {
|
||||||
|
'build-server' : {
|
||||||
|
'git_url': "https://git.sublimity.de/cronekorkn/build-server.git",
|
||||||
|
'port': 4000,
|
||||||
|
'app_module': 'build_server',
|
||||||
|
'user': 'build-server',
|
||||||
|
'group': 'build-server',
|
||||||
|
'timeout': 900,
|
||||||
|
'env': {
|
||||||
|
'CONFIG': '/etc/build-server.json',
|
||||||
|
'STRATEGIES_DIR': '/opt/build-server/strategies',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'users': {
|
||||||
|
'build-server': {
|
||||||
|
'home': '/var/lib/build-server',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'build-server',
|
||||||
|
)
|
||||||
|
def agent_conf(metadata):
|
||||||
|
download_server = repo.get_node(metadata.get('build-server/download_server'))
|
||||||
|
return {
|
||||||
|
'build-server': {
|
||||||
|
'architectures': {
|
||||||
|
architecture: {
|
||||||
|
'ip': str(ip_interface(repo.get_node(conf['node']).metadata.get('network/internal/ipv4')).ip),
|
||||||
|
}
|
||||||
|
for architecture, conf in metadata.get('build-server/architectures').items()
|
||||||
|
},
|
||||||
|
'download_server_ip': str(ip_interface(download_server.metadata.get('network/internal/ipv4')).ip),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'build-server',
|
||||||
|
)
|
||||||
|
def ci(metadata):
|
||||||
|
return {
|
||||||
|
'build-server': {
|
||||||
|
'ci': {
|
||||||
|
f'{repo}@{other_node.name}': {
|
||||||
|
'hostname': other_node.metadata.get('hostname'),
|
||||||
|
'repo': repo,
|
||||||
|
**options,
|
||||||
|
}
|
||||||
|
for other_node in repo.nodes
|
||||||
|
if other_node.has_bundle('build-ci')
|
||||||
|
for repo, options in other_node.metadata.get('build-ci').items()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'nginx/vhosts',
|
||||||
|
)
|
||||||
|
def nginx(metadata):
|
||||||
|
return {
|
||||||
|
'nginx': {
|
||||||
|
'vhosts': {
|
||||||
|
metadata.get('build-server/hostname'): {
|
||||||
|
'content': 'nginx/proxy_pass.conf',
|
||||||
|
'context': {
|
||||||
|
'target': 'http://127.0.0.1:4000',
|
||||||
|
},
|
||||||
|
'check_path': '/status',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
21
bundles/crystal/metadata.py
Normal file
21
bundles/crystal/metadata.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
debian_version = min([node.os_version, (11,)])[0] # FIXME
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'crystal': {},
|
||||||
|
},
|
||||||
|
'sources': {
|
||||||
|
'crystal': {
|
||||||
|
# https://software.opensuse.org/download.html?project=devel%3Alanguages%3Acrystal&package=crystal
|
||||||
|
# curl -fsSL https://download.opensuse.org/repositories/devel:/languages:/crystal/Debian_Testing/Release.key
|
||||||
|
'urls': {
|
||||||
|
'http://download.opensuse.org/repositories/devel:/languages:/crystal/Debian_Testing/',
|
||||||
|
},
|
||||||
|
'suites': {
|
||||||
|
'/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
24
bundles/dm-crypt/README.md
Normal file
24
bundles/dm-crypt/README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
dm-crypt
|
||||||
|
========
|
||||||
|
|
||||||
|
Create encrypted block devices using `dm-crypt` on GNU/Linux. Unlocking
|
||||||
|
these devices will be done on runs of `bw apply`.
|
||||||
|
|
||||||
|
Metadata
|
||||||
|
--------
|
||||||
|
|
||||||
|
'dm-crypt': {
|
||||||
|
'encrypted-devices': {
|
||||||
|
'foobar': {
|
||||||
|
'device': '/dev/sdb',
|
||||||
|
# either
|
||||||
|
'salt': 'muWWU7dr+5Wtk+56OLdqUNZccnzXPUTJprMSMxkstR8=',
|
||||||
|
# or
|
||||||
|
'password': vault.decrypt('passphrase'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
This will encrypt `/dev/sdb` using the specified passphrase. When the
|
||||||
|
device is going to be unlocked, it will be available as
|
||||||
|
`/dev/mapper/foobar`.
|
||||||
46
bundles/dm-crypt/items.py
Normal file
46
bundles/dm-crypt/items.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
for name, conf in node.metadata.get('dm-crypt').items():
|
||||||
|
actions[f'dm-crypt_format_{name}'] = {
|
||||||
|
'command': f"cryptsetup --batch-mode luksFormat --cipher aes-xts-plain64 --key-size 512 '{conf['device']}'",
|
||||||
|
'data_stdin': conf['password'],
|
||||||
|
'unless': f"blkid -t TYPE=crypto_LUKS '{conf['device']}'",
|
||||||
|
'comment': f"WARNING: This DESTROYS the contents of the device: '{conf['device']}'",
|
||||||
|
'needs': {
|
||||||
|
'pkg_apt:cryptsetup',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
actions[f'dm-crypt_test_{name}'] = {
|
||||||
|
'command': 'false',
|
||||||
|
'unless': f"! cryptsetup --batch-mode luksOpen --test-passphrase '{conf['device']}'",
|
||||||
|
'data_stdin': conf['password'],
|
||||||
|
'needs': {
|
||||||
|
f"action:dm-crypt_format_{name}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
actions[f'dm-crypt_open_{name}'] = {
|
||||||
|
'command': f"cryptsetup --batch-mode luksOpen '{conf['device']}' '{name}'",
|
||||||
|
'data_stdin': conf['password'],
|
||||||
|
'unless': f"test -e /dev/mapper/{name}",
|
||||||
|
'comment': f"Unlocks the device '{conf['device']}' and makes it available in: '/dev/mapper/{name}'",
|
||||||
|
'needs': {
|
||||||
|
f"action:dm-crypt_test_{name}",
|
||||||
|
},
|
||||||
|
'needed_by': set(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.has_bundle('zfs'):
|
||||||
|
for pool, pool_conf in node.metadata.get('zfs/pools').items():
|
||||||
|
if f'/dev/mapper/{name}' in pool_conf['devices']:
|
||||||
|
actions[f'dm-crypt_open_{name}']['needed_by'].add(f'zfs_pool:{pool}')
|
||||||
|
|
||||||
|
actions[f'zpool_import_{name}'] = {
|
||||||
|
'command': f"zpool import -d /dev/mapper/{name} {pool}",
|
||||||
|
'unless': f"zpool status {pool}",
|
||||||
|
'needs': {
|
||||||
|
f"action:dm-crypt_open_{name}",
|
||||||
|
},
|
||||||
|
'needed_by': {
|
||||||
|
f"zfs_pool:{pool}",
|
||||||
|
},
|
||||||
|
}
|
||||||
22
bundles/dm-crypt/metadata.py
Normal file
22
bundles/dm-crypt/metadata.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'cryptsetup': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'dm-crypt': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'dm-crypt',
|
||||||
|
)
|
||||||
|
def password_from_salt(metadata):
|
||||||
|
return {
|
||||||
|
'dm-crypt': {
|
||||||
|
name: {
|
||||||
|
'password': repo.vault.password_for(f"dm-crypt/{metadata.get('id')}/{name}"),
|
||||||
|
}
|
||||||
|
for name, conf in metadata.get('dm-crypt').items()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
DOVECOT
|
DOVECOT
|
||||||
=======
|
=======
|
||||||
|
|
||||||
rescan index: https://doc.dovecot.org/configuration_manual/fts/#rescan
|
rescan index
|
||||||
|
------------
|
||||||
|
|
||||||
|
https://doc.dovecot.org/configuration_manual/fts/#rescan
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo -u vmail doveadm fts rescan -u 'test@mail2.sublimity.de'
|
doveadm fts rescan -u 'i@ckn.li'
|
||||||
sudo -u vmail doveadm index -u 'test@mail2.sublimity.de' -q '*'
|
doveadm index -u 'i@ckn.li' -q '*'
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,7 @@ xmlunzip() {
|
||||||
trap "rm -rf $path $tempdir" 0 1 2 3 14 15
|
trap "rm -rf $path $tempdir" 0 1 2 3 14 15
|
||||||
cd $tempdir || exit 1
|
cd $tempdir || exit 1
|
||||||
unzip -q "$path" 2>/dev/null || exit 0
|
unzip -q "$path" 2>/dev/null || exit 0
|
||||||
find . -name "$name" -print0 | xargs -0 cat |
|
find . -name "$name" -print0 | xargs -0 cat | /usr/lib/dovecot/xml2text
|
||||||
$libexec_dir/xml2text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_timeout() {
|
wait_timeout() {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
connect = host=${host} dbname=${name} user=${user} password=${password}
|
|
||||||
driver = pgsql
|
|
||||||
default_pass_scheme = ARGON2ID
|
|
||||||
|
|
||||||
password_query = SELECT CONCAT(users.name, '@', domains.name) AS user, password\
|
|
||||||
FROM users \
|
|
||||||
LEFT JOIN domains ON users.domain_id = domains.id \
|
|
||||||
WHERE redirect IS NULL \
|
|
||||||
AND users.name = SPLIT_PART('%u', '@', 1) \
|
|
||||||
AND domains.name = SPLIT_PART('%u', '@', 2)
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
|
dovecot_config_version = ${config_version}
|
||||||
|
dovecot_storage_version = ${storage_version}
|
||||||
|
|
||||||
protocols = imap lmtp sieve
|
protocols = imap lmtp sieve
|
||||||
auth_mechanisms = plain login
|
auth_mechanisms = plain login
|
||||||
mail_privileged_group = mail
|
|
||||||
ssl = required
|
ssl = required
|
||||||
ssl_cert = </var/lib/dehydrated/certs/${node.metadata.get('mailserver/hostname')}/fullchain.pem
|
ssl_server_cert_file = /var/lib/dehydrated/certs/${hostname}/fullchain.pem
|
||||||
ssl_key = </var/lib/dehydrated/certs/${node.metadata.get('mailserver/hostname')}/privkey.pem
|
ssl_server_key_file = /var/lib/dehydrated/certs/${hostname}/privkey.pem
|
||||||
ssl_dh = </etc/dovecot/dhparam.pem
|
ssl_server_dh_file = /etc/dovecot/dhparam.pem
|
||||||
ssl_client_ca_dir = /etc/ssl/certs
|
ssl_client_ca_dir = /etc/ssl/certs
|
||||||
mail_location = maildir:~
|
mail_driver = maildir
|
||||||
mail_plugins = fts fts_xapian
|
mail_path = ${maildir}/%{user}
|
||||||
|
mail_index_path = ${maildir}/index/%{user}
|
||||||
|
mail_plugins = fts fts_flatcurve
|
||||||
|
|
||||||
namespace inbox {
|
namespace inbox {
|
||||||
inbox = yes
|
inbox = yes
|
||||||
|
|
@ -30,13 +34,46 @@ namespace inbox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
passdb {
|
# postgres passdb userdb
|
||||||
driver = sql
|
|
||||||
args = /etc/dovecot/dovecot-sql.conf
|
sql_driver = pgsql
|
||||||
|
|
||||||
|
pgsql main {
|
||||||
|
parameters {
|
||||||
|
host = ${db_host}
|
||||||
|
dbname = ${db_name}
|
||||||
|
user = ${db_user}
|
||||||
|
password = ${db_password}
|
||||||
}
|
}
|
||||||
userdb {
|
}
|
||||||
driver = static
|
|
||||||
args = uid=vmail gid=vmail home=/var/vmail/%u
|
passdb sql {
|
||||||
|
passdb_default_password_scheme = ARGON2ID
|
||||||
|
|
||||||
|
query = SELECT \
|
||||||
|
CONCAT(users.name, '@', domains.name) AS "user", \
|
||||||
|
password \
|
||||||
|
FROM users \
|
||||||
|
LEFT JOIN domains ON users.domain_id = domains.id \
|
||||||
|
WHERE redirect IS NULL \
|
||||||
|
AND users.name = SPLIT_PART('%{user}', '@', 1) \
|
||||||
|
AND domains.name = SPLIT_PART('%{user}', '@', 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_uid = vmail
|
||||||
|
mail_gid = vmail
|
||||||
|
|
||||||
|
userdb sql {
|
||||||
|
query = SELECT \
|
||||||
|
'/var/vmail/%{user}' AS home, \
|
||||||
|
'vmail' AS uid, \
|
||||||
|
'vmail' AS gid
|
||||||
|
|
||||||
|
iterate_query = SELECT \
|
||||||
|
CONCAT(users.name, '@', domains.name) AS username \
|
||||||
|
FROM users \
|
||||||
|
LEFT JOIN domains ON users.domain_id = domains.id \
|
||||||
|
WHERE redirect IS NULL
|
||||||
}
|
}
|
||||||
|
|
||||||
service auth {
|
service auth {
|
||||||
|
|
@ -66,10 +103,9 @@ service stats {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
service managesieve-login {
|
service managesieve-login {
|
||||||
inet_listener sieve {
|
#inet_listener sieve {}
|
||||||
}
|
process_min_avail = 1
|
||||||
process_min_avail = 0
|
process_limit = 1
|
||||||
service_count = 1
|
|
||||||
vsz_limit = 64 M
|
vsz_limit = 64 M
|
||||||
}
|
}
|
||||||
service managesieve {
|
service managesieve {
|
||||||
|
|
@ -77,31 +113,53 @@ service managesieve {
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol imap {
|
protocol imap {
|
||||||
mail_plugins = $mail_plugins imap_sieve
|
mail_plugins = fts fts_flatcurve imap_sieve
|
||||||
mail_max_userip_connections = 50
|
mail_max_userip_connections = 50
|
||||||
imap_idle_notify_interval = 29 mins
|
imap_idle_notify_interval = 29 mins
|
||||||
}
|
}
|
||||||
protocol lmtp {
|
protocol lmtp {
|
||||||
mail_plugins = $mail_plugins sieve
|
mail_plugins = fts fts_flatcurve sieve
|
||||||
}
|
}
|
||||||
protocol sieve {
|
|
||||||
plugin {
|
# Persönliches Skript (deine alte Datei /var/vmail/sieve/%u.sieve)
|
||||||
sieve = /var/vmail/sieve/%u.sieve
|
sieve_script personal {
|
||||||
sieve_storage = /var/vmail/sieve/%u/
|
driver = file
|
||||||
|
# Verzeichnis mit (evtl. mehreren) Sieve-Skripten des Users
|
||||||
|
path = /var/vmail/sieve/%{user}/
|
||||||
|
# Aktives Skript (entspricht früher "sieve = /var/vmail/sieve/%u.sieve")
|
||||||
|
active_path = /var/vmail/sieve/%{user}.sieve
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Globales After-Skript (dein früheres "sieve_after = …")
|
||||||
|
sieve_script after {
|
||||||
|
type = after
|
||||||
|
driver = file
|
||||||
|
path = /var/vmail/sieve/global/spam-to-folder.sieve
|
||||||
}
|
}
|
||||||
|
|
||||||
# fulltext search
|
# fulltext search
|
||||||
plugin {
|
language en {
|
||||||
fts = xapian
|
|
||||||
fts_xapian = partial=3 full=20 verbose=0
|
|
||||||
fts_autoindex = yes
|
|
||||||
fts_enforced = yes
|
|
||||||
# Index attachements
|
|
||||||
fts_decoder = decode2text
|
|
||||||
}
|
}
|
||||||
|
language de {
|
||||||
|
default = yes
|
||||||
|
}
|
||||||
|
language_tokenizers = generic email-address
|
||||||
|
|
||||||
|
fts flatcurve {
|
||||||
|
substring_search = yes
|
||||||
|
# rotate_count = 5000 # DB-Rotation nach X Mails
|
||||||
|
# rotate_time = 5s # oder zeitbasiert rotieren
|
||||||
|
# optimize_limit = 10
|
||||||
|
# min_term_size = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
fts_autoindex = yes
|
||||||
|
fts_decoder_driver = script
|
||||||
|
fts_decoder_script_socket_path = decode2text
|
||||||
|
|
||||||
service indexer-worker {
|
service indexer-worker {
|
||||||
vsz_limit = ${indexer_ram}
|
process_limit = ${indexer_cores}
|
||||||
|
vsz_limit = ${indexer_ram}M
|
||||||
}
|
}
|
||||||
service decode2text {
|
service decode2text {
|
||||||
executable = script /usr/local/libexec/dovecot/decode2text.sh
|
executable = script /usr/local/libexec/dovecot/decode2text.sh
|
||||||
|
|
@ -111,24 +169,39 @@ service decode2text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# spam filter
|
mailbox Junk {
|
||||||
plugin {
|
sieve_script learn_spam {
|
||||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
driver = file
|
||||||
sieve_dir = /var/vmail/sieve/%u/
|
type = before
|
||||||
sieve = /var/vmail/sieve/%u.sieve
|
cause = copy
|
||||||
sieve_pipe_bin_dir = /var/vmail/sieve/
|
path = /var/vmail/sieve/global/learn-spam.sieve
|
||||||
sieve_extensions = +vnd.dovecot.pipe
|
|
||||||
|
|
||||||
sieve_before = /var/vmail/sieve/global/spam-global.sieve
|
|
||||||
|
|
||||||
# From elsewhere to Spam folder
|
|
||||||
imapsieve_mailbox1_name = Junk
|
|
||||||
imapsieve_mailbox1_causes = COPY
|
|
||||||
imapsieve_mailbox1_before = file:/var/vmail/sieve/global/learn-spam.sieve
|
|
||||||
|
|
||||||
# From Spam folder to elsewhere
|
|
||||||
imapsieve_mailbox2_name = *
|
|
||||||
imapsieve_mailbox2_from = Junk
|
|
||||||
imapsieve_mailbox2_causes = COPY
|
|
||||||
imapsieve_mailbox2_before = file:/var/vmail/sieve/global/learn-ham.sieve
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imapsieve_from Junk {
|
||||||
|
sieve_script learn_ham {
|
||||||
|
driver = file
|
||||||
|
type = before
|
||||||
|
cause = copy
|
||||||
|
path = /var/vmail/sieve/global/learn-ham.sieve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extprograms-Plugin einschalten
|
||||||
|
sieve_plugins {
|
||||||
|
sieve_extprograms = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
# Welche Sieve-Erweiterungen dürfen genutzt werden?
|
||||||
|
# Empfehlung: nur global erlauben (nicht in User-Skripten):
|
||||||
|
sieve_global_extensions {
|
||||||
|
vnd.dovecot.pipe = yes
|
||||||
|
# vnd.dovecot.filter = yes # nur falls gebraucht
|
||||||
|
# vnd.dovecot.execute = yes # nur falls gebraucht
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verzeichnis mit deinen Skripten/Binaries für :pipe
|
||||||
|
sieve_pipe_bin_dir = /var/vmail/sieve/bin
|
||||||
|
# (optional, analog für :filter / :execute)
|
||||||
|
# sieve_filter_bin_dir = /var/vmail/sieve/filter
|
||||||
|
# sieve_execute_bin_dir = /var/vmail/sieve/execute
|
||||||
2
bundles/dovecot/files/learn-ham.sh
Normal file
2
bundles/dovecot/files/learn-ham.sh
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec /usr/bin/rspamc learn_ham
|
||||||
|
|
@ -4,4 +4,4 @@ if string "${mailbox}" "Trash" {
|
||||||
stop;
|
stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
pipe :copy "rspamd-learn-ham.sh";
|
pipe :copy "learn-ham.sh";
|
||||||
|
|
|
||||||
2
bundles/dovecot/files/learn-spam.sh
Normal file
2
bundles/dovecot/files/learn-spam.sh
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec /usr/bin/rspamc learn_spam
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
require ["vnd.dovecot.pipe", "copy", "imapsieve"];
|
require ["vnd.dovecot.pipe", "copy", "imapsieve"];
|
||||||
|
|
||||||
pipe :copy "rspamd-learn-spam.sh";
|
pipe :copy "learn-spam.sh";
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/doveadm pw -s ARGON2ID
|
|
||||||
|
|
@ -19,7 +19,23 @@ directories = {
|
||||||
'/var/vmail': {
|
'/var/vmail': {
|
||||||
'owner': 'vmail',
|
'owner': 'vmail',
|
||||||
'group': 'vmail',
|
'group': 'vmail',
|
||||||
}
|
},
|
||||||
|
'/var/vmail/index': {
|
||||||
|
'owner': 'vmail',
|
||||||
|
'group': 'vmail',
|
||||||
|
},
|
||||||
|
'/var/vmail/sieve': {
|
||||||
|
'owner': 'vmail',
|
||||||
|
'group': 'vmail',
|
||||||
|
},
|
||||||
|
'/var/vmail/sieve/global': {
|
||||||
|
'owner': 'vmail',
|
||||||
|
'group': 'vmail',
|
||||||
|
},
|
||||||
|
'/var/vmail/sieve/bin': {
|
||||||
|
'owner': 'vmail',
|
||||||
|
'group': 'vmail',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
files = {
|
files = {
|
||||||
|
|
@ -28,6 +44,16 @@ files = {
|
||||||
'context': {
|
'context': {
|
||||||
'admin_email': node.metadata.get('mailserver/admin_email'),
|
'admin_email': node.metadata.get('mailserver/admin_email'),
|
||||||
'indexer_ram': node.metadata.get('dovecot/indexer_ram'),
|
'indexer_ram': node.metadata.get('dovecot/indexer_ram'),
|
||||||
|
'config_version': node.metadata.get('dovecot/config_version'),
|
||||||
|
'storage_version': node.metadata.get('dovecot/storage_version'),
|
||||||
|
'maildir': node.metadata.get('mailserver/maildir'),
|
||||||
|
'hostname': node.metadata.get('mailserver/hostname'),
|
||||||
|
'db_host': node.metadata.get('mailserver/database/host'),
|
||||||
|
'db_name': node.metadata.get('mailserver/database/name'),
|
||||||
|
'db_user': node.metadata.get('mailserver/database/user'),
|
||||||
|
'db_password': node.metadata.get('mailserver/database/password'),
|
||||||
|
'indexer_cores': node.metadata.get('vm/cores'),
|
||||||
|
'indexer_ram': node.metadata.get('vm/ram')//2,
|
||||||
},
|
},
|
||||||
'needs': {
|
'needs': {
|
||||||
'pkg_apt:'
|
'pkg_apt:'
|
||||||
|
|
@ -36,40 +62,41 @@ files = {
|
||||||
'svc_systemd:dovecot:restart',
|
'svc_systemd:dovecot:restart',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/etc/dovecot/dovecot-sql.conf': {
|
|
||||||
'content_type': 'mako',
|
|
||||||
'context': node.metadata.get('mailserver/database'),
|
|
||||||
'needs': {
|
|
||||||
'pkg_apt:'
|
|
||||||
},
|
|
||||||
'triggers': {
|
|
||||||
'svc_systemd:dovecot:restart',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'/etc/dovecot/dhparam.pem': {
|
'/etc/dovecot/dhparam.pem': {
|
||||||
'content_type': 'any',
|
'content_type': 'any',
|
||||||
},
|
},
|
||||||
'/etc/dovecot/dovecot-sql.conf': {
|
'/var/vmail/sieve/global/spam-to-folder.sieve': {
|
||||||
'content_type': 'mako',
|
'owner': 'vmail',
|
||||||
'context': node.metadata.get('mailserver/database'),
|
'group': 'vmail',
|
||||||
'needs': {
|
|
||||||
'pkg_apt:'
|
|
||||||
},
|
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'svc_systemd:dovecot:restart',
|
'svc_systemd:dovecot:restart',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/var/mail/vmail/sieve/global/learn-ham.sieve': {
|
'/var/vmail/sieve/global/learn-ham.sieve': {
|
||||||
'owner': 'nobody',
|
'owner': 'vmail',
|
||||||
'group': 'nogroup',
|
'group': 'vmail',
|
||||||
|
'triggers': {
|
||||||
|
'svc_systemd:dovecot:restart',
|
||||||
},
|
},
|
||||||
'/var/mail/vmail/sieve/global/learn-spam.sieve': {
|
|
||||||
'owner': 'nobody',
|
|
||||||
'group': 'nogroup',
|
|
||||||
},
|
},
|
||||||
'/var/mail/vmail/sieve/global/spam-global.sieve': {
|
'/var/vmail/sieve/bin/learn-ham.sh': {
|
||||||
'owner': 'nobody',
|
'owner': 'vmail',
|
||||||
'group': 'nogroup',
|
'group': 'vmail',
|
||||||
|
'mode': '550',
|
||||||
|
},
|
||||||
|
'/var/vmail/sieve/global/learn-spam.sieve': {
|
||||||
|
'owner': 'vmail',
|
||||||
|
'group': 'vmail',
|
||||||
|
'triggers': {
|
||||||
|
'svc_systemd:dovecot:restart',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
# /usr/local/libexec/dovecot?
|
||||||
|
# /usr/lib/dovecot/sieve-pipe?
|
||||||
|
'/var/vmail/sieve/bin/learn-spam.sh': {
|
||||||
|
'owner': 'vmail',
|
||||||
|
'group': 'vmail',
|
||||||
|
'mode': '550',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +121,6 @@ svc_systemd = {
|
||||||
'action:letsencrypt_update_certificates',
|
'action:letsencrypt_update_certificates',
|
||||||
'action:dovecot_generate_dhparam',
|
'action:dovecot_generate_dhparam',
|
||||||
'file:/etc/dovecot/dovecot.conf',
|
'file:/etc/dovecot/dovecot.conf',
|
||||||
'file:/etc/dovecot/dovecot-sql.conf',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,31 @@ defaults = {
|
||||||
'dovecot-sieve': {},
|
'dovecot-sieve': {},
|
||||||
'dovecot-managesieved': {},
|
'dovecot-managesieved': {},
|
||||||
# fulltext search
|
# fulltext search
|
||||||
'dovecot-fts-xapian': {}, # buster-backports
|
'dovecot-flatcurve': {}, # buster-backports
|
||||||
'poppler-utils': {}, # pdftotext
|
'poppler-utils': {}, # pdftotext
|
||||||
'catdoc': {}, # catdoc, catppt, xls2csv
|
'catdoc': {}, # catdoc, catppt, xls2csv
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'dovecot': {
|
||||||
|
'database': {
|
||||||
|
'dbname': 'mailserver',
|
||||||
|
'dbuser': 'mailserver',
|
||||||
|
},
|
||||||
|
},
|
||||||
'letsencrypt': {
|
'letsencrypt': {
|
||||||
'reload_after': {
|
'reload_after': {
|
||||||
'dovecot',
|
'dovecot',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'dovecot': {
|
'nftables': {
|
||||||
'database': {
|
'input': {
|
||||||
'dbname': 'mailserver',
|
'tcp dport {143, 993, 4190} accept',
|
||||||
'dbuser': 'mailserver',
|
},
|
||||||
|
},
|
||||||
|
'systemd-timers': {
|
||||||
|
'dovecot-optimize-index': {
|
||||||
|
'command': '/usr/bin/doveadm fts optimize -A',
|
||||||
|
'when': 'daily',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
bundles/download-server/metadata.py
Normal file
66
bundles/download-server/metadata.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
defaults = {
|
||||||
|
'users': {
|
||||||
|
'downloads': {
|
||||||
|
'home': '/var/lib/downloads',
|
||||||
|
'needs': {
|
||||||
|
'zfs_dataset:tank/downloads'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'zfs': {
|
||||||
|
'datasets': {
|
||||||
|
'tank/downloads': {
|
||||||
|
'mountpoint': '/var/lib/downloads',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'systemd-mount'
|
||||||
|
)
|
||||||
|
def mount_certs(metadata):
|
||||||
|
return {
|
||||||
|
'systemd-mount': {
|
||||||
|
'/var/lib/downloads_nginx': {
|
||||||
|
'source': '/var/lib/downloads',
|
||||||
|
'user': 'www-data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'nginx/vhosts',
|
||||||
|
)
|
||||||
|
def nginx(metadata):
|
||||||
|
return {
|
||||||
|
'nginx': {
|
||||||
|
'vhosts': {
|
||||||
|
metadata.get('download-server/hostname'): {
|
||||||
|
'content': 'nginx/directory_listing.conf',
|
||||||
|
'context': {
|
||||||
|
'directory': '/var/lib/downloads_nginx',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'users/downloads/authorized_users',
|
||||||
|
)
|
||||||
|
def ssh_keys(metadata):
|
||||||
|
return {
|
||||||
|
'users': {
|
||||||
|
'downloads': {
|
||||||
|
'authorized_users': {
|
||||||
|
f'build-server@{other_node.name}'
|
||||||
|
for other_node in repo.nodes
|
||||||
|
if other_node.has_bundle('build-server')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
54
bundles/flask/README.md
Normal file
54
bundles/flask/README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Flask
|
||||||
|
|
||||||
|
This bundle can deploy one or more Flask applications per node.
|
||||||
|
|
||||||
|
```python
|
||||||
|
'flask': {
|
||||||
|
'myapp': {
|
||||||
|
'app_module': "myapp",
|
||||||
|
'apt_dependencies': [
|
||||||
|
"libffi-dev",
|
||||||
|
"libssl-dev",
|
||||||
|
],
|
||||||
|
'env': {
|
||||||
|
'APP_SECRETS': "/opt/client_secrets.json",
|
||||||
|
},
|
||||||
|
'json_config': {
|
||||||
|
'this json': 'is_visible',
|
||||||
|
'inside': 'your template.cfg',
|
||||||
|
},
|
||||||
|
'git_url': "ssh://git@bitbucket.apps.seibert-media.net:7999/smedia/myapp.git",
|
||||||
|
'git_branch': "master",
|
||||||
|
'deployment_triggers': ["action:do-a-thing"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The git repo containing the application has to obey some conventions:
|
||||||
|
|
||||||
|
* requirements-frozen.txt (preferred) or requirements.txt
|
||||||
|
* minimal setup.py to allow for installation with pip
|
||||||
|
|
||||||
|
The `app` instance has to exists in the module defined by `app_module`.
|
||||||
|
|
||||||
|
It is also very advisable to enable logging in your app (otherwise HTTP 500s won't be logged):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
|
||||||
|
if not app.debug:
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
stream_handler.setLevel(logging.INFO)
|
||||||
|
app.logger.addHandler(stream_handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you specify `json_config`, then `/opt/${app}/config.json` will be
|
||||||
|
created. The environment variable `$APP_CONFIG` will point to the exact
|
||||||
|
name. You can use it in your app to load your config:
|
||||||
|
|
||||||
|
```python
|
||||||
|
app.config.from_json(environ['APP_CONFIG'])
|
||||||
|
```
|
||||||
|
|
||||||
|
If `json_config` is *not* specified, you *can* put a static file in
|
||||||
|
`data/flask/files/cfg/$app_name`.
|
||||||
10
bundles/flask/files/flask.cfg
Normal file
10
bundles/flask/files/flask.cfg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<%
|
||||||
|
from json import dumps
|
||||||
|
from bundlewrap.metadata import MetadataJSONEncoder
|
||||||
|
%>
|
||||||
|
${dumps(
|
||||||
|
json_config,
|
||||||
|
cls=MetadataJSONEncoder,
|
||||||
|
indent=4,
|
||||||
|
sort_keys=True,
|
||||||
|
)}
|
||||||
14
bundles/flask/files/flask.service
Normal file
14
bundles/flask/files/flask.service
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=flask application ${name}
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
% for key, value in env.items():
|
||||||
|
Environment=${key}=${value}
|
||||||
|
% endfor
|
||||||
|
User=${user}
|
||||||
|
Group=${group}
|
||||||
|
ExecStart=/opt/${name}/venv/bin/gunicorn -w ${workers} -b ${host}:${port} ${app_module}:app
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
119
bundles/flask/items.py
Normal file
119
bundles/flask/items.py
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
for name, conf in node.metadata.get('flask').items():
|
||||||
|
for dep in conf.get('apt_dependencies', []):
|
||||||
|
pkg_apt[dep] = {
|
||||||
|
'needed_by': {
|
||||||
|
f'svc_systemd:{name}',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
directories[f'/opt/{name}'] = {
|
||||||
|
'owner': conf['user'],
|
||||||
|
'group': conf['group'],
|
||||||
|
}
|
||||||
|
directories[f'/opt/{name}/src'] = {}
|
||||||
|
|
||||||
|
git_deploy[f'/opt/{name}/src'] = {
|
||||||
|
'repo': conf['git_url'],
|
||||||
|
'rev': conf.get('git_branch', 'master'),
|
||||||
|
'triggers': [
|
||||||
|
f'action:flask_{name}_pip_install_deps',
|
||||||
|
*conf.get('deployment_triggers', []),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# CONFIG
|
||||||
|
|
||||||
|
env = conf.get('env', {})
|
||||||
|
|
||||||
|
if conf.get('json_config', {}):
|
||||||
|
env['APP_CONFIG'] = f'/opt/{name}/config.json'
|
||||||
|
files[env['APP_CONFIG']] = {
|
||||||
|
'source': 'flask.cfg',
|
||||||
|
'context': {
|
||||||
|
'json_config': conf.get('json_config', {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'APP_CONFIG' in env:
|
||||||
|
files[env['APP_CONFIG']].update({
|
||||||
|
'content_type': 'mako',
|
||||||
|
'group': 'www-data',
|
||||||
|
'needed_by': [
|
||||||
|
f'svc_systemd:{name}',
|
||||||
|
],
|
||||||
|
'triggers': [
|
||||||
|
f'svc_systemd:{name}:restart',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
# secrets
|
||||||
|
|
||||||
|
if 'secrets.json' in conf:
|
||||||
|
env['APP_SECRETS'] = f'/opt/{name}/secrets.json'
|
||||||
|
files[env['APP_SECRETS']] = {
|
||||||
|
'content': conf['secrets.json'],
|
||||||
|
'mode': '0600',
|
||||||
|
'owner': conf.get('user', 'www-data'),
|
||||||
|
'group': conf.get('group', 'www-data'),
|
||||||
|
'needed_by': [
|
||||||
|
f'svc_systemd:{name}',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# VENV
|
||||||
|
|
||||||
|
actions[f'flask_{name}_create_virtualenv'] = {
|
||||||
|
'cascade_skip': False,
|
||||||
|
'command': f'python3 -m venv /opt/{name}/venv',
|
||||||
|
'unless': f'test -d /opt/{name}/venv',
|
||||||
|
'needs': [
|
||||||
|
f'directory:/opt/{name}',
|
||||||
|
'pkg_apt:python3-venv',
|
||||||
|
],
|
||||||
|
'triggers': [
|
||||||
|
f'action:flask_{name}_pip_install_deps',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
actions[f'flask_{name}_pip_install_deps'] = {
|
||||||
|
'cascade_skip': False,
|
||||||
|
'command': f'/opt/{name}/venv/bin/pip3 install -r /opt/{name}/src/requirements-frozen.txt || /opt/{name}/venv/bin/pip3 install -r /opt/{name}/src/requirements.txt',
|
||||||
|
'triggered': True, # TODO: https://stackoverflow.com/questions/16294819/check-if-my-python-has-all-required-packages
|
||||||
|
'needs': [
|
||||||
|
f'git_deploy:/opt/{name}/src',
|
||||||
|
'pkg_apt:python3-pip',
|
||||||
|
],
|
||||||
|
'triggers': [
|
||||||
|
f'action:flask_{name}_pip_install_gunicorn',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
actions[f'flask_{name}_pip_install_gunicorn'] = {
|
||||||
|
'command': f'/opt/{name}/venv/bin/pip3 install -U gunicorn',
|
||||||
|
'triggered': True,
|
||||||
|
'cascade_skip': False,
|
||||||
|
'needs': [
|
||||||
|
f'action:flask_{name}_create_virtualenv',
|
||||||
|
],
|
||||||
|
'triggers': [
|
||||||
|
f'action:flask_{name}_pip_install',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
actions[f'flask_{name}_pip_install'] = {
|
||||||
|
'command': f'/opt/{name}/venv/bin/pip3 install -e /opt/{name}/src',
|
||||||
|
'triggered': True,
|
||||||
|
'cascade_skip': False,
|
||||||
|
'triggers': [
|
||||||
|
f'svc_systemd:{name}:restart',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# UNIT
|
||||||
|
|
||||||
|
svc_systemd[name] = {
|
||||||
|
'needs': [
|
||||||
|
f'action:flask_{name}_pip_install',
|
||||||
|
f'file:/usr/local/lib/systemd/system/{name}.service',
|
||||||
|
],
|
||||||
|
}
|
||||||
61
bundles/flask/metadata.py
Normal file
61
bundles/flask/metadata.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'python3-pip': {},
|
||||||
|
'python3-dev': {},
|
||||||
|
'python3-venv': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'flask': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'flask',
|
||||||
|
)
|
||||||
|
def app_defaults(metadata):
|
||||||
|
return {
|
||||||
|
'flask': {
|
||||||
|
name: {
|
||||||
|
'user': 'root',
|
||||||
|
'group': 'root',
|
||||||
|
'workers': 8,
|
||||||
|
'timeout': 30,
|
||||||
|
**conf,
|
||||||
|
}
|
||||||
|
for name, conf in metadata.get('flask').items()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'systemd/units',
|
||||||
|
)
|
||||||
|
def units(metadata):
|
||||||
|
return {
|
||||||
|
'systemd': {
|
||||||
|
'units': {
|
||||||
|
f'{name}.service': {
|
||||||
|
'Unit': {
|
||||||
|
'Description': name,
|
||||||
|
'After': 'network.target',
|
||||||
|
},
|
||||||
|
'Service': {
|
||||||
|
'Environment': {
|
||||||
|
f'{k}={v}'
|
||||||
|
for k, v in conf.get('env', {}).items()
|
||||||
|
},
|
||||||
|
'User': conf['user'],
|
||||||
|
'Group': conf['group'],
|
||||||
|
'ExecStart': f"/opt/{name}/venv/bin/gunicorn -w {conf['workers']} -b 127.0.0.1:{conf['port']} --timeout {conf['timeout']} {conf['app_module']}:app"
|
||||||
|
},
|
||||||
|
'Install': {
|
||||||
|
'WantedBy': {
|
||||||
|
'multi-user.target'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, conf in metadata.get('flask').items()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
bundles/freescout/README.md
Normal file
23
bundles/freescout/README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
Pg Pass workaround: set manually:
|
||||||
|
|
||||||
|
```
|
||||||
|
root@freescout /ro psql freescout
|
||||||
|
psql (15.6 (Debian 15.6-0+deb12u1))
|
||||||
|
Type "help" for help.
|
||||||
|
|
||||||
|
freescout=# \password freescout
|
||||||
|
Enter new password for user "freescout":
|
||||||
|
Enter it again:
|
||||||
|
freescout=#
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# problems
|
||||||
|
|
||||||
|
# check if /opt/freescout/.env is resettet
|
||||||
|
# ckeck `psql -h localhost -d freescout -U freescout -W`with pw from .env
|
||||||
|
# chown -R www-data:www-data /opt/freescout
|
||||||
|
# sudo su - www-data -c 'php /opt/freescout/artisan freescout:clear-cache' -s /bin/bash
|
||||||
|
# javascript funny? `sudo su - www-data -c 'php /opt/freescout/artisan storage:link' -s /bin/bash`
|
||||||
|
# benutzer bilder weg? aus dem backup holen: `/opt/freescout/.zfs/snapshot/zfs-auto-snap_hourly-2024-11-22-1700/storage/app/public/users` `./customers`
|
||||||
66
bundles/freescout/items.py
Normal file
66
bundles/freescout/items.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide
|
||||||
|
run_as = repo.libs.tools.run_as
|
||||||
|
php_version = node.metadata.get('php/version')
|
||||||
|
|
||||||
|
|
||||||
|
directories = {
|
||||||
|
'/opt/freescout': {
|
||||||
|
'owner': 'www-data',
|
||||||
|
'group': 'www-data',
|
||||||
|
# chown -R www-data:www-data /opt/freescout
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
actions = {
|
||||||
|
# 'clone_freescout': {
|
||||||
|
# 'command': run_as('www-data', 'git clone https://github.com/freescout-helpdesk/freescout.git /opt/freescout'),
|
||||||
|
# 'unless': 'test -e /opt/freescout/.git',
|
||||||
|
# 'needs': [
|
||||||
|
# 'pkg_apt:git',
|
||||||
|
# 'directory:/opt/freescout',
|
||||||
|
# ],
|
||||||
|
# },
|
||||||
|
# 'pull_freescout': {
|
||||||
|
# 'command': run_as('www-data', 'git -C /opt/freescout fetch origin dist && git -C /opt/freescout reset --hard origin/dist && git -C /opt/freescout clean -f'),
|
||||||
|
# 'unless': run_as('www-data', 'git -C /opt/freescout fetch origin && git -C /opt/freescout status -uno | grep -q "Your branch is up to date"'),
|
||||||
|
# 'needs': [
|
||||||
|
# 'action:clone_freescout',
|
||||||
|
# ],
|
||||||
|
# 'triggers': [
|
||||||
|
# 'action:freescout_artisan_update',
|
||||||
|
# f'svc_systemd:php{php_version}-fpm.service:restart',
|
||||||
|
# ],
|
||||||
|
# },
|
||||||
|
# 'freescout_artisan_update': {
|
||||||
|
# 'command': run_as('www-data', 'php /opt/freescout/artisan freescout:after-app-update'),
|
||||||
|
# 'triggered': True,
|
||||||
|
# 'needs': [
|
||||||
|
# f'svc_systemd:php{php_version}-fpm.service:restart',
|
||||||
|
# 'action:pull_freescout',
|
||||||
|
# ],
|
||||||
|
# },
|
||||||
|
}
|
||||||
|
|
||||||
|
# svc_systemd = {
|
||||||
|
# f'freescout-cron.service': {},
|
||||||
|
# }
|
||||||
|
|
||||||
|
# files = {
|
||||||
|
# '/opt/freescout/.env': {
|
||||||
|
# # https://github.com/freescout-helpdesk/freescout/blob/dist/.env.example
|
||||||
|
# # Every time you are making changes in .env file, in order changes to take an effect you need to run:
|
||||||
|
# # ´sudo su - www-data -c 'php /opt/freescout/artisan freescout:clear-cache' -s /bin/bash´
|
||||||
|
# 'owner': 'www-data',
|
||||||
|
# 'content': '\n'.join(
|
||||||
|
# f'{k}={v}' for k, v in
|
||||||
|
# sorted(node.metadata.get('freescout/env').items())
|
||||||
|
# ) + '\n',
|
||||||
|
# 'needs': [
|
||||||
|
# 'directory:/opt/freescout',
|
||||||
|
# 'action:clone_freescout',
|
||||||
|
# ],
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
|
||||||
|
#sudo su - www-data -s /bin/bash -c 'php /opt/freescout/artisan freescout:create-user --role admin --firstName M --lastName W --email freescout@freibrief.net --password gyh.jzv2bnf6hvc.HKG --no-interaction'
|
||||||
|
#sudo su - www-data -s /bin/bash -c 'php /opt/freescout/artisan freescout:create-user --role admin --firstName M --lastName W --email freescout@freibrief.net --password gyh.jzv2bnf6hvc.HKG --no-interaction'
|
||||||
121
bundles/freescout/metadata.py
Normal file
121
bundles/freescout/metadata.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
from base64 import b64decode
|
||||||
|
|
||||||
|
# hash: SCRAM-SHA-256$4096:tQNfqQi7seqNDwJdHqCHbg==$r3ibECluHJaY6VRwpvPqrtCjgrEK7lAkgtUO8/tllTU=:+eeo4M0L2SowfyHFxT2FRqGzezve4ZOEocSIo11DATA=
|
||||||
|
database_password = repo.vault.password_for(f'{node.name} postgresql freescout').value
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'git': {},
|
||||||
|
'php': {},
|
||||||
|
'php-pgsql': {},
|
||||||
|
'php-fpm': {},
|
||||||
|
'php-mbstring': {},
|
||||||
|
'php-xml': {},
|
||||||
|
'php-imap': {},
|
||||||
|
'php-zip': {},
|
||||||
|
'php-gd': {},
|
||||||
|
'php-curl': {},
|
||||||
|
'php-intl': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'freescout': {
|
||||||
|
'env': {
|
||||||
|
'APP_TIMEZONE': 'Europe/Berlin',
|
||||||
|
'DB_CONNECTION': 'pgsql',
|
||||||
|
'DB_HOST': '127.0.0.1',
|
||||||
|
'DB_PORT': '5432',
|
||||||
|
'DB_DATABASE': 'freescout',
|
||||||
|
'DB_USERNAME': 'freescout',
|
||||||
|
'DB_PASSWORD': database_password,
|
||||||
|
'APP_KEY': 'base64:' + repo.vault.random_bytes_as_base64_for(f'{node.name} freescout APP_KEY', length=32).value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'php': {
|
||||||
|
'php.ini': {
|
||||||
|
'cgi': {
|
||||||
|
'fix_pathinfo': '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'postgresql': {
|
||||||
|
'roles': {
|
||||||
|
'freescout': {
|
||||||
|
'password_hash': repo.libs.postgres.generate_scram_sha_256(
|
||||||
|
database_password,
|
||||||
|
b64decode(repo.vault.random_bytes_as_base64_for(f'{node.name} postgres freescout', length=16).value.encode()),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'databases': {
|
||||||
|
'freescout': {
|
||||||
|
'owner': 'freescout',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
# 'systemd': {
|
||||||
|
# 'units': {
|
||||||
|
# f'freescout-cron.service': {
|
||||||
|
# 'Unit': {
|
||||||
|
# 'Description': 'Freescout Cron',
|
||||||
|
# 'After': 'network.target',
|
||||||
|
# },
|
||||||
|
# 'Service': {
|
||||||
|
# 'User': 'www-data',
|
||||||
|
# 'Nice': 10,
|
||||||
|
# 'ExecStart': f"/usr/bin/php /opt/freescout/artisan schedule:run"
|
||||||
|
# },
|
||||||
|
# 'Install': {
|
||||||
|
# 'WantedBy': {
|
||||||
|
# 'multi-user.target'
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
'systemd-timers': {
|
||||||
|
'freescout-cron': {
|
||||||
|
'command': '/usr/bin/php /opt/freescout/artisan schedule:run',
|
||||||
|
'when': '*-*-* *:*:00',
|
||||||
|
'RuntimeMaxSec': '180',
|
||||||
|
'user': 'www-data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'zfs': {
|
||||||
|
'datasets': {
|
||||||
|
'tank/freescout': {
|
||||||
|
'mountpoint': '/opt/freescout',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'freescout/env/APP_URL',
|
||||||
|
)
|
||||||
|
def freescout(metadata):
|
||||||
|
return {
|
||||||
|
'freescout': {
|
||||||
|
'env': {
|
||||||
|
'APP_URL': 'https://' + metadata.get('freescout/domain') + '/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'nginx/vhosts',
|
||||||
|
)
|
||||||
|
def nginx(metadata):
|
||||||
|
return {
|
||||||
|
'nginx': {
|
||||||
|
'vhosts': {
|
||||||
|
metadata.get('freescout/domain'): {
|
||||||
|
'content': 'freescout/vhost.conf',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,15 @@ defaults = {
|
||||||
'python3-crcmod': {},
|
'python3-crcmod': {},
|
||||||
},
|
},
|
||||||
'sources': {
|
'sources': {
|
||||||
'deb https://packages.cloud.google.com/apt cloud-sdk main',
|
'google-cloud': {
|
||||||
|
'url': 'https://packages.cloud.google.com/apt/',
|
||||||
|
'suites': {
|
||||||
|
'cloud-sdk',
|
||||||
|
},
|
||||||
|
'components': {
|
||||||
|
'main',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
[DEFAULT]
|
||||||
APP_NAME = ckn-gitea
|
APP_NAME = ckn-gitea
|
||||||
RUN_USER = git
|
RUN_USER = git
|
||||||
RUN_MODE = prod
|
RUN_MODE = prod
|
||||||
|
|
@ -13,40 +14,24 @@ MEMBERS_PAGING_NUM = 100
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
PROTOCOL = http
|
PROTOCOL = http
|
||||||
SSH_DOMAIN = ${domain}
|
|
||||||
DOMAIN = ${domain}
|
|
||||||
HTTP_ADDR = 0.0.0.0
|
HTTP_ADDR = 0.0.0.0
|
||||||
HTTP_PORT = 3500
|
HTTP_PORT = 3500
|
||||||
ROOT_URL = https://${domain}/
|
|
||||||
DISABLE_SSH = true
|
DISABLE_SSH = true
|
||||||
SSH_PORT = 22
|
SSH_PORT = 22
|
||||||
LFS_START_SERVER = true
|
LFS_START_SERVER = true
|
||||||
LFS_CONTENT_PATH = /var/lib/gitea/data/lfs
|
LFS_CONTENT_PATH = /var/lib/gitea/data/lfs
|
||||||
LFS_JWT_SECRET = ${lfs_secret_key}
|
|
||||||
OFFLINE_MODE = true
|
OFFLINE_MODE = true
|
||||||
START_SSH_SERVER = false
|
START_SSH_SERVER = false
|
||||||
DISABLE_ROUTER_LOG = true
|
DISABLE_ROUTER_LOG = true
|
||||||
LANDING_PAGE = explore
|
LANDING_PAGE = explore
|
||||||
|
|
||||||
[database]
|
|
||||||
DB_TYPE = postgres
|
|
||||||
HOST = ${database.get('host')}:${database.get('port')}
|
|
||||||
NAME = ${database.get('database')}
|
|
||||||
USER = ${database.get('username')}
|
|
||||||
PASSWD = ${database.get('password')}
|
|
||||||
SSL_MODE = disable
|
|
||||||
LOG_SQL = false
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
DEFAULT_EMAIL_NOTIFICATIONS = onmention
|
DEFAULT_EMAIL_NOTIFICATIONS = onmention
|
||||||
DISABLE_REGULAR_ORG_CREATION = true
|
DISABLE_REGULAR_ORG_CREATION = true
|
||||||
|
|
||||||
[security]
|
[security]
|
||||||
INTERNAL_TOKEN = ${internal_token}
|
|
||||||
INSTALL_LOCK = true
|
INSTALL_LOCK = true
|
||||||
SECRET_KEY = ${security_secret_key}
|
|
||||||
LOGIN_REMEMBER_DAYS = 30
|
LOGIN_REMEMBER_DAYS = 30
|
||||||
DISABLE_GIT_HOOKS = ${str(not enable_git_hooks).lower()}
|
|
||||||
|
|
||||||
[openid]
|
[openid]
|
||||||
ENABLE_OPENID_SIGNIN = false
|
ENABLE_OPENID_SIGNIN = false
|
||||||
|
|
@ -55,19 +40,13 @@ ENABLE_OPENID_SIGNUP = false
|
||||||
[service]
|
[service]
|
||||||
REGISTER_EMAIL_CONFIRM = true
|
REGISTER_EMAIL_CONFIRM = true
|
||||||
ENABLE_NOTIFY_MAIL = true
|
ENABLE_NOTIFY_MAIL = true
|
||||||
DISABLE_REGISTRATION = false
|
DISABLE_REGISTRATION = true
|
||||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||||
ENABLE_CAPTCHA = false
|
ENABLE_CAPTCHA = false
|
||||||
REQUIRE_SIGNIN_VIEW = false
|
REQUIRE_SIGNIN_VIEW = false
|
||||||
DEFAULT_KEEP_EMAIL_PRIVATE = true
|
DEFAULT_KEEP_EMAIL_PRIVATE = true
|
||||||
DEFAULT_ALLOW_CREATE_ORGANIZATION = false
|
DEFAULT_ALLOW_CREATE_ORGANIZATION = false
|
||||||
DEFAULT_ENABLE_TIMETRACKING = true
|
DEFAULT_ENABLE_TIMETRACKING = true
|
||||||
NO_REPLY_ADDRESS = noreply.${domain}
|
|
||||||
|
|
||||||
[mailer]
|
|
||||||
ENABLED = true
|
|
||||||
MAILER_TYPE = sendmail
|
|
||||||
FROM = "${app_name}" <noreply@${domain}>
|
|
||||||
|
|
||||||
[session]
|
[session]
|
||||||
PROVIDER = file
|
PROVIDER = file
|
||||||
|
|
@ -80,9 +59,17 @@ ENABLE_FEDERATED_AVATAR = false
|
||||||
MODE = console
|
MODE = console
|
||||||
LEVEL = warn
|
LEVEL = warn
|
||||||
|
|
||||||
[oauth2]
|
|
||||||
JWT_SECRET = ${oauth_secret_key}
|
|
||||||
|
|
||||||
[other]
|
[other]
|
||||||
SHOW_FOOTER_BRANDING = true
|
SHOW_FOOTER_BRANDING = true
|
||||||
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false
|
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false
|
||||||
|
|
||||||
|
[webhook]
|
||||||
|
ALLOWED_HOST_LIST = *
|
||||||
|
DELIVER_TIMEOUT = 600
|
||||||
|
|
||||||
|
[indexer]
|
||||||
|
REPO_INDEXER_ENABLED = true
|
||||||
|
MAX_FILE_SIZE = 10240000
|
||||||
|
|
||||||
|
[queue.issue_indexer]
|
||||||
|
LENGTH = 20
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
version = version=node.metadata.get('gitea/version')
|
from os.path import join
|
||||||
|
from bundlewrap.utils.dicts import merge_dict
|
||||||
|
|
||||||
|
|
||||||
|
version = node.metadata.get('gitea/version')
|
||||||
|
assert not version.startswith('v')
|
||||||
|
arch = node.metadata.get('system/architecture')
|
||||||
|
|
||||||
downloads['/usr/local/bin/gitea'] = {
|
downloads['/usr/local/bin/gitea'] = {
|
||||||
'url': f'https://dl.gitea.io/gitea/{version}/gitea-{version}-linux-amd64',
|
# https://forgejo.org/releases/
|
||||||
'sha256': node.metadata.get('gitea/sha256'),
|
'url': f'https://codeberg.org/forgejo/forgejo/releases/download/v{version}/forgejo-{version}-linux-{arch}',
|
||||||
|
'sha256_url': '{url}.sha256',
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'svc_systemd:gitea:restart',
|
'svc_systemd:gitea:restart',
|
||||||
},
|
},
|
||||||
|
|
@ -11,8 +18,6 @@ downloads['/usr/local/bin/gitea'] = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
users['git'] = {}
|
|
||||||
|
|
||||||
directories['/var/lib/gitea'] = {
|
directories['/var/lib/gitea'] = {
|
||||||
'owner': 'git',
|
'owner': 'git',
|
||||||
'mode': '0700',
|
'mode': '0700',
|
||||||
|
|
@ -36,9 +41,15 @@ actions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
files['/etc/gitea/app.ini'] = {
|
files['/etc/gitea/app.ini'] = {
|
||||||
'content_type': 'mako',
|
'content': repo.libs.ini.dumps(
|
||||||
|
merge_dict(
|
||||||
|
repo.libs.ini.parse(open(join(repo.path, 'bundles', 'gitea', 'files', 'app.ini')).read()),
|
||||||
|
node.metadata.get('gitea/conf'),
|
||||||
|
),
|
||||||
|
),
|
||||||
'owner': 'git',
|
'owner': 'git',
|
||||||
'context': node.metadata['gitea'],
|
'mode': '0600',
|
||||||
|
'context': node.metadata.get('gitea'),
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'svc_systemd:gitea:restart',
|
'svc_systemd:gitea:restart',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,30 @@
|
||||||
database_password = repo.vault.password_for(f'{node.name} postgresql gitea')
|
database_password = repo.vault.password_for(f'{node.name} postgresql gitea').value
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
'gitea': {
|
'apt': {
|
||||||
'database': {
|
'packages': {
|
||||||
'host': 'localhost',
|
'git': {
|
||||||
'port': '5432',
|
'needed_by': {
|
||||||
'username': 'gitea',
|
'svc_systemd:gitea',
|
||||||
'password': database_password,
|
}
|
||||||
'database': 'gitea',
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'gitea': {
|
||||||
|
'conf': {
|
||||||
|
'DEFAULT': {
|
||||||
|
'WORK_PATH': '/var/lib/gitea',
|
||||||
|
},
|
||||||
|
'database': {
|
||||||
|
'DB_TYPE': 'postgres',
|
||||||
|
'HOST': 'localhost:5432',
|
||||||
|
'NAME': 'gitea',
|
||||||
|
'USER': 'gitea',
|
||||||
|
'PASSWD': database_password,
|
||||||
|
'SSL_MODE': 'disable',
|
||||||
|
'LOG_SQL': 'false',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'app_name': 'Gitea',
|
|
||||||
'lfs_secret_key': repo.vault.password_for(f'{node.name} gitea lfs_secret_key', length=43),
|
|
||||||
'security_secret_key': repo.vault.password_for(f'{node.name} gitea security_secret_key'),
|
|
||||||
'oauth_secret_key': repo.vault.password_for(f'{node.name} gitea oauth_secret_key', length=43),
|
|
||||||
'internal_token': repo.vault.password_for(f'{node.name} gitea internal_token'),
|
|
||||||
},
|
},
|
||||||
'postgresql': {
|
'postgresql': {
|
||||||
'roles': {
|
'roles': {
|
||||||
|
|
@ -32,8 +43,7 @@ defaults = {
|
||||||
'gitea.service': {
|
'gitea.service': {
|
||||||
'Unit': {
|
'Unit': {
|
||||||
'Description': 'gitea',
|
'Description': 'gitea',
|
||||||
'After': 'syslog.target',
|
'After': {'syslog.target', 'network.target'},
|
||||||
'After': 'network.target',
|
|
||||||
'Requires': 'postgresql.service',
|
'Requires': 'postgresql.service',
|
||||||
},
|
},
|
||||||
'Service': {
|
'Service': {
|
||||||
|
|
@ -47,11 +57,16 @@ defaults = {
|
||||||
'Environment': 'USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea',
|
'Environment': 'USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea',
|
||||||
},
|
},
|
||||||
'Install': {
|
'Install': {
|
||||||
'WantedBy': 'multi-user.target',
|
'WantedBy': {'multi-user.target'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'users': {
|
||||||
|
'git': {
|
||||||
|
'home': '/home/git',
|
||||||
|
},
|
||||||
|
},
|
||||||
'zfs': {
|
'zfs': {
|
||||||
'datasets': {
|
'datasets': {
|
||||||
'tank/gitea': {
|
'tank/gitea': {
|
||||||
|
|
@ -62,6 +77,36 @@ defaults = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'gitea/conf',
|
||||||
|
)
|
||||||
|
def conf(metadata):
|
||||||
|
domain = metadata.get('gitea/domain')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'gitea': {
|
||||||
|
'conf': {
|
||||||
|
'server': {
|
||||||
|
'SSH_DOMAIN': domain,
|
||||||
|
'DOMAIN': domain,
|
||||||
|
'ROOT_URL': f'https://{domain}/',
|
||||||
|
'LFS_JWT_SECRET': repo.vault.password_for(f'{node.name} gitea lfs_secret_key', length=43),
|
||||||
|
},
|
||||||
|
'security': {
|
||||||
|
'INTERNAL_TOKEN': repo.vault.password_for(f'{node.name} gitea internal_token'),
|
||||||
|
'SECRET_KEY': repo.vault.password_for(f'{node.name} gitea security_secret_key'),
|
||||||
|
},
|
||||||
|
'service': {
|
||||||
|
'NO_REPLY_ADDRESS': f'noreply.{domain}',
|
||||||
|
},
|
||||||
|
'oauth2': {
|
||||||
|
'JWT_SECRET': repo.vault.password_for(f'{node.name} gitea oauth_secret_key', length=43),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'nginx/vhosts',
|
'nginx/vhosts',
|
||||||
)
|
)
|
||||||
|
|
@ -73,7 +118,7 @@ def nginx(metadata):
|
||||||
'content': 'nginx/proxy_pass.conf',
|
'content': 'nginx/proxy_pass.conf',
|
||||||
'context': {
|
'context': {
|
||||||
'target': 'http://127.0.0.1:3500',
|
'target': 'http://127.0.0.1:3500',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
61
bundles/gollum/items.py
Normal file
61
bundles/gollum/items.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
from shlex import quote
|
||||||
|
|
||||||
|
users = {
|
||||||
|
'gollum': {
|
||||||
|
'home': '/var/lib/gollum',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
directories = {
|
||||||
|
'/opt/gollum': {
|
||||||
|
'owner': 'gollum',
|
||||||
|
},
|
||||||
|
'/opt/gollum/.bundle': {
|
||||||
|
'owner': 'gollum',
|
||||||
|
},
|
||||||
|
'/var/lib/gollum': {
|
||||||
|
'owner': 'gollum',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'/opt/gollum/.bundle/config': {
|
||||||
|
'content': 'BUNDLE_PATH: ".bundle/gems"',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
git_deploy = {
|
||||||
|
'/opt/gollum': {
|
||||||
|
'repo': 'https://github.com/gollum/gollum.git',
|
||||||
|
'rev': f"v{node.metadata.get('gollum/version')}",
|
||||||
|
},
|
||||||
|
'/var/lib/gollum': {
|
||||||
|
'repo': node.metadata.get('gollum/wiki'),
|
||||||
|
'rev': 'main',
|
||||||
|
'unless': 'test -e /var/lib/gollum/.git',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
return f"su gollum -c " + quote(f"cd /opt/gollum && {cmd}")
|
||||||
|
|
||||||
|
actions = {
|
||||||
|
'gollum_install_bundler': {
|
||||||
|
'command': run("gem install bundler --user"),
|
||||||
|
'unless': run("test -e $(ruby -e 'puts Gem.user_dir')/bin/bundle"),
|
||||||
|
'needs': [
|
||||||
|
'file:/opt/gollum/.bundle/config',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'gollum_bundle_install': {
|
||||||
|
'command': run("$(ruby -e 'puts Gem.user_dir')/bin/bundle install"),
|
||||||
|
'unless': run("$(ruby -e 'puts Gem.user_dir')/bin/bundle check"),
|
||||||
|
'needs': [
|
||||||
|
'git_deploy:/opt/gollum',
|
||||||
|
'action:gollum_install_bundler',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: AUTH
|
||||||
|
#https://github.com/bjoernalbers/gollum-auth
|
||||||
49
bundles/gollum/metadata.py
Normal file
49
bundles/gollum/metadata.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'libgit2-dev': {},
|
||||||
|
'libssl-dev': {},
|
||||||
|
'cmake': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'systemd': {
|
||||||
|
'units': {
|
||||||
|
'gollum.service': {
|
||||||
|
'Unit': {
|
||||||
|
'Description': 'gollum',
|
||||||
|
'After': 'syslog.target',
|
||||||
|
'After': 'network.target',
|
||||||
|
'Requires': 'postgresql.service',
|
||||||
|
},
|
||||||
|
'Service': {
|
||||||
|
'User': 'gollum',
|
||||||
|
'Group': 'gollum',
|
||||||
|
'WorkingDirectory': '/opt/gollum',
|
||||||
|
'ExecStart': 'true',
|
||||||
|
'Restart': 'always',
|
||||||
|
},
|
||||||
|
'Install': {
|
||||||
|
'WantedBy': {'multi-user.target'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'nginx/vhosts',
|
||||||
|
)
|
||||||
|
def nginx(metadata):
|
||||||
|
return {
|
||||||
|
'nginx': {
|
||||||
|
'vhosts': {
|
||||||
|
metadata.get('gollum/domain'): {
|
||||||
|
'content': 'nginx/proxy_pass.conf',
|
||||||
|
'context': {
|
||||||
|
'target': 'http://127.0.0.1:3600',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -18,16 +18,17 @@ admin_password = node.metadata.get('grafana/config/security/admin_password')
|
||||||
port = node.metadata.get('grafana/config/server/http_port')
|
port = node.metadata.get('grafana/config/server/http_port')
|
||||||
actions['reset_grafana_admin_password'] = {
|
actions['reset_grafana_admin_password'] = {
|
||||||
'command': f"grafana-cli admin reset-admin-password {quote(admin_password)}",
|
'command': f"grafana-cli admin reset-admin-password {quote(admin_password)}",
|
||||||
'unless': f"curl http://admin:{quote(admin_password)}@localhost:{port}/api/org",
|
'unless': f"sleep 5 && curl http://admin:{quote(admin_password)}@localhost:{port}/api/org --fail",
|
||||||
'needs': [
|
'needs': [
|
||||||
'svc_systemd:grafana-server',
|
'svc_systemd:grafana-server',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
directories = {
|
directories = {
|
||||||
'/etc/grafana': {
|
'/etc/grafana': {},
|
||||||
},
|
|
||||||
'/etc/grafana/provisioning': {
|
'/etc/grafana/provisioning': {
|
||||||
|
'owner': 'grafana',
|
||||||
|
'group': 'grafana',
|
||||||
},
|
},
|
||||||
'/etc/grafana/provisioning/datasources': {
|
'/etc/grafana/provisioning/datasources': {
|
||||||
'purge': True,
|
'purge': True,
|
||||||
|
|
@ -35,13 +36,25 @@ directories = {
|
||||||
'/etc/grafana/provisioning/dashboards': {
|
'/etc/grafana/provisioning/dashboards': {
|
||||||
'purge': True,
|
'purge': True,
|
||||||
},
|
},
|
||||||
'/var/lib/grafana': {},
|
'/var/lib/grafana': {
|
||||||
'/var/lib/grafana/dashboards': {},
|
'owner': 'grafana',
|
||||||
|
'group': 'grafana',
|
||||||
|
},
|
||||||
|
'/var/lib/grafana/dashboards': {
|
||||||
|
'owner': 'grafana',
|
||||||
|
'group': 'grafana',
|
||||||
|
'purge': True,
|
||||||
|
'triggers': [
|
||||||
|
'svc_systemd:grafana-server:restart',
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
files = {
|
files = {
|
||||||
'/etc/grafana/grafana.ini': {
|
'/etc/grafana/grafana.ini': {
|
||||||
'content': repo.libs.ini.dumps(node.metadata.get('grafana/config')),
|
'content': repo.libs.ini.dumps(node.metadata.get('grafana/config')),
|
||||||
|
'owner': 'grafana',
|
||||||
|
'group': 'grafana',
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:grafana-server:restart',
|
'svc_systemd:grafana-server:restart',
|
||||||
],
|
],
|
||||||
|
|
@ -51,6 +64,8 @@ files = {
|
||||||
'apiVersion': 1,
|
'apiVersion': 1,
|
||||||
'datasources': list(node.metadata.get('grafana/datasources').values()),
|
'datasources': list(node.metadata.get('grafana/datasources').values()),
|
||||||
}),
|
}),
|
||||||
|
'owner': 'grafana',
|
||||||
|
'group': 'grafana',
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:grafana-server:restart',
|
'svc_systemd:grafana-server:restart',
|
||||||
],
|
],
|
||||||
|
|
@ -67,6 +82,8 @@ files = {
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
}),
|
}),
|
||||||
|
'owner': 'grafana',
|
||||||
|
'group': 'grafana',
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:grafana-server:restart',
|
'svc_systemd:grafana-server:restart',
|
||||||
],
|
],
|
||||||
|
|
@ -110,7 +127,7 @@ for dashboard_id, monitored_node in enumerate(monitored_nodes, start=1):
|
||||||
panel['gridPos']['y'] = (row_id - 1) * panel['gridPos']['h']
|
panel['gridPos']['y'] = (row_id - 1) * panel['gridPos']['h']
|
||||||
|
|
||||||
if 'display_name' in panel_config:
|
if 'display_name' in panel_config:
|
||||||
panel['fieldConfig']['defaults']['displayName'] = '${'+panel_config['display_name']+'}'
|
panel['fieldConfig']['defaults']['displayName'] = panel_config['display_name']
|
||||||
|
|
||||||
if panel_config.get('stacked'):
|
if panel_config.get('stacked'):
|
||||||
panel['fieldConfig']['defaults']['custom']['stacking']['mode'] = 'normal'
|
panel['fieldConfig']['defaults']['custom']['stacking']['mode'] = 'normal'
|
||||||
|
|
@ -122,7 +139,16 @@ for dashboard_id, monitored_node in enumerate(monitored_nodes, start=1):
|
||||||
panel['fieldConfig']['defaults']['min'] = panel_config['min']
|
panel['fieldConfig']['defaults']['min'] = panel_config['min']
|
||||||
if 'max' in panel_config:
|
if 'max' in panel_config:
|
||||||
panel['fieldConfig']['defaults']['max'] = panel_config['max']
|
panel['fieldConfig']['defaults']['max'] = panel_config['max']
|
||||||
|
if 'soft_max' in panel_config:
|
||||||
|
panel['fieldConfig']['defaults']['custom']['axisSoftMax'] = panel_config['soft_max']
|
||||||
|
|
||||||
|
if 'legend' in panel_config:
|
||||||
|
panel['options']['legend'].update(panel_config['legend'])
|
||||||
|
|
||||||
|
if 'tooltip' in panel_config:
|
||||||
|
panel['options']['tooltip']['mode'] = panel_config['tooltip']
|
||||||
|
if panel_config['tooltip'] == 'multi':
|
||||||
|
panel['options']['tooltip']['sort'] = 'desc'
|
||||||
|
|
||||||
for query_name, query_config in panel_config['queries'].items():
|
for query_name, query_config in panel_config['queries'].items():
|
||||||
panel['targets'].append({
|
panel['targets'].append({
|
||||||
|
|
@ -131,11 +157,15 @@ for dashboard_id, monitored_node in enumerate(monitored_nodes, start=1):
|
||||||
bucket=bucket,
|
bucket=bucket,
|
||||||
host=monitored_node.name,
|
host=monitored_node.name,
|
||||||
negative=query_config.get('negative', False),
|
negative=query_config.get('negative', False),
|
||||||
|
boolean_to_int=query_config.get('boolean_to_int', False),
|
||||||
|
over=query_config.get('over', None),
|
||||||
filters={
|
filters={
|
||||||
'host': monitored_node.name,
|
'host': monitored_node.name,
|
||||||
**query_config['filters'],
|
**query_config['filters'],
|
||||||
},
|
},
|
||||||
|
exists=query_config.get('exists', []),
|
||||||
function=query_config.get('function', None),
|
function=query_config.get('function', None),
|
||||||
|
multiply=query_config.get('multiply', None),
|
||||||
).strip()
|
).strip()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -143,8 +173,9 @@ for dashboard_id, monitored_node in enumerate(monitored_nodes, start=1):
|
||||||
|
|
||||||
files[f'/var/lib/grafana/dashboards/{monitored_node.name}.json'] = {
|
files[f'/var/lib/grafana/dashboards/{monitored_node.name}.json'] = {
|
||||||
'content': json.dumps(dashboard, indent=4),
|
'content': json.dumps(dashboard, indent=4),
|
||||||
|
'owner': 'grafana',
|
||||||
|
'group': 'grafana',
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'svc_systemd:grafana-server:restart',
|
'svc_systemd:grafana-server:restart',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,33 @@ defaults = {
|
||||||
'grafana': {},
|
'grafana': {},
|
||||||
},
|
},
|
||||||
'sources': {
|
'sources': {
|
||||||
'deb https://packages.grafana.com/oss/deb stable main',
|
'grafana': {
|
||||||
|
'urls': {
|
||||||
|
'https://packages.grafana.com/oss/deb',
|
||||||
},
|
},
|
||||||
|
'suites': {
|
||||||
|
'stable',
|
||||||
|
},
|
||||||
|
'components': {
|
||||||
|
'main',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
'grafana': {
|
'grafana': {
|
||||||
'config': {
|
'config': {
|
||||||
'server': {
|
'server': {
|
||||||
'http_port': 8300,
|
'http_port': 8300,
|
||||||
|
'http_addr': '127.0.0.1',
|
||||||
|
'enable_gzip': True,
|
||||||
},
|
},
|
||||||
'database': {
|
'database': {
|
||||||
'url': f'postgres://grafana:{postgres_password}@localhost:5432/grafana',
|
'type': 'postgres',
|
||||||
|
'host': '127.0.0.1:5432',
|
||||||
|
'name': 'grafana',
|
||||||
|
'user': 'grafana',
|
||||||
|
'password': postgres_password,
|
||||||
},
|
},
|
||||||
'remote_cache': {
|
'remote_cache': {
|
||||||
'type': 'redis',
|
'type': 'redis',
|
||||||
|
|
@ -55,6 +72,20 @@ defaults = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'grafana/config/server/domain',
|
||||||
|
)
|
||||||
|
def domain(metadata):
|
||||||
|
return {
|
||||||
|
'grafana': {
|
||||||
|
'config': {
|
||||||
|
'server': {
|
||||||
|
'domain': metadata.get('grafana/hostname'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'grafana/datasources',
|
'grafana/datasources',
|
||||||
)
|
)
|
||||||
|
|
@ -102,23 +133,22 @@ def datasource_key_to_name(metadata):
|
||||||
def dns(metadata):
|
def dns(metadata):
|
||||||
return {
|
return {
|
||||||
'dns': {
|
'dns': {
|
||||||
metadata.get('grafana/hostname'): repo.libs.dns.get_a_records(metadata),
|
metadata.get('grafana/hostname'): repo.libs.ip.get_a_records(metadata),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
|
'nginx/has_websockets',
|
||||||
'nginx/vhosts',
|
'nginx/vhosts',
|
||||||
)
|
)
|
||||||
def nginx(metadata):
|
def nginx(metadata):
|
||||||
return {
|
return {
|
||||||
'nginx': {
|
'nginx': {
|
||||||
|
'has_websockets': True,
|
||||||
'vhosts': {
|
'vhosts': {
|
||||||
metadata.get('grafana/hostname'): {
|
metadata.get('grafana/hostname'): {
|
||||||
'content': 'nginx/proxy_pass.conf',
|
'content': 'grafana/vhost.conf',
|
||||||
'context': {
|
|
||||||
'target': 'http://127.0.0.1:8300',
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
5
bundles/grub/files/grub
Normal file
5
bundles/grub/files/grub
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
GRUB_DEFAULT=0
|
||||||
|
GRUB_TIMEOUT=1
|
||||||
|
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
|
||||||
|
GRUB_CMDLINE_LINUX_DEFAULT="${' '.join(kernel_params)}"
|
||||||
|
GRUB_CMDLINE_LINUX=""
|
||||||
20
bundles/grub/items.py
Normal file
20
bundles/grub/items.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
files = {
|
||||||
|
'/etc/default/grub': {
|
||||||
|
'content_type': 'mako',
|
||||||
|
'context': {
|
||||||
|
'timeout': node.metadata.get('grub/timeout'),
|
||||||
|
'kernel_params': node.metadata.get('grub/kernel_params'),
|
||||||
|
},
|
||||||
|
'mode': '0644',
|
||||||
|
'triggers': {
|
||||||
|
'action:update-grub',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions = {
|
||||||
|
'update-grub': {
|
||||||
|
'command': 'update-grub',
|
||||||
|
'triggered': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
6
bundles/grub/metadata.py
Normal file
6
bundles/grub/metadata.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
defaults = {
|
||||||
|
'grub': {
|
||||||
|
'timeout': 1,
|
||||||
|
'kernel_params': set(),
|
||||||
|
},
|
||||||
|
}
|
||||||
10
bundles/hardware/files/cpu_frequency
Normal file
10
bundles/hardware/files/cpu_frequency
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
date=$(date --utc +%s%N)
|
||||||
|
|
||||||
|
for cpu in $(cat /sys/devices/system/cpu/cpu0/cpufreq/affected_cpus)
|
||||||
|
do
|
||||||
|
# echo "cpu_frequency,cpu=$cpu min=$(expr $(cat /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_min_freq) / 1000) $date"
|
||||||
|
echo "cpu_frequency,cpu=$cpu current=$(expr $(cat /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_cur_freq) / 1000) $date"
|
||||||
|
# echo "cpu_frequency,cpu=$cpu max=$(expr $(cat /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_max_freq) / 1000) $date"
|
||||||
|
done
|
||||||
8
bundles/hardware/items.py
Normal file
8
bundles/hardware/items.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
files = {
|
||||||
|
'/usr/local/share/telegraf/cpu_frequency': {
|
||||||
|
'mode': '0755',
|
||||||
|
'triggers': {
|
||||||
|
'svc_systemd:telegraf.service:restart',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
38
bundles/hardware/metadata.py
Normal file
38
bundles/hardware/metadata.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'lm-sensors': {},
|
||||||
|
'console-data': {}, # leykeys de
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'grafana_rows': {
|
||||||
|
'health',
|
||||||
|
},
|
||||||
|
'sudoers': {
|
||||||
|
'telegraf': {
|
||||||
|
'/usr/local/share/telegraf/cpu_frequency',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'telegraf': {
|
||||||
|
'config': {
|
||||||
|
'inputs': {
|
||||||
|
'sensors': {repo.libs.hashable.hashable({
|
||||||
|
'timeout': '2s',
|
||||||
|
})},
|
||||||
|
'exec': {
|
||||||
|
repo.libs.hashable.hashable({
|
||||||
|
'commands': ["sudo /usr/local/share/telegraf/cpu_frequency"],
|
||||||
|
'name_override': "cpu_frequency",
|
||||||
|
'data_format': "influx",
|
||||||
|
}),
|
||||||
|
# repo.libs.hashable.hashable({
|
||||||
|
# 'commands': ["/bin/bash -c 'expr $(cat /sys/class/thermal/thermal_zone0/temp) / 1000'"],
|
||||||
|
# 'name_override': "cpu_temperature",
|
||||||
|
# 'data_format': "value",
|
||||||
|
# 'data_type': "integer",
|
||||||
|
# }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
files['/etc/hostname'] = {
|
files[node.metadata.get('hostname_file')] = {
|
||||||
'content': node.metadata.get('hostname'),
|
'content': node.metadata.get('hostname'),
|
||||||
'triggers': [
|
'triggers': [
|
||||||
'action:update_hostname',
|
'action:update_hostname',
|
||||||
|
|
@ -6,6 +6,6 @@ files['/etc/hostname'] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
actions["update_hostname"] = {
|
actions["update_hostname"] = {
|
||||||
"command": "hostname -F /etc/hostname",
|
"command": f"hostname -F {node.metadata.get('hostname_file')}",
|
||||||
'triggered': True,
|
'triggered': True,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,21 @@ defaults = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor.provides(
|
||||||
|
'hostname_file',
|
||||||
|
)
|
||||||
|
def hostname_file(metadata):
|
||||||
|
return {
|
||||||
|
'hostname_file': node.metadata.get('hostname_file', '/etc/hostname'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'dns',
|
'dns',
|
||||||
)
|
)
|
||||||
def dns(metadata):
|
def dns(metadata):
|
||||||
return {
|
return {
|
||||||
'dns': {
|
'dns': {
|
||||||
metadata.get('hostname'): repo.libs.dns.get_a_records(metadata, external=False),
|
metadata.get('hostname'): repo.libs.ip.get_a_records(metadata),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
bundles/htop/files/htoprc.global
Normal file
39
bundles/htop/files/htoprc.global
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Beware! This file is rewritten by htop when settings are changed in the interface.
|
||||||
|
# The parser is also very primitive, and not human-friendly.
|
||||||
|
fields=0 48 17 18 38 39 40 2 46 47 109 110 49 1
|
||||||
|
sort_key=46
|
||||||
|
sort_direction=-1
|
||||||
|
tree_sort_key=0
|
||||||
|
tree_sort_direction=1
|
||||||
|
hide_kernel_threads=0
|
||||||
|
hide_userland_threads=0
|
||||||
|
shadow_other_users=0
|
||||||
|
show_thread_names=0
|
||||||
|
show_program_path=1
|
||||||
|
highlight_base_name=0
|
||||||
|
highlight_megabytes=1
|
||||||
|
highlight_threads=1
|
||||||
|
highlight_changes=0
|
||||||
|
highlight_changes_delay_secs=5
|
||||||
|
find_comm_in_cmdline=1
|
||||||
|
strip_exe_from_cmdline=1
|
||||||
|
show_merged_command=0
|
||||||
|
tree_view=0
|
||||||
|
tree_view_always_by_pid=0
|
||||||
|
header_margin=1
|
||||||
|
detailed_cpu_time=0
|
||||||
|
cpu_count_from_one=1
|
||||||
|
show_cpu_usage=0
|
||||||
|
show_cpu_frequency=1
|
||||||
|
show_cpu_temperature=0
|
||||||
|
degree_fahrenheit=0
|
||||||
|
update_process_names=0
|
||||||
|
account_guest_in_cpu_meter=0
|
||||||
|
color_scheme=0
|
||||||
|
enable_mouse=1
|
||||||
|
delay=20
|
||||||
|
left_meters=Hostname Tasks DiskIO NetworkIO Blank CPU Memory Swap Blank LeftCPUs${cpus_per_row}
|
||||||
|
left_meter_modes=2 2 2 2 2 1 1 1 2 1
|
||||||
|
right_meters=CPU Blank PressureStallCPUSome PressureStallMemorySome PressureStallIOSome Blank RightCPUs${cpus_per_row}
|
||||||
|
right_meter_modes=3 2 1 1 1 2 1
|
||||||
|
hide_function_bar=0
|
||||||
8
bundles/htop/items.py
Normal file
8
bundles/htop/items.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
files = {
|
||||||
|
'/etc/htoprc.global': {
|
||||||
|
'content_type': 'mako',
|
||||||
|
'context': {
|
||||||
|
'cpus_per_row': 4 if node.metadata.get('vm/threads', node.metadata.get('vm/cores', 1)) > 8 else 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
7
bundles/htop/metadata.py
Normal file
7
bundles/htop/metadata.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
defaults = {
|
||||||
|
'apt': {
|
||||||
|
'packages': {
|
||||||
|
'htop': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
36
bundles/icinga2/files/check_by_sshmon
Normal file
36
bundles/icinga2/files/check_by_sshmon
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
UNKNOWN=3
|
||||||
|
|
||||||
|
if [ -z "$SSHMON_COMMAND" ]
|
||||||
|
then
|
||||||
|
echo 'check_by_sshmon: Env SSHMON_COMMAND missing' >&2
|
||||||
|
exit $UNKNOWN
|
||||||
|
elif [ -z "$SSHMON_HOST" ]
|
||||||
|
then
|
||||||
|
echo 'check_by_sshmon: Env SSHMON_HOST missing' >&2
|
||||||
|
exit $UNKNOWN
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$SSHMON_SUDO" ]
|
||||||
|
then
|
||||||
|
PREFIX=""
|
||||||
|
else
|
||||||
|
PREFIX="sudo "
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh sshmon@"$SSHMON_HOST" "$PREFIX$SSHMON_COMMAND"
|
||||||
|
|
||||||
|
exitcode=$?
|
||||||
|
|
||||||
|
if [ "$exitcode" = 124 ]
|
||||||
|
then
|
||||||
|
echo 'check_by_sshmon: Timeout while running check remotely' >&2
|
||||||
|
exit $UNKNOWN
|
||||||
|
elif [ "$exitcode" = 255 ]
|
||||||
|
then
|
||||||
|
echo 'check_by_sshmon: SSH error' >&2
|
||||||
|
exit $UNKNOWN
|
||||||
|
else
|
||||||
|
exit $exitcode
|
||||||
|
fi
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue