Compare commits
551 Commits
remove-bui
...
cli-clan-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fea4c2dc48 | ||
|
|
80bbc6d7a3 | ||
|
|
765bdb262a | ||
|
|
05c00fbe82 | ||
|
|
7e97734797 | ||
|
|
6384c4654e | ||
|
|
72d3ad09a4 | ||
|
|
a535450ec0 | ||
|
|
aaeb616f82 | ||
|
|
434edeaae1 | ||
|
|
a4efd3cb16 | ||
|
|
13131ccd6e | ||
|
|
3a8309b01f | ||
|
|
10065a7c8f | ||
|
|
176b54e29d | ||
|
|
be048d8307 | ||
|
|
52fcab30e7 | ||
|
|
d3b423328f | ||
|
|
1177e84dcc | ||
|
|
414952dfa3 | ||
|
|
24194011ac | ||
|
|
4f78a8ff94 | ||
|
|
068b5d4c1e | ||
|
|
adccef4757 | ||
|
|
980d94d47d | ||
|
|
a50b25eea2 | ||
|
|
017989841d | ||
|
|
c14a5fcc69 | ||
|
|
4f60345ba7 | ||
|
|
ece48d3b5f | ||
|
|
4eea8d24f0 | ||
|
|
49099df3fb | ||
|
|
62ccba9fb5 | ||
|
|
0b44770f1f | ||
|
|
61c3d7284a | ||
|
|
44b1be5ed4 | ||
|
|
88871bea69 | ||
|
|
5141ea047c | ||
|
|
ff6a03a646 | ||
|
|
bc379c985d | ||
|
|
69d8b029d6 | ||
|
|
f3617b0407 | ||
|
|
a5205681cc | ||
|
|
9880847d43 | ||
|
|
8aa88b22ab | ||
|
|
ff979eba61 | ||
|
|
5d1abbd303 | ||
|
|
92e9bb2ed8 | ||
|
|
ea75c9bfa9 | ||
|
|
2adf65482d | ||
|
|
5684ddf104 | ||
|
|
f74e444120 | ||
|
|
0ef57bfc8e | ||
|
|
8f43af3c48 | ||
|
|
eeaec583cb | ||
|
|
a9d1ff83f2 | ||
|
|
89cb22147c | ||
|
|
1006fc755e | ||
|
|
f100177df3 | ||
|
|
cbd3b08296 | ||
|
|
2608bee30a | ||
|
|
a29459a384 | ||
|
|
1abdd45821 | ||
|
|
b058fcc8eb | ||
|
|
24ae95a007 | ||
|
|
39510b613f | ||
|
|
dcdab61d13 | ||
|
|
f86fe07b63 | ||
|
|
84bf9fe3cf | ||
|
|
9737ce51b1 | ||
|
|
3c1c6c1942 | ||
|
|
0904c9da60 | ||
|
|
73e03c21f7 | ||
|
|
743f05a1b5 | ||
|
|
e28b8dc944 | ||
|
|
8d871aafe8 | ||
|
|
26559ff88b | ||
|
|
b39aead1db | ||
|
|
caae6c7a55 | ||
|
|
df90fb20a0 | ||
|
|
d6577ec05d | ||
|
|
9a66170aa1 | ||
|
|
3effd8fd9a | ||
|
|
86d6b42f4c | ||
|
|
5e2ffa9491 | ||
|
|
ba58d6d91a | ||
|
|
38e2d00bbc | ||
|
|
4f29f2e2ca | ||
|
|
5c23e24315 | ||
|
|
1009c61c9f | ||
|
|
0817f83b0b | ||
|
|
4f191f3ebe | ||
|
|
9f48b7a2fa | ||
|
|
b17466c84b | ||
|
|
d2814efcde | ||
|
|
6a5a83f57a | ||
|
|
9e9ab22c37 | ||
|
|
2840d3a5fc | ||
|
|
a305f98586 | ||
|
|
96fe387399 | ||
|
|
d022f26c2c | ||
|
|
d1f5a8e263 | ||
|
|
a6a5c1e21d | ||
|
|
f1867bdd7a | ||
|
|
ee8e44d255 | ||
|
|
f730f4fa06 | ||
|
|
567570e89c | ||
|
|
54797dd5f5 | ||
|
|
c70c588c1c | ||
|
|
bb6fab1168 | ||
|
|
0859a86ce0 | ||
|
|
1524dc963e | ||
|
|
eebd3fa4ec | ||
|
|
a1ff794d57 | ||
|
|
6693cda465 | ||
|
|
bf0691587d | ||
|
|
deecb966ce | ||
|
|
2d2d9c9dca | ||
|
|
e0e16de144 | ||
|
|
75c60a6103 | ||
|
|
1373670dfc | ||
|
|
03b13e9ed4 | ||
|
|
a79027c312 | ||
|
|
bdcdf4e788 | ||
|
|
e3ed9d7b4b | ||
|
|
ddf2b57b3f | ||
|
|
5ab3a164c8 | ||
|
|
073027f7c6 | ||
|
|
d0374c0d7c | ||
|
|
6137701532 | ||
|
|
1560c5f8cf | ||
|
|
5d884cecc2 | ||
|
|
8a3cade082 | ||
|
|
10b4389309 | ||
|
|
2879c72a89 | ||
|
|
547e912c4e | ||
|
|
87125f1ff7 | ||
|
|
804f606384 | ||
|
|
997c7de942 | ||
|
|
e7323999f2 | ||
|
|
31d3997358 | ||
|
|
62b748624d | ||
|
|
29f440a482 | ||
|
|
f15fd1be52 | ||
|
|
beaacf81c6 | ||
|
|
1ae023f4bf | ||
|
|
9becd4e0c4 | ||
|
|
454b09a67e | ||
|
|
787781c2ad | ||
|
|
469c6ba42d | ||
|
|
21f335fa15 | ||
|
|
d98b76e734 | ||
|
|
e2cb1fd83f | ||
|
|
91646b323a | ||
|
|
121548ffb7 | ||
|
|
966a3ee919 | ||
|
|
d007b0f1b3 | ||
|
|
034982bff2 | ||
|
|
7c37bddeea | ||
|
|
c1a87e5c6a | ||
|
|
e5bea3d49a | ||
|
|
40682972ef | ||
|
|
6035455cba | ||
|
|
9be53a9a63 | ||
|
|
29ec9dbe26 | ||
|
|
a1874c940e | ||
|
|
d115705cb4 | ||
|
|
eceb6eb999 | ||
|
|
d25cace522 | ||
|
|
3c6567e67d | ||
|
|
628e45293e | ||
|
|
954c14513a | ||
|
|
cb8a01d448 | ||
|
|
8e53e42b74 | ||
|
|
ed596a57f0 | ||
|
|
b6bccd218a | ||
|
|
1df9b6e97d | ||
|
|
58fa7ac32b | ||
|
|
110d1d4921 | ||
|
|
46aee098c4 | ||
|
|
9d6735e8c4 | ||
|
|
47c94c51b6 | ||
|
|
1eb567682c | ||
|
|
fcd83e7a60 | ||
|
|
af4b00408a | ||
|
|
aaff3b9b38 | ||
|
|
c13741602c | ||
|
|
6cbe221f44 | ||
|
|
3cf8f605d5 | ||
|
|
cc07e0ea44 | ||
|
|
ccb9340478 | ||
|
|
df096fe53b | ||
|
|
f248cc91ad | ||
|
|
1129862293 | ||
|
|
e2cb75784c | ||
|
|
a8d48b22f8 | ||
|
|
c0f2bcf751 | ||
|
|
20c23fa64b | ||
|
|
23573e16c4 | ||
|
|
eaee4e8cad | ||
|
|
10e43a8884 | ||
|
|
dc1cd03717 | ||
|
|
a71a5880c1 | ||
|
|
6b137f21de | ||
|
|
fbc14bf20f | ||
|
|
2f2f3b6898 | ||
|
|
3ae0f37bcb | ||
|
|
e49d432542 | ||
|
|
76955533cf | ||
|
|
d0ebc75135 | ||
|
|
40503306d1 | ||
|
|
da99407e74 | ||
|
|
915178765b | ||
|
|
518de45d41 | ||
|
|
7d23189c1c | ||
|
|
eec55f73a2 | ||
|
|
484d274c3c | ||
|
|
a4b20f9167 | ||
|
|
dc7291c62b | ||
|
|
a814a44bc6 | ||
|
|
86a6177126 | ||
|
|
4536a5b4f5 | ||
|
|
a9cfda9acb | ||
|
|
b9f60218d7 | ||
|
|
f69e28a133 | ||
|
|
1968230c28 | ||
|
|
9cad074732 | ||
|
|
4859a9ab7c | ||
|
|
b53ecdc89d | ||
|
|
19603e1a1c | ||
|
|
7d20f3a33b | ||
|
|
fa03c190f8 | ||
|
|
65101ad55a | ||
|
|
e5db3e269b | ||
|
|
073750e4c5 | ||
|
|
8bafbcb295 | ||
|
|
dbef6ced77 | ||
|
|
65e7f9e6ca | ||
|
|
e1062ed97c | ||
|
|
2eb1a56d8f | ||
|
|
0f499fc651 | ||
|
|
bcb7a1aa60 | ||
|
|
273c83ec27 | ||
|
|
c74d7857da | ||
|
|
11405966c6 | ||
|
|
220839598d | ||
|
|
44dcfa7844 | ||
|
|
98f685f3ca | ||
|
|
9e43285ba8 | ||
|
|
c0bc0417a6 | ||
|
|
c90b69d499 | ||
|
|
0240acdf3e | ||
|
|
92726ecebc | ||
|
|
b8e9546762 | ||
|
|
2039f034b1 | ||
|
|
0a329f43a8 | ||
|
|
bde0a2845c | ||
|
|
af3c6282c9 | ||
|
|
73ab4d2a6e | ||
|
|
cc269c4f58 | ||
|
|
20021a92ea | ||
|
|
7b54e9b033 | ||
|
|
7971eceb74 | ||
|
|
49a5763f69 | ||
|
|
10694e58c8 | ||
|
|
0d919c4fce | ||
|
|
8cccf757a8 | ||
|
|
80c8cc8628 | ||
|
|
ab63f0d7a4 | ||
|
|
06e0461ec9 | ||
|
|
60ba00dd8f | ||
|
|
90ef55f040 | ||
|
|
de81a5d810 | ||
|
|
3fe65f1f12 | ||
|
|
6bb998f9dd | ||
|
|
af7ce9b8ed | ||
|
|
b74193514d | ||
|
|
c33fd4e504 | ||
|
|
65f3cb562a | ||
|
|
355ff648d7 | ||
|
|
f314eb04d6 | ||
|
|
ebe206cdc0 | ||
|
|
2a138d3248 | ||
|
|
77810b1d4f | ||
|
|
77c840c9ba | ||
|
|
9df7e6df1e | ||
|
|
a5e51f658d | ||
|
|
98d5b3651b | ||
|
|
713a1a550e | ||
|
|
d51d656391 | ||
|
|
0f79af697e | ||
|
|
0119fc06ca | ||
|
|
5361261bd5 | ||
|
|
86e7bcc389 | ||
|
|
79281aba90 | ||
|
|
dade91c292 | ||
|
|
d285a0e716 | ||
|
|
a97128db17 | ||
|
|
ff7b49be5f | ||
|
|
0b816a2672 | ||
|
|
e6ec331da0 | ||
|
|
0b05b0b1ec | ||
|
|
efd9beba15 | ||
|
|
dc03a9183f | ||
|
|
ab3158ca07 | ||
|
|
75a1f7b67f | ||
|
|
d453720a57 | ||
|
|
a4331cc109 | ||
|
|
434ce1af49 | ||
|
|
488ee1ae63 | ||
|
|
fc2e619046 | ||
|
|
cf6c3604ca | ||
|
|
a3ea62caba | ||
|
|
e2e4837b29 | ||
|
|
96fc3d409a | ||
|
|
392f244361 | ||
|
|
d2529704d5 | ||
|
|
62a3503987 | ||
|
|
c39aa89e29 | ||
|
|
d19ac1b9f5 | ||
|
|
57eec8edb4 | ||
|
|
e99981cfaf | ||
|
|
ae0ea37437 | ||
|
|
15557cb532 | ||
|
|
8f3a0b59f3 | ||
|
|
10f731c974 | ||
|
|
0e5c8d1a33 | ||
|
|
e5f8c515cd | ||
|
|
e856d4018a | ||
|
|
17b75500fb | ||
|
|
cf8b7f63fc | ||
|
|
62c4f735ed | ||
|
|
cba951b2c5 | ||
|
|
ef6f652b92 | ||
|
|
3d51cee4bb | ||
|
|
1791743444 | ||
|
|
6208a6e857 | ||
|
|
4759cce8a4 | ||
|
|
c7ad875e7e | ||
|
|
2ef292942f | ||
|
|
b83f5d2ffc | ||
|
|
567e8b57cd | ||
|
|
7f1a7da5c7 | ||
|
|
bb92ffb898 | ||
|
|
7ed62c427c | ||
|
|
596458d809 | ||
|
|
f677d96acf | ||
|
|
2c3b0f3771 | ||
|
|
ae20230a57 | ||
|
|
549ba9bdc2 | ||
|
|
e167137672 | ||
|
|
e36735119c | ||
|
|
f8cdac2a63 | ||
|
|
ea63b4411e | ||
|
|
a070fc74c1 | ||
|
|
b30686269b | ||
|
|
1626d179a0 | ||
|
|
6ec38c33d7 | ||
|
|
fdfbed1a3f | ||
|
|
f44b8c63c2 | ||
|
|
092ac21dcd | ||
|
|
bd6f7b03af | ||
|
|
0908a2efb8 | ||
|
|
6c84b2e100 | ||
|
|
de65619442 | ||
|
|
85dda9e125 | ||
|
|
7961a92d32 | ||
|
|
50ba21316e | ||
|
|
08342578f1 | ||
|
|
9954653657 | ||
|
|
6e71b541aa | ||
|
|
0f72f12461 | ||
|
|
db579e169c | ||
|
|
31438d6781 | ||
|
|
eac21c5176 | ||
|
|
2bd432bdb7 | ||
|
|
7ef09343ed | ||
|
|
8c2cee0e44 | ||
|
|
b421698f70 | ||
|
|
857b9d0260 | ||
|
|
2776294de0 | ||
|
|
c90b8d7401 | ||
|
|
5c746311c7 | ||
|
|
7784df8180 | ||
|
|
5d0ca5aff8 | ||
|
|
3ef6b2f715 | ||
|
|
58053748b9 | ||
|
|
19a8101e98 | ||
|
|
e5cb5afb4b | ||
|
|
b75cf516f6 | ||
|
|
3c58e2f04e | ||
|
|
d814e98e94 | ||
|
|
35315d9596 | ||
|
|
86ac1c4405 | ||
|
|
a06ba7f0f9 | ||
|
|
323de27651 | ||
|
|
782e8b330d | ||
|
|
682d8c786c | ||
|
|
9e32be4e48 | ||
|
|
686976a143 | ||
|
|
a2404f5fbb | ||
|
|
a6a25075f7 | ||
|
|
ec71badc3c | ||
|
|
1c4469e20c | ||
|
|
6fa4348aa6 | ||
|
|
dac06531d4 | ||
|
|
cb89fb97f1 | ||
|
|
6a8d7aa5fd | ||
|
|
63bcfc4809 | ||
|
|
e73350f6af | ||
|
|
98a0b9600b | ||
|
|
abeb517a22 | ||
|
|
fbdbcfa6d5 | ||
|
|
303af9af6b | ||
|
|
414e412e7e | ||
|
|
c2e84f11af | ||
|
|
bf2eb000d5 | ||
|
|
b01029ccd4 | ||
|
|
798c1a9277 | ||
|
|
d6327e0bc9 | ||
|
|
f5b2be63d5 | ||
|
|
6e904de655 | ||
|
|
0a43721a45 | ||
|
|
51eb7bd0b5 | ||
|
|
1d8ac7b1b5 | ||
|
|
5b5f1975c5 | ||
|
|
bac2f15668 | ||
|
|
3804c62c7d | ||
|
|
326f418c88 | ||
|
|
9ebba12e5b | ||
|
|
1924d222e1 | ||
|
|
15d88ba595 | ||
|
|
986e74663a | ||
|
|
2d85230097 | ||
|
|
0e1fe60d8a | ||
|
|
cad7d2d95f | ||
|
|
e1f57cd618 | ||
|
|
51b4b0b647 | ||
|
|
abc78bac57 | ||
|
|
510ab2811a | ||
|
|
5e81b26b87 | ||
|
|
2618d0d68f | ||
|
|
55d944ff55 | ||
|
|
1a5b77d47a | ||
|
|
9e85c64139 | ||
|
|
7dd9e6b97c | ||
|
|
6cd75f5abd | ||
|
|
6cea3e6c60 | ||
|
|
f5b4e44aed | ||
|
|
b6a04e4f12 | ||
|
|
caaf9dc4f3 | ||
|
|
9668c318dc | ||
|
|
e5befb9226 | ||
|
|
033f34c0b8 | ||
|
|
7146c97362 | ||
|
|
428451dca6 | ||
|
|
d3d1489829 | ||
|
|
b74aa31b87 | ||
|
|
20550baa38 | ||
|
|
f18e70dda6 | ||
|
|
5ddeb41a5d | ||
|
|
5d431094bb | ||
|
|
fb5229a5f3 | ||
|
|
694059d3ce | ||
|
|
2299feb809 | ||
|
|
59105bd1da | ||
|
|
9018ffce7a | ||
|
|
94662b722d | ||
|
|
0ffad32657 | ||
|
|
50803c2e25 | ||
|
|
334fe45adc | ||
|
|
ebdd3e8413 | ||
|
|
ffe58fc189 | ||
|
|
7065464227 | ||
|
|
5f567e2473 | ||
|
|
46ffcdf182 | ||
|
|
9afeec5683 | ||
|
|
329047e865 | ||
|
|
5c7e6b3830 | ||
|
|
1e51439414 | ||
|
|
a472f7f696 | ||
|
|
29c764773f | ||
|
|
af056f2355 | ||
|
|
6803f3c6f5 | ||
|
|
6b9ce0da66 | ||
|
|
38d62af1ba | ||
|
|
c880ab7cc1 | ||
|
|
613a1fb553 | ||
|
|
14f255c2d5 | ||
|
|
eaa5a9a204 | ||
|
|
34ccbcc13d | ||
|
|
f58a120db1 | ||
|
|
5b59cfbc34 | ||
|
|
cc69892e3b | ||
|
|
c94330ee9c | ||
|
|
377056e80c | ||
|
|
1dbaff7b61 | ||
|
|
bf416f1b5f | ||
|
|
d83bcf638f | ||
|
|
acfe3b0a04 | ||
|
|
04f36a4cb1 | ||
|
|
41a0138c16 | ||
|
|
f1be729206 | ||
|
|
cacd853374 | ||
|
|
07caa6890f | ||
|
|
9706285474 | ||
|
|
1510b4014b | ||
|
|
d5e0f7e505 | ||
|
|
b9e5cf1220 | ||
|
|
f4eb59c373 | ||
|
|
09b92084c8 | ||
|
|
06257d044a | ||
|
|
34ca7a4a7b | ||
|
|
ce70be5ca3 | ||
|
|
dd3051d62b | ||
|
|
5f290fed7f | ||
|
|
a34ec8ed22 | ||
|
|
4597b207e7 | ||
|
|
9257cb02ee | ||
|
|
cd8a1d9a32 | ||
|
|
ee9ae21bd2 | ||
|
|
bd1451ce18 | ||
|
|
a94cc4b7f7 | ||
|
|
cf2ccd7e14 | ||
|
|
69ab00b34b | ||
|
|
0043870882 | ||
|
|
0ea42ae541 | ||
|
|
ad50cfbcbb | ||
|
|
cf65ae81cf | ||
|
|
19ca7d9a77 | ||
|
|
0b2ee45526 | ||
|
|
28e39ada84 | ||
|
|
fb52b955cc | ||
|
|
77f75b916d | ||
|
|
97022ba873 | ||
|
|
aee71b3fd6 | ||
|
|
76535852e4 | ||
|
|
a694e8d122 | ||
|
|
93fee8263f | ||
|
|
28859641eb | ||
|
|
3a2be243c0 | ||
|
|
9fdf41813a | ||
|
|
04f3a9480f | ||
|
|
f7762b3119 | ||
|
|
634e4116cf | ||
|
|
015c09b0e5 | ||
|
|
6e0a43c777 | ||
|
|
7fc527b649 | ||
|
|
2f0ba0782a | ||
|
|
bc3b6c792f | ||
|
|
b5a3d617fd |
@@ -1,28 +0,0 @@
|
||||
name: "Update pinned clan-core for checks"
|
||||
on:
|
||||
repository_dispatch:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "51 2 * * *"
|
||||
jobs:
|
||||
update-pinned-clan-core:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Update clan-core for checks
|
||||
run: nix run .#update-clan-core-for-checks
|
||||
- name: Create pull request
|
||||
env:
|
||||
CI_BOT_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
|
||||
run: |
|
||||
export GIT_AUTHOR_NAME=clan-bot GIT_AUTHOR_EMAIL=clan-bot@clan.lol GIT_COMMITTER_NAME=clan-bot GIT_COMMITTER_EMAIL=clan-bot@clan.lol
|
||||
git commit -am "Update pinned clan-core for checks"
|
||||
|
||||
# Use shared PR creation script
|
||||
export PR_BRANCH="update-clan-core-for-checks"
|
||||
export PR_TITLE="Update Clan Core for Checks"
|
||||
export PR_BODY="This PR updates the pinned clan-core flake input that is used for checks."
|
||||
|
||||
./.gitea/workflows/create-pr.sh
|
||||
@@ -19,8 +19,7 @@ jobs:
|
||||
uses: Mic92/update-flake-inputs-gitea@main
|
||||
with:
|
||||
# Exclude private flakes and update-clan-core checks flake
|
||||
|
||||
exclude-patterns: "devFlake/private/flake.nix,checks/impure/flake.nix"
|
||||
exclude-patterns: "checks/impure/flake.nix"
|
||||
auto-merge: true
|
||||
gitea-token: ${{ secrets.CI_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.CI_BOT_GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
name: "Update private flake inputs"
|
||||
on:
|
||||
repository_dispatch:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * *" # Run daily at 3 AM
|
||||
jobs:
|
||||
update-private-flake:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Update private flake inputs
|
||||
run: |
|
||||
# Update the private flake lock file
|
||||
cd devFlake/private
|
||||
nix flake update
|
||||
cd ../..
|
||||
|
||||
# Update the narHash
|
||||
bash ./devFlake/update-private-narhash
|
||||
- name: Create pull request
|
||||
env:
|
||||
CI_BOT_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
|
||||
run: |
|
||||
export GIT_AUTHOR_NAME=clan-bot GIT_AUTHOR_EMAIL=clan-bot@clan.lol GIT_COMMITTER_NAME=clan-bot GIT_COMMITTER_EMAIL=clan-bot@clan.lol
|
||||
|
||||
# Check if there are any changes
|
||||
if ! git diff --quiet; then
|
||||
git add devFlake/private/flake.lock devFlake/private.narHash
|
||||
git commit -m "Update dev flake"
|
||||
|
||||
# Use shared PR creation script
|
||||
export PR_BRANCH="update-dev-flake"
|
||||
export PR_TITLE="Update dev flake"
|
||||
export PR_BODY="This PR updates the dev flake inputs and corresponding narHash."
|
||||
else
|
||||
echo "No changes detected in dev flake inputs"
|
||||
fi
|
||||
@@ -24,7 +24,7 @@ If you're new to Clan and eager to dive in, start with our quickstart guide and
|
||||
|
||||
In the Clan ecosystem, security is paramount. Learn how to handle secrets effectively:
|
||||
|
||||
- **Secrets Management**: Securely manage secrets by consulting [secrets](https://docs.clan.lol/guides/getting-started/secrets/)<!-- [secrets.md](docs/site/guides/getting-started/secrets.md) -->.
|
||||
- **Secrets Management**: Securely manage secrets by consulting [Vars](https://docs.clan.lol/concepts/generators/)<!-- [secrets.md](docs/site/concepts/generators.md) -->.
|
||||
|
||||
### Contributing to Clan
|
||||
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
{ self, ... }:
|
||||
{
|
||||
clan.machines.test-backup = {
|
||||
imports = [ self.nixosModules.test-backup ];
|
||||
fileSystems."/".device = "/dev/null";
|
||||
boot.loader.grub.device = "/dev/null";
|
||||
};
|
||||
clan.inventory.services = {
|
||||
borgbackup.test-backup = {
|
||||
roles.client.machines = [ "test-backup" ];
|
||||
roles.server.machines = [ "test-backup" ];
|
||||
};
|
||||
};
|
||||
flake.nixosModules = {
|
||||
test-backup =
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
dependencies =
|
||||
[
|
||||
pkgs.stdenv.drvPath
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues (builtins.removeAttrs self.inputs [ "self" ]));
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
# Do not import inventory modules. They should be configured via 'clan.inventory'
|
||||
#
|
||||
# TODO: Configure localbackup via inventory
|
||||
self.clanModules.localbackup
|
||||
];
|
||||
# Borgbackup overrides
|
||||
services.borgbackup.repos.test-backups = {
|
||||
path = "/var/lib/borgbackup/test-backups";
|
||||
authorizedKeys = [ (builtins.readFile ../assets/ssh/pubkey) ];
|
||||
};
|
||||
clan.borgbackup.destinations.test-backup.repo = lib.mkForce "borg@machine:.";
|
||||
|
||||
clan.core.networking.targetHost = "machine";
|
||||
networking.hostName = "machine";
|
||||
|
||||
programs.ssh.knownHosts = {
|
||||
machine.hostNames = [ "machine" ];
|
||||
machine.publicKey = builtins.readFile ../assets/ssh/pubkey;
|
||||
};
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings.UsePAM = false;
|
||||
settings.UseDns = false;
|
||||
hostKeys = [
|
||||
{
|
||||
path = "/root/.ssh/id_ed25519";
|
||||
type = "ed25519";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
users.users.root.openssh.authorizedKeys.keyFiles = [ ../assets/ssh/pubkey ];
|
||||
|
||||
# This is needed to unlock the user for sshd
|
||||
# Because we use sshd without setuid binaries
|
||||
users.users.borg.initialPassword = "hello";
|
||||
|
||||
systemd.tmpfiles.settings."vmsecrets" = {
|
||||
"/root/.ssh/id_ed25519" = {
|
||||
C.argument = "${../assets/ssh/privkey}";
|
||||
z = {
|
||||
mode = "0400";
|
||||
user = "root";
|
||||
};
|
||||
};
|
||||
"/etc/secrets/ssh.id_ed25519" = {
|
||||
C.argument = "${../assets/ssh/privkey}";
|
||||
z = {
|
||||
mode = "0400";
|
||||
user = "root";
|
||||
};
|
||||
};
|
||||
"/etc/secrets/borgbackup/borgbackup.ssh" = {
|
||||
C.argument = "${../assets/ssh/privkey}";
|
||||
z = {
|
||||
mode = "0400";
|
||||
user = "root";
|
||||
};
|
||||
};
|
||||
"/etc/secrets/borgbackup/borgbackup.repokey" = {
|
||||
C.argument = builtins.toString (pkgs.writeText "repokey" "repokey12345");
|
||||
z = {
|
||||
mode = "0400";
|
||||
user = "root";
|
||||
};
|
||||
};
|
||||
};
|
||||
clan.core.facts.secretStore = "vm";
|
||||
clan.core.vars.settings.secretStore = "vm";
|
||||
|
||||
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
||||
environment.etc.install-closure.source = "${closureInfo}/store-paths";
|
||||
nix.settings = {
|
||||
substituters = lib.mkForce [ ];
|
||||
hashed-mirrors = null;
|
||||
connect-timeout = lib.mkForce 3;
|
||||
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
||||
};
|
||||
system.extraDependencies = dependencies;
|
||||
clan.core.state.test-backups.folders = [ "/var/test-backups" ];
|
||||
|
||||
clan.core.state.test-service = {
|
||||
preBackupScript = ''
|
||||
touch /var/test-service/pre-backup-command
|
||||
'';
|
||||
preRestoreScript = ''
|
||||
touch /var/test-service/pre-restore-command
|
||||
'';
|
||||
postRestoreScript = ''
|
||||
touch /var/test-service/post-restore-command
|
||||
'';
|
||||
folders = [ "/var/test-service" ];
|
||||
};
|
||||
|
||||
fileSystems."/mnt/external-disk" = {
|
||||
device = "/dev/vdb"; # created in tests with virtualisation.emptyDisks
|
||||
autoFormat = true;
|
||||
fsType = "ext4";
|
||||
options = [
|
||||
"defaults"
|
||||
"noauto"
|
||||
];
|
||||
};
|
||||
|
||||
clan.localbackup.targets.hdd = {
|
||||
directory = "/mnt/external-disk";
|
||||
preMountHook = ''
|
||||
touch /run/mount-external-disk
|
||||
'';
|
||||
postUnmountHook = ''
|
||||
touch /run/unmount-external-disk
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
perSystem =
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
clanCore = self.checks.x86_64-linux.clan-core-for-checks;
|
||||
in
|
||||
{
|
||||
checks = pkgs.lib.mkIf pkgs.stdenv.isLinux {
|
||||
nixos-test-backups = self.clanLib.test.containerTest {
|
||||
name = "nixos-test-backups";
|
||||
nodes.machine = {
|
||||
imports =
|
||||
[
|
||||
self.nixosModules.clanCore
|
||||
# Some custom overrides for the backup tests
|
||||
self.nixosModules.test-backup
|
||||
]
|
||||
++
|
||||
# import the inventory generated nixosModules
|
||||
self.clan.clanInternals.inventoryClass.machines.test-backup.machineImports;
|
||||
clan.core.settings.directory = ./.;
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
start_all()
|
||||
|
||||
# dummy data
|
||||
machine.succeed("mkdir -p /var/test-backups /var/test-service")
|
||||
machine.succeed("echo testing > /var/test-backups/somefile")
|
||||
|
||||
# create
|
||||
machine.succeed("clan backups create --debug --flake ${clanCore} test-backup")
|
||||
machine.wait_until_succeeds("! systemctl is-active borgbackup-job-test-backup >&2")
|
||||
machine.succeed("test -f /run/mount-external-disk")
|
||||
machine.succeed("test -f /run/unmount-external-disk")
|
||||
|
||||
# list
|
||||
backup_id = json.loads(machine.succeed("borg-job-test-backup list --json"))["archives"][0]["archive"]
|
||||
out = machine.succeed("clan backups list --debug --flake ${clanCore} test-backup").strip()
|
||||
print(out)
|
||||
assert backup_id in out, f"backup {backup_id} not found in {out}"
|
||||
localbackup_id = "hdd::/mnt/external-disk/snapshot.0"
|
||||
assert localbackup_id in out, "localbackup not found in {out}"
|
||||
|
||||
## borgbackup restore
|
||||
machine.succeed("rm -f /var/test-backups/somefile")
|
||||
machine.succeed(f"clan backups restore --debug --flake ${clanCore} test-backup borgbackup 'test-backup::borg@machine:.::{backup_id}' >&2")
|
||||
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
|
||||
machine.succeed("test -f /var/test-service/pre-restore-command")
|
||||
machine.succeed("test -f /var/test-service/post-restore-command")
|
||||
machine.succeed("test -f /var/test-service/pre-backup-command")
|
||||
|
||||
## localbackup restore
|
||||
machine.succeed("rm -rf /var/test-backups/somefile /var/test-service/ && mkdir -p /var/test-service")
|
||||
machine.succeed(f"clan backups restore --debug --flake ${clanCore} test-backup localbackup '{localbackup_id}' >&2")
|
||||
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
|
||||
machine.succeed("test -f /var/test-service/pre-restore-command")
|
||||
machine.succeed("test -f /var/test-service/post-restore-command")
|
||||
machine.succeed("test -f /var/test-service/pre-backup-command")
|
||||
'';
|
||||
} { inherit pkgs self; };
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{ fetchgit }:
|
||||
fetchgit {
|
||||
url = "https://git.clan.lol/clan/clan-core.git";
|
||||
rev = "eea93ea22c9818da67e148ba586277bab9e73cea";
|
||||
sha256 = "sha256-PV0Z+97QuxQbkYSVuNIJwUNXMbHZG/vhsA9M4cDTCOE=";
|
||||
rev = "5d884cecc2585a29b6a3596681839d081b4de192";
|
||||
sha256 = "09is1afmncamavb2q88qac37vmsijxzsy1iz1vr6gsyjq2rixaxc";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
self,
|
||||
lib,
|
||||
inputs,
|
||||
privateInputs ? { },
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -19,18 +20,29 @@ let
|
||||
nixosLib = import (self.inputs.nixpkgs + "/nixos/lib") { };
|
||||
in
|
||||
{
|
||||
imports = filter pathExists [
|
||||
./backups/flake-module.nix
|
||||
../nixosModules/clanCore/machine-id/tests/flake-module.nix
|
||||
../nixosModules/clanCore/state-version/tests/flake-module.nix
|
||||
./devshell/flake-module.nix
|
||||
./flash/flake-module.nix
|
||||
./impure/flake-module.nix
|
||||
./installation/flake-module.nix
|
||||
./morph/flake-module.nix
|
||||
./nixos-documentation/flake-module.nix
|
||||
./dont-depend-on-repo-root.nix
|
||||
];
|
||||
imports =
|
||||
let
|
||||
clanCoreModulesDir = ../nixosModules/clanCore;
|
||||
getClanCoreTestModules =
|
||||
let
|
||||
moduleNames = attrNames (builtins.readDir clanCoreModulesDir);
|
||||
testPaths = map (
|
||||
moduleName: clanCoreModulesDir + "/${moduleName}/tests/flake-module.nix"
|
||||
) moduleNames;
|
||||
in
|
||||
filter pathExists testPaths;
|
||||
in
|
||||
getClanCoreTestModules
|
||||
++ filter pathExists [
|
||||
./devshell/flake-module.nix
|
||||
./flash/flake-module.nix
|
||||
./impure/flake-module.nix
|
||||
./installation/flake-module.nix
|
||||
./update/flake-module.nix
|
||||
./morph/flake-module.nix
|
||||
./nixos-documentation/flake-module.nix
|
||||
./dont-depend-on-repo-root.nix
|
||||
];
|
||||
flake.check = genAttrs [ "x86_64-linux" "aarch64-darwin" ] (
|
||||
system:
|
||||
let
|
||||
@@ -88,11 +100,11 @@ in
|
||||
nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs;
|
||||
nixos-test-zt-tcp-relay = self.clanLib.test.containerTest ./zt-tcp-relay nixosTestArgs;
|
||||
nixos-test-matrix-synapse = self.clanLib.test.containerTest ./matrix-synapse nixosTestArgs;
|
||||
nixos-test-postgresql = self.clanLib.test.containerTest ./postgresql nixosTestArgs;
|
||||
nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs;
|
||||
nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;
|
||||
|
||||
service-dummy-test = import ./service-dummy-test nixosTestArgs;
|
||||
wireguard = import ./wireguard nixosTestArgs;
|
||||
service-dummy-test-from-flake = import ./service-dummy-test-from-flake nixosTestArgs;
|
||||
};
|
||||
|
||||
@@ -146,9 +158,12 @@ in
|
||||
'';
|
||||
|
||||
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
|
||||
cp -r ${pkgs.callPackage ./clan-core-for-checks.nix { }} $out
|
||||
chmod +w $out/flake.lock
|
||||
cp -r ${privateInputs.clan-core-for-checks} $out
|
||||
chmod -R +w $out
|
||||
cp ${../flake.lock} $out/flake.lock
|
||||
|
||||
# Create marker file to disable private flake loading in tests
|
||||
touch $out/.skip-private-inputs
|
||||
'';
|
||||
};
|
||||
packages = lib.optionalAttrs (pkgs.stdenv.isLinux) {
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.toplevel
|
||||
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript
|
||||
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript.drvPath
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
in
|
||||
{
|
||||
@@ -60,6 +61,10 @@
|
||||
nodes.target = {
|
||||
virtualisation.emptyDiskImages = [ 4096 ];
|
||||
virtualisation.memorySize = 4096;
|
||||
|
||||
virtualisation.useNixStoreImage = true;
|
||||
virtualisation.writableStore = true;
|
||||
|
||||
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
||||
environment.etc."install-closure".source = "${closureInfo}/store-paths";
|
||||
|
||||
@@ -78,8 +83,8 @@
|
||||
start_all()
|
||||
|
||||
# Some distros like to automount disks with spaces
|
||||
machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdb && mount /dev/vdb "/mnt/with spaces"')
|
||||
machine.succeed("clan flash write --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdb test-flash-machine-${pkgs.hostPlatform.system}")
|
||||
machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdc && mount /dev/vdc "/mnt/with spaces"')
|
||||
machine.succeed("clan flash write --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdc test-flash-machine-${pkgs.hostPlatform.system}")
|
||||
'';
|
||||
} { inherit pkgs self; };
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
self,
|
||||
lib,
|
||||
privateInputs,
|
||||
|
||||
...
|
||||
}:
|
||||
@@ -149,17 +150,17 @@
|
||||
# vm-test-run-test-installation-> target: To debug, enter the VM and run 'systemctl status backdoor.service'.
|
||||
checks =
|
||||
let
|
||||
# Custom Python package for port management utilities
|
||||
closureInfo = pkgs.closureInfo {
|
||||
rootPaths = [
|
||||
self.checks.x86_64-linux.clan-core-for-checks
|
||||
privateInputs.clan-core-for-checks
|
||||
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.toplevel
|
||||
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.initialRamdisk
|
||||
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.diskoScript
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.bash.drvPath
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
};
|
||||
in
|
||||
pkgs.lib.mkIf (pkgs.stdenv.isLinux && !pkgs.stdenv.isAarch64) {
|
||||
@@ -225,7 +226,7 @@
|
||||
"install",
|
||||
"--phases", "disko,install",
|
||||
"--debug",
|
||||
"--flake", flake_dir,
|
||||
"--flake", str(flake_dir),
|
||||
"--yes", "test-install-machine-without-system",
|
||||
"--target-host", f"nonrootuser@localhost:{ssh_conn.host_port}",
|
||||
"-i", ssh_conn.ssh_key,
|
||||
@@ -289,9 +290,6 @@
|
||||
assert not os.path.exists(hw_config_file), "hardware-configuration.nix should not exist initially"
|
||||
assert not os.path.exists(facter_file), "facter.json should not exist initially"
|
||||
|
||||
# Set CLAN_FLAKE for the commands
|
||||
os.environ["CLAN_FLAKE"] = flake_dir
|
||||
|
||||
# Test facter backend
|
||||
clan_cmd = [
|
||||
"${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
|
||||
|
||||
@@ -159,7 +159,8 @@ let
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.bash.drvPath
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
};
|
||||
|
||||
in
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.stdenvNoCC
|
||||
self.nixosConfigurations.test-morph-machine.config.system.build.toplevel
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
in
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
({
|
||||
name = "postgresql";
|
||||
|
||||
nodes.machine =
|
||||
{ self, config, ... }:
|
||||
{
|
||||
imports = [
|
||||
self.nixosModules.clanCore
|
||||
self.clanModules.postgresql
|
||||
self.clanModules.localbackup
|
||||
];
|
||||
clan.postgresql.users.test = { };
|
||||
clan.postgresql.databases.test.create.options.OWNER = "test";
|
||||
clan.postgresql.databases.test.restore.stopOnRestore = [ "sample-service" ];
|
||||
clan.localbackup.targets.hdd.directory = "/mnt/external-disk";
|
||||
clan.core.settings.directory = ./.;
|
||||
|
||||
systemd.services.sample-service = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
script = ''
|
||||
while true; do
|
||||
echo "Hello, world!"
|
||||
sleep 5
|
||||
done
|
||||
'';
|
||||
};
|
||||
|
||||
environment.systemPackages = [ config.services.postgresql.package ];
|
||||
};
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
''
|
||||
start_all()
|
||||
machine.wait_for_unit("postgresql")
|
||||
machine.wait_for_unit("sample-service")
|
||||
# Create a test table
|
||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -c 'CREATE TABLE test (id serial PRIMARY KEY);' test")
|
||||
|
||||
machine.succeed("/run/current-system/sw/bin/localbackup-create >&2")
|
||||
timestamp_before = int(machine.succeed("systemctl show --property=ExecMainStartTimestampMonotonic sample-service | cut -d= -f2").strip())
|
||||
|
||||
machine.succeed("test -e /mnt/external-disk/snapshot.0/machine/var/backup/postgres/test/pg-dump || { echo 'pg-dump not found'; exit 1; }")
|
||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c 'INSERT INTO test DEFAULT VALUES;'")
|
||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c 'DROP TABLE test;'")
|
||||
machine.succeed("test -e /var/backup/postgres/test/pg-dump || { echo 'pg-dump not found'; exit 1; }")
|
||||
|
||||
machine.succeed("rm -rf /var/backup/postgres")
|
||||
|
||||
machine.succeed("NAME=/mnt/external-disk/snapshot.0 FOLDERS=/var/backup/postgres/test /run/current-system/sw/bin/localbackup-restore >&2")
|
||||
machine.succeed("test -e /var/backup/postgres/test/pg-dump || { echo 'pg-dump not found'; exit 1; }")
|
||||
|
||||
machine.succeed("""
|
||||
set -x
|
||||
${nodes.machine.clan.core.state.test.postRestoreCommand}
|
||||
""")
|
||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -l >&2")
|
||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c '\dt' >&2")
|
||||
|
||||
timestamp_after = int(machine.succeed("systemctl show --property=ExecMainStartTimestampMonotonic sample-service | cut -d= -f2").strip())
|
||||
assert timestamp_before < timestamp_after, f"{timestamp_before} >= {timestamp_after}: expected sample-service to be restarted after restore"
|
||||
|
||||
# Check that the table is still there
|
||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c 'SELECT * FROM test;'")
|
||||
output = machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql --csv -c \"SELECT datdba::regrole FROM pg_database WHERE datname = 'test'\"")
|
||||
owner = output.split("\n")[1]
|
||||
assert owner == "test", f"Expected database owner to be 'test', got '{owner}'"
|
||||
|
||||
# check if restore works if the database does not exist
|
||||
machine.succeed("runuser -u postgres -- dropdb test")
|
||||
machine.succeed("${nodes.machine.clan.core.state.test.postRestoreCommand}")
|
||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c '\dt' >&2")
|
||||
'';
|
||||
})
|
||||
@@ -29,18 +29,10 @@ nixosLib.runTest (
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
''
|
||||
import subprocess
|
||||
from nixos_test_lib.nix_setup import setup_nix_in_nix # type: ignore[import-untyped]
|
||||
setup_nix_in_nix(None) # No closure info for this test
|
||||
|
||||
def run_clan(cmd: list[str], **kwargs) -> str:
|
||||
import subprocess
|
||||
clan = "${clan-core.packages.${hostPkgs.system}.clan-cli}/bin/clan"
|
||||
clan_args = ["--flake", "${config.clan.test.flakeForSandbox}"]
|
||||
return subprocess.run(
|
||||
["${hostPkgs.util-linux}/bin/unshare", "--user", "--map-user", "1000", "--map-group", "1000", clan, *cmd, *clan_args],
|
||||
**kwargs,
|
||||
check=True,
|
||||
).stdout
|
||||
setup_nix_in_nix(None) # No closure info for this test
|
||||
|
||||
start_all()
|
||||
admin1.wait_for_unit("multi-user.target")
|
||||
@@ -60,7 +52,13 @@ nixosLib.runTest (
|
||||
# Check that the file is in the '0644' mode
|
||||
assert "-rw-r--r--" in ls_out, f"File is not in the '0644' mode: {ls_out}"
|
||||
|
||||
run_clan(["machines", "list"])
|
||||
# Run clan command
|
||||
result = subprocess.run(
|
||||
["${
|
||||
clan-core.packages.${hostPkgs.system}.clan-cli
|
||||
}/bin/clan", "machines", "list", "--flake", "${config.clan.test.flakeForSandbox}"],
|
||||
check=True
|
||||
)
|
||||
'';
|
||||
}
|
||||
)
|
||||
|
||||
307
checks/update/flake-module.nix
Normal file
307
checks/update/flake-module.nix
Normal file
@@ -0,0 +1,307 @@
|
||||
{ self, ... }:
|
||||
{
|
||||
# Machine for update test
|
||||
clan.machines.test-update-machine = {
|
||||
imports = [
|
||||
self.nixosModules.test-update-machine
|
||||
# Import the configuration file that will be created/updated during the test
|
||||
./test-update-machine/configuration.nix
|
||||
];
|
||||
};
|
||||
flake.nixosModules.test-update-machine =
|
||||
{ lib, modulesPath, ... }:
|
||||
{
|
||||
imports = [
|
||||
(modulesPath + "/testing/test-instrumentation.nix")
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
self.clanLib.test.minifyModule
|
||||
../../lib/test/container-test-driver/nixos-module.nix
|
||||
];
|
||||
|
||||
# Apply patch to fix x-initrd.mount filesystem handling in switch-to-configuration-ng
|
||||
nixpkgs.overlays = [
|
||||
(_final: prev: {
|
||||
switch-to-configuration-ng = prev.switch-to-configuration-ng.overrideAttrs (old: {
|
||||
patches = (old.patches or [ ]) ++ [ ./switch-to-configuration-initrd-mount-fix.patch ];
|
||||
});
|
||||
})
|
||||
];
|
||||
|
||||
networking.hostName = "update-machine";
|
||||
|
||||
environment.etc."install-successful".text = "ok";
|
||||
|
||||
# Enable SSH and add authorized key for testing
|
||||
services.openssh.enable = true;
|
||||
services.openssh.settings.PasswordAuthentication = false;
|
||||
users.users.root.openssh.authorizedKeys.keys = [ (builtins.readFile ../assets/ssh/pubkey) ];
|
||||
services.openssh.knownHosts.localhost.publicKeyFile = ../assets/ssh/pubkey;
|
||||
services.openssh.hostKeys = [
|
||||
{
|
||||
path = ../assets/ssh/privkey;
|
||||
type = "ed25519";
|
||||
}
|
||||
];
|
||||
security.sudo.wheelNeedsPassword = false;
|
||||
|
||||
boot.consoleLogLevel = lib.mkForce 100;
|
||||
boot.kernelParams = [ "boot.shell_on_fail" ];
|
||||
|
||||
boot.isContainer = true;
|
||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||
# Preserve the IP addresses assigned by the test framework
|
||||
# (based on virtualisation.vlans = [1] and node number 1)
|
||||
networking.interfaces.eth1 = {
|
||||
useDHCP = false;
|
||||
ipv4.addresses = [
|
||||
{
|
||||
address = "192.168.1.1";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
ipv6.addresses = [
|
||||
{
|
||||
address = "2001:db8:1::1";
|
||||
prefixLength = 64;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Define the mounts that exist in the container to prevent them from being stopped
|
||||
fileSystems = {
|
||||
"/" = {
|
||||
device = "/dev/disk/by-label/nixos";
|
||||
fsType = "ext4";
|
||||
options = [ "x-initrd.mount" ];
|
||||
};
|
||||
"/nix/.rw-store" = {
|
||||
device = "tmpfs";
|
||||
fsType = "tmpfs";
|
||||
options = [
|
||||
"mode=0755"
|
||||
];
|
||||
};
|
||||
"/nix/store" = {
|
||||
device = "overlay";
|
||||
fsType = "overlay";
|
||||
options = [
|
||||
"lowerdir=/nix/.ro-store"
|
||||
"upperdir=/nix/.rw-store/upper"
|
||||
"workdir=/nix/.rw-store/work"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
checks =
|
||||
pkgs.lib.optionalAttrs (pkgs.stdenv.isLinux && pkgs.stdenv.hostPlatform.system == "x86_64-linux")
|
||||
{
|
||||
nixos-test-update =
|
||||
let
|
||||
closureInfo = pkgs.closureInfo {
|
||||
rootPaths = [
|
||||
self.packages.${pkgs.system}.clan-cli
|
||||
self.checks.${pkgs.system}.clan-core-for-checks
|
||||
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-update-machine.config.system.build.toplevel
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.bash.drvPath
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
};
|
||||
in
|
||||
self.clanLib.test.containerTest {
|
||||
name = "update";
|
||||
nodes.machine = {
|
||||
imports = [ self.nixosModules.test-update-machine ];
|
||||
};
|
||||
extraPythonPackages = _p: [
|
||||
self.legacyPackages.${pkgs.system}.nixosTestLib
|
||||
];
|
||||
|
||||
testScript = ''
|
||||
import tempfile
|
||||
import os
|
||||
import subprocess
|
||||
from nixos_test_lib.ssh import setup_ssh_connection # type: ignore[import-untyped]
|
||||
from nixos_test_lib.nix_setup import prepare_test_flake # type: ignore[import-untyped]
|
||||
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# Verify initial state
|
||||
machine.succeed("test -f /etc/install-successful")
|
||||
machine.fail("test -f /etc/update-successful")
|
||||
|
||||
# Set up test environment
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Prepare test flake and Nix store
|
||||
flake_dir = prepare_test_flake(
|
||||
temp_dir,
|
||||
"${self.checks.x86_64-linux.clan-core-for-checks}",
|
||||
"${closureInfo}"
|
||||
)
|
||||
(flake_dir / ".clan-flake").write_text("") # Ensure .clan-flake exists
|
||||
|
||||
# Set up SSH connection
|
||||
ssh_conn = setup_ssh_connection(
|
||||
machine,
|
||||
temp_dir,
|
||||
"${../assets/ssh/privkey}"
|
||||
)
|
||||
|
||||
# Update the machine configuration to add a new file
|
||||
machine_config_path = os.path.join(flake_dir, "machines", "test-update-machine", "configuration.nix")
|
||||
os.makedirs(os.path.dirname(machine_config_path), exist_ok=True)
|
||||
|
||||
# Note: update command doesn't accept -i flag, SSH key must be in ssh-agent
|
||||
# Start ssh-agent and add the key
|
||||
agent_output = subprocess.check_output(["${pkgs.openssh}/bin/ssh-agent", "-s"], text=True)
|
||||
for line in agent_output.splitlines():
|
||||
if line.startswith("SSH_AUTH_SOCK="):
|
||||
os.environ["SSH_AUTH_SOCK"] = line.split("=", 1)[1].split(";")[0]
|
||||
elif line.startswith("SSH_AGENT_PID="):
|
||||
os.environ["SSH_AGENT_PID"] = line.split("=", 1)[1].split(";")[0]
|
||||
|
||||
# Add the SSH key to the agent
|
||||
subprocess.run(["${pkgs.openssh}/bin/ssh-add", ssh_conn.ssh_key], check=True)
|
||||
|
||||
|
||||
##############
|
||||
print("TEST: update with --build-host local")
|
||||
with open(machine_config_path, "w") as f:
|
||||
f.write("""
|
||||
{
|
||||
environment.etc."update-build-local-successful".text = "ok";
|
||||
}
|
||||
""")
|
||||
|
||||
# rsync the flake into the container
|
||||
os.environ["PATH"] = f"{os.environ['PATH']}:${pkgs.openssh}/bin"
|
||||
subprocess.run(
|
||||
[
|
||||
"${pkgs.rsync}/bin/rsync",
|
||||
"-a",
|
||||
"--delete",
|
||||
"-e",
|
||||
"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no",
|
||||
f"{str(flake_dir)}/",
|
||||
f"root@192.168.1.1:/flake",
|
||||
],
|
||||
check=True
|
||||
)
|
||||
|
||||
# allow machine to ssh into itself
|
||||
subprocess.run([
|
||||
"ssh",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
f"root@192.168.1.1",
|
||||
"mkdir -p /root/.ssh && chmod 700 /root/.ssh && echo \"$(cat \"${../assets/ssh/privkey}\")\" > /root/.ssh/id_ed25519 && chmod 600 /root/.ssh/id_ed25519",
|
||||
], check=True)
|
||||
|
||||
# install the clan-cli package into the container's Nix store
|
||||
subprocess.run(
|
||||
[
|
||||
"${pkgs.nix}/bin/nix",
|
||||
"copy",
|
||||
"--to",
|
||||
"ssh://root@192.168.1.1",
|
||||
"--no-check-sigs",
|
||||
f"${self.packages.${pkgs.system}.clan-cli}",
|
||||
"--extra-experimental-features", "nix-command flakes",
|
||||
"--from", f"{os.environ["TMPDIR"]}/store"
|
||||
],
|
||||
check=True,
|
||||
env={
|
||||
**os.environ,
|
||||
"NIX_SSHOPTS": "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no",
|
||||
},
|
||||
)
|
||||
|
||||
# Run ssh on the host to run the clan update command via --build-host local
|
||||
subprocess.run([
|
||||
"ssh",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
f"root@192.168.1.1",
|
||||
"${self.packages.${pkgs.system}.clan-cli}/bin/clan",
|
||||
"machines",
|
||||
"update",
|
||||
"--debug",
|
||||
"--flake", "/flake",
|
||||
"--host-key-check", "none",
|
||||
"--upload-inputs", # Use local store instead of fetching from network
|
||||
"--build-host", "localhost",
|
||||
"test-update-machine",
|
||||
"--target-host", f"root@localhost",
|
||||
], check=True)
|
||||
|
||||
# Verify the update was successful
|
||||
machine.succeed("test -f /etc/update-build-local-successful")
|
||||
|
||||
|
||||
##############
|
||||
print("TEST: update with --target-host")
|
||||
|
||||
with open(machine_config_path, "w") as f:
|
||||
f.write("""
|
||||
{
|
||||
environment.etc."target-host-update-successful".text = "ok";
|
||||
}
|
||||
""")
|
||||
|
||||
# Run clan update command
|
||||
subprocess.run([
|
||||
"${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
|
||||
"machines",
|
||||
"update",
|
||||
"--debug",
|
||||
"--flake", flake_dir,
|
||||
"--host-key-check", "none",
|
||||
"--upload-inputs", # Use local store instead of fetching from network
|
||||
"test-update-machine",
|
||||
"--target-host", f"root@192.168.1.1:{ssh_conn.host_port}",
|
||||
], check=True)
|
||||
|
||||
# Verify the update was successful
|
||||
machine.succeed("test -f /etc/target-host-update-successful")
|
||||
|
||||
|
||||
##############
|
||||
print("TEST: update with --build-host")
|
||||
# Update configuration again
|
||||
with open(machine_config_path, "w") as f:
|
||||
f.write("""
|
||||
{
|
||||
environment.etc."build-host-update-successful".text = "ok";
|
||||
}
|
||||
""")
|
||||
|
||||
# Run clan update command with --build-host
|
||||
subprocess.run([
|
||||
"${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
|
||||
"machines",
|
||||
"update",
|
||||
"--debug",
|
||||
"--flake", flake_dir,
|
||||
"--host-key-check", "none",
|
||||
"--upload-inputs", # Use local store instead of fetching from network
|
||||
"--build-host", f"root@192.168.1.1:{ssh_conn.host_port}",
|
||||
"test-update-machine",
|
||||
"--target-host", f"root@192.168.1.1:{ssh_conn.host_port}",
|
||||
], check=True)
|
||||
|
||||
# Verify the second update was successful
|
||||
machine.succeed("test -f /etc/build-host-update-successful")
|
||||
'';
|
||||
} { inherit pkgs self; };
|
||||
};
|
||||
};
|
||||
}
|
||||
17
checks/update/switch-to-configuration-initrd-mount-fix.patch
Normal file
17
checks/update/switch-to-configuration-initrd-mount-fix.patch
Normal file
@@ -0,0 +1,17 @@
|
||||
diff --git a/src/main.rs b/src/main.rs
|
||||
index 8baf5924a7db..1234567890ab 100644
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -1295,6 +1295,12 @@ won't take effect until you reboot the system.
|
||||
|
||||
for (mountpoint, current_filesystem) in current_filesystems {
|
||||
// Use current version of systemctl binary before daemon is reexeced.
|
||||
+
|
||||
+ // Skip filesystem comparison if x-initrd.mount is present in options
|
||||
+ if current_filesystem.options.contains("x-initrd.mount") {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
let unit = path_to_unit_name(¤t_system_bin, &mountpoint);
|
||||
if let Some(new_filesystem) = new_filesystems.get(&mountpoint) {
|
||||
if current_filesystem.fs_type != new_filesystem.fs_type
|
||||
3
checks/update/test-update-machine/configuration.nix
Normal file
3
checks/update/test-update-machine/configuration.nix
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
# Initial empty configuration
|
||||
}
|
||||
115
checks/wireguard/default.nix
Normal file
115
checks/wireguard/default.nix
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
pkgs,
|
||||
nixosLib,
|
||||
clan-core,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
nixosLib.runTest (
|
||||
{ ... }:
|
||||
|
||||
let
|
||||
machines = [
|
||||
"controller1"
|
||||
"controller2"
|
||||
"peer1"
|
||||
"peer2"
|
||||
"peer3"
|
||||
];
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
clan-core.modules.nixosTest.clanTest
|
||||
];
|
||||
|
||||
hostPkgs = pkgs;
|
||||
|
||||
name = "wireguard";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
modules."@clan/wireguard" = import ../../clanServices/wireguard/default.nix;
|
||||
inventory = {
|
||||
|
||||
machines = lib.genAttrs machines (_: { });
|
||||
|
||||
instances = {
|
||||
|
||||
/*
|
||||
wg-test-one
|
||||
┌───────────────────────────────┐
|
||||
│ ◄───────────── │
|
||||
│ controller2 controller1
|
||||
│ ▲ ─────────────► ▲ ▲
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ │ │ │ └───────────────┐ │ │ │ │
|
||||
│ │ │ └──────────────┐ │ │ │ │ │
|
||||
│ ▼ │ ▼ ▼ ▼
|
||||
└─► peer2 │ peer1 peer3
|
||||
│ ▲
|
||||
└──────────┘
|
||||
*/
|
||||
|
||||
wg-test-one = {
|
||||
|
||||
module.name = "@clan/wireguard";
|
||||
module.input = "self";
|
||||
|
||||
roles.controller.machines."controller1".settings = {
|
||||
endpoint = "192.168.1.1";
|
||||
};
|
||||
|
||||
roles.controller.machines."controller2".settings = {
|
||||
endpoint = "192.168.1.2";
|
||||
};
|
||||
|
||||
roles.peer.machines = {
|
||||
peer1.settings.controller = "controller1";
|
||||
peer2.settings.controller = "controller2";
|
||||
peer3.settings.controller = "controller1";
|
||||
};
|
||||
};
|
||||
|
||||
# TODO: Will this actually work with conflicting ports? Can we re-use interfaces?
|
||||
#wg-test-two = {
|
||||
# module.name = "@clan/wireguard";
|
||||
|
||||
# roles.controller.machines."controller1".settings = {
|
||||
# endpoint = "192.168.1.1";
|
||||
# port = 51922;
|
||||
# };
|
||||
|
||||
# roles.peer.machines = {
|
||||
# peer1 = { };
|
||||
# };
|
||||
#};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
# Show all addresses
|
||||
machines = [peer1, peer2, peer3, controller1, controller2]
|
||||
for m in machines:
|
||||
m.systemctl("start network-online.target")
|
||||
|
||||
for m in machines:
|
||||
m.wait_for_unit("network-online.target")
|
||||
m.wait_for_unit("systemd-networkd.service")
|
||||
|
||||
print("\n\n" + "="*60)
|
||||
print("STARTING PING TESTS")
|
||||
print("="*60)
|
||||
|
||||
for m1 in machines:
|
||||
for m2 in machines:
|
||||
if m1 != m2:
|
||||
print(f"\n--- Pinging from {m1.name} to {m2.name}.wg-test-one ---")
|
||||
m1.wait_until_succeeds(f"ping -c1 {m2.name}.wg-test-one >&2")
|
||||
'';
|
||||
}
|
||||
)
|
||||
6
checks/wireguard/sops/machines/controller1/key.json
Executable file
6
checks/wireguard/sops/machines/controller1/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1rnkc2vmrupy9234clyu7fpur5kephuqs3v7qauaw5zeg00jqjdasefn3cc",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
6
checks/wireguard/sops/machines/controller2/key.json
Executable file
6
checks/wireguard/sops/machines/controller2/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1t2hhg99d4p2yymuhngcy5ccutp8mvu7qwvg5cdhck303h9e7ha9qnlt635",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
6
checks/wireguard/sops/machines/peer1/key.json
Executable file
6
checks/wireguard/sops/machines/peer1/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1jts52rzlqcwjc36jkp56a7fmjn3czr7kl9ta2spkfzhvfama33sqacrzzd",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
6
checks/wireguard/sops/machines/peer2/key.json
Executable file
6
checks/wireguard/sops/machines/peer2/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age12nqnp0zd435ckp5p0v2fv4p2x4cvur2mnxe8use2sx3fgy883vaq4ae75e",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
6
checks/wireguard/sops/machines/peer3/key.json
Executable file
6
checks/wireguard/sops/machines/peer3/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1sglr4zp34drjfydzeweq43fz3uwpul3hkh53lsfa9drhuzwmkqyqn5jegp",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
15
checks/wireguard/sops/secrets/controller1-age.key/secret
Normal file
15
checks/wireguard/sops/secrets/controller1-age.key/secret
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:zDF0RiBqaawpg+GaFkuLPomJ01Xu+lgY5JfUzaIk2j03XkCzIf8EMrmn6pRtBP3iUjPBm+gQSTQk6GHTONrixA5hRNyETV+UgQw=,iv:zUUCAGZ0cz4Tc2t/HOjVYNsdnrAOtid/Ns5ak7rnyCk=,tag:z43WtNSue4Ddf7AVu21IKA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlY1NEdjAzQm5RMFZWY3BJ\nclp6c01FdlZFK3dOSDB4cHc1NTdwMXErMFJFCnIrRVFNZEFYOG1rVUhFd2xsbTJ2\nVkJHNmdOWXlOcHJoQ0QzM1VyZmxmcGcKLS0tIFk1cEx4dFdvNGRwK1FWdDZsb1lR\nV2d1RFZtNzZqVFdtQ1FzNStEcEgyUUkKx8tkxqJz/Ko3xgvhvd6IYiV/lRGmrY13\nUZpYWR9tsQwZAR9dLjCyVU3JRuXeGB1unXC1CO0Ff3R0A/PuuRHh+g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:19:37Z",
|
||||
"mac": "ENC[AES256_GCM,data:8RGOUhZ2LGmC9ugULwHDgdMrtdo9vzBm3BJmL4XTuNJKm0NlKfgNLi1E4n9DMQ+kD4hKvcwbiUcwSGE8jZD6sm7Sh3bJi/HZCoiWm/O/OIzstli2NNDBGvQBgyWZA5H+kDjZ6aEi6icNWIlm5gsty7KduABnf5B3p0Bn5Uf5Bio=,iv:sGZp0XF+mgocVzAfHF8ATdlSE/5zyz5WUSRMJqNeDQs=,tag:ymYVBRwF5BOSAu5ONU2qKw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
1
checks/wireguard/sops/secrets/controller1-age.key/users/admin
Symbolic link
1
checks/wireguard/sops/secrets/controller1-age.key/users/admin
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
15
checks/wireguard/sops/secrets/controller2-age.key/secret
Normal file
15
checks/wireguard/sops/secrets/controller2-age.key/secret
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:dHM7zWzqnC1QLRKYpbI2t63kOFnSaQy6ur9zlkLQf17Q03CNrqUsZtdEbwMnLR3llu7eVMhtvVRkXjEkvn3leb9HsNFmtk/DP70=,iv:roEZsBFqRypM106O5sehTzo7SySOJUJgAR738rTtOo8=,tag:VDd9/6uU0SAM7pWRLIUhUQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKTEVYUmVGbUtOcHZ4cnc3\nKzNETnlxaVRKYTI3eWVHdEoyc3l2SnhsZ1J3CnB2RnZrOXM5Uml6TThDUlZjY25J\nbkJ6eUZ2ckN1NWpNUU9IaE93UDJQdlEKLS0tIC95ZDhkU0R1VHhCdldxdW4zSmps\nN3NqL1cvd05hRTRPdDA3R2pzNUFFajgKS+DJH14fH9AvEAa3PoUC1jEqKAzTmExN\nl32FeHTHbGMo1PKeaFm+Eg0WSpAmFE7beBunc5B73SW30ok6x4FcQw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:19:47Z",
|
||||
"mac": "ENC[AES256_GCM,data:77EnuBQyguvkCtobUg8/6zoLHjmeGDrSBZuIXOZBMxdbJjzhRg++qxQjuu6t0FoWATtz7u4Y3/jzUMGffr/N5HegqSq0D2bhv7AqJwBiVaOwd80fRTtM+YiP/zXsCk52Pj/Gadapg208bDPQ1BBDOyz/DrqZ7w//j+ARJjAnugI=,iv:IuTDmJKZEuHXJXjxrBw0gP2t6vpxAYEqbtpnVbavVCY=,tag:4EnpX6rOamtg1O+AaEQahQ==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
1
checks/wireguard/sops/secrets/controller2-age.key/users/admin
Symbolic link
1
checks/wireguard/sops/secrets/controller2-age.key/users/admin
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
15
checks/wireguard/sops/secrets/peer1-age.key/secret
Normal file
15
checks/wireguard/sops/secrets/peer1-age.key/secret
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:wcSsqxTKiMAnzPwxs5DNjcSdLyjVQ9UOrZxfSbOkVfniwx6F7xz6dLNhaDq7MHQ0vRWpg28yNs7NHrp52bYFnb/+eZsis46WiCw=,iv:B4t1lvS2gC601MtsmZfEiEulLWvSGei3/LSajwFS9Vs=,tag:hnRXlZyYEFfLJUrw1SqbSQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAybUgya2VEdzMvRG1hdkpu\nM2pGNmcyVmcvYVZ1ZjJlY3A1bXFUUUtkMTI0CmJoRFZmejZjN2UxUXNuc1k5WnE2\nNmxIcnpNQ1lJZ3ZKSmhtSlVURXJTSUUKLS0tIGU4Wi9yZ3VYekJkVW9pNWFHblFk\na0gzbTVKUWdSam1sVjRUaUlTdVd5YWMKntRc9yb9VPOTMibp8QM5m57DilP01N/X\nPTQaw8oI40znnHdctTZz7S+W/3Te6sRnkOhFyalWmsKY0CWg/FELlA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:19:58Z",
|
||||
"mac": "ENC[AES256_GCM,data:8nq+ugkUJxE24lUIySySs/cAF8vnfqr936L/5F0O1QFwNrbpPmKRXkuwa6u0V+187L2952Id20Fym4ke59f3fJJsF840NCKDwDDZhBZ20q9GfOqIKImEom/Nzw6D0WXQLUT3w8EMyJ/F+UaJxnBNPR6f6+Kx4YgStYzCcA6Ahzg=,iv:VBPktEz7qwWBBnXE+xOP/EUVy7/AmNCHPoK56Yt/ZNc=,tag:qXONwOLFAlopymBEf5p4Sw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
1
checks/wireguard/sops/secrets/peer1-age.key/users/admin
Symbolic link
1
checks/wireguard/sops/secrets/peer1-age.key/users/admin
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
15
checks/wireguard/sops/secrets/peer2-age.key/secret
Normal file
15
checks/wireguard/sops/secrets/peer2-age.key/secret
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:4d3ri0EsDmWRtA8vzvpPRLMsSp4MIMKwvtn0n0pRY05uBPXs3KcjnweMPIeTE1nIhqnMR2o2MfLah5TCPpaFax9+wxIt74uacbg=,iv:0LBAldTC/hN4QLCxgXTl6d9UB8WmUTnj4sD2zHQuG2w=,tag:zr/RhG/AU4g9xj9l2BprKw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvV0JnZDhlU1piU1g2cng0\ncytKOEZ6WlZlNGRGUjV3MmVMd2Nzc0ZwelgwCjBGdThCUGlXbVFYdnNoZWpJZ3Vm\nc2xkRXhxS09vdzltSVoxLzhFSVduak0KLS0tIE5DRjJ6cGxiVlB1eElHWXhxN1pJ\nYWtIMDMvb0Z6akJjUzlqeEFsNHkxL2cKpghv/QegnXimeqd9OPFouGM//jYvoVmw\n2d4mLT2JSMkEhpfGcqb6vswhdJfCiKuqr2B4bqwAnPMaykhsm8DFRQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:20:08Z",
|
||||
"mac": "ENC[AES256_GCM,data:BzlQVAJ7HzcxNPKB3JhabqRX/uU0EElj172YecjmOflHnzz/s9xgfdAfJK/c53hXlX4LtGPnubH7a8jOolRq98zmZeBYE27+WLs2aN7Ufld6mYk90/i7u4CqR+Fh2Kfht04SlUJCjnS5A9bTPwU9XGRHJ0BiOhzTuSMUJTRaPRM=,iv:L50K5zc1o99Ix9nP0pb9PRH+VIN2yvq7JqKeVHxVXmc=,tag:XFLkSCsdbTPxbasDYYxcFQ==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
1
checks/wireguard/sops/secrets/peer2-age.key/users/admin
Symbolic link
1
checks/wireguard/sops/secrets/peer2-age.key/users/admin
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
15
checks/wireguard/sops/secrets/peer3-age.key/secret
Normal file
15
checks/wireguard/sops/secrets/peer3-age.key/secret
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:qfLm6+g1vYnESCik9uyBeKsY6Ju2Gq3arnn2I8HHNO67Ri5BWbOQTvtz7WT8/q94RwVjv8SGeJ/fsJSpwLSrJSbqTZCPAnYwzzQ=,iv:PnA9Ao8RRELNhNQYbaorstc0KaIXRU7h3+lgDCXZFHk=,tag:VeLgYQYwqthYihIoQTwYiA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWVVQaDJFd0N3WHptRC9Z\nZTgxTWh5bnU1SkpqRWRXZnhPaFhpSVJmVEhrCjFvdHFYenNWaFNrdXlha09iS2xj\nOTZDcUNkcHkvTDUwNjM4Z3gxUkxreUEKLS0tIE5oY3Q2bWhsb2FSQTVGTWVSclJw\nWllrelRwT3duYjJJbTV0d3FwU1VuNlkK2eN3fHFX/sVUWom8TeZC9fddqnSCsC1+\nJRCZsG46uHDxqLcKIfdFWh++2t16XupQYk3kn+NUR/aMc3fR32Uwjw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:20:18Z",
|
||||
"mac": "ENC[AES256_GCM,data:nUwsPcP1bsDjAHFjQ1NlVkTwyZY4B+BpzNkMx9gl0rE14j425HVLtlhlLndhRp+XMpnDldQppLAAtSdzMsrw8r5efNgTRl7cu4Fy/b9cHt84k7m0aou5lrGus9SV1bM7/fzC9Xm7CSXBcRzyDGVsKC6UBl1rx+ybh7HyAN05XSo=,iv:It57H+zUUNPkoN1D8sYwyZx5zIFIga7mydhGUHYBCGE=,tag:mBQdYqUpjPknbYa13qESyw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
1
checks/wireguard/sops/secrets/peer3-age.key/users/admin
Symbolic link
1
checks/wireguard/sops/secrets/peer3-age.key/users/admin
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
4
checks/wireguard/sops/users/admin/key.json
Normal file
4
checks/wireguard/sops/users/admin/key.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"type": "age"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/controller1
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:noe913+28JWkoDkGGMu++cc1+j5NPDoyIhWixdsowoiVO3cTWGkZ88SUGO5D,iv:ynYMljwqMcBdk8RpVcw/2Jflg2RCF28r4fKUgIAF8B4=,tag:+TsXDJgfUhKgg4iQVXKKlQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhYVRReTZBQ05GYmVBVjhS\nNXM5aFlhVzZRaVl6UHl6S3JnMC9Sb1dwZ1ZjCmVuS2dEVExYZWROVklUZWFCSnM2\nZnlxbVNseTM2c0Q0TjhsT3NzYmtqREUKLS0tIHBRTFpvVGt6d1cxZ2lFclRsUVhZ\nZDlWaG9PcXVrNUZKaEgxWndjUDVpYjgKt0eOhAgcYdkg9JSEakx4FjChLTn3pis+\njOkuGd4JfXMKcwC7vJV5ygQBxzVJSBw+RucP7sYCBPK0m8Voj94ntw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1rnkc2vmrupy9234clyu7fpur5kephuqs3v7qauaw5zeg00jqjdasefn3cc",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6MFJqNHNraG9DSnJZMFdz\ndU8zVXNTamxROFd1dWtuK2RiekhPdHhleVhFCi8zNWJDNXJMRUlDdjc4Q0UycTIz\nSGFGSmdnNU0wZWlDaTEwTzBqWjh6SFkKLS0tIEJOdjhOMDY2TUFLb3RPczNvMERx\nYkpSeW5VOXZvMlEvdm53MDE3aUFTNjgKyelSTjrTIR9I3rJd3krvzpsrKF1uGs4J\n4MtmQj0/3G+zPYZVBx7b3HF6B3f1Z7LYh05+z7nCnN/duXyPnDjNcg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:19:37Z",
|
||||
"mac": "ENC[AES256_GCM,data:+DmIkPG/H6tCtf8CvB98E1QFXv08QfTcCB3CRsi+XWnIRBkryRd/Au9JahViHMdK7MED8WNf84NWTjY2yH4y824/DjI8XXNMF1iVMo0CqY42xbVHtUuhXrYeT+c8CyEw+M6zfy1jC0+Bm3WQWgagz1G6A9SZk3D2ycu0N08+axA=,iv:kwBjTYebIy5i2hagAajSwwuKnSkrM9GyrnbeQXB2e/w=,tag:EgKJ5gVGYj1NGFUduxLGfg==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
lQfR7GhivN87XoXruTGOPjVPhNu1Brt//wyc3pdwE20=
|
||||
@@ -0,0 +1 @@
|
||||
7470bb5c79df224a9b7f5a2259acd2e46db763c27e24cb3416c8b591cb328077
|
||||
@@ -0,0 +1 @@
|
||||
fd51:19c1:3b:f700
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/controller2
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:2kehACgvNgoYGPwnW7p86BR0yUu689Chth6qZf9zoJtuTY9ATS68dxDyBc5S,iv:qb2iDUtExegTeN3jt6SA8RnU61W5GDDhn56QXiQT4gw=,tag:pSGPICX5p6qlZ1WMVoIEYQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSTTR5TDY4RE9VYmlCK1dL\nWkVRcVZqVDlsbmQvUlJmdzF2b1Z1S0k3NngwCkFWNzRVaERtSmFsd0o2aFJOb0ZX\nSU9yUnVaNi9IUjJWeGRFcEpDUXo5WkEKLS0tIEczNkxiYnJsTWRoLzFhQVF1M21n\nWnZEdGV1N2N5d1FZQkJUQ1IrdGFLblkKPTpha2bxS8CCAMXWTDKX/WOcdvggaP3Y\nqewyahDNzb4ggP+LNKp55BtwFjdvoPoq4BpYOOgMRbQMMk+H1o9WFw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1t2hhg99d4p2yymuhngcy5ccutp8mvu7qwvg5cdhck303h9e7ha9qnlt635",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYcEZ6Tzk3M0pkV0tOdTBj\nenF2a0tHNnhBa0NrazMwV1VBbXBZR3pzSHpvCnBZOEU0VlFHS1FHcVpTTDdPczVV\nV0RFSlZ0VmIzWGoydEdKVXlIUE9OOEkKLS0tIFZ0cWVBR1loeVlWa2c4U3oweXE2\ncm1ja0JCS3U5Nk41dlAzV2NabDc2bDQKdgCDNnpRZlFPnEGlX6fo0SQX4yOB+E6r\ntnSwofR3xxZvkyme/6JJU5qBZXyCXEAhKMRkFyvJANXzMJAUo/Osow==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:19:48Z",
|
||||
"mac": "ENC[AES256_GCM,data:e3EkL8vwRhLsec83Zi9DE3PKT+4RwgiffpN4QHcJKTgmDW6hzizWc5kAxbNWGJ9Qqe6sso2KY7tc+hg1lHEsmzjCbg153p8h+7lVI2XT6adi/CS8WZ2VpeL+0X9zDQCjqHmrESZAYFBdkLqO4jucdf0Pc3CKKD+N3BDDTwSUvHM=,iv:xvR7dJL8sdYen00ovrYT8PNxhB9XxSWDSRz1IK23I/o=,tag:OyhAvllBgfAp3eGeNpR/Nw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
5Z7gbLFbXpEFfomW2pKyZBpZN5xvUtiqrIL0GVfNtQ8=
|
||||
@@ -0,0 +1 @@
|
||||
c3672fdb9fb31ddaf6572fc813cf7a8fe50488ef4e9d534c62d4f29da60a1a99
|
||||
@@ -0,0 +1 @@
|
||||
fd51:19c1:c1:aa00
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer1
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:b+akw85T3D9xc75CPLHucR//k7inpxKDvgpR8tCNKwNDRVjVHjcABhfZNLXW,iv:g11fZE8UI0MVh9GKdjR6leBlxa4wN7ZubozXG/VlBbw=,tag:0YkzWCW3zJ3Mt3br/jmTYw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1jts52rzlqcwjc36jkp56a7fmjn3czr7kl9ta2spkfzhvfama33sqacrzzd",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXWkJUR0pIa2xOSEw2dThm\nYlNuOHZCVW93Wkc5LzE4YmpUTHRkZlk3ckc4CnN4M3ZRMWNFVitCT3FyWkxaR0di\nb0NmSXFhRHJmTWg0d05OcWx1LytscEEKLS0tIEtleTFqU3JrRjVsdHpJeTNuVUhF\nWEtnOVlXVXRFamFSak5ia2F2b0JiTzAKlhOBZvZ4AN+QqAYQXvd6YNmgVS4gtkWT\nbV3bLNTgwtrDtet9NDHM8vdF+cn5RZxwFfgmTbDEow6Zm8EXfpxj/g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6YVYyQkZqMTJYQTlyRG5Y\nbnJ2UkE1TS9FZkpSa2tQbk1hQjViMi9OcGk0CjFaZUdjU3JtNzh0bDFXdTdUVW4x\nanFqZHZjZjdzKzA2MC8vTWh3Uy82UGcKLS0tIDhyOFl3UGs3czdoMlpza3UvMlB1\nSE90MnpGc05sSCtmVWg0UVNVdmRvN2MKHlCr4U+7bsoYb+2fgT4mEseZCEjxrtLu\n55sR/4YH0vqMnIBnLTSA0e+WMrs3tQfseeJM5jY/ZNnpec1LbxkGTg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:19:58Z",
|
||||
"mac": "ENC[AES256_GCM,data:gEoEC9D2Z7k5F8egaY1qPXT5/96FFVsyofSBivQ28Ir/9xHX2j40PAQrYRJUWsk/GAUMOyi52Wm7kPuacw+bBcdtQ0+MCDEmjkEnh1V83eZ/baey7iMmg05uO92MYY5o4e7ZkwzXoAeMCMcfO0GqjNvsYJHF1pSNa+UNDj+eflw=,iv:dnIYpvhAdvUDe9md53ll42krb0sxcHy/toqGc7JFxNA=,tag:0WkZU7GeKMD1DQTYaI+1dg==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
juK7P/92N2t2t680aLIRobHc3ts49CsZBvfZOyIKpUc=
|
||||
@@ -0,0 +1 @@
|
||||
b36142569a74a0de0f9b229f2a040ae33a22d53bef5e62aa6939912d0cda05ba
|
||||
@@ -0,0 +1 @@
|
||||
6987:50a0:9b93:4337
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer2
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:apX2sLwtq6iQgLJslFwiRMNBUe0XLzLQbhKfmb2pKiJG7jGNHUgHJz3Ls4Ca,iv:HTDatm3iD5wACTkkd3LdRNvJfnfg75RMtn9G6Q7Fqd4=,tag:Mfehlljnes5CFD1NJdk27A==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age12nqnp0zd435ckp5p0v2fv4p2x4cvur2mnxe8use2sx3fgy883vaq4ae75e",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVZzFyMUZsd2V2VWxOUmhP\nZE8yZTc4Q0RkZisxR25NemR1TzVDWmJZVjBVClA1MWhsU0xzSG16aUx3cWFWKzlG\nSkxrT09OTkVqLzlWejVESE1QWHVJaFkKLS0tIGxlaGVuWU43RXErNTB3c3FaUnM3\nT0N5M253anZkbnFkZWw2VHA0eWhxQW8Kd1PMtEX1h0Hd3fDLMi++gKJkzPi9FXUm\n+uYhx+pb+pJM+iLkPwP/q6AWC7T0T4bHfekkdzxrbsKMi73x/GrOiw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqVzRIMWdlNjVwTURyMFkv\nSUhiajZkZVNuWklRYit6cno4UzNDa2szOFN3CkQ2TWhHb25pbmR1MlBsRXNLL2lx\ncVZ3c3BsWXN2aS9UUVYvN3I4S0xUSmMKLS0tIE5FV0U5aXVUZk9XL0U0Z2ZSNGd5\nbU9zY3IvMlpSNVFLYkRNQUpUYVZOWFUK7j4Otzb8CJTcT7aAj9/irxHEDXh1HkTg\nzz7Ho8/ZncNtaCVHlHxjTgVW9d5aIx8fSsV9LRCFwHMtNzvwj1Nshg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:20:08Z",
|
||||
"mac": "ENC[AES256_GCM,data:e7WNVEz78noHBiz6S3A6qNfop+yBXB3rYN0k4GvaQKz3b99naEHuqIF8Smzzt4XrbbiPKu2iLa5ddLBlqqsi32UQUB8JS9TY7hvW8ol+jpn0VxusGCXW9ThdDEsM/hXiPyr331C73zTvbOYI1hmcGMlJL9cunVRO9rkMtEqhEfo=,iv:6zt7wjIs1y5xDHNK+yLOwoOuUpY7/dOGJGT6UWAFeOg=,tag:gzFTgoxhoLzUV0lvzOhhfg==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
XI9uSaQRDBCb82cMnGzGJcbqRfDG/IXZobyeL+kV03k=
|
||||
@@ -0,0 +1 @@
|
||||
360f9fce4a984eb87ce2a673eb5341ecb89c0f62126548d45ef25ff5243dd646
|
||||
@@ -0,0 +1 @@
|
||||
3b21:3ced:003e:89b3
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer3
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:Gluvjes/3oH5YsDq00JDJyJgoEFcj56smioMArPSt309MDGExYX2QsCzeO1q,iv:oBBJRDdTj/1dWEvzhdFKQ2WfeCKyavKMLmnMbqnU5PM=,tag:2WNFxKz2dWyVcybpm5N4iw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtQWpjRmhZTFdPa2VSZkFN\nbUczMlY5bDBmMTdoMy8xcWxMaXpWVitMZGdjCnRWb2Y3eGpHU1hmNHRJVFBqbU5w\nVEZGdUIrQXk0U0dUUEZ6bE5EMFpTRHMKLS0tIGpYSmZmQThJUTlvTHpjc05ZVlM4\nQWhTOWxnUHZnYlJ3czE3ZUJ0L3ozWTQK3a7N0Zpzo4sUezYveqvKR49RUdJL23eD\n+cK5lk2xbtj+YHkeG+dg7UlHfDaicj0wnFH1KLuWmNd1ONa6eQp3BQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1sglr4zp34drjfydzeweq43fz3uwpul3hkh53lsfa9drhuzwmkqyqn5jegp",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3a2FOWlVsSkdnendrYmUz\ndEpuL1hZSWNFTUtDYm14S3V1aW9KS3hsazJRCkp2SkFFbi9hbGJpNks1MlNTL0s5\nTk5pcUMxaEJobkcvWmRGeU9jMkdNdzAKLS0tIDR6M0Y5eE1ETHJJejAzVW1EYy9v\nZCtPWHJPUkhuWnRzSGhMUUtTa280UmMKXvtnxyop7PmRvTOFkV80LziDjhGh93Pf\nYwhD/ByD/vMmr21Fd6PVHOX70FFT30BdnMc1/wt7c/0iAw4w4GoQsA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-08-13T09:20:18Z",
|
||||
"mac": "ENC[AES256_GCM,data:3nXMTma0UYXCco+EM8UW45cth7DVMboFBKyesL86GmaG6OlTkA2/25AeDrtSVO13a5c2jC6yNFK5dE6pSe5R9f0BoDF7d41mgc85zyn+LGECNWKC6hy6gADNSDD6RRuV1S3FisFQl1F1LD8LiSWmg/XNMZzChNlHYsCS8M+I84g=,iv:pu5VVXAVPmVoXy0BJ+hq5Ar8R0pZttKSYa4YS+dhDNc=,tag:xp1S/4qExnxMTGwhfLJrkA==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
t6qN4VGLR+VMhrBDNKQEXZVyRsEXs1/nGFRs5DI82F8=
|
||||
@@ -0,0 +1 @@
|
||||
e3facc99b73fe029d4c295f71829a83f421f38d82361cf412326398175da162a
|
||||
@@ -0,0 +1 @@
|
||||
e42b:bf85:33f4:f0b1
|
||||
@@ -4,7 +4,7 @@ description = "Statically configure borgbackup with sane defaults."
|
||||
!!! Danger "Deprecated"
|
||||
Use [borgbackup](borgbackup.md) instead.
|
||||
|
||||
Don't use borgbackup-static through [inventory](../../guides/inventory.md).
|
||||
Don't use borgbackup-static through [inventory](../../concepts/inventory.md).
|
||||
|
||||
This module implements the `borgbackup` backend and implements sane defaults
|
||||
for backup management through `borgbackup` for members of the clan.
|
||||
|
||||
@@ -112,125 +112,124 @@ in
|
||||
'';
|
||||
in
|
||||
lib.mkIf (cfg.targets != { }) {
|
||||
environment.systemPackages =
|
||||
[
|
||||
(pkgs.writeShellScriptBin "localbackup-create" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.rsnapshot
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
]
|
||||
}
|
||||
${lib.concatMapStringsSep "\n" (target: ''
|
||||
${mountHook target}
|
||||
echo "Creating backup '${target.name}'"
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeShellScriptBin "localbackup-create" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.rsnapshot
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
]
|
||||
}
|
||||
${lib.concatMapStringsSep "\n" (target: ''
|
||||
${mountHook target}
|
||||
echo "Creating backup '${target.name}'"
|
||||
|
||||
${lib.optionalString (target.preBackupHook != null) ''
|
||||
(
|
||||
${target.preBackupHook}
|
||||
)
|
||||
''}
|
||||
|
||||
declare -A preCommandErrors
|
||||
${lib.concatMapStringsSep "\n" (
|
||||
state:
|
||||
lib.optionalString (state.preBackupCommand != null) ''
|
||||
echo "Running pre-backup command for ${state.name}"
|
||||
if ! /run/current-system/sw/bin/${state.preBackupCommand}; then
|
||||
preCommandErrors["${state.name}"]=1
|
||||
fi
|
||||
''
|
||||
) (builtins.attrValues config.clan.core.state)}
|
||||
|
||||
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" sync
|
||||
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" snapshot
|
||||
'') (builtins.attrValues cfg.targets)}'')
|
||||
(pkgs.writeShellScriptBin "localbackup-list" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.jq
|
||||
pkgs.findutils
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
]
|
||||
}
|
||||
(${
|
||||
lib.concatMapStringsSep "\n" (target: ''
|
||||
(
|
||||
${mountHook target}
|
||||
find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \
|
||||
| jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.name}::" + .)}'
|
||||
)
|
||||
'') (builtins.attrValues cfg.targets)
|
||||
}) | jq -s .
|
||||
'')
|
||||
(pkgs.writeShellScriptBin "localbackup-restore" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.rsync
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.gawk
|
||||
]
|
||||
}
|
||||
if [[ "''${NAME:-}" == "" ]]; then
|
||||
echo "No backup name given via NAME environment variable"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "''${FOLDERS:-}" == "" ]]; then
|
||||
echo "No folders given via FOLDERS environment variable"
|
||||
exit 1
|
||||
fi
|
||||
name=$(awk -F'::' '{print $1}' <<< $NAME)
|
||||
backupname=''${NAME#$name::}
|
||||
|
||||
if command -v localbackup-mount-$name; then
|
||||
localbackup-mount-$name
|
||||
fi
|
||||
if command -v localbackup-unmount-$name; then
|
||||
trap "localbackup-unmount-$name" EXIT
|
||||
fi
|
||||
|
||||
if [[ ! -d $backupname ]]; then
|
||||
echo "No backup found $backupname"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=':' read -ra FOLDER <<< "''$FOLDERS"
|
||||
for folder in "''${FOLDER[@]}"; do
|
||||
mkdir -p "$folder"
|
||||
rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder"
|
||||
done
|
||||
'')
|
||||
]
|
||||
++ (lib.mapAttrsToList (
|
||||
name: target:
|
||||
pkgs.writeShellScriptBin ("localbackup-mount-" + name) ''
|
||||
set -efu -o pipefail
|
||||
${lib.optionalString (target.preMountHook != null) target.preMountHook}
|
||||
${lib.optionalString (target.mountpoint != null) ''
|
||||
if ! ${pkgs.util-linux}/bin/mountpoint -q ${lib.escapeShellArg target.mountpoint}; then
|
||||
${pkgs.util-linux}/bin/mount -o X-mount.mkdir ${lib.escapeShellArg target.mountpoint}
|
||||
fi
|
||||
${lib.optionalString (target.preBackupHook != null) ''
|
||||
(
|
||||
${target.preBackupHook}
|
||||
)
|
||||
''}
|
||||
${lib.optionalString (target.postMountHook != null) target.postMountHook}
|
||||
''
|
||||
) cfg.targets)
|
||||
++ lib.mapAttrsToList (
|
||||
name: target:
|
||||
pkgs.writeShellScriptBin ("localbackup-unmount-" + name) ''
|
||||
set -efu -o pipefail
|
||||
${lib.optionalString (target.preUnmountHook != null) target.preUnmountHook}
|
||||
${lib.optionalString (
|
||||
target.mountpoint != null
|
||||
) "${pkgs.util-linux}/bin/umount ${lib.escapeShellArg target.mountpoint}"}
|
||||
${lib.optionalString (target.postUnmountHook != null) target.postUnmountHook}
|
||||
''
|
||||
) cfg.targets;
|
||||
|
||||
declare -A preCommandErrors
|
||||
${lib.concatMapStringsSep "\n" (
|
||||
state:
|
||||
lib.optionalString (state.preBackupCommand != null) ''
|
||||
echo "Running pre-backup command for ${state.name}"
|
||||
if ! /run/current-system/sw/bin/${state.preBackupCommand}; then
|
||||
preCommandErrors["${state.name}"]=1
|
||||
fi
|
||||
''
|
||||
) (builtins.attrValues config.clan.core.state)}
|
||||
|
||||
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" sync
|
||||
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" snapshot
|
||||
'') (builtins.attrValues cfg.targets)}'')
|
||||
(pkgs.writeShellScriptBin "localbackup-list" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.jq
|
||||
pkgs.findutils
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
]
|
||||
}
|
||||
(${
|
||||
lib.concatMapStringsSep "\n" (target: ''
|
||||
(
|
||||
${mountHook target}
|
||||
find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \
|
||||
| jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.name}::" + .)}'
|
||||
)
|
||||
'') (builtins.attrValues cfg.targets)
|
||||
}) | jq -s .
|
||||
'')
|
||||
(pkgs.writeShellScriptBin "localbackup-restore" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.rsync
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.gawk
|
||||
]
|
||||
}
|
||||
if [[ "''${NAME:-}" == "" ]]; then
|
||||
echo "No backup name given via NAME environment variable"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "''${FOLDERS:-}" == "" ]]; then
|
||||
echo "No folders given via FOLDERS environment variable"
|
||||
exit 1
|
||||
fi
|
||||
name=$(awk -F'::' '{print $1}' <<< $NAME)
|
||||
backupname=''${NAME#$name::}
|
||||
|
||||
if command -v localbackup-mount-$name; then
|
||||
localbackup-mount-$name
|
||||
fi
|
||||
if command -v localbackup-unmount-$name; then
|
||||
trap "localbackup-unmount-$name" EXIT
|
||||
fi
|
||||
|
||||
if [[ ! -d $backupname ]]; then
|
||||
echo "No backup found $backupname"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=':' read -ra FOLDER <<< "''$FOLDERS"
|
||||
for folder in "''${FOLDER[@]}"; do
|
||||
mkdir -p "$folder"
|
||||
rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder"
|
||||
done
|
||||
'')
|
||||
]
|
||||
++ (lib.mapAttrsToList (
|
||||
name: target:
|
||||
pkgs.writeShellScriptBin ("localbackup-mount-" + name) ''
|
||||
set -efu -o pipefail
|
||||
${lib.optionalString (target.preMountHook != null) target.preMountHook}
|
||||
${lib.optionalString (target.mountpoint != null) ''
|
||||
if ! ${pkgs.util-linux}/bin/mountpoint -q ${lib.escapeShellArg target.mountpoint}; then
|
||||
${pkgs.util-linux}/bin/mount -o X-mount.mkdir ${lib.escapeShellArg target.mountpoint}
|
||||
fi
|
||||
''}
|
||||
${lib.optionalString (target.postMountHook != null) target.postMountHook}
|
||||
''
|
||||
) cfg.targets)
|
||||
++ lib.mapAttrsToList (
|
||||
name: target:
|
||||
pkgs.writeShellScriptBin ("localbackup-unmount-" + name) ''
|
||||
set -efu -o pipefail
|
||||
${lib.optionalString (target.preUnmountHook != null) target.preUnmountHook}
|
||||
${lib.optionalString (
|
||||
target.mountpoint != null
|
||||
) "${pkgs.util-linux}/bin/umount ${lib.escapeShellArg target.mountpoint}"}
|
||||
${lib.optionalString (target.postUnmountHook != null) target.postUnmountHook}
|
||||
''
|
||||
) cfg.targets;
|
||||
|
||||
clan.core.backups.providers.localbackup = {
|
||||
# TODO list needs to run locally or on the remote machine
|
||||
|
||||
@@ -61,7 +61,6 @@ in
|
||||
};
|
||||
};
|
||||
imports = [
|
||||
../postgresql
|
||||
(lib.mkRemovedOptionModule [
|
||||
"clan"
|
||||
"matrix-synapse"
|
||||
@@ -106,57 +105,56 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
clan.postgresql.users.matrix-synapse = { };
|
||||
clan.postgresql.databases.matrix-synapse.create.options = {
|
||||
clan.core.postgresql.enable = true;
|
||||
clan.core.postgresql.users.matrix-synapse = { };
|
||||
clan.core.postgresql.databases.matrix-synapse.create.options = {
|
||||
TEMPLATE = "template0";
|
||||
LC_COLLATE = "C";
|
||||
LC_CTYPE = "C";
|
||||
ENCODING = "UTF8";
|
||||
OWNER = "matrix-synapse";
|
||||
};
|
||||
clan.postgresql.databases.matrix-synapse.restore.stopOnRestore = [ "matrix-synapse" ];
|
||||
clan.core.postgresql.databases.matrix-synapse.restore.stopOnRestore = [ "matrix-synapse" ];
|
||||
|
||||
clan.core.vars.generators =
|
||||
{
|
||||
"matrix-synapse" = {
|
||||
files."synapse-registration_shared_secret" = { };
|
||||
runtimeInputs = with pkgs; [
|
||||
coreutils
|
||||
pwgen
|
||||
];
|
||||
migrateFact = "matrix-synapse";
|
||||
script = ''
|
||||
echo -n "$(pwgen -s 32 1)" > "$out"/synapse-registration_shared_secret
|
||||
'';
|
||||
};
|
||||
clan.core.vars.generators = {
|
||||
"matrix-synapse" = {
|
||||
files."synapse-registration_shared_secret" = { };
|
||||
runtimeInputs = with pkgs; [
|
||||
coreutils
|
||||
pwgen
|
||||
];
|
||||
migrateFact = "matrix-synapse";
|
||||
script = ''
|
||||
echo -n "$(pwgen -s 32 1)" > "$out"/synapse-registration_shared_secret
|
||||
'';
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs' (
|
||||
name: user:
|
||||
lib.nameValuePair "matrix-password-${user.name}" {
|
||||
files."matrix-password-${user.name}" = { };
|
||||
migrateFact = "matrix-password-${user.name}";
|
||||
runtimeInputs = with pkgs; [ xkcdpass ];
|
||||
script = ''
|
||||
xkcdpass -n 4 -d - > "$out"/${lib.escapeShellArg "matrix-password-${user.name}"}
|
||||
'';
|
||||
}
|
||||
// lib.mapAttrs' (
|
||||
name: user:
|
||||
lib.nameValuePair "matrix-password-${user.name}" {
|
||||
files."matrix-password-${user.name}" = { };
|
||||
migrateFact = "matrix-password-${user.name}";
|
||||
runtimeInputs = with pkgs; [ xkcdpass ];
|
||||
script = ''
|
||||
xkcdpass -n 4 -d - > "$out"/${lib.escapeShellArg "matrix-password-${user.name}"}
|
||||
'';
|
||||
}
|
||||
) cfg.users;
|
||||
) cfg.users;
|
||||
|
||||
systemd.services.matrix-synapse =
|
||||
let
|
||||
usersScript =
|
||||
''
|
||||
while ! ${pkgs.netcat}/bin/nc -z -v ::1 8008; do
|
||||
if ! kill -0 "$MAINPID"; then exit 1; fi
|
||||
sleep 1;
|
||||
done
|
||||
''
|
||||
+ lib.concatMapStringsSep "\n" (user: ''
|
||||
# only create user if it doesn't exist
|
||||
/run/current-system/sw/bin/matrix-synapse-register_new_matrix_user --exists-ok --password-file ${
|
||||
config.clan.core.vars.generators."matrix-password-${user.name}".files."matrix-password-${user.name}".path
|
||||
} --user "${user.name}" ${if user.admin then "--admin" else "--no-admin"}
|
||||
'') (lib.attrValues cfg.users);
|
||||
usersScript = ''
|
||||
while ! ${pkgs.netcat}/bin/nc -z -v ::1 8008; do
|
||||
if ! kill -0 "$MAINPID"; then exit 1; fi
|
||||
sleep 1;
|
||||
done
|
||||
''
|
||||
+ lib.concatMapStringsSep "\n" (user: ''
|
||||
# only create user if it doesn't exist
|
||||
/run/current-system/sw/bin/matrix-synapse-register_new_matrix_user --exists-ok --password-file ${
|
||||
config.clan.core.vars.generators."matrix-password-${user.name}".files."matrix-password-${user.name}".path
|
||||
} --user "${user.name}" ${if user.admin then "--admin" else "--no-admin"}
|
||||
'') (lib.attrValues cfg.users);
|
||||
in
|
||||
{
|
||||
path = [ pkgs.curl ];
|
||||
|
||||
@@ -1,224 +1,9 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
createDatabaseState =
|
||||
db:
|
||||
let
|
||||
folder = "/var/backup/postgres/${db.name}";
|
||||
current = "${folder}/pg-dump";
|
||||
compression = lib.optionalString (lib.versionAtLeast config.services.postgresql.package.version "16") "--compress=zstd";
|
||||
in
|
||||
{
|
||||
folders = [ folder ];
|
||||
preBackupScript = ''
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
config.services.postgresql.package
|
||||
config.systemd.package
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.zstd
|
||||
]
|
||||
}
|
||||
while [[ "$(systemctl is-active postgresql)" == activating ]]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
mkdir -p "${folder}"
|
||||
runuser -u postgres -- pg_dump ${compression} --dbname=${db.name} -Fc -c > "${current}.tmp"
|
||||
mv "${current}.tmp" ${current}
|
||||
'';
|
||||
postRestoreScript = ''
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
config.services.postgresql.package
|
||||
config.systemd.package
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.zstd
|
||||
pkgs.gnugrep
|
||||
]
|
||||
}
|
||||
while [[ "$(systemctl is-active postgresql)" == activating ]]; do
|
||||
sleep 1
|
||||
done
|
||||
echo "Waiting for postgres to be ready..."
|
||||
while ! runuser -u postgres -- psql --port=${builtins.toString config.services.postgresql.settings.port} -d postgres -c "" ; do
|
||||
if ! systemctl is-active postgresql; then exit 1; fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [[ -e "${current}" ]]; then
|
||||
(
|
||||
systemctl stop ${lib.concatStringsSep " " db.restore.stopOnRestore}
|
||||
trap "systemctl start ${lib.concatStringsSep " " db.restore.stopOnRestore}" EXIT
|
||||
|
||||
mkdir -p "${folder}"
|
||||
if runuser -u postgres -- psql -d postgres -c "SELECT 1 FROM pg_database WHERE datname = '${db.name}'" | grep -q 1; then
|
||||
runuser -u postgres -- dropdb "${db.name}"
|
||||
fi
|
||||
runuser -u postgres -- pg_restore -C -d postgres "${current}"
|
||||
)
|
||||
else
|
||||
echo No database backup found, skipping restore
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
createDatabase = db: ''
|
||||
CREATE DATABASE "${db.name}" ${
|
||||
lib.concatStringsSep " " (
|
||||
lib.mapAttrsToList (name: value: "${name} = '${value}'") db.create.options
|
||||
)
|
||||
}
|
||||
'';
|
||||
cfg = config.clan.postgresql;
|
||||
|
||||
userClauses = lib.mapAttrsToList (
|
||||
_: user:
|
||||
''$PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"' ''
|
||||
) cfg.users;
|
||||
databaseClauses = lib.mapAttrsToList (
|
||||
name: db:
|
||||
lib.optionalString db.create.enable ''$PSQL -d postgres -c "SELECT 1 FROM pg_database WHERE datname = '${name}'" | grep -q 1 || $PSQL -d postgres -c ${lib.escapeShellArg (createDatabase db)} ''
|
||||
) cfg.databases;
|
||||
in
|
||||
{
|
||||
options.clan.postgresql = {
|
||||
# we are reimplemeting ensureDatabase and ensureUser options here to allow to create databases with options
|
||||
databases = lib.mkOption {
|
||||
description = "Databases to create";
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
description = "Database name.";
|
||||
};
|
||||
service = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
description = "Service name that we associate with the database.";
|
||||
};
|
||||
# set to false, in case the upstream module uses ensureDatabase option
|
||||
create.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Create the database if it does not exist.";
|
||||
};
|
||||
create.options = lib.mkOption {
|
||||
description = "Options to pass to the CREATE DATABASE command.";
|
||||
type = lib.types.lazyAttrsOf lib.types.str;
|
||||
default = { };
|
||||
example = {
|
||||
TEMPLATE = "template0";
|
||||
LC_COLLATE = "C";
|
||||
LC_CTYPE = "C";
|
||||
ENCODING = "UTF8";
|
||||
OWNER = "foo";
|
||||
};
|
||||
};
|
||||
restore.stopOnRestore = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "List of systemd services to stop before restoring the database.";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
users = lib.mkOption {
|
||||
description = "Users to create";
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options.name = lib.mkOption {
|
||||
description = "User name";
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
config = {
|
||||
services.postgresql.settings = {
|
||||
wal_level = "replica";
|
||||
max_wal_senders = 3;
|
||||
};
|
||||
|
||||
services.postgresql.enable = true;
|
||||
# We are duplicating a bit the upstream module but allow to create databases with options
|
||||
systemd.services.postgresql.postStart = ''
|
||||
PSQL="psql --port=${builtins.toString config.services.postgresql.settings.port}"
|
||||
|
||||
while ! $PSQL -d postgres -c "" 2> /dev/null; do
|
||||
if ! kill -0 "$MAINPID"; then exit 1; fi
|
||||
sleep 0.1
|
||||
done
|
||||
${lib.concatStringsSep "\n" userClauses}
|
||||
${lib.concatStringsSep "\n" databaseClauses}
|
||||
'';
|
||||
|
||||
clan.core.state = lib.mapAttrs' (
|
||||
_: db: lib.nameValuePair db.service (createDatabaseState db)
|
||||
) config.clan.postgresql.databases;
|
||||
|
||||
environment.systemPackages = builtins.map (
|
||||
db:
|
||||
let
|
||||
folder = "/var/backup/postgres/${db.name}";
|
||||
current = "${folder}/pg-dump";
|
||||
in
|
||||
pkgs.writeShellScriptBin "postgres-db-restore-command-${db.name}" ''
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
config.services.postgresql.package
|
||||
config.systemd.package
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.zstd
|
||||
pkgs.gnugrep
|
||||
]
|
||||
}
|
||||
while [[ "$(systemctl is-active postgresql)" == activating ]]; do
|
||||
sleep 1
|
||||
done
|
||||
echo "Waiting for postgres to be ready..."
|
||||
while ! runuser -u postgres -- psql --port=${builtins.toString config.services.postgresql.settings.port} -d postgres -c "" ; do
|
||||
if ! systemctl is-active postgresql; then exit 1; fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [[ -e "${current}" ]]; then
|
||||
(
|
||||
${lib.optionalString (db.restore.stopOnRestore != [ ]) ''
|
||||
systemctl stop ${builtins.toString db.restore.stopOnRestore}
|
||||
trap "systemctl start ${builtins.toString db.restore.stopOnRestore}" EXIT
|
||||
''}
|
||||
|
||||
mkdir -p "${folder}"
|
||||
if runuser -u postgres -- psql -d postgres -c "SELECT 1 FROM pg_database WHERE datname = '${db.name}'" | grep -q 1; then
|
||||
runuser -u postgres -- dropdb "${db.name}"
|
||||
fi
|
||||
runuser -u postgres -- pg_restore -C -d postgres "${current}"
|
||||
)
|
||||
else
|
||||
echo No database backup found, skipping restore
|
||||
fi
|
||||
''
|
||||
) (builtins.attrValues config.clan.postgresql.databases);
|
||||
};
|
||||
imports = [
|
||||
(lib.mkRemovedOptionModule [
|
||||
"clan"
|
||||
"postgresql"
|
||||
] "The postgresql module has been migrated to a clan core option. Use clan.core.postgresql instead")
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ After the system was installed/deployed the following command can be used to dis
|
||||
clan vars get [machine_name] root-password/root-password
|
||||
```
|
||||
|
||||
See also: [Vars](../../guides/vars-backend.md)
|
||||
See also: [Vars](../../concepts/generators.md)
|
||||
|
||||
To regenerate the password run:
|
||||
```
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
config.clan.core.vars.generators.root-password.files.password-hash.path;
|
||||
|
||||
clan.core.vars.generators.root-password = {
|
||||
files.password-hash =
|
||||
{
|
||||
neededFor = "users";
|
||||
}
|
||||
// (lib.optionalAttrs (_class == "nixos") {
|
||||
restartUnits = lib.optional (config.services.userborn.enable) "userborn.service";
|
||||
});
|
||||
files.password-hash = {
|
||||
neededFor = "users";
|
||||
}
|
||||
// (lib.optionalAttrs (_class == "nixos") {
|
||||
restartUnits = lib.optional (config.services.userborn.enable) "userborn.service";
|
||||
});
|
||||
files.password = {
|
||||
deploy = false;
|
||||
};
|
||||
|
||||
@@ -32,17 +32,16 @@ in
|
||||
cfg.certificate.searchDomains != [ ]
|
||||
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
|
||||
|
||||
hostKeys =
|
||||
[
|
||||
{
|
||||
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
|
||||
type = "ed25519";
|
||||
}
|
||||
]
|
||||
++ lib.optional cfg.hostKeys.rsa.enable {
|
||||
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
|
||||
type = "rsa";
|
||||
};
|
||||
hostKeys = [
|
||||
{
|
||||
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
|
||||
type = "ed25519";
|
||||
}
|
||||
]
|
||||
++ lib.optional cfg.hostKeys.rsa.enable {
|
||||
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
|
||||
type = "rsa";
|
||||
};
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh = {
|
||||
@@ -62,7 +61,8 @@ in
|
||||
hostNames = [
|
||||
"localhost"
|
||||
config.networking.hostName
|
||||
] ++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
|
||||
]
|
||||
++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
|
||||
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
---
|
||||
description = "Statically configure syncthing peers through clan"
|
||||
description = "DEPRECATED: Statically configure syncthing peers through clan"
|
||||
---
|
||||
|
||||
# ⚠️ DEPRECATED
|
||||
|
||||
This module has been migrated to the new clanServices system.
|
||||
|
||||
Please use the new syncthing service instead:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.syncthing = {
|
||||
instances.default = {
|
||||
roles.peer.machines = {
|
||||
machine1 = { };
|
||||
machine2 = { };
|
||||
machine3 = {
|
||||
excludeMachines = [ "machine4" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The new service provides the same functionality with better integration into clan's inventory system.
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# DEPRECATED: This module has been migrated to clanServices/syncthing
|
||||
# Please use the syncthing service instead: services.syncthing.instances.default.roles.peer.machines = { ... };
|
||||
let
|
||||
dir = config.clan.core.settings.directory;
|
||||
machineVarDir = "${dir}/vars/per-machine/";
|
||||
@@ -32,14 +34,15 @@ let
|
||||
value = {
|
||||
name = machine;
|
||||
id = (lib.removeSuffix "\n" (builtins.readFile (syncthingPublicKeyPath machine)));
|
||||
addresses =
|
||||
[ "dynamic" ]
|
||||
++ (
|
||||
if (lib.elem machine networkIpMachines) then
|
||||
[ "tcp://[${(lib.removeSuffix "\n" (builtins.readFile (zerotierIpMachinePath machine)))}]:22000" ]
|
||||
else
|
||||
[ ]
|
||||
);
|
||||
addresses = [
|
||||
"dynamic"
|
||||
]
|
||||
++ (
|
||||
if (lib.elem machine networkIpMachines) then
|
||||
[ "tcp://[${(lib.removeSuffix "\n" (builtins.readFile (zerotierIpMachinePath machine)))}]:22000" ]
|
||||
else
|
||||
[ ]
|
||||
);
|
||||
};
|
||||
}) syncthingPublicKeyMachines;
|
||||
in
|
||||
|
||||
@@ -16,7 +16,7 @@ After the system was installed/deployed the following command can be used to dis
|
||||
clan vars get [machine_name] root-password/root-password
|
||||
```
|
||||
|
||||
See also: [Vars](../../guides/vars-backend.md)
|
||||
See also: [Vars](../../concepts/generators.md)
|
||||
|
||||
To regenerate the password run:
|
||||
```
|
||||
|
||||
@@ -10,7 +10,6 @@ in
|
||||
|
||||
{
|
||||
imports = [
|
||||
../postgresql
|
||||
(lib.mkRemovedOptionModule [
|
||||
"clan"
|
||||
"vaultwarden"
|
||||
@@ -57,15 +56,17 @@ in
|
||||
|
||||
config = {
|
||||
|
||||
clan.postgresql.users.vaultwarden = { };
|
||||
clan.postgresql.databases.vaultwarden.create.options = {
|
||||
clan.core.postgresql.enable = true;
|
||||
|
||||
clan.core.postgresql.users.vaultwarden = { };
|
||||
clan.core.postgresql.databases.vaultwarden.create.options = {
|
||||
TEMPLATE = "template0";
|
||||
LC_COLLATE = "C";
|
||||
LC_CTYPE = "C";
|
||||
ENCODING = "UTF8";
|
||||
OWNER = "vaultwarden";
|
||||
};
|
||||
clan.postgresql.databases.vaultwarden.restore.stopOnRestore = [ "vaultwarden" ];
|
||||
clan.core.postgresql.databases.vaultwarden.restore.stopOnRestore = [ "vaultwarden" ];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
|
||||
@@ -41,25 +41,13 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
# We don't have a good way to specify dependencies between
|
||||
# clanServices for now. When it get's implemtende, we should just
|
||||
# use the ssh and users modules here.
|
||||
./ssh.nix
|
||||
./root-password.nix
|
||||
];
|
||||
|
||||
_module.args = { inherit settings; };
|
||||
|
||||
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues settings.allowedKeys;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# We don't have a good way to specify dependencies between
|
||||
# clanServices for now. When it get's implemtende, we should just
|
||||
# use the ssh and users modules here.
|
||||
imports = [
|
||||
./ssh.nix
|
||||
./root-password.nix
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,39 +1,55 @@
|
||||
# We don't have a way of specifying dependencies between clanServices for now.
|
||||
# When it get's added this file should be removed and the users module used instead.
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
roles.default.perInstance =
|
||||
{ ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
users.mutableUsers = false;
|
||||
users.users.root.hashedPasswordFile =
|
||||
config.clan.core.vars.generators.root-password.files.password-hash.path;
|
||||
users.mutableUsers = false;
|
||||
users.users.root.hashedPasswordFile =
|
||||
config.clan.core.vars.generators.root-password.files.password-hash.path;
|
||||
|
||||
clan.core.vars.generators.root-password = {
|
||||
files.password-hash.neededFor = "users";
|
||||
clan.core.vars.generators.root-password = {
|
||||
files.password-hash.neededFor = "users";
|
||||
|
||||
files.password.deploy = false;
|
||||
files.password.deploy = false;
|
||||
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.mkpasswd
|
||||
pkgs.xkcdpass
|
||||
];
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.mkpasswd
|
||||
pkgs.xkcdpass
|
||||
];
|
||||
|
||||
prompts.password.type = "hidden";
|
||||
prompts.password.persist = true;
|
||||
prompts.password.description = "You can autogenerate a password, if you leave this prompt blank.";
|
||||
prompts.password.display = {
|
||||
group = "Root User";
|
||||
label = "Password";
|
||||
required = false;
|
||||
helperText = ''
|
||||
Your password will be encrypted and stored securely using the secret store you've configured.
|
||||
'';
|
||||
};
|
||||
|
||||
script = ''
|
||||
prompt_value="$(cat "$prompts"/password)"
|
||||
if [[ -n "''${prompt_value-}" ]]; then
|
||||
echo "$prompt_value" | tr -d "\n" > "$out"/password
|
||||
else
|
||||
xkcdpass --numwords 5 --delimiter - --count 1 | tr -d "\n" > "$out"/password
|
||||
fi
|
||||
mkpasswd -s -m sha-512 < "$out"/password | tr -d "\n" > "$out"/password-hash
|
||||
'';
|
||||
};
|
||||
prompts.password.type = "hidden";
|
||||
prompts.password.persist = true;
|
||||
prompts.password.description = "Leave empty to generate automatically";
|
||||
|
||||
script = ''
|
||||
prompt_value="$(cat "$prompts"/password)"
|
||||
if [[ -n "''${prompt_value-}" ]]; then
|
||||
echo "$prompt_value" | tr -d "\n" > "$out"/password
|
||||
else
|
||||
xkcdpass --numwords 5 --delimiter - --count 1 | tr -d "\n" > "$out"/password
|
||||
fi
|
||||
mkpasswd -s -m sha-512 < "$out"/password | tr -d "\n" > "$out"/password-hash
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,115 +1,124 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
let
|
||||
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
|
||||
roles.default.perInstance =
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
|
||||
domains = stringSet settings.certificateSearchDomains;
|
||||
|
||||
in
|
||||
{
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings.PasswordAuthentication = false;
|
||||
|
||||
settings.HostCertificate = lib.mkIf (
|
||||
settings.certificateSearchDomains != [ ]
|
||||
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
|
||||
|
||||
hostKeys =
|
||||
[
|
||||
{
|
||||
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
|
||||
type = "ed25519";
|
||||
}
|
||||
]
|
||||
++ lib.optional settings.rsaHostKey.enable {
|
||||
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
|
||||
type = "rsa";
|
||||
};
|
||||
};
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
|
||||
|
||||
clan.core.vars.generators.openssh = {
|
||||
files."ssh.id_ed25519" = { };
|
||||
files."ssh.id_ed25519.pub".secret = false;
|
||||
migrateFact = "openssh";
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
|
||||
'';
|
||||
};
|
||||
domains = stringSet settings.certificateSearchDomains;
|
||||
|
||||
programs.ssh.knownHosts.clan-sshd-self-ed25519 = {
|
||||
hostNames = [
|
||||
"localhost"
|
||||
config.networking.hostName
|
||||
] ++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
|
||||
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
clan.core.vars.generators.openssh-rsa = lib.mkIf settings.rsaHostKey.enable {
|
||||
files."ssh.id_rsa" = { };
|
||||
files."ssh.id_rsa.pub".secret = false;
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
|
||||
'';
|
||||
};
|
||||
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues settings.allowedKeys;
|
||||
|
||||
clan.core.vars.generators.openssh-cert = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
files."ssh.id_ed25519-cert.pub".secret = false;
|
||||
dependencies = [
|
||||
"openssh"
|
||||
"openssh-ca"
|
||||
];
|
||||
validation = {
|
||||
name = config.clan.core.settings.machine.name;
|
||||
domains = lib.genAttrs settings.certificateSearchDomains lib.id;
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings.PasswordAuthentication = false;
|
||||
|
||||
settings.HostCertificate = lib.mkIf (
|
||||
settings.certificateSearchDomains != [ ]
|
||||
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
|
||||
|
||||
hostKeys = [
|
||||
{
|
||||
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
|
||||
type = "ed25519";
|
||||
}
|
||||
]
|
||||
++ lib.optional settings.rsaHostKey.enable {
|
||||
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
|
||||
type = "rsa";
|
||||
};
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh = {
|
||||
files."ssh.id_ed25519" = { };
|
||||
files."ssh.id_ed25519.pub".secret = false;
|
||||
migrateFact = "openssh";
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
programs.ssh.knownHosts.clan-sshd-self-ed25519 = {
|
||||
hostNames = [
|
||||
"localhost"
|
||||
config.networking.hostName
|
||||
]
|
||||
++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
|
||||
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh-rsa = lib.mkIf settings.rsaHostKey.enable {
|
||||
files."ssh.id_rsa" = { };
|
||||
files."ssh.id_rsa.pub".secret = false;
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
|
||||
'';
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh-cert = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
files."ssh.id_ed25519-cert.pub".secret = false;
|
||||
dependencies = [
|
||||
"openssh"
|
||||
"openssh-ca"
|
||||
];
|
||||
validation = {
|
||||
name = config.clan.core.settings.machine.name;
|
||||
domains = lib.genAttrs settings.certificateSearchDomains lib.id;
|
||||
};
|
||||
runtimeInputs = [
|
||||
pkgs.openssh
|
||||
pkgs.jq
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen \
|
||||
-s $in/openssh-ca/id_ed25519 \
|
||||
-I ${config.clan.core.settings.machine.name} \
|
||||
-h \
|
||||
-n ${lib.concatMapStringsSep "," (d: "${config.clan.core.settings.machine.name}.${d}") domains} \
|
||||
$in/openssh/ssh.id_ed25519.pub
|
||||
mv $in/openssh/ssh.id_ed25519-cert.pub "$out"/ssh.id_ed25519-cert.pub
|
||||
'';
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
share = true;
|
||||
files.id_ed25519.deploy = false;
|
||||
files."id_ed25519.pub" = {
|
||||
deploy = false;
|
||||
secret = false;
|
||||
};
|
||||
runtimeInputs = [
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
certAuthority = true;
|
||||
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificateSearchDomains;
|
||||
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
|
||||
};
|
||||
};
|
||||
};
|
||||
runtimeInputs = [
|
||||
pkgs.openssh
|
||||
pkgs.jq
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen \
|
||||
-s $in/openssh-ca/id_ed25519 \
|
||||
-I ${config.clan.core.settings.machine.name} \
|
||||
-h \
|
||||
-n ${lib.concatMapStringsSep "," (d: "${config.clan.core.settings.machine.name}.${d}") domains} \
|
||||
$in/openssh/ssh.id_ed25519.pub
|
||||
mv $in/openssh/ssh.id_ed25519-cert.pub "$out"/ssh.id_ed25519-cert.pub
|
||||
'';
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
share = true;
|
||||
files.id_ed25519.deploy = false;
|
||||
files."id_ed25519.pub" = {
|
||||
deploy = false;
|
||||
secret = false;
|
||||
};
|
||||
runtimeInputs = [
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
certAuthority = true;
|
||||
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificateSearchDomains;
|
||||
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,59 @@
|
||||
BorgBackup (short: Borg) gives you:
|
||||
## Usage
|
||||
|
||||
- Space efficient storage of backups.
|
||||
- Secure, authenticated encryption.
|
||||
- Compression: lz4, zstd, zlib, lzma or none.
|
||||
- Mountable backups with FUSE.
|
||||
```nix
|
||||
inventory.instances = {
|
||||
borgbackup = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan";
|
||||
};
|
||||
roles.client.machines."jon".settings = {
|
||||
destinations."storagebox" = {
|
||||
repo = "username@$hostname:/./borgbackup";
|
||||
rsh = ''ssh -oPort=23 -i /run/secrets/vars/borgbackup/borgbackup.ssh'';
|
||||
};
|
||||
};
|
||||
roles.server.machines = { };
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
The input should be named according to your flake input. Jon is configured as a
|
||||
client machine with a destination pointing to a Hetzner Storage Box.
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to set up and manage
|
||||
[BorgBackup](https://borgbackup.readthedocs.io/) for secure, efficient backups
|
||||
in a clan network. BorgBackup provides:
|
||||
|
||||
- Space efficient storage of backups with deduplication
|
||||
- Secure, authenticated encryption
|
||||
- Compression: lz4, zstd, zlib, lzma or none
|
||||
- Mountable backups with FUSE
|
||||
- Easy installation on multiple platforms: Linux, macOS, BSD, …
|
||||
- Free software (BSD license).
|
||||
- Backed by a large and active open-source community.
|
||||
|
||||
## Roles
|
||||
|
||||
### 1. Client
|
||||
|
||||
Clients are machines that create and send backups to various destinations. Each
|
||||
client can have multiple backup destinations configured.
|
||||
|
||||
### 2. Server
|
||||
|
||||
Servers act as backup repositories, receiving and storing backups from client
|
||||
machines. They can be dedicated backup servers within your clan network.
|
||||
|
||||
## Backup destinations
|
||||
|
||||
This service allows you to perform backups to multiple `destinations`.
|
||||
Destinations can be:
|
||||
|
||||
- **Local**: Local disk storage
|
||||
- **Server**: Your own borgbackup server (using the `server` role)
|
||||
- **Third-party services**: Such as Hetzner's Storage Box
|
||||
|
||||
For a more comprehensive guide on backups look into the guide section.
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
services.data-mesher.initNetwork =
|
||||
let
|
||||
# for a given machine, read it's public key and remove any new lines
|
||||
readHostKey =
|
||||
machine:
|
||||
let
|
||||
path = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/data-mesher-host-key/public_key/value";
|
||||
in
|
||||
builtins.elemAt (lib.splitString "\n" (builtins.readFile path)) 1;
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
keyPath = config.clan.core.vars.generators.data-mesher-network-key.files.private_key.path;
|
||||
|
||||
tld = settings.network.tld;
|
||||
hostTTL = settings.network.hostTTL;
|
||||
|
||||
# admin and signer host public keys
|
||||
signingKeys = builtins.map readHostKey (builtins.attrNames settings.bootstrapNodes);
|
||||
};
|
||||
}
|
||||
@@ -5,31 +5,15 @@ let
|
||||
{
|
||||
options = {
|
||||
bootstrapNodes = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
|
||||
# the default bootstrap nodes are any machines with the admin or signers role
|
||||
# we iterate through those machines, determining an IP address for them based on their VPN
|
||||
# currently only supports zerotier
|
||||
# default = builtins.foldl' (
|
||||
# urls: name:
|
||||
# let
|
||||
# ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value";
|
||||
# in
|
||||
# if builtins.pathExists ipPath then
|
||||
# let
|
||||
# ip = builtins.readFile ipPath;
|
||||
# in
|
||||
# urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ]
|
||||
# else
|
||||
# urls
|
||||
# ) [ ] (dmLib.machines config).bootstrap;
|
||||
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
||||
description = ''
|
||||
A list of bootstrap nodes that act as an initial gateway when joining
|
||||
the cluster.
|
||||
'';
|
||||
example = {
|
||||
"node1" = "192.168.1.1:7946";
|
||||
"node2" = "192.168.1.2:7946";
|
||||
};
|
||||
example = [
|
||||
"192.168.1.1:7946"
|
||||
"192.168.1.2:7946"
|
||||
];
|
||||
};
|
||||
|
||||
network = {
|
||||
@@ -55,6 +39,59 @@ let
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkBootstrapNodes =
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
roles,
|
||||
settings,
|
||||
}:
|
||||
lib.mkDefault (
|
||||
builtins.foldl' (
|
||||
urls: name:
|
||||
let
|
||||
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value";
|
||||
in
|
||||
if builtins.pathExists ipPath then
|
||||
let
|
||||
ip = builtins.readFile ipPath;
|
||||
in
|
||||
urls ++ [ "[${ip}]:${builtins.toString settings.network.port}" ]
|
||||
else
|
||||
urls
|
||||
) [ ] (builtins.attrNames ((roles.admin.machines or { }) // (roles.signer.machines or { })))
|
||||
);
|
||||
|
||||
mkDmService = dmSettings: config: {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
|
||||
settings = {
|
||||
log_level = "warn";
|
||||
state_dir = "/var/lib/data-mesher";
|
||||
|
||||
# read network id from vars
|
||||
network.id = config.clan.core.vars.generators.data-mesher-network-key.files.public_key.value;
|
||||
|
||||
host = {
|
||||
names = [ config.networking.hostName ];
|
||||
key_path = config.clan.core.vars.generators.data-mesher-host-key.files.private_key.path;
|
||||
};
|
||||
|
||||
cluster = {
|
||||
port = dmSettings.network.port;
|
||||
join_interval = "30s";
|
||||
push_pull_interval = "30s";
|
||||
interface = dmSettings.network.interface;
|
||||
bootstrap_nodes = dmSettings.bootstrapNodes;
|
||||
};
|
||||
|
||||
http.port = 7331;
|
||||
http.interface = "lo";
|
||||
};
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
_class = "clan.service";
|
||||
@@ -67,11 +104,9 @@ in
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
|
||||
imports = [ sharedInterface ];
|
||||
|
||||
options = {
|
||||
|
||||
network = {
|
||||
tld = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
@@ -89,54 +124,117 @@ in
|
||||
};
|
||||
};
|
||||
perInstance =
|
||||
{ settings, roles, ... }:
|
||||
{
|
||||
nixosModule = {
|
||||
imports = [
|
||||
./admin.nix
|
||||
./shared.nix
|
||||
];
|
||||
_module.args = { inherit settings roles; };
|
||||
};
|
||||
extendSettings,
|
||||
roles,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{ config, ... }:
|
||||
let
|
||||
settings = extendSettings {
|
||||
bootstrapNodes = mkBootstrapNodes {
|
||||
inherit
|
||||
config
|
||||
lib
|
||||
roles
|
||||
settings
|
||||
;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [ ./shared.nix ];
|
||||
|
||||
services.data-mesher = (mkDmService settings config) // {
|
||||
initNetwork =
|
||||
let
|
||||
# for a given machine, read it's public key and remove any new lines
|
||||
readHostKey =
|
||||
machine:
|
||||
let
|
||||
path = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/data-mesher-host-key/public_key/value";
|
||||
in
|
||||
builtins.elemAt (lib.splitString "\n" (builtins.readFile path)) 1;
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
keyPath = config.clan.core.vars.generators.data-mesher-network-key.files.private_key.path;
|
||||
|
||||
tld = settings.network.tld;
|
||||
hostTTL = settings.network.hostTTL;
|
||||
|
||||
# admin and signer host public keys
|
||||
signingKeys = builtins.map readHostKey (
|
||||
builtins.attrNames ((roles.admin.machines or { }) // (roles.signer.machines or { }))
|
||||
);
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
roles.signer = {
|
||||
interface =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ sharedInterface ];
|
||||
};
|
||||
interface = sharedInterface;
|
||||
perInstance =
|
||||
{ settings, roles, ... }:
|
||||
{
|
||||
nixosModule = {
|
||||
imports = [
|
||||
./signer.nix
|
||||
./shared.nix
|
||||
];
|
||||
_module.args = { inherit settings roles; };
|
||||
};
|
||||
extendSettings,
|
||||
lib,
|
||||
roles,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{ config, ... }:
|
||||
let
|
||||
settings = extendSettings {
|
||||
bootstrapNodes = mkBootstrapNodes {
|
||||
inherit
|
||||
config
|
||||
lib
|
||||
roles
|
||||
settings
|
||||
;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [ ./shared.nix ];
|
||||
services.data-mesher = (mkDmService settings config);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
roles.peer = {
|
||||
interface =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ sharedInterface ];
|
||||
};
|
||||
interface = sharedInterface;
|
||||
perInstance =
|
||||
{ settings, roles, ... }:
|
||||
{
|
||||
nixosModule = {
|
||||
imports = [
|
||||
./peer.nix
|
||||
./shared.nix
|
||||
];
|
||||
_module.args = { inherit settings roles; };
|
||||
};
|
||||
extendSettings,
|
||||
lib,
|
||||
roles,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{ config, ... }:
|
||||
let
|
||||
settings = extendSettings {
|
||||
bootstrapNodes = mkBootstrapNodes {
|
||||
inherit
|
||||
config
|
||||
lib
|
||||
roles
|
||||
settings
|
||||
;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [ ./shared.nix ];
|
||||
services.data-mesher = (mkDmService settings config);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
{
|
||||
config,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
services.data-mesher = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
|
||||
settings = {
|
||||
log_level = "warn";
|
||||
state_dir = "/var/lib/data-mesher";
|
||||
|
||||
# read network id from vars
|
||||
network.id = config.clan.core.vars.generators.data-mesher-network-key.files.public_key.value;
|
||||
|
||||
host = {
|
||||
names = [ config.networking.hostName ];
|
||||
key_path = config.clan.core.vars.generators.data-mesher-host-key.files.private_key.path;
|
||||
};
|
||||
|
||||
cluster = {
|
||||
port = settings.network.port;
|
||||
join_interval = "30s";
|
||||
push_pull_interval = "30s";
|
||||
interface = settings.network.interface;
|
||||
bootstrap_nodes = (builtins.attrValues settings.bootstrapNodes);
|
||||
};
|
||||
|
||||
http.port = 7331;
|
||||
http.interface = "lo";
|
||||
};
|
||||
};
|
||||
|
||||
# Generate host key.
|
||||
clan.core.vars.generators.data-mesher-host-key = {
|
||||
files =
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
instances = {
|
||||
data-mesher =
|
||||
let
|
||||
bootstrapNodes = {
|
||||
admin = "[2001:db8:1::1]:7946";
|
||||
peer = "[2001:db8:1::2]:7946";
|
||||
# signer = "2001:db8:1::3:7946";
|
||||
};
|
||||
bootstrapNodes = [
|
||||
"[2001:db8:1::1]:7946" # admin
|
||||
"[2001:db8:1::2]:7946" # peer
|
||||
# "2001:db8:1::3:7946" #signer
|
||||
];
|
||||
in
|
||||
{
|
||||
roles.peer.machines.peer.settings = {
|
||||
|
||||
86
clanServices/dyndns/README.md
Normal file
86
clanServices/dyndns/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
|
||||
A Dynamic-DNS (DDNS) service continuously keeps one or more DNS records in sync with the current public IP address of your machine.
|
||||
In *clan* this service is backed by [qdm12/ddns-updater](https://github.com/qdm12/ddns-updater).
|
||||
|
||||
> Info
|
||||
> ddns-updater itself is **heavily opinionated and version-specific**. Whenever you need the exhaustive list of flags or
|
||||
> provider-specific fields refer to its *versioned* documentation – **not** the GitHub README
|
||||
---
|
||||
|
||||
# 1. Configuration model
|
||||
|
||||
Internally ddns-updater consumes a single file named `config.json`.
|
||||
A minimal configuration for the registrar *Namecheap* looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "sub.example.com",
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Another example for *Porkbun*:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "porkbun",
|
||||
"domain": "domain.com",
|
||||
"api_key": "sk1_…",
|
||||
"secret_api_key": "pk1_…",
|
||||
"ip_version": "ipv4",
|
||||
"ipv6_suffix": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When you write a `clan.nix` the **common** fields (`provider`, `domain`, `period`, …) are already exposed as typed
|
||||
*Nix options*.
|
||||
Registrar-specific or very new keys can be passed through an open attribute set called **extraSettings**.
|
||||
|
||||
---
|
||||
|
||||
# 2. Full Porkbun example
|
||||
|
||||
Manage three records – `@`, `home` and `test` – of the domain
|
||||
`jon.blog` and refresh them every 15 minutes:
|
||||
|
||||
```nix title="clan.nix" hl_lines="10-11"
|
||||
inventory.instances = {
|
||||
dyndns = {
|
||||
roles.default.machines."jon" = { };
|
||||
roles.default.settings = {
|
||||
period = 15; # minutes
|
||||
settings = {
|
||||
"all-jon-blog" = {
|
||||
provider = "porkbun";
|
||||
domain = "jon.blog";
|
||||
|
||||
# (1) tell the secret-manager which key we are going to store
|
||||
secret_field_name = "secret_api_key";
|
||||
|
||||
# everything below is copied verbatim into config.json
|
||||
extraSettings = {
|
||||
host = "@,home,test"; # (2) comma-separated list of sub-domains
|
||||
ip_version = "ipv4";
|
||||
ipv6_suffix = "";
|
||||
api_key = "pk1_4bb2b231275a02fdc23b7e6f3552s01S213S"; # (3) public – safe to commit
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
1. `secret_field_name` tells the *vars-generator* to store the entered secret under the specified JSON field name in the configuration.
|
||||
2. ddns-updater allows multiple hosts by separating them with a comma.
|
||||
3. The `api_key` above is *public*; the corresponding **private key** is retrieved through `secret_field_name`.
|
||||
|
||||
277
clanServices/dyndns/default.nix
Normal file
277
clanServices/dyndns/default.nix
Normal file
@@ -0,0 +1,277 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/dyndns";
|
||||
manifest.description = "A dynamic DNS service to update domain IPs";
|
||||
manifest.categories = [ "Network" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.default = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
server = {
|
||||
enable = lib.mkEnableOption "dyndns webserver";
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Domain to serve the webservice on";
|
||||
};
|
||||
port = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 54805;
|
||||
description = "Port to listen on";
|
||||
};
|
||||
acmeEmail = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
Email address for account creation and correspondence from the CA.
|
||||
It is recommended to use the same email for all certs to avoid account
|
||||
creation limits.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
period = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 5;
|
||||
description = "Domain update period in minutes";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ ... }:
|
||||
{
|
||||
options = {
|
||||
provider = lib.mkOption {
|
||||
example = "namecheap";
|
||||
type = lib.types.str;
|
||||
description = "The dyndns provider to use";
|
||||
};
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "example.com";
|
||||
description = "The top level domain to update.";
|
||||
};
|
||||
secret_field_name = lib.mkOption {
|
||||
example = "api_key";
|
||||
|
||||
type = lib.types.enum [
|
||||
"password"
|
||||
"token"
|
||||
"api_key"
|
||||
"secret_api_key"
|
||||
];
|
||||
default = "password";
|
||||
description = "The field name for the secret";
|
||||
};
|
||||
extraSettings = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
description = ''
|
||||
Extra settings for the provider.
|
||||
Provider specific settings: https://github.com/qdm12/ddns-updater#configuration
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = { };
|
||||
description = "Configuration for which domains to update";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
name = "dyndns";
|
||||
cfg = settings;
|
||||
|
||||
# We dedup secrets if they have the same provider + base domain
|
||||
secret_id = opt: "${name}-${opt.provider}-${opt.domain}";
|
||||
secret_path =
|
||||
opt: config.clan.core.vars.generators."${secret_id opt}".files."${secret_id opt}".path;
|
||||
|
||||
# We check that a secret has not been set in extraSettings.
|
||||
extraSettingsSafe =
|
||||
opt:
|
||||
if (builtins.hasAttr opt.secret_field_name opt.extraSettings) then
|
||||
throw "Please do not set ${opt.secret_field_name} in extraSettings, it is automatically set by the dyndns module."
|
||||
else
|
||||
opt.extraSettings;
|
||||
|
||||
service_config = {
|
||||
settings = builtins.catAttrs "value" (
|
||||
builtins.attrValues (
|
||||
lib.mapAttrs (_: opt: {
|
||||
value =
|
||||
(extraSettingsSafe opt)
|
||||
// {
|
||||
domain = opt.domain;
|
||||
provider = opt.provider;
|
||||
}
|
||||
// {
|
||||
"${opt.secret_field_name}" = secret_id opt;
|
||||
};
|
||||
}) cfg.settings
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
secret_generator = _: opt: {
|
||||
name = secret_id opt;
|
||||
value = {
|
||||
share = true;
|
||||
migrateFact = "${secret_id opt}";
|
||||
prompts.${secret_id opt} = {
|
||||
type = "hidden";
|
||||
persist = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = lib.optional cfg.server.enable (
|
||||
lib.modules.importApply ./nginx.nix {
|
||||
inherit config;
|
||||
inherit settings;
|
||||
inherit lib;
|
||||
}
|
||||
);
|
||||
|
||||
clan.core.vars.generators = lib.mkIf (cfg.settings != { }) (
|
||||
lib.mapAttrs' secret_generator cfg.settings
|
||||
);
|
||||
|
||||
users.groups.${name} = lib.mkIf (cfg.settings != { }) { };
|
||||
users.users.${name} = lib.mkIf (cfg.settings != { }) {
|
||||
group = name;
|
||||
isSystemUser = true;
|
||||
description = "User for ${name} service";
|
||||
home = "/var/lib/${name}";
|
||||
createHome = true;
|
||||
};
|
||||
|
||||
services.nginx = lib.mkIf cfg.server.enable {
|
||||
virtualHosts = {
|
||||
"${cfg.server.domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://localhost:${toString cfg.server.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.${name} = lib.mkIf (cfg.settings != { }) {
|
||||
path = [ ];
|
||||
description = "Dynamic DNS updater";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = {
|
||||
MYCONFIG = "${builtins.toJSON service_config}";
|
||||
SERVER_ENABLED = if cfg.server.enable then "yes" else "no";
|
||||
PERIOD = "${toString cfg.period}m";
|
||||
LISTENING_ADDRESS = ":${toString cfg.server.port}";
|
||||
GODEBUG = "netdns=go"; # We need to set this untill this has been merged. https://github.com/NixOS/nixpkgs/pull/432758
|
||||
};
|
||||
|
||||
serviceConfig =
|
||||
let
|
||||
pyscript =
|
||||
pkgs.writers.writePython3Bin "generate_secret_config.py"
|
||||
{
|
||||
libraries = [ ];
|
||||
doCheck = false;
|
||||
}
|
||||
''
|
||||
import json
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
cred_dir = Path(os.getenv("CREDENTIALS_DIRECTORY"))
|
||||
config_str = os.getenv("MYCONFIG")
|
||||
|
||||
|
||||
def get_credential(name):
|
||||
secret_p = cred_dir / name
|
||||
with open(secret_p, 'r') as f:
|
||||
return f.read().strip()
|
||||
|
||||
|
||||
config = json.loads(config_str)
|
||||
print(f"Config: {config}")
|
||||
for attrset in config["settings"]:
|
||||
if "password" in attrset:
|
||||
attrset['password'] = get_credential(attrset['password'])
|
||||
elif "token" in attrset:
|
||||
attrset['token'] = get_credential(attrset['token'])
|
||||
elif "secret_api_key" in attrset:
|
||||
attrset['secret_api_key'] = get_credential(attrset['secret_api_key'])
|
||||
elif "api_key" in attrset:
|
||||
attrset['api_key'] = get_credential(attrset['api_key'])
|
||||
else:
|
||||
raise ValueError(f"Missing secret field in {attrset}")
|
||||
|
||||
# create directory data if it does not exist
|
||||
data_dir = Path('data')
|
||||
data_dir.mkdir(mode=0o770, exist_ok=True)
|
||||
|
||||
# Create a temporary config file
|
||||
# with appropriate permissions
|
||||
tmp_config_path = data_dir / '.config.json'
|
||||
tmp_config_path.touch(mode=0o660, exist_ok=False)
|
||||
|
||||
# Write the config with secrets back
|
||||
with open(tmp_config_path, 'w') as f:
|
||||
f.write(json.dumps(config, indent=4))
|
||||
|
||||
# Move config into place
|
||||
config_path = data_dir / 'config.json'
|
||||
tmp_config_path.rename(config_path)
|
||||
|
||||
# Set file permissions to read
|
||||
# and write only by the user and group
|
||||
for file in data_dir.iterdir():
|
||||
file.chmod(0o660)
|
||||
'';
|
||||
in
|
||||
{
|
||||
ExecStartPre = lib.getExe pyscript;
|
||||
ExecStart = lib.getExe pkgs.ddns-updater;
|
||||
LoadCredential = lib.mapAttrsToList (_: opt: "${secret_id opt}:${secret_path opt}") cfg.settings;
|
||||
User = name;
|
||||
Group = name;
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ReadOnlyPaths = "/";
|
||||
PrivateDevices = "yes";
|
||||
ProtectKernelModules = "yes";
|
||||
ProtectKernelTunables = "yes";
|
||||
WorkingDirectory = "/var/lib/${name}";
|
||||
ReadWritePaths = [
|
||||
"/proc/self"
|
||||
"/var/lib/${name}"
|
||||
];
|
||||
|
||||
Restart = "always";
|
||||
RestartSec = 60;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
19
clanServices/dyndns/flake-module.nix
Normal file
19
clanServices/dyndns/flake-module.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
dyndns = module;
|
||||
};
|
||||
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.dyndns = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules."@clan/dyndns" = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
50
clanServices/dyndns/nginx.nix
Normal file
50
clanServices/dyndns/nginx.nix
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
security.acme.acceptTerms = true;
|
||||
security.acme.defaults.email = settings.server.acmeEmail;
|
||||
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
443
|
||||
80
|
||||
];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
|
||||
statusPage = lib.mkDefault true;
|
||||
recommendedBrotliSettings = lib.mkDefault true;
|
||||
recommendedGzipSettings = lib.mkDefault true;
|
||||
recommendedOptimisation = lib.mkDefault true;
|
||||
recommendedProxySettings = lib.mkDefault true;
|
||||
recommendedTlsSettings = lib.mkDefault true;
|
||||
|
||||
# Nginx sends all the access logs to /var/log/nginx/access.log by default.
|
||||
# instead of going to the journal!
|
||||
commonHttpConfig = "access_log syslog:server=unix:/dev/log;";
|
||||
|
||||
resolver.addresses =
|
||||
let
|
||||
isIPv6 = addr: builtins.match ".*:.*:.*" addr != null;
|
||||
escapeIPv6 = addr: if isIPv6 addr then "[${addr}]" else addr;
|
||||
cloudflare = [
|
||||
"1.1.1.1"
|
||||
"2606:4700:4700::1111"
|
||||
];
|
||||
resolvers =
|
||||
if config.networking.nameservers == [ ] then cloudflare else config.networking.nameservers;
|
||||
in
|
||||
map escapeIPv6 resolvers;
|
||||
|
||||
sslDhparam = config.security.dhparams.params.nginx.path;
|
||||
};
|
||||
|
||||
security.dhparams = {
|
||||
enable = true;
|
||||
params.nginx = { };
|
||||
};
|
||||
}
|
||||
77
clanServices/dyndns/tests/vm/default.nix
Normal file
77
clanServices/dyndns/tests/vm/default.nix
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "service-dyndns";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
machines.server = { };
|
||||
|
||||
instances = {
|
||||
dyndns-test = {
|
||||
module.name = "@clan/dyndns";
|
||||
module.input = "self";
|
||||
roles.default.machines."server".settings = {
|
||||
server = {
|
||||
enable = true;
|
||||
domain = "test.example.com";
|
||||
port = 54805;
|
||||
acmeEmail = "test@example.com";
|
||||
};
|
||||
period = 1;
|
||||
settings = {
|
||||
"test.example.com" = {
|
||||
provider = "namecheap";
|
||||
domain = "example.com";
|
||||
secret_field_name = "password";
|
||||
extraSettings = {
|
||||
host = "test";
|
||||
server = "dynamicdns.park-your-domain.com";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes = {
|
||||
server = {
|
||||
# Disable firewall for testing
|
||||
networking.firewall.enable = false;
|
||||
|
||||
# Mock ACME for testing (avoid real certificate requests)
|
||||
security.acme.defaults.server = "https://localhost:14000/dir";
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
# Test that dyndns service starts (will fail without secrets, but that's expected)
|
||||
server.wait_for_unit("multi-user.target")
|
||||
|
||||
# Test that nginx service is running
|
||||
server.wait_for_unit("nginx.service")
|
||||
|
||||
# Test that nginx is listening on expected ports
|
||||
server.wait_for_open_port(80)
|
||||
server.wait_for_open_port(443)
|
||||
|
||||
# Test that the dyndns user was created
|
||||
# server.succeed("getent passwd dyndns")
|
||||
# server.succeed("getent group dyndns")
|
||||
#
|
||||
# Test that the home directory was created
|
||||
server.succeed("test -d /var/lib/dyndns")
|
||||
|
||||
# Test that nginx configuration includes our domain
|
||||
server.succeed("${pkgs.nginx}/bin/nginx -t")
|
||||
|
||||
print("All tests passed!")
|
||||
'';
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
# Example clan service. See https://docs.clan.lol/guides/services/community/
|
||||
# for more details
|
||||
|
||||
# The test for this module in ./tests/vm/default.nix shows an example of how
|
||||
# the service is used.
|
||||
|
||||
{ packages }:
|
||||
{ ... }:
|
||||
{
|
||||
@@ -5,30 +11,94 @@
|
||||
manifest.name = "clan-core/hello-word";
|
||||
manifest.description = "This is a test";
|
||||
|
||||
roles.peer = {
|
||||
# This service provides two roles: "morning" and "evening". Roles can be
|
||||
# defined in this file directly (e.g. the "morning" role) or split up into a
|
||||
# separate file (e.g. the "evening" role)
|
||||
roles.morning = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.foo = lib.mkOption {
|
||||
# Here we define the settings for this role. They will be accessible
|
||||
# via `roles.morning.settings` in the role
|
||||
|
||||
options.greeting = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
# default = "";
|
||||
description = "Some option";
|
||||
default = "Good morning";
|
||||
description = "The greeting to use";
|
||||
};
|
||||
};
|
||||
# Maps over all instances and produces one result per instance.
|
||||
perInstance =
|
||||
{
|
||||
# Role settings for this machine/instance
|
||||
settings,
|
||||
|
||||
# The name of this instance of the service
|
||||
instanceName,
|
||||
|
||||
# The current machine
|
||||
machine,
|
||||
|
||||
# All roles of this service, with their assigned machines
|
||||
roles,
|
||||
...
|
||||
}:
|
||||
{
|
||||
# Analog to 'perSystem' of flake-parts.
|
||||
# For every instance of this service we will add a nixosModule to a morning-machine
|
||||
nixosModule =
|
||||
{ config, ... }:
|
||||
{
|
||||
# Interaction examples what you could do here:
|
||||
# - Get some settings of this machine
|
||||
# settings.ipRanges
|
||||
#
|
||||
# - Get all evening names:
|
||||
# allEveningNames = lib.attrNames roles.evening.machines
|
||||
#
|
||||
# - Get all roles of the machine:
|
||||
# machine.roles
|
||||
#
|
||||
# - Get the settings that where applied to a specific evening machine:
|
||||
# roles.evening.machines.peer1.settings
|
||||
imports = [ ];
|
||||
environment.etc.hello.text = "${settings.greeting} World!";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# The impnlementation of the evening role is in a separate file. We have kept
|
||||
# the interface here, so we can see all settings of the service in one place,
|
||||
# but you can also move it to the respective file
|
||||
roles.evening = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.greeting = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "Good evening";
|
||||
description = "The greeting to use";
|
||||
};
|
||||
};
|
||||
};
|
||||
imports = [ ./evening.nix ];
|
||||
|
||||
# This part gets applied to all machines, regardless of their role.
|
||||
perMachine =
|
||||
{ machine, ... }:
|
||||
{
|
||||
nixosModule = {
|
||||
clan.core.vars.generators.hello = {
|
||||
files.hello = {
|
||||
secret = false;
|
||||
};
|
||||
script = ''
|
||||
echo "Hello world from ${machine.name}" > $out/hello
|
||||
'';
|
||||
nixosModule =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeShellScriptBin "greet-world" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
cat /etc/hello
|
||||
echo " I'm ${machine.name}"
|
||||
'')
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
12
clanServices/hello-world/evening.nix
Normal file
12
clanServices/hello-world/evening.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
roles.evening.perInstance =
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ ];
|
||||
environment.etc.hello.text = "${settings.greeting} World!";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -27,20 +27,10 @@ let
|
||||
module.name = "hello-world";
|
||||
module.input = "self";
|
||||
|
||||
roles.peer.machines.jon = { };
|
||||
roles.evening.machines.jon = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
# NOTE:
|
||||
# If you wonder why 'self-zerotier-redux':
|
||||
# A local module has prefix 'self', otherwise it is the name of the 'input'
|
||||
# The rest is the name of the service as in the instance 'module.name';
|
||||
#
|
||||
# -> ${module.input}-${module.name}
|
||||
# In this case it is 'self-zerotier-redux'
|
||||
# This is usually only used internally, but we can use it to test the evaluation of service module in isolation
|
||||
# evaluatedService =
|
||||
# testFlake.clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.self-zerotier-redux.config;
|
||||
in
|
||||
{
|
||||
test_simple = {
|
||||
|
||||
@@ -5,22 +5,35 @@
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
machines.peer1 = { };
|
||||
machines.peer2 = { };
|
||||
|
||||
instances."test" = {
|
||||
module.name = "hello-service";
|
||||
module.input = "self";
|
||||
roles.peer.machines.peer1 = { };
|
||||
|
||||
# Assign the roles to the two machines
|
||||
roles.morning.machines.peer1 = { };
|
||||
|
||||
roles.evening.machines.peer2 = {
|
||||
# Set roles settings for the peers, where we want to differ from
|
||||
# the role defaults
|
||||
settings = {
|
||||
greeting = "Good night";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
{ ... }:
|
||||
''
|
||||
start_all()
|
||||
|
||||
# peer1 should have the 'hello' file
|
||||
value = peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.hello.files.hello.path}")
|
||||
assert value.strip() == "Hello world from peer1", value
|
||||
value = peer1.succeed("greet-world")
|
||||
assert value.strip() == "Good morning World! I'm peer1", value
|
||||
|
||||
value = peer2.succeed("greet-world")
|
||||
assert value.strip() == "Good night World! I'm peer2", value
|
||||
'';
|
||||
}
|
||||
|
||||
35
clanServices/localbackup/README.md
Normal file
35
clanServices/localbackup/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
## Features
|
||||
|
||||
- Creates incremental snapshots using rsnapshot
|
||||
- Supports multiple backup targets
|
||||
- Mount/unmount hooks for external storage
|
||||
- Pre/post backup hooks for custom scripts
|
||||
- Configurable snapshot retention
|
||||
- Automatic state folder detection
|
||||
|
||||
## Usage
|
||||
|
||||
Enable the localbackup service and configure backup targets:
|
||||
|
||||
```nix
|
||||
instances = {
|
||||
localbackup = {
|
||||
module.name = "@clan/localbackup";
|
||||
module.input = "self";
|
||||
roles.default.machines."machine".settings = {
|
||||
targets.external= {
|
||||
directory = "/mnt/backup";
|
||||
mountpoint = "/mnt/backup";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
The service provides these commands:
|
||||
|
||||
- `localbackup-create`: Create a new backup
|
||||
- `localbackup-list`: List available backups
|
||||
- `localbackup-restore`: Restore from backup (requires NAME and FOLDERS environment variables)
|
||||
267
clanServices/localbackup/default.nix
Normal file
267
clanServices/localbackup/default.nix
Normal file
@@ -0,0 +1,267 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "localbackup";
|
||||
manifest.description = "Automatically backups current machine to local directory.";
|
||||
manifest.categories = [ "System" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.default = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
|
||||
options = {
|
||||
|
||||
targets = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
|
||||
default = name;
|
||||
description = "the name of the backup job";
|
||||
};
|
||||
directory = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "the directory to backup";
|
||||
};
|
||||
mountpoint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "mountpoint of the directory to backup. If set, the directory will be mounted before the backup and unmounted afterwards";
|
||||
};
|
||||
preMountHook = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.lines;
|
||||
default = null;
|
||||
description = "Shell commands to run before the directory is mounted";
|
||||
};
|
||||
postMountHook = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.lines;
|
||||
default = null;
|
||||
description = "Shell commands to run after the directory is mounted";
|
||||
};
|
||||
preUnmountHook = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.lines;
|
||||
default = null;
|
||||
description = "Shell commands to run before the directory is unmounted";
|
||||
};
|
||||
postUnmountHook = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.lines;
|
||||
default = null;
|
||||
description = "Shell commands to run after the directory is unmounted";
|
||||
};
|
||||
preBackupHook = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.lines;
|
||||
default = null;
|
||||
description = "Shell commands to run before the backup";
|
||||
};
|
||||
postBackupHook = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.lines;
|
||||
default = null;
|
||||
description = "Shell commands to run after the backup";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
# default = { };
|
||||
description = "List of directories where backups are stored";
|
||||
};
|
||||
|
||||
snapshots = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 20;
|
||||
description = "Number of snapshots to keep";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
mountHook = target: ''
|
||||
if [[ -x /run/current-system/sw/bin/localbackup-mount-${target.name} ]]; then
|
||||
/run/current-system/sw/bin/localbackup-mount-${target.name}
|
||||
fi
|
||||
if [[ -x /run/current-system/sw/bin/localbackup-unmount-${target.name} ]]; then
|
||||
trap "/run/current-system/sw/bin/localbackup-unmount-${target.name}" EXIT
|
||||
fi
|
||||
'';
|
||||
|
||||
uniqueFolders = lib.unique (
|
||||
lib.flatten (lib.mapAttrsToList (_name: state: state.folders) config.clan.core.state)
|
||||
);
|
||||
|
||||
rsnapshotConfig = target: ''
|
||||
config_version 1.2
|
||||
snapshot_root ${target.directory}
|
||||
sync_first 1
|
||||
cmd_cp ${pkgs.coreutils}/bin/cp
|
||||
cmd_rm ${pkgs.coreutils}/bin/rm
|
||||
cmd_rsync ${pkgs.rsync}/bin/rsync
|
||||
cmd_ssh ${pkgs.openssh}/bin/ssh
|
||||
cmd_logger ${pkgs.inetutils}/bin/logger
|
||||
cmd_du ${pkgs.coreutils}/bin/du
|
||||
cmd_rsnapshot_diff ${pkgs.rsnapshot}/bin/rsnapshot-diff
|
||||
|
||||
${lib.optionalString (target.postBackupHook != null) ''
|
||||
cmd_postexec ${pkgs.writeShellScript "postexec.sh" ''
|
||||
set -efu -o pipefail
|
||||
${target.postBackupHook}
|
||||
''}
|
||||
''}
|
||||
retain snapshot ${builtins.toString settings.snapshots}
|
||||
${lib.concatMapStringsSep "\n" (folder: ''
|
||||
backup ${folder} ${config.networking.hostName}/
|
||||
'') uniqueFolders}
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeShellScriptBin "localbackup-create" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.rsnapshot
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
]
|
||||
}
|
||||
${lib.concatMapStringsSep "\n" (target: ''
|
||||
${mountHook target}
|
||||
echo "Creating backup '${target.name}'"
|
||||
|
||||
${lib.optionalString (target.preBackupHook != null) ''
|
||||
(
|
||||
${target.preBackupHook}
|
||||
)
|
||||
''}
|
||||
|
||||
declare -A preCommandErrors
|
||||
${lib.concatMapStringsSep "\n" (
|
||||
state:
|
||||
lib.optionalString (state.preBackupCommand != null) ''
|
||||
echo "Running pre-backup command for ${state.name}"
|
||||
if ! /run/current-system/sw/bin/${state.preBackupCommand}; then
|
||||
preCommandErrors["${state.name}"]=1
|
||||
fi
|
||||
''
|
||||
) (builtins.attrValues config.clan.core.state)}
|
||||
|
||||
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" sync
|
||||
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" snapshot
|
||||
'') (builtins.attrValues settings.targets)}'')
|
||||
(pkgs.writeShellScriptBin "localbackup-list" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.jq
|
||||
pkgs.findutils
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
]
|
||||
}
|
||||
(${
|
||||
lib.concatMapStringsSep "\n" (target: ''
|
||||
(
|
||||
${mountHook target}
|
||||
find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \
|
||||
| jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.name}::" + .)}'
|
||||
)
|
||||
'') (builtins.attrValues settings.targets)
|
||||
}) | jq -s .
|
||||
'')
|
||||
(pkgs.writeShellScriptBin "localbackup-restore" ''
|
||||
set -efu -o pipefail
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.rsync
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.gawk
|
||||
]
|
||||
}
|
||||
if [[ "''${NAME:-}" == "" ]]; then
|
||||
echo "No backup name given via NAME environment variable"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "''${FOLDERS:-}" == "" ]]; then
|
||||
echo "No folders given via FOLDERS environment variable"
|
||||
exit 1
|
||||
fi
|
||||
name=$(awk -F'::' '{print $1}' <<< $NAME)
|
||||
backupname=''${NAME#$name::}
|
||||
|
||||
if command -v localbackup-mount-$name; then
|
||||
localbackup-mount-$name
|
||||
fi
|
||||
if command -v localbackup-unmount-$name; then
|
||||
trap "localbackup-unmount-$name" EXIT
|
||||
fi
|
||||
|
||||
if [[ ! -d $backupname ]]; then
|
||||
echo "No backup found $backupname"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=':' read -ra FOLDER <<< "''$FOLDERS"
|
||||
for folder in "''${FOLDER[@]}"; do
|
||||
mkdir -p "$folder"
|
||||
rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder"
|
||||
done
|
||||
'')
|
||||
]
|
||||
++ (lib.mapAttrsToList (
|
||||
name: target:
|
||||
pkgs.writeShellScriptBin ("localbackup-mount-" + name) ''
|
||||
set -efu -o pipefail
|
||||
${lib.optionalString (target.preMountHook != null) target.preMountHook}
|
||||
${lib.optionalString (target.mountpoint != null) ''
|
||||
if ! ${pkgs.util-linux}/bin/mountpoint -q ${lib.escapeShellArg target.mountpoint}; then
|
||||
${pkgs.util-linux}/bin/mount -o X-mount.mkdir ${lib.escapeShellArg target.mountpoint}
|
||||
fi
|
||||
''}
|
||||
${lib.optionalString (target.postMountHook != null) target.postMountHook}
|
||||
''
|
||||
) settings.targets)
|
||||
++ lib.mapAttrsToList (
|
||||
name: target:
|
||||
pkgs.writeShellScriptBin ("localbackup-unmount-" + name) ''
|
||||
set -efu -o pipefail
|
||||
${lib.optionalString (target.preUnmountHook != null) target.preUnmountHook}
|
||||
${lib.optionalString (
|
||||
target.mountpoint != null
|
||||
) "${pkgs.util-linux}/bin/umount ${lib.escapeShellArg target.mountpoint}"}
|
||||
${lib.optionalString (target.postUnmountHook != null) target.postUnmountHook}
|
||||
''
|
||||
) settings.targets;
|
||||
|
||||
clan.core.backups.providers.localbackup = {
|
||||
# TODO list needs to run locally or on the remote machine
|
||||
list = "localbackup-list";
|
||||
create = "localbackup-create";
|
||||
restore = "localbackup-restore";
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
16
clanServices/localbackup/flake-module.nix
Normal file
16
clanServices/localbackup/flake-module.nix
Normal file
@@ -0,0 +1,16 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules.localbackup = module;
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.localbackup = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules."@clan/localbackup" = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
62
clanServices/localbackup/tests/vm/default.nix
Normal file
62
clanServices/localbackup/tests/vm/default.nix
Normal file
@@ -0,0 +1,62 @@
|
||||
{ ... }:
|
||||
{
|
||||
name = "service-localbackup";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
test.useContainers = true;
|
||||
inventory = {
|
||||
|
||||
machines.machine = { };
|
||||
|
||||
instances = {
|
||||
localbackup = {
|
||||
module.name = "@clan/localbackup";
|
||||
module.input = "self";
|
||||
roles.default.machines."machine".settings = {
|
||||
|
||||
targets.hdd = {
|
||||
directory = "/mnt/external-disk";
|
||||
preMountHook = ''
|
||||
touch /run/mount-external-disk
|
||||
'';
|
||||
postUnmountHook = ''
|
||||
touch /run/unmount-external-disk
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes.machine = {
|
||||
clan.core.state.test-backups.folders = [ "/var/test-backups" ];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
start_all()
|
||||
|
||||
machine.systemctl("start network-online.target")
|
||||
machine.wait_for_unit("network-online.target")
|
||||
|
||||
# dummy data
|
||||
machine.succeed("mkdir -p /var/test-backups")
|
||||
machine.succeed("echo testing > /var/test-backups/somefile")
|
||||
|
||||
# create
|
||||
machine.succeed("localbackup-create >&2")
|
||||
machine.wait_until_succeeds("! systemctl is-active localbackup-job-serverone >&2")
|
||||
|
||||
# list
|
||||
snapshot_list = machine.succeed("localbackup-list").strip()
|
||||
assert json.loads(snapshot_list)[0]["name"].strip() == "hdd::/mnt/external-disk/snapshot.0"
|
||||
|
||||
# borgbackup restore
|
||||
machine.succeed("rm -f /var/test-backups/somefile")
|
||||
|
||||
machine.succeed("NAME=/mnt/external-disk/snapshot.0 FOLDERS=/var/test-backups /run/current-system/sw/bin/localbackup-restore >&2")
|
||||
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
|
||||
'';
|
||||
}
|
||||
@@ -184,24 +184,24 @@
|
||||
settings.certificate.searchDomains != [ ]
|
||||
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
|
||||
|
||||
hostKeys =
|
||||
[
|
||||
{
|
||||
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
|
||||
type = "ed25519";
|
||||
}
|
||||
]
|
||||
++ lib.optional settings.hostKeys.rsa.enable {
|
||||
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
|
||||
type = "rsa";
|
||||
};
|
||||
hostKeys = [
|
||||
{
|
||||
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
|
||||
type = "ed25519";
|
||||
}
|
||||
]
|
||||
++ lib.optional settings.hostKeys.rsa.enable {
|
||||
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
|
||||
type = "rsa";
|
||||
};
|
||||
};
|
||||
|
||||
programs.ssh.knownHosts.clan-sshd-self-ed25519 = {
|
||||
hostNames = [
|
||||
"localhost"
|
||||
config.networking.hostName
|
||||
] ++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
|
||||
]
|
||||
++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
|
||||
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
|
||||
};
|
||||
};
|
||||
|
||||
20
clanServices/syncthing/README.md
Normal file
20
clanServices/syncthing/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## Usage
|
||||
|
||||
```nix
|
||||
{
|
||||
instances.syncthing = {
|
||||
roles.peer.tags.all = { };
|
||||
roles.peer.settings.folders = {
|
||||
documents = {
|
||||
path = "~/syncthing/documents";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Now the folder `~/syncthing/documents` will be shared with all your machines.
|
||||
|
||||
|
||||
## Documentation
|
||||
Extensive documentation is available on the [Syncthing](https://docs.syncthing.net/) website.
|
||||
243
clanServices/syncthing/default.nix
Normal file
243
clanServices/syncthing/default.nix
Normal file
@@ -0,0 +1,243 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/syncthing";
|
||||
manifest.description = "Syncthing is a continuous file synchronization program with automatic peer discovery";
|
||||
manifest.categories = [
|
||||
"Utility"
|
||||
"System"
|
||||
"Network"
|
||||
];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.peer = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.openDefaultPorts = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to open the default syncthing ports in the firewall.
|
||||
'';
|
||||
};
|
||||
|
||||
options.folders = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Path to the folder to sync";
|
||||
};
|
||||
devices = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "List of device names to share this folder with. Empty list means all peers and extraDevices.";
|
||||
};
|
||||
ignorePerms = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Ignore permission changes";
|
||||
};
|
||||
rescanIntervalS = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 3600;
|
||||
description = "Rescan interval in seconds";
|
||||
};
|
||||
type = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"sendreceive"
|
||||
"sendonly"
|
||||
"receiveonly"
|
||||
];
|
||||
default = "sendreceive";
|
||||
description = "Folder type";
|
||||
};
|
||||
versioning = lib.mkOption {
|
||||
type = lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
type = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"external"
|
||||
"simple"
|
||||
"staggered"
|
||||
"trashcan"
|
||||
];
|
||||
description = "Versioning type";
|
||||
};
|
||||
params = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
description = "Versioning parameters";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = null;
|
||||
description = "Versioning configuration";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
description = "Folders to synchronize between all peers";
|
||||
};
|
||||
|
||||
options.extraDevices = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
id = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Device ID of the external syncthing device";
|
||||
example = "P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2";
|
||||
};
|
||||
addresses = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "dynamic" ];
|
||||
description = "List of addresses for the device";
|
||||
example = [
|
||||
"dynamic"
|
||||
"tcp://192.168.1.100:22000"
|
||||
];
|
||||
};
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = "Human readable name for the device";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
description = "External syncthing devices not managed by clan (e.g., mobile phones)";
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
phone = {
|
||||
id = "P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2";
|
||||
name = "My Phone";
|
||||
addresses = [ "dynamic" ];
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{
|
||||
settings,
|
||||
roles,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
allPeerMachines = lib.attrNames roles.peer.machines;
|
||||
|
||||
readMachineVar =
|
||||
machine: varPath: default:
|
||||
let
|
||||
fullPath = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/${varPath}";
|
||||
in
|
||||
if builtins.pathExists fullPath then
|
||||
lib.removeSuffix "\n" (builtins.readFile fullPath)
|
||||
else
|
||||
default;
|
||||
|
||||
peerDevices = lib.listToAttrs (
|
||||
lib.forEach allPeerMachines (machine: {
|
||||
name = machine;
|
||||
value = {
|
||||
name = machine;
|
||||
id = readMachineVar machine "syncthing/id/value" "";
|
||||
addresses = [
|
||||
"dynamic"
|
||||
]
|
||||
++
|
||||
lib.optional (readMachineVar machine "zerotier/zerotier-ip/value" null != null)
|
||||
"tcp://[${readMachineVar machine "zerotier/zerotier-ip/value" ""}]:22000";
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
extraDevicesConfig = lib.mapAttrs (deviceName: deviceConfig: {
|
||||
inherit (deviceConfig) id addresses;
|
||||
name = if deviceConfig.name != "" then deviceConfig.name else deviceName;
|
||||
}) settings.extraDevices;
|
||||
|
||||
allDevices = peerDevices // extraDevicesConfig;
|
||||
|
||||
validDevices = lib.filterAttrs (_: device: device.id != "") allDevices;
|
||||
|
||||
syncthingFolders = lib.mapAttrs (
|
||||
_folderName: folderConfig:
|
||||
let
|
||||
targetDevices =
|
||||
if folderConfig.devices == [ ] then lib.attrNames validDevices else folderConfig.devices;
|
||||
in
|
||||
folderConfig
|
||||
// {
|
||||
devices = targetDevices;
|
||||
}
|
||||
) settings.folders;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./vars.nix
|
||||
];
|
||||
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
services.syncthing = {
|
||||
enable = true;
|
||||
configDir = "/var/lib/syncthing";
|
||||
group = "syncthing";
|
||||
|
||||
key = lib.mkDefault config.clan.core.vars.generators.syncthing.files.key.path or null;
|
||||
cert = lib.mkDefault config.clan.core.vars.generators.syncthing.files.cert.path or null;
|
||||
|
||||
settings = {
|
||||
devices = validDevices;
|
||||
folders = syncthingFolders;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
# Conditionally open firewall ports
|
||||
(lib.mkIf settings.openDefaultPorts {
|
||||
services.syncthing.openDefaultPorts = true;
|
||||
# Syncthing ports: 8384 for remote access to GUI
|
||||
# 22000 TCP and/or UDP for sync traffic
|
||||
# 21027/UDP for discovery
|
||||
# source: https://docs.syncthing.net/users/firewall.html
|
||||
networking.firewall.interfaces."zt+".allowedTCPPorts = [
|
||||
8384
|
||||
22000
|
||||
];
|
||||
networking.firewall.interfaces."zt+".allowedUDPPorts = [
|
||||
22000
|
||||
21027
|
||||
];
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
perMachine = _: {
|
||||
nixosModule =
|
||||
{ lib, ... }:
|
||||
{
|
||||
# Activates inotify compatibility on syncthing
|
||||
# use mkOverride 900 here as it otherwise would collide with the default of the
|
||||
# upstream nixos xserver.nix
|
||||
boot.kernel.sysctl."fs.inotify.max_user_watches" = lib.mkOverride 900 524288;
|
||||
};
|
||||
};
|
||||
}
|
||||
54
clanServices/syncthing/flake-module.nix
Normal file
54
clanServices/syncthing/flake-module.nix
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
self,
|
||||
lib,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix {
|
||||
inherit (self) packages;
|
||||
};
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
syncthing = module;
|
||||
};
|
||||
perSystem =
|
||||
let
|
||||
unit-test-module = (
|
||||
self.clanLib.test.flakeModules.makeEvalChecks {
|
||||
inherit module;
|
||||
inherit inputs;
|
||||
fileset = lib.fileset.unions [
|
||||
# The zerotier service being tested
|
||||
../../clanServices/syncthing
|
||||
# Required modules
|
||||
../../nixosModules/clanCore
|
||||
# Dependencies like clan-cli
|
||||
../../pkgs/clan-cli
|
||||
];
|
||||
testName = "syncthing";
|
||||
tests = ./tests/eval-tests.nix;
|
||||
testArgs = { };
|
||||
}
|
||||
);
|
||||
in
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
unit-test-module
|
||||
];
|
||||
/**
|
||||
1. Prepare the test vars
|
||||
nix run .#generate-test-vars -- clanServices/syncthing/tests/vm syncthing-service
|
||||
|
||||
2. To run the test
|
||||
nix build .#checks.x86_64-linux.syncthing-service
|
||||
*/
|
||||
clan.nixosTests.syncthing-service = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules.syncthing-service = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
62
clanServices/syncthing/tests/eval-tests.nix
Normal file
62
clanServices/syncthing/tests/eval-tests.nix
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
module,
|
||||
clanLib,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
testFlake =
|
||||
(clanLib.clan {
|
||||
self = { };
|
||||
directory = ./vm;
|
||||
|
||||
machines.machine1 = {
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
};
|
||||
machines.machine2 = {
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
};
|
||||
machines.machine3 = {
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
};
|
||||
machines.machine4 = {
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
};
|
||||
|
||||
modules.syncthing = module;
|
||||
|
||||
inventory.instances = {
|
||||
default = {
|
||||
module.name = "syncthing";
|
||||
module.input = "self";
|
||||
|
||||
roles.peer.tags.all = { };
|
||||
roles.peer.settings.extraDevices.phone = {
|
||||
id = "P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2";
|
||||
};
|
||||
};
|
||||
};
|
||||
}).config;
|
||||
in
|
||||
{
|
||||
test_machine1_peers = {
|
||||
expr = {
|
||||
devices = lib.attrNames testFlake.nixosConfigurations.machine1.config.services.syncthing.settings.devices;
|
||||
machine4_ID =
|
||||
testFlake.nixosConfigurations.machine1.config.services.syncthing.settings.devices.machine1.id;
|
||||
externalPhoneId =
|
||||
testFlake.nixosConfigurations.machine1.config.services.syncthing.settings.devices.phone.id;
|
||||
};
|
||||
expected = {
|
||||
devices = [
|
||||
"machine1"
|
||||
"machine2"
|
||||
"machine3"
|
||||
"machine4"
|
||||
"phone"
|
||||
];
|
||||
machine4_ID = "LJOGYGS-RQPWIHV-HD4B3GK-JZPVPK6-VI3IAY5-CWQWIXK-NJSQMFH-KXHOHA4";
|
||||
externalPhoneId = "P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2";
|
||||
};
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user