Compare commits
146 Commits
try-fix-fl
...
update-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
587a9eee84 | ||
|
|
faa3497eeb | ||
|
|
970a168c2a | ||
|
|
ab067e3466 | ||
|
|
c673c07164 | ||
|
|
0524aadd50 | ||
|
|
d9e5db2596 | ||
|
|
2ded6cbac4 | ||
|
|
a4823c3ffa | ||
|
|
7413d3620b | ||
|
|
6fe2b195a9 | ||
|
|
6ccee60e39 | ||
|
|
463db1537a | ||
|
|
fc4f4987ff | ||
|
|
e39333abed | ||
|
|
e407009183 | ||
|
|
9ff0215781 | ||
|
|
84d6400c25 | ||
|
|
8c583180ac | ||
|
|
1bc6d8c046 | ||
|
|
b2e424fa2e | ||
|
|
1568bb3860 | ||
|
|
b549012aa1 | ||
|
|
45594e118b | ||
|
|
b36abb8fcd | ||
|
|
63b4813c46 | ||
|
|
3d103fdb26 | ||
|
|
ed470ed2b1 | ||
|
|
4d7aad78ae | ||
|
|
5c0ac5d0cc | ||
|
|
4cc149b3c3 | ||
|
|
db592a565d | ||
|
|
84865f37b8 | ||
|
|
21f8a69989 | ||
|
|
fb745beda5 | ||
|
|
86db003973 | ||
|
|
d9368ec01c | ||
|
|
f6bf1481f5 | ||
|
|
0ac0b422e6 | ||
|
|
2ecb9a533d | ||
|
|
379d675372 | ||
|
|
10f89d6612 | ||
|
|
cde9df1536 | ||
|
|
8c1587e400 | ||
|
|
e88b05dd9c | ||
|
|
318cc4b1ec | ||
|
|
6ff2e8de94 | ||
|
|
346e56191a | ||
|
|
696e4b984f | ||
|
|
de1d0c8747 | ||
|
|
86ea1b0a60 | ||
|
|
241550921f | ||
|
|
f69dd29f79 | ||
|
|
648f3ec084 | ||
|
|
f362cfb983 | ||
|
|
66ddc399d0 | ||
|
|
20a6375c2a | ||
|
|
2882e9e8da | ||
|
|
2c910f8616 | ||
|
|
5e80e0a833 | ||
|
|
055cf3d924 | ||
|
|
3d8ddd1be1 | ||
|
|
71ee2fcbb6 | ||
|
|
279df893cc | ||
|
|
ed2663ac7b | ||
|
|
c4f67ca44d | ||
|
|
5f8d65bd80 | ||
|
|
98185217bd | ||
|
|
876e57e81e | ||
|
|
d601237853 | ||
|
|
2439d508ef | ||
|
|
0dd5b284eb | ||
|
|
a47d65d3ed | ||
|
|
5484b584f1 | ||
|
|
461c628a98 | ||
|
|
70454878ff | ||
|
|
7b6e63d6ca | ||
|
|
67eb2274ab | ||
|
|
794872e235 | ||
|
|
7765e7155e | ||
|
|
3871cb7ab4 | ||
|
|
a4131a0822 | ||
|
|
02111109f8 | ||
|
|
3e489d5cff | ||
|
|
2f027cad3c | ||
|
|
16d70c6441 | ||
|
|
40bf79e5c6 | ||
|
|
c9dc21fb72 | ||
|
|
9830e711fd | ||
|
|
9ab5afb9b9 | ||
|
|
b22668629d | ||
|
|
400c51cdf3 | ||
|
|
e9275de8d7 | ||
|
|
30fbe76e8d | ||
|
|
c44bf846de | ||
|
|
cff445229d | ||
|
|
2895c18bba | ||
|
|
34abd4b8ce | ||
|
|
1449ff622f | ||
|
|
4d25f29ce7 | ||
|
|
fccae71ebb | ||
|
|
3a1c36e7b0 | ||
|
|
c12a6cad27 | ||
|
|
63ad20b157 | ||
|
|
d3def537b4 | ||
|
|
456150744d | ||
|
|
5528a1af3f | ||
|
|
8874e0311d | ||
|
|
c42de173b3 | ||
|
|
4d554cad6a | ||
|
|
58a06d2261 | ||
|
|
7e6d94795b | ||
|
|
5142794fa3 | ||
|
|
335f1c7e4c | ||
|
|
4de2df7c86 | ||
|
|
3d26214009 | ||
|
|
dd12104e2f | ||
|
|
f8ecd4372e | ||
|
|
0a8c7d9e10 | ||
|
|
d9e034d878 | ||
|
|
230f3ad36c | ||
|
|
a18cd40525 | ||
|
|
1cb1c53dfd | ||
|
|
2281e61232 | ||
|
|
9300fd9dc7 | ||
|
|
6ad5d8d28c | ||
|
|
dd1429c89f | ||
|
|
8d4099d13d | ||
|
|
e3a882002c | ||
|
|
150e070a09 | ||
|
|
cf3e5befda | ||
|
|
b53ff99248 | ||
|
|
0f1b816844 | ||
|
|
9f1eabd3e1 | ||
|
|
74489d399a | ||
|
|
7c11ed1d8d | ||
|
|
ac7e082ce4 | ||
|
|
c76f7bb020 | ||
|
|
317cd7b5f5 | ||
|
|
3fbf34044a | ||
|
|
ab7d4409f6 | ||
|
|
65778cb9fe | ||
|
|
4008d2c165 | ||
|
|
1c269d1eaa | ||
|
|
84a21d1bab | ||
|
|
6855ab859d |
27
.gitea/workflows/update-flake-inputs.yml
Normal file
27
.gitea/workflows/update-flake-inputs.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Update Flake Inputs
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Sunday at 4:00 AM UTC
|
||||||
|
- cron: "0 4 * * 0"
|
||||||
|
workflow_dispatch:
|
||||||
|
repository_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-flake-inputs:
|
||||||
|
runs-on: nix
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
git config --global user.email "clan-bot@clan.lol"
|
||||||
|
git config --global user.name "clan-bot"
|
||||||
|
|
||||||
|
- name: Update flake inputs
|
||||||
|
uses: Mic92/update-flake-inputs-gitea@main
|
||||||
|
env:
|
||||||
|
# Exclude private flakes and update-clan-core checks flake
|
||||||
|
EXCLUDE_PATTERNS: "devFlake/private/flake.nix,checks/impure/flake.nix"
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
{
|
|
||||||
pkgs,
|
|
||||||
nixosLib,
|
|
||||||
clan-core,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
machines = [
|
|
||||||
"admin"
|
|
||||||
"peer"
|
|
||||||
"signer"
|
|
||||||
];
|
|
||||||
in
|
|
||||||
nixosLib.runTest (
|
|
||||||
{ ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
clan-core.modules.nixosTest.clanTest
|
|
||||||
];
|
|
||||||
|
|
||||||
hostPkgs = pkgs;
|
|
||||||
name = "service-data-mesher";
|
|
||||||
|
|
||||||
clan = {
|
|
||||||
directory = ./.;
|
|
||||||
inventory = {
|
|
||||||
machines = lib.genAttrs machines (_: { });
|
|
||||||
services = {
|
|
||||||
data-mesher.default = {
|
|
||||||
roles.peer.machines = [ "peer" ];
|
|
||||||
roles.admin.machines = [ "admin" ];
|
|
||||||
roles.signer.machines = [ "signer" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
defaults =
|
|
||||||
{ config, ... }:
|
|
||||||
{
|
|
||||||
environment.systemPackages = [
|
|
||||||
config.services.data-mesher.package
|
|
||||||
];
|
|
||||||
|
|
||||||
clan.data-mesher.network.interface = "eth1";
|
|
||||||
clan.data-mesher.bootstrapNodes = [
|
|
||||||
"[2001:db8:1::1]:7946" # peer1
|
|
||||||
"[2001:db8:1::2]:7946" # peer2
|
|
||||||
];
|
|
||||||
|
|
||||||
# speed up for testing
|
|
||||||
services.data-mesher.settings = {
|
|
||||||
cluster.join_interval = lib.mkForce "2s";
|
|
||||||
cluster.push_pull_interval = lib.mkForce "5s";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
nodes = {
|
|
||||||
admin.clan.data-mesher.network.tld = "foo";
|
|
||||||
};
|
|
||||||
|
|
||||||
# TODO Add better test script.
|
|
||||||
testScript = ''
|
|
||||||
|
|
||||||
def resolve(node, success = {}, fail = [], timeout = 60):
|
|
||||||
for hostname, ips in success.items():
|
|
||||||
for ip in ips:
|
|
||||||
node.wait_until_succeeds(f"getent ahosts {hostname} | grep {ip}", timeout)
|
|
||||||
|
|
||||||
for hostname in fail:
|
|
||||||
node.wait_until_fails(f"getent ahosts {hostname}")
|
|
||||||
|
|
||||||
start_all()
|
|
||||||
|
|
||||||
admin.wait_for_unit("data-mesher")
|
|
||||||
signer.wait_for_unit("data-mesher")
|
|
||||||
peer.wait_for_unit("data-mesher")
|
|
||||||
|
|
||||||
# check dns resolution
|
|
||||||
for node in [admin, signer, peer]:
|
|
||||||
resolve(node, {
|
|
||||||
"admin.foo": ["2001:db8:1::1", "192.168.1.1"],
|
|
||||||
"peer.foo": ["2001:db8:1::2", "192.168.1.2"],
|
|
||||||
"signer.foo": ["2001:db8:1::3", "192.168.1.3"]
|
|
||||||
})
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"publickey": "age10zxkj45fah3qa8uyg3a36jsd06d839xfq64nrez9etrsf4km0gtsp45gsz",
|
|
||||||
"type": "age"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"publickey": "age1faqrml2ukc6unfm75d3v2vnaf62v92rdxaagg3ty3cfna7vt99gqlzs43l",
|
|
||||||
"type": "age"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"publickey": "age153mke8v2qksyqjc7vta7wglzdqr5epazt83nch0ur5v7kl87cfdsr07qld",
|
|
||||||
"type": "age"
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"data": "ENC[AES256_GCM,data:7xyb6WoaN7uRWEO8QRkBw7iytP5hFrA94VRi+sy/UhzqT9AyDPmxB/F8ASFsBbzJUwi0Oqd2E1CeIYRoDhG7JHnDyL2bYonz2RQ=,iv:slh3x774m6oTHAXFwcen1qF+jEchOKCyNsJMbNhqXHE=,tag:wtK8H8PZCESPA1vZCd7Ptw==,type:str]",
|
|
||||||
"sops": {
|
|
||||||
"kms": null,
|
|
||||||
"gcp_kms": null,
|
|
||||||
"azure_kv": null,
|
|
||||||
"hc_vault": null,
|
|
||||||
"age": [
|
|
||||||
{
|
|
||||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPTzZ4RTVNb2I1MTBRMEcy\neU1Eek9GakkydEJBVm9kR3AyY1pEYkorNUYwCkh2WHhNQmc1eWI2cCtEUFFWdzJq\nS0FvQWtoOFkzRVBxVzhuczc0aVprbkkKLS0tIFRLdmpnbzY1Uk9LdklEWnQzZHM2\nVEx3dzhMSnMwaWE0V0J6VTZ5ZVFYMjgKdaICa/hprHxhH89XD7ri0vyTT4rM+Si0\niHcQU4x64dgoJa4gKxgr4k9XncjoNEjJhxL7i/ZNZ5deaaLRn5rKMg==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastmodified": "2025-04-08T13:24:55Z",
|
|
||||||
"mac": "ENC[AES256_GCM,data:TJWDHGSRBfOCW8Q+t3YxG3vlpf9a5u7B27AamnOk95huqIv0htqWV3RuV7NoOZ5v2ijqSe/pLfpwrmtdhO2sUBEvhdhJm8UzLShP7AbH9lxV+icJOsY7VSrp+R5W526V46ONP6p47b7fOQBbp03BMz01G191N68WYOf6k2arGxU=,iv:nEyTBwJ2EA+OAl8Ulo5cvFX6Ow2FwzTWooF/rdkPiXg=,tag:oYcG16zR+Fb5XzVsHhq2Qw==,type:str]",
|
|
||||||
"pgp": null,
|
|
||||||
"unencrypted_suffix": "_unencrypted",
|
|
||||||
"version": "3.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"data": "ENC[AES256_GCM,data:JOOhvl0clDD/b5YO45CXR3wVopBSNe9dYBG+p5iD+nniN2OgOwBgYPNSCVtc+NemqutD12hFUSfCzXidkv0ijhD1JZeLar9Ygxc=,iv:XctQwSYSvKhDRk/XMacC9uMydZ8e9hnhpoWTgyXiFI0=,tag:foAhBlg4DwpQU2G9DzTo5g==,type:str]",
|
|
||||||
"sops": {
|
|
||||||
"kms": null,
|
|
||||||
"gcp_kms": null,
|
|
||||||
"azure_kv": null,
|
|
||||||
"hc_vault": null,
|
|
||||||
"age": [
|
|
||||||
{
|
|
||||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBVWMvWkp5TnZQcGs5Ykhp\nWC91YkoyZERqdXpxQm5JVmRhaUhueEJETDJVCkM4V0hSYldkV1U2Q0d1TGh3eGNR\nVjJ1VFd6ZEN0SXZjSVEvcnV2WW0vbVUKLS0tIFRCNW9nWHdYaUxLSVVUSXM0OGtN\nVFMzRXExNkYxcFE3QWlxVUM3ay9INm8KV6r8ftpwarly3qXoU9y8KxKrUKLvP9KX\nGsP0pORsaM+qPMsdfEo35CqhAeQu0+6DWd7/67+fUMp6Jr0DthtTmg==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastmodified": "2025-04-08T13:25:28Z",
|
|
||||||
"mac": "ENC[AES256_GCM,data:scY9+/fcXhfHEdrsZJLOM6nfjpRaURgTVbCRepUjhUo24B4ByEsAo2B8psVAaGEHEsFRZuoiByqrGzKhyUASmUs+wn+ziOKBTLzu55fOakp8PWYtQ4miiz2TQffp80gCQRJpykcbUgqIKXNSNutt4tosTBL7osXwCEnEQWd+SaA=,iv:1VXNvLP6DUxZYEr1juOLJmZCGbLp33DlwhxHQV9AMD4=,tag:uFM1R8OmkFS74/zkUG0k8A==,type:str]",
|
|
||||||
"pgp": null,
|
|
||||||
"unencrypted_suffix": "_unencrypted",
|
|
||||||
"version": "3.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"data": "ENC[AES256_GCM,data:i1YBJdK8XmWnVnZKBpmWggSN8JSOr8pm2Zx+CeE8qqeLZ7xwMO8SYCutM8l94M5vzmmX0CmwzeMZ/JVPbEwFd3ZAImUfh685HOY=,iv:N4rHNaX+WmoPb0EZPqMt+CT1BzaWO9LyoemBxKn+u/s=,tag:PnzSvdGwVnTMK8Do8VzFaQ==,type:str]",
|
|
||||||
"sops": {
|
|
||||||
"kms": null,
|
|
||||||
"gcp_kms": null,
|
|
||||||
"azure_kv": null,
|
|
||||||
"hc_vault": null,
|
|
||||||
"age": [
|
|
||||||
{
|
|
||||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4RXlmcVNGTnlkY2ZqZFlH\nVnh0eHhRNE5hRDNDVkt0TEE0bmRNN2JIVkN3CkxnaGM4Y3M3a0xoK2xMRzBLMHRV\nT1FzKzNRMFZOeWc2K3E5K2FzdUsvWmsKLS0tIENtVlFSWElHN3RtOUY2alhxajhs\naXI1MmR4WC9EVGVFK3dHM1gvVnlZMVUKCyLz0DkdbWfSfccShO1xjWfxhunEIbD0\n6imeIBhZHvVJmZLXnVl7B0pNXo6be7WSBMAUM9gUtCNh4zaChBNwGw==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastmodified": "2025-04-08T13:25:52Z",
|
|
||||||
"mac": "ENC[AES256_GCM,data:WFGysoXN95e/RxL094CoL4iueqEcSqCSQZLahwz9HMLi+8HWZIXr55a+jyK7piqR8nBS4BquU5fKhlC6BvEbZFt69t4onTA+LxS3D7A8/TO0CWS0RymUjW9omJUseRQWwAHtE7l0qI5hdOUKhQ+o5pU+2bc3PUlaONM0aOCCoFo=,iv:l1f4aVqLl5VAMfjNxDbxQEQp/qY/nxzgv2GTuPVBoBA=,tag:4PPDCmDrviqdn42RLHQYbA==,type:str]",
|
|
||||||
"pgp": null,
|
|
||||||
"unencrypted_suffix": "_unencrypted",
|
|
||||||
"version": "3.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"data": "ENC[AES256_GCM,data:w3bU23Pfe8W89lF+tOmEYPU/A4FkY6n7rgQ6yo+eqCJFxTyHydV6Mg4/g4jaL+4wwIqNYRiMR8J8jLhSvw3Bc59u7Ul+RGwdpiKoBBJfsHjO8r6uOz2u9Raa+iUJH1EJWmGvsQXAILpliZ+klS96VWnGN3pYMEI=,iv:7QbUxta6NPQLZrh6AOcNe+0wkrADuTI9VKVp8q+XoZ8=,tag:ZH0t3RylfQk5U23ZHWaw0g==,type:str]",
|
|
||||||
"sops": {
|
|
||||||
"kms": null,
|
|
||||||
"gcp_kms": null,
|
|
||||||
"azure_kv": null,
|
|
||||||
"hc_vault": null,
|
|
||||||
"age": [
|
|
||||||
{
|
|
||||||
"recipient": "age10zxkj45fah3qa8uyg3a36jsd06d839xfq64nrez9etrsf4km0gtsp45gsz",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKaTBoSFJVSTdZeW4wZG9p\nWFR1LzVmYS8xWmRqTlNtWFVkSW9jZXpVejJBCkpqZm12L1dDSmNhekVsK1JBOU9r\nZThScGdDakFlRzNsVXp1eE5yOStFSW8KLS0tIFRrTkZBQlRsR2VNcUJvNEkzS2pw\nNksvM296UkFWTkZDVVp1ZVZMNUs4cWsKWTteB1G9Oo38a81PeqKO09NUQetuqosC\nhrToQ6NMo5O7/StmVG228MHbJS3KLXsvh2AFOEPyZrbpB2Opd2wwoA==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6U2FWRThRNkVQdk9yZ0VE\nM09iSVhmeldMcDZVaFRDNGtjWTdBa0VIT2pJCkdtd04xSXdicDY3OHI1WXl5TndB\nemtQeW1SS2tVVllPUHhLUTRla3haZGMKLS0tIGN0NVNEN3RKeWM0azBBMnBpQU4r\nTFFzQ0lOcGt0ek9UZmZZRjhibTNTc0EKReUwYBVM1NKX0FD/ZeokFAAknwju5Azq\nGzl4UVJBi5Es0GWORdCGElPXMd7jMud1SwgY04AdZj/dzinCSW4CZw==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastmodified": "2025-04-08T13:25:10Z",
|
|
||||||
"mac": "ENC[AES256_GCM,data:0vl9Gt4QeH+GJcnl8FuWSaqQXC8S6Pe50NmeDg5Nl2NWagz8aLCvOFyTqX/Icp/bTi1XQ5icHHhF3YhM+QAvdUL3aO0WGbh92dPRnFuvlZsdtwCFhT+LyHyYHFf6yP+0h/uFpJv9fE6xY22CezA6ZVQ8ywi1epaC548Gr27uVe4=,iv:G4hZVCLkIpbg9uwB7Y8xtHLdnlmBvFrPjxSoqdyHNvM=,tag:uvKwakhUY2aa7v0tmR/o8A==,type:str]",
|
|
||||||
"pgp": null,
|
|
||||||
"unencrypted_suffix": "_unencrypted",
|
|
||||||
"version": "3.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MCowBQYDK2VwAyEAm204bpSFi4jOjZuXDpIZ/rcJBrbG4zAc7OSA4rAVSYE=
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"data": "ENC[AES256_GCM,data:kERPY40pyvke0mRBnafa4zOaF46rbueRbhpUCXjYP5ORpC7zoOhbdlVBhOsPqE2vfEP4RWkH+ZPdDYXOKXwotBCmlq2i7TfZeoNXFkzWXc3GyM5mndnjCc8hvYEQF1w6xkkVSUt4n06BAw/gT0ppz+vo5dExIA8=,iv:JmYD2o4DGqds6DV7ucUmUD0BRB61exbRsNAtINOR8cQ=,tag:Z58gVnHD+4s21Z84IRw+Vw==,type:str]",
|
|
||||||
"sops": {
|
|
||||||
"kms": null,
|
|
||||||
"gcp_kms": null,
|
|
||||||
"azure_kv": null,
|
|
||||||
"hc_vault": null,
|
|
||||||
"age": [
|
|
||||||
{
|
|
||||||
"recipient": "age1faqrml2ukc6unfm75d3v2vnaf62v92rdxaagg3ty3cfna7vt99gqlzs43l",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4OFluVThBdUJSTmRVTk94\neFZnLytvcnNSdmQvR3ZkT2UvWFVieFV1SUFNCm9jWHlyZXRwaVdFaG9ocnd4S3FU\ndTZ2dklBbkFVL0hVT0Y2L1o5dnUyNG8KLS0tIGFvYlBJR3l2b3F6OU9uMTFkYjli\nNVFLOWQzOStpU2kzb0xyZUFCMnBmMVUK5Jzssf1XBX25bq0RKlJY8NwtKIytxL/c\nBPPFDZywJiUgw1izsdfGVkRhhSFCQIz+yWIJWzr01NU2jLyFjSfCNw==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzYW92c3Q4SktwSnJ1TkRJ\nZEJyZk96cG8ybkpPQzYzVk0xZGs0eCtISVR3CmhDaWxTem1FMjJKNmZNaTkxN01n\nenUvdFI1UkFmL1lzNlM5N0Ixd0dpc1EKLS0tIHpyS2VHaHRRdUovQVgvRmRHaXh3\naFpSNURjTWkxaW9TOXpKL2IvcUFEbmMKq4Ch7DIL34NetFV+xygTdcpQjjmV8v1n\nlvYcjUO/9c3nVkxNMJYGjuxFLuFc4Gw+AyawCjpsIYXRskYRW4UR1w==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastmodified": "2025-04-08T13:25:43Z",
|
|
||||||
"mac": "ENC[AES256_GCM,data:YhL2d6i0VpUd15B4ow2BgRpyEm0KEA8NSb7jZcjI58d7d4lAqBMcDQB+8a9e2NZbPk8p1EYl3q4VXbEnuwsJiPZI2kabRusy/IGoHzUTUMFfVaOuUcC0eyINNVSmzJxnCbLCAA1Aj1yXzgRQ0MWr7r0RHMKw0D1e0HxdEsuAPrA=,iv:yPlMmE6+NEEQ9uOZzD3lUTBcfUwGX/Ar+bCu0XKnjIg=,tag:eR22BCFVAlRHdggg9oCeaA==,type:str]",
|
|
||||||
"pgp": null,
|
|
||||||
"unencrypted_suffix": "_unencrypted",
|
|
||||||
"version": "3.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MCowBQYDK2VwAyEAv5dICFue2fYO0Zi1IyfYjoNfR6713WpISo7+2bSjL18=
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"data": "ENC[AES256_GCM,data:U8F7clQ2Tuj8zy5EoEga/Mc9N3LLZrlFf5m7UJKrP5yybFRCJSBs05hOcNe+LQZdEAvvr0Qbkry1pQyE84gCVbxHvwkD+l3GbguBuLMsW96bHcmstb6AvZyhMDBpm73Azf4lXhNaiB8p2pDWdxV77E+PPw1MNYI=,iv:hQhN6Ak8tB6cXSCnTmmQqHEpXWpWck3uIVCk5pUqFqU=,tag:uC4ljcs92WPlUOfwSkrK9Q==,type:str]",
|
|
||||||
"sops": {
|
|
||||||
"kms": null,
|
|
||||||
"gcp_kms": null,
|
|
||||||
"azure_kv": null,
|
|
||||||
"hc_vault": null,
|
|
||||||
"age": [
|
|
||||||
{
|
|
||||||
"recipient": "age153mke8v2qksyqjc7vta7wglzdqr5epazt83nch0ur5v7kl87cfdsr07qld",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvV05lejQrdUQvQjZPOG9v\nZ01naXlYZ1JxWHhDT1M1aUs1RWJDSU1acVFFCmdHY094aGRPYWxpdVVxSFVHRU9v\nNnVaeTlpSEdtSWRDMmVMSjdSOEQ4ZlEKLS0tIFo5NVk2bzBxYjZ5ZWpDWTMrQ2VF\nVThWUk0rVXpTY2svSCtiVDhTQ2kvbFkKEM2DBuFtdEj1G/vS1TsyIfQxSFFvPTDq\nCmO7L/J5lHdyfIXzp/FlhdKpjvmchb8gbfJn7IWpKopc7Zimy/JnGQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArNzVUaHkzUzVEMlh1Q3Qr\nOEo0aDJIMG91amJiZG50MEhqblRCTWxRRVVRCk4xZlp4SkJuUHc2UnFyU1prczkz\nNGtlQlRlNnBDRFFvUGhReTh6MTBZaXMKLS0tIGxtaXhUMDM0RU4yQytualdzdTFt\nWGRiVG54MnYrR2lqZVZoT0VkbmV5WUUKbzAnOkn8RYOo7z4RISQ0yN875vSEQMDa\nnnttzVrQuK0/iZvzJ0Zq8U9+JJJKvFB1tHqye6CN0zMbv55CLLnA0g==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastmodified": "2025-04-08T13:26:07Z",
|
|
||||||
"mac": "ENC[AES256_GCM,data:uMss4+BiVupFqX7nHnMo+0yZ8RPuFD8VHYK2EtJSqzgurQrZVT4tJwY50mz2gVmwbrm49QYKk5S+H29DU0cM0HiEOgB5P5ObpXTRJPagWQ48CEFrDpBzLplobxulwnN6jJ1dpL3JF3jfrzrnSDFXMvx+n5x/86/AYXYRsi/UeyY=,iv:mPT1svKrNGmYpbL9hh2Bxxakml69q+U6gQ0ZnEcbEyg=,tag:zcZx1lTw/bEsX/1g+6T04g==,type:str]",
|
|
||||||
"pgp": null,
|
|
||||||
"unencrypted_suffix": "_unencrypted",
|
|
||||||
"version": "3.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MCowBQYDK2VwAyEAeUkW5UIwA1svbNY71ePyJKX68UhxrqIUGQ2jd06w5WM=
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"data": "ENC[AES256_GCM,data:nRlCMF58cnkdUAE2aVHEG1+vAckKtVt48Jr21Bklfbsqe1yTiHPFAMLL1ywgWWWd7FjI/Z8WID9sWzh9J8Vmotw4aJWU/rIQSeF8cJHALvfOxarJIIyb7purAiPoPPs6ggGmSmVFGB1aw8kH1JMcppQN8OItdQM=,iv:qTwaL2mgw6g7heN/H5qcjei3oY+h46PdSe3v2hDlkTs=,tag:jYNULrOPl9mcQTTrx1SDeA==,type:str]",
|
|
||||||
"sops": {
|
|
||||||
"kms": null,
|
|
||||||
"gcp_kms": null,
|
|
||||||
"azure_kv": null,
|
|
||||||
"hc_vault": null,
|
|
||||||
"age": [
|
|
||||||
{
|
|
||||||
"recipient": "age153mke8v2qksyqjc7vta7wglzdqr5epazt83nch0ur5v7kl87cfdsr07qld",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRcG44cGFBWXk2Z0pmNklv\nTnJ5b0svLytzZmNNRkxCVU1zaDVhNUs2cld3CklsenpWd0g2OEdKKzBMQlNEejRn\nTlEvY01HYjdvVExadnN3aXZIRTZ4YlEKLS0tIGRPUXdNSHZCRDBMbno2MjJqRHBl\nSzdiSURDYitQWFpaSElkdmdicDVjMWsKweQiRqyzXmzabmU2fmgwHtOa9uDmhx9O\ns9NfUhC3ifooQUSeYp58b1ZGJQx5O5bn9q/DaEoit5LTOUprt1pUPA==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiTEdlL29sVWFpSDNNaXRJ\ndTJDRkU4VzFPQ0M4MkFha2IxV2FXN2o3ZEFRCjF3UnZ5U1hTc3VvSTIzcWxOZjl0\ncHlLVEFqRk1UbGdxaUxEeDFqbFVYaU0KLS0tIFFyMnJkZnRHdWg4Z1IyRHFkY0I5\nQjdIMGtGLzRGMFM0ektDZ3hzZDdHSmMKvxOQuKgePom0QfPSvn+4vsGHhJ4BoOvW\nc27Vn4/i4hbjfJr4JpULAwyIwt3F0RaTA2M6EkFkY8otEi3vkcpWvA==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"recipient": "age10zxkj45fah3qa8uyg3a36jsd06d839xfq64nrez9etrsf4km0gtsp45gsz",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5ZzdsaVRnSmsrMGR1Ylg3\nZkpscTdwNUl5NUVXN3kvMU1icE0yZU1WSEJBClB6SlJYZUhDSElRREx5b0VueFUw\nNVFRU3BSU24yWEtpRnJoUC83SDVaUWsKLS0tIGVxNEo3TjlwakpDZlNsSkVCOXlz\nNDgwaE1xNjZkSnJBVlU5YXVHeGxVNFEKsXKyTzq9VsERpXzbFJGv/pbAghFAcXkf\nMmCgQHsfIMBJQUstcO8sAkxv3ced0dAEz8O6NUd0FS2zlhBzt29Rnw==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"recipient": "age1faqrml2ukc6unfm75d3v2vnaf62v92rdxaagg3ty3cfna7vt99gqlzs43l",
|
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkK1hDMGxCc1IvYXlJMnBF\nWncxaXBQa1RpTWdwUHc3Yk16My8rVHNJc2dFCkNlK2h0dy9oU3Z5ZGhwRWVLYVUz\ncVBKT2x5VnlhbXNmdHkwbmZzVG5sd0EKLS0tIHJaMzhDanF4Rkl3akN4MEIxOHFC\nYWRUZ08xb1UwOFNRaktkMjIzNXZmNkUK1rlbJ96oUNQZLmCmPNDOKxfDMMa+Bl2E\nJPxcNc7XY3WBHa3xFUbcqiPxWxDyaZjhq/LYQGpepiGonGMEzR5JOQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastmodified": "2025-04-08T13:25:20Z",
|
|
||||||
"mac": "ENC[AES256_GCM,data:za9ku+9lu1TTRjbPcd5LYDM4tJsAYF/yuWFCGkAhqcYguEducsIfoKBwL42ahAzqLjCZp91YJuINtw16mM+Hmlhi/BVwhnXNHqcfnKoAS/zg9KJvWcvXwKMmjEjaBovqaCWXWoKS7dn/wZ7nfGrlsiUilCDkW4BzTIzkqNkyREU=,iv:2X9apXMatwCPRBIRbPxz6PJQwGrlr7O+z+MrsnFq+sQ=,tag:IYvitoV4MhyJyRO1ySxbLQ==,type:str]",
|
|
||||||
"pgp": null,
|
|
||||||
"unencrypted_suffix": "_unencrypted",
|
|
||||||
"version": "3.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MCowBQYDK2VwAyEA/5j+Js7oxwWvZdfjfEO/3UuRqMxLKXsaNc3/5N2WSaw=
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
@@ -94,7 +94,6 @@ in
|
|||||||
|
|
||||||
service-dummy-test = import ./service-dummy-test nixosTestArgs;
|
service-dummy-test = import ./service-dummy-test nixosTestArgs;
|
||||||
service-dummy-test-from-flake = import ./service-dummy-test-from-flake nixosTestArgs;
|
service-dummy-test-from-flake = import ./service-dummy-test-from-flake nixosTestArgs;
|
||||||
service-data-mesher = import ./data-mesher nixosTestArgs;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
packagesToBuild = lib.removeAttrs self'.packages [
|
packagesToBuild = lib.removeAttrs self'.packages [
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
config,
|
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
@@ -9,9 +8,14 @@
|
|||||||
config = {
|
config = {
|
||||||
|
|
||||||
warnings = [
|
warnings = [
|
||||||
"The clan.disk-id module is deprecated and will be removed on 2025-07-15.
|
''
|
||||||
Please migrate to user-maintained configuration or the new equivalent clan services
|
The clan.disk-id module is deprecated and will be removed on 2025-07-15.
|
||||||
(https://docs.clan.lol/reference/clanServices)."
|
For migration see: https://docs.clan.lol/guides/migrations/disk-id/
|
||||||
|
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!!! Please migrate. Otherwise you may not be able to boot your system after that date. !!!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
''
|
||||||
];
|
];
|
||||||
clan.core.vars.generators.disk-id = {
|
clan.core.vars.generators.disk-id = {
|
||||||
files.diskId.secret = false;
|
files.diskId.secret = false;
|
||||||
|
|||||||
@@ -9,15 +9,37 @@
|
|||||||
interface =
|
interface =
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
options.allowedKeys = lib.mkOption {
|
|
||||||
default = { };
|
options = {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
allowedKeys = lib.mkOption {
|
||||||
description = "The allowed public keys for ssh access to the admin user";
|
default = { };
|
||||||
example = {
|
type = lib.types.attrsOf lib.types.str;
|
||||||
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
|
description = "The allowed public keys for ssh access to the admin user";
|
||||||
|
example = {
|
||||||
|
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
rsaHostKey.enable = lib.mkEnableOption "Generate RSA host key";
|
||||||
|
|
||||||
|
# TODO: allow per-server domains that we than collect in the inventory
|
||||||
|
#certicficateDomains = lib.mkOption {
|
||||||
|
# type = lib.types.listOf lib.types.str;
|
||||||
|
# default = [ ];
|
||||||
|
# example = [ "git.mydomain.com" ];
|
||||||
|
# description = "List of domains to include in the certificate. This option will not prepend the machine name in front of each domain.";
|
||||||
|
#};
|
||||||
|
|
||||||
|
certificateSearchDomains = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [ "mydomain.com" ];
|
||||||
|
description = ''
|
||||||
|
List of domains to include in the certificate.
|
||||||
|
This option will prepend the machine name in front of each domain before adding it to the certificate.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
perInstance =
|
perInstance =
|
||||||
@@ -27,10 +49,15 @@
|
|||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../../clanModules/sshd
|
# We don't have a good way to specify dependencies between
|
||||||
../../clanModules/root-password
|
# 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;
|
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues settings.allowedKeys;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
39
clanServices/admin/root-password.nix
Normal file
39
clanServices/admin/root-password.nix
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 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,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
files.password.deploy = false;
|
||||||
|
|
||||||
|
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.";
|
||||||
|
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
115
clanServices/admin/ssh.nix
Normal file
115
clanServices/admin/ssh.nix
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
settings,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
|
||||||
|
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
29
clanServices/data-mesher/README.md
Normal file
29
clanServices/data-mesher/README.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
This service will set up data-mesher.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```nix
|
||||||
|
inventory.instances = {
|
||||||
|
data-mesher = {
|
||||||
|
module = {
|
||||||
|
name = "data-mesher";
|
||||||
|
input = "clan-core";
|
||||||
|
};
|
||||||
|
roles.admin.machines.server0 = {
|
||||||
|
settings = {
|
||||||
|
bootstrapNodes = {
|
||||||
|
node1 = "192.168.1.1:7946";
|
||||||
|
node2 = "192.168.1.2:7946";
|
||||||
|
};
|
||||||
|
|
||||||
|
network = {
|
||||||
|
hostTTL = "24h";
|
||||||
|
interface = "tailscale0";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
roles.peer.machines.server1 = { };
|
||||||
|
roles.signer.machines.server2 = { };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
29
clanServices/data-mesher/admin.nix
Normal file
29
clanServices/data-mesher/admin.nix
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
142
clanServices/data-mesher/default.nix
Normal file
142
clanServices/data-mesher/default.nix
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
{ ... }:
|
||||||
|
let
|
||||||
|
sharedInterface =
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
network = {
|
||||||
|
interface = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = ''
|
||||||
|
The interface over which cluster communication should be performed.
|
||||||
|
All the ip addresses associate with this interface will be part of
|
||||||
|
our host claim, including both ipv4 and ipv6.
|
||||||
|
|
||||||
|
This should be set to an internal/VPN interface.
|
||||||
|
'';
|
||||||
|
example = "tailscale0";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 7946;
|
||||||
|
description = ''
|
||||||
|
Port to listen on for cluster communication.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
_class = "clan.service";
|
||||||
|
manifest.name = "data-mesher";
|
||||||
|
manifest.description = "Set up data-mesher";
|
||||||
|
manifest.categories = [ "System" ];
|
||||||
|
manifest.readme = builtins.readFile ./README.md;
|
||||||
|
|
||||||
|
roles.admin = {
|
||||||
|
interface =
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
|
||||||
|
imports = [ sharedInterface ];
|
||||||
|
|
||||||
|
options = {
|
||||||
|
|
||||||
|
network = {
|
||||||
|
tld = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "clan";
|
||||||
|
description = "Top level domain to use for the network";
|
||||||
|
};
|
||||||
|
|
||||||
|
hostTTL = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "${toString (24 * 28)}h";
|
||||||
|
example = "24h";
|
||||||
|
description = "The TTL for hosts in the network, in the form of a Go time.Duration";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
perInstance =
|
||||||
|
{ settings, roles, ... }:
|
||||||
|
{
|
||||||
|
nixosModule = {
|
||||||
|
imports = [
|
||||||
|
./admin.nix
|
||||||
|
./shared.nix
|
||||||
|
];
|
||||||
|
_module.args = { inherit settings roles; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
roles.signer = {
|
||||||
|
interface =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [ sharedInterface ];
|
||||||
|
};
|
||||||
|
perInstance =
|
||||||
|
{ settings, roles, ... }:
|
||||||
|
{
|
||||||
|
nixosModule = {
|
||||||
|
imports = [
|
||||||
|
./signer.nix
|
||||||
|
./shared.nix
|
||||||
|
];
|
||||||
|
_module.args = { inherit settings roles; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
roles.peer = {
|
||||||
|
interface =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [ sharedInterface ];
|
||||||
|
};
|
||||||
|
perInstance =
|
||||||
|
{ settings, roles, ... }:
|
||||||
|
{
|
||||||
|
nixosModule = {
|
||||||
|
imports = [
|
||||||
|
./peer.nix
|
||||||
|
./shared.nix
|
||||||
|
];
|
||||||
|
_module.args = { inherit settings roles; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
17
clanServices/data-mesher/flake-module.nix
Normal file
17
clanServices/data-mesher/flake-module.nix
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
module = lib.modules.importApply ./default.nix { };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
clan.modules = {
|
||||||
|
data-mesher = module;
|
||||||
|
};
|
||||||
|
perSystem =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
clan.nixosTests.service-data-mesher = {
|
||||||
|
imports = [ ./tests/vm/default.nix ];
|
||||||
|
clan.modules."@clan/data-mesher" = module;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
2
clanServices/data-mesher/peer.nix
Normal file
2
clanServices/data-mesher/peer.nix
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
86
clanServices/data-mesher/shared.nix
Normal file
86
clanServices/data-mesher/shared.nix
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
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 =
|
||||||
|
let
|
||||||
|
owner = config.users.users.data-mesher.name;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
private_key = {
|
||||||
|
inherit owner;
|
||||||
|
};
|
||||||
|
public_key.secret = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
runtimeInputs = [
|
||||||
|
config.services.data-mesher.package
|
||||||
|
];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
data-mesher generate keypair \
|
||||||
|
--public-key-path "$out"/public_key \
|
||||||
|
--private-key-path "$out"/private_key
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
clan.core.vars.generators.data-mesher-network-key = {
|
||||||
|
# generated once per clan
|
||||||
|
share = true;
|
||||||
|
|
||||||
|
files =
|
||||||
|
let
|
||||||
|
owner = config.users.users.data-mesher.name;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
private_key = {
|
||||||
|
inherit owner;
|
||||||
|
};
|
||||||
|
public_key.secret = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
runtimeInputs = [
|
||||||
|
config.services.data-mesher.package
|
||||||
|
];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
data-mesher generate keypair \
|
||||||
|
--public-key-path "$out"/public_key \
|
||||||
|
--private-key-path "$out"/private_key
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
2
clanServices/data-mesher/signer.nix
Normal file
2
clanServices/data-mesher/signer.nix
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
90
clanServices/data-mesher/tests/vm/default.nix
Normal file
90
clanServices/data-mesher/tests/vm/default.nix
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
name = "service-data-mesher";
|
||||||
|
|
||||||
|
clan = {
|
||||||
|
directory = ./.;
|
||||||
|
test.useContainers = true;
|
||||||
|
inventory = {
|
||||||
|
|
||||||
|
machines.peer = { };
|
||||||
|
machines.admin = { };
|
||||||
|
machines.signer = { };
|
||||||
|
|
||||||
|
instances = {
|
||||||
|
data-mesher =
|
||||||
|
let
|
||||||
|
bootstrapNodes = {
|
||||||
|
admin = "[2001:db8:1::1]:7946";
|
||||||
|
peer = "[2001:db8:1::2]:7946";
|
||||||
|
# signer = "2001:db8:1::3:7946";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
roles.peer.machines.peer.settings = {
|
||||||
|
network.interface = "eth1";
|
||||||
|
inherit bootstrapNodes;
|
||||||
|
};
|
||||||
|
roles.signer.machines.signer.settings = {
|
||||||
|
network.interface = "eth1";
|
||||||
|
inherit bootstrapNodes;
|
||||||
|
};
|
||||||
|
roles.admin.machines.admin.settings = {
|
||||||
|
network.tld = "foo";
|
||||||
|
network.interface = "eth1";
|
||||||
|
inherit bootstrapNodes;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes =
|
||||||
|
let
|
||||||
|
commonConfig =
|
||||||
|
{ lib, config, ... }:
|
||||||
|
{
|
||||||
|
environment.systemPackages = [
|
||||||
|
config.services.data-mesher.package
|
||||||
|
];
|
||||||
|
|
||||||
|
# speed up for testing
|
||||||
|
services.data-mesher.settings = {
|
||||||
|
cluster.join_interval = lib.mkForce "2s";
|
||||||
|
cluster.push_pull_interval = lib.mkForce "5s";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
peer = commonConfig;
|
||||||
|
admin = commonConfig;
|
||||||
|
signer = commonConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
def resolve(node, success = {}, fail = [], timeout = 60):
|
||||||
|
for hostname, ips in success.items():
|
||||||
|
for ip in ips:
|
||||||
|
node.wait_until_succeeds(f"getent ahosts {hostname} | grep {ip}", timeout)
|
||||||
|
|
||||||
|
for hostname in fail:
|
||||||
|
node.wait_until_fails(f"getent ahosts {hostname}")
|
||||||
|
|
||||||
|
start_all()
|
||||||
|
|
||||||
|
admin.wait_for_unit("data-mesher")
|
||||||
|
signer.wait_for_unit("data-mesher")
|
||||||
|
peer.wait_for_unit("data-mesher")
|
||||||
|
|
||||||
|
# check dns resolution
|
||||||
|
for node in [admin, signer, peer]:
|
||||||
|
resolve(node, {
|
||||||
|
"admin.foo": ["2001:db8:1::1", "192.168.1.1"],
|
||||||
|
"peer.foo": ["2001:db8:1::2", "192.168.1.2"],
|
||||||
|
"signer.foo": ["2001:db8:1::3", "192.168.1.3"]
|
||||||
|
})
|
||||||
|
'';
|
||||||
|
}
|
||||||
6
clanServices/data-mesher/tests/vm/sops/machines/admin/key.json
Executable file
6
clanServices/data-mesher/tests/vm/sops/machines/admin/key.json
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"publickey": "age1r99qtxl0v86wg8ndcem87yk5wag5xcsk98ngaumqzww6t7pyms0q5cyl80",
|
||||||
|
"type": "age"
|
||||||
|
}
|
||||||
|
]
|
||||||
6
clanServices/data-mesher/tests/vm/sops/machines/peer/key.json
Executable file
6
clanServices/data-mesher/tests/vm/sops/machines/peer/key.json
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"publickey": "age1hgjs2yqxhcxfgtvhydnfe5wzlagxw2dw4hu658e8neduy0lkye0skmjfc7",
|
||||||
|
"type": "age"
|
||||||
|
}
|
||||||
|
]
|
||||||
6
clanServices/data-mesher/tests/vm/sops/machines/signer/key.json
Executable file
6
clanServices/data-mesher/tests/vm/sops/machines/signer/key.json
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"publickey": "age1k6h9mespmnr9zhtwwqlhnla80x5jhpd4c2p7hp0nfanr5tspup0s0rld2f",
|
||||||
|
"type": "age"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:87WFWukgpTGlH67MTkHxzTosABK/6flJObt+u9UrGSOzBr1lx4V5IsMQ9HAM4jvLpveBNH4hlFDCxbD5666n2oYylGoyBph2vAg=,iv:GKLcU7Xqmb0ImvY7M71NddkOlUDSPa/fcXrXny2iZ1o=,tag:589QMSZeXdmTxRFtMFasZg==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFaXlqZEU0eHRZZjBncDE1\nV2hzTGZiVy9rM0NnWjc1NlpHVVZEUFd5S2pJCmo3Nm11bGQyWWt1R2tHS2pOYlpn\nY3lGa0w3UFpDT1RLSDU4cnJ2YVBkSU0KLS0tIEJjZVc1YXJqcHczYSt6WjV3ai93\nakdPd3VHWkVnWkdhNCtZakp4VXhBUG8Kg3xd9w5oW3/q+s59LkDy5N+xmvuvHRmh\njUv6KFLaB81yv3kb7bzj8E3aMzX0x2fMIDZ3EoPVggqA/sCWQu0p5Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2025-07-09T10:02:45Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:IWKfE1Y6SNg/SK+OOAmra5SwqAUfhepCNPClWPDWpOyJDwXSpk/OKl7hi3KFfIZOGupaC0xV2tTni0Uj6IBwf8zW2Mb/b1T+fWkGiyafoKlucfNPXPCob/fyf4Ju4iD/u1mD5BYYYqNTNqJWE+MCyQigL0MPE4tXGEPDa7htM6w=,iv:5RKArbEKnYjacopfL+4QhzGB8txqc3gnlwNPfRWQSlM=,tag:mdXf02nYiW7CexIbUUaMyw==,type:str]",
|
||||||
|
"unencrypted_suffix": "_unencrypted",
|
||||||
|
"version": "3.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:C9evAr01JpYiMBwuy31h+G9phm+uOYoQu+PegPFAMRbjgkjh0R+uolKtweedtHumMhzEkvz7y+BlfrriVh16ceyMozfzDEkVSWM=,iv:jM4Qx4B/j5Mvc3ybOf+10hKU19l1fCc5KcKulKgMP3c=,tag:mz01kIv5kU6u3f2+FeItYA==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAydzZrdDVidGpyd1NXT0Fu\nUEtZV3I4S0p5Z095QjBGaXpwOExJSkxVclVJCm54Vk12czQ5dm5TUExNNzlEcFNp\nUWorcWc1c1pvL3pkUFlQY3BJUGhUS3MKLS0tIHd2a291M0xkcjJvTXNnelRNZXda\nQi93R3FQVm0xTXBGR3E3SVpIMzgvR3MKmps5ObV1nODBQ0TKgZ++RLkjCEQM6sMn\nzonKtBingYzfeq+0+cASVkHZJpt/t0G5wmTgivKfv0OIP5eNSgIWFw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2025-07-09T10:02:57Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:Jk5eL2SmNpakrGF4N/31Q/PWShV5KYfA8NmlxEkD82UsIpPiIJ4Nec6NOoo7Y4bl/J53MLjK3u0/S6q7vv0Tih6+ze6hIddMJHTCp2qqclJvpH2xn6Ln+2ZK4okK2ZbWeSDF+LHc6nIpBak8JVjC/d8dQFT2L49Dkufc1nCD46w=,iv:oR0aQzjaEpFNrpWGc1TX6/zpg0WSfQjVG6VjAMwoLTI=,tag:pigUaCkVv91tynuaNoZenA==,type:str]",
|
||||||
|
"unencrypted_suffix": "_unencrypted",
|
||||||
|
"version": "3.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:bIx3chjDwy4epCyFuJoZlO7EglT/vEg6pdf6x+ISxqekGrrGNdiGtw3Z9foXWAPQrzngVztbwIlcEpUusKwoRPpdGIj5YzbGZbU=,iv:Gi1hjn6cL8z+LP5g6o3bUMsuIzoZRr8e3j3EBwG3p+Y=,tag:ttIfOLhDroV/WK57KBFd0w==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHNVh6OGE4aGJxbFd2Zks1\nL1ZoNkgrQjFSVFFUL2UzOGNqRXFkZURTMkJRCnZMWk00enRndzNXQmFvMG1UekI0\nUjhwZW9sQnFvb0FGbVE0N042UjF2OTAKLS0tIEdickxQdDdaZkVmN3RsemJzSElY\nWThGQVNMcnpxRlJ3bC9wVE56blljQUUK21wWOBiQc0Kyvl047nJ1N6QKR0/5Dd6r\nlqhhdFWninzqfVXJUk2pcMio8RVlvBujDsyjrPuhbRceSi+bUXIn+w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2025-07-09T10:03:08Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:kA2KCDZkZuR5rD7uU4xn5sIkizcnpGcoa3PYMbl73eux7JJYuSpUojFBRcYo1WCwMeOQUGsqo8LVF/rYhH4BVJ9LERs5zTLBaUsTarY8r/UK0Q5lNYZqIrqcb5LgOf1uCvfdXg5yfaFgPFJrEqjeekb9bx8xvhDZXpsND93rrUI=,iv:B6JqWWcQV/MxP4ucAIe7EnLiq9c4pnAUj3dnEp9IXJU=,tag:1i0Fv2i7Lak5JzIbPa2/cw==,type:str]",
|
||||||
|
"unencrypted_suffix": "_unencrypted",
|
||||||
|
"version": "3.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:2FgvnmawAdk+/k+RVWNsKQlUFUF+pZrrEBuupdG50uLNyxHd7Gi772gKNgHWyzZ/lpODg5mQi0rL+GmZYQwtZ7h76AGUEeQvuMMTzVUop69txxwhJD2dxZyhUAxZpibwo/St84ai+8+VksLkCSYfTXCulaeOVh4=,iv:YkPNq4zDj35PRNgt2kHEkHhbLcVc9dHP/zrAwdd94sM=,tag:KwW/74C7Z/+3dNoXB3NHwQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoaS94M0JsR2Q5N21DNnFB\nUHgvelRTK3FKZkNKcTJFbEJ1VGFIM256MVVRCmw5YjdyTVlXMlFpWnczV2dTSzhu\nSm5mMVRPeU1pYVFZNEN5MjJFZHVTejgKLS0tIDB0V2hSRkt5QzFYald0TWVza1lC\ncGNXemhGcklENTJiV1QvTFZxUDNRRlUK2dVEzSbdDNXZy7rQi5/Vq4KyHq5rMtEz\npTI8i1rFKIAy4TC7to03bOIudOIzKSCCzX31xARkM6qON0vEU9aHFg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"recipient": "age1r99qtxl0v86wg8ndcem87yk5wag5xcsk98ngaumqzww6t7pyms0q5cyl80",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEOEMzcExzTTF3MmpaenRN\ncS9RbnM0aStZSjNqbjF4QisrRjhoaDg1T0ZBCmFVOWJYZkFaOXBOUGJTdytYWk52\nVXV1MDdmSWQ1OS9iODAvN2c2Q3VGYXMKLS0tIEQxeWR4bmRoOWJ2Z1FyUk1PUk1n\nM0c5Ri9FdG9FNE9CZ29VSmgvN2xDdjgKjfG38gVOXXN2ftGiCPxMFbnh7lKM1USl\nqf11k+rgvR8M9XsDy2SnirKAaNmpks1dR6Zs5ppQuYJDEYyQCrEO5g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2025-07-09T10:02:45Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:TEH57vUZ/swTsWQPJ1X3J//xa1Q1LYPETZS7fuXCH1LCK51u88XGqVpNzSETREQ8LAOt34qN284b03UQIBGTeTr7I9cqt+/l8ew/0rFTiO3aiaT49q9aBkeFZlA+gy47r4hkhMmzGQJMUenvnzTHwT3Pw2RES5Vjs/2TSitpqlA=,iv:ffIotRGKU8y6j/VDLKbTmA8dZJVP5vafeG4F3wd60tc=,tag:q4xOwzLw5jxDR0pPIy2irA==,type:str]",
|
||||||
|
"unencrypted_suffix": "_unencrypted",
|
||||||
|
"version": "3.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MCowBQYDK2VwAyEAi6qF8u2uvPXlSflB4fzJNlOhj5PgAmRiv+JyyYOOgg4=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:LUNuEP/xSmzJ44sheoIYN6F24Qpr3svn6rTVUpr4KZA8uVJ9gPUd4ko4+pDisc9PyXCcxx+cYGRqr1cBp8Q3R+IyFFlR2HzuReQJaScvgjlntGtMJ2hin/aBp4pHS0F4nqPcKKROiZvIN4NHsxQ6XRVDOZbI3kE=,iv:BdRHjQXJL/OGgmqWaEDLit/zHgduNfPe3GUmYDrWLPw=,tag:N0n7CCiu+COgrfrwHUwQBQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"recipient": "age1hgjs2yqxhcxfgtvhydnfe5wzlagxw2dw4hu658e8neduy0lkye0skmjfc7",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCYlhrU2c1NnIyTzlVRHEx\nQTFxOUY1OWJXcHl1OHpPdWN1ZGpQV0UvZ1NzCnlKbmx0bllWMTd1ZnIxUHY0ZUU0\nVG9Jb3grSEdWeVpwaHoyQUxvNERqT00KLS0tIGtwZm5aMU1DOUhJbVVpVzIxZFow\nNVEvMy91SEg3M094MEFBSkVMRkhKZmMKuUzbEITGkYS39G14JXbKWLjiQFd4SVft\nWH34B97TFhOqusVF3zHsSCMxm/0BMeBvLxO/3RmzlwBtgNiKOqLwtQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2SUhJQW5EN0VKVHpQdlZC\nYTczdVJiRFdFNGtURFc2SmxKWFFycjZkQUgwCnRBVkJvUytuUDlhVlhFYno2cnBR\nRUdjL0lab1MwZzhGTklyVWZDVFJmN3cKLS0tIFRjOC9DS3llWGZWMGI2aThVYTRu\nVEFhK2Y2YkRTZHEyMWV0Q05ISHdhVVUKo9bPdV1dUeIkm4gI0r9V/s1dAfJC+H5Z\nEIUdYA7fl3jRZ01cSZ0iYWlvdl2jj0XzKafZsEQU7rL0jg9zbA2s2g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2025-07-09T10:02:59Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:+JtuPacwUMHXtp93DZmkiVne7bQUP8J7VpoS8koM0oJWJqZoQRHd9qH/04lrpp8q/YoOXtqXwhViZvFLieJVRexiXf/AAHfAfMn0EI7ois9oHhscN88Ps9nY6JUxhNd0h0OrUA58KKhrkGoqreAKAPADtVhaVCmWbU7vMUu1StE=,iv:BmJnTsgMSbl4XsBUkhSLfKd0XjhrEQfurEkaRJ6uD/g=,tag:jg21c4y4bQp0RwWTXkxF1A==,type:str]",
|
||||||
|
"unencrypted_suffix": "_unencrypted",
|
||||||
|
"version": "3.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MCowBQYDK2VwAyEA7kRKjQpj+BXPe5buvDZtBAcU1HIcfGmbuHZqaVm3zCo=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:armAfuTE0mkoy1fxAysCX/UPNM4/mt9P6/zEDwtagTSvQjMTwVzzsM+kRdLOUV4fbZ7HdqMceaZWzurAQJenXvWlBXgn87YFOFBSpf3OnpEwCTUs9H8dsVrdSUk4SrKjCjV33mybTrae/h9tMHdkRhKJzPD1+/8=,iv:x9KVGqT2Ug6B6PNwzL7NVDQqyOmFUptUsHAJEdn30dg=,tag:XSSO6JvXaXq8aezYvpF65Q==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"recipient": "age1k6h9mespmnr9zhtwwqlhnla80x5jhpd4c2p7hp0nfanr5tspup0s0rld2f",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIMVUwMEFzVjJhYXg5MXR4\nMzZPZUFrUWdEU2hPWUVDNHpVVENpdEdYSWtnCnN0R2pVdEIxYWZXYWNBb3N5bGNK\naVpWOXp5aWVJWG9vUWtMUnhYSmMyV0UKLS0tIEtMdFAybk1PN0t2M2lkaEYzUTY3\nVzVOdTBFbnlNVTAvRU5kU0dReEZ6MlUKNHIkAUUAqnuMtXbvXqLxQwuFALsnD/i0\naBCiz6J4S18uqt3kFbXAEksbD7jCexI8m5SMp4iuumWJ/Bx1lL4TWg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkbzBFSGt1dXI2bDN5TmFU\nY3N6djNmMTh2ek4vUzdHbTF6Z1hDQ2t5WVNJClEzZDZiaVpBekFrYTYweDNsNmk5\nTlhYZGRNd0llMndyMkZWMyt5N3pwTE0KLS0tIGJJbU9vbnBhSE5vRW1pRG83cEFJ\nR2xDTHk3VkJaVUZSVThRV3Jldkp6cnMK1V37txaSFYfLQM0qqRWjojyTN4fTJkRm\nGO3yHX9uwo/4D2xI7LM48n4vnNhSF05bWpq0X4r13fI4DofCJeEo1g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2025-07-09T10:03:11Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:qD1w+DO8cWFDQMBOrmO9FvxvJRn+mlUbh13exTGgmsdPn3uzTXknIDDHeWfkpF699nSzS6wRmgrB21e55rBU6iHMx1TW16S8wvCoYMFwib8zTrJzND7EJr/gRwQa0N080kBY3xBivKLUFlctgKtFUYZ9GQ6UTQeq18QKPoROjww=,iv:1mt8Er6YHxQ42F5Kb+xNtjbCAzokbeoNlHesC9Uzmhk=,tag:provO4tKDzoL5PHDg5EmhA==,type:str]",
|
||||||
|
"unencrypted_suffix": "_unencrypted",
|
||||||
|
"version": "3.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MCowBQYDK2VwAyEAVA6c25s+yNe5225PnELDV9FwbWi9ppLoTfgmdY8kILo=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:VzcB/JABSPoFdKYhRSn+nKxasn9zO/9fyNMrg3XstBelQNPpbO8mhmcnSamc/7e5GkpoVWgLRSULvosv+o6sz9EHRZ3UpSLBBTkDGAJmoBnkR8DbstPA9EgScpQ9IGOUP5tQ0oEOcJC3FrivdbWIzeXjpWb9BrU=,iv:6BNUrubJ9aNCkgonDRNgdyckCTndkPVDLE4X3J5d2zA=,tag:YqHTiGslEkslzUk24bmPZg==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"recipient": "age1hgjs2yqxhcxfgtvhydnfe5wzlagxw2dw4hu658e8neduy0lkye0skmjfc7",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwK2lMUTkrSmM4dHQxU0tI\nRVV6Wm4wWlJMYXBGbGdubExrMi8vRnJjdVd3CjI3aFVpdHRURHp6UEk3ZEZMcDZT\nZWZWaGFWYmY2Mk1iQ1BjalZkUnpUUm8KLS0tIEhFUVhBUjg1dC9LWHg2TytkRTlX\nNnlJZkJQc2ExK1BwaVVFcEw2b3BLZjQK8kqf3ZP9uLtbjCJLSEYpAqgq9zOS2HrY\n5MbPAKQI8iCUfnegti6hU+/MxjvPlaX1vT4V0Kd3gT4Khjl+OPw0Og==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"recipient": "age1r99qtxl0v86wg8ndcem87yk5wag5xcsk98ngaumqzww6t7pyms0q5cyl80",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWeUk3ZW9rdnZBTk9vQlFZ\nTzFZVDAvcXZyQjdkcGNNbnA0T3UyM3lzVERvCjFreE9RdWxnb2xWWmI4amJVdHBv\nNE9JN2tFazRnSGhiM0FId2RCUHNKWVEKLS0tIGlmM3JNSVZtR21ndFliUVpLTzJO\ncHJ2SjI1OExQK2hEN01WdG9wZ3RmVTAKi0BXp9yV2/9a9NeT7aTSK2CfkQ5yColJ\nm0+uv5AJndZ9IsaZGJxNOdAOspYdvsW38hFdfjUtVuUCyIOPc20WUg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"recipient": "age1k6h9mespmnr9zhtwwqlhnla80x5jhpd4c2p7hp0nfanr5tspup0s0rld2f",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkSUR1QVMvZ3F0NUxXd00z\nOWJGZFlsUy8vUmMxa1NoakZRVmJrSmd1RzBrCk1ZcDlBMFB0WVdWeFZaT3ZBTTh5\nS2RReWpUOGRBdGV6MDdjcEY5dFYrdjAKLS0tIG9oRWhUaWJZSElRdmlOZmRKSnNq\nUUNDZFdZbmM0c25MOGpvem1JSm9pVWsKxCLPivdHc6IN6Jbf9FujLGJaXP6ieO1S\nKsrs3Fe0RdYcEKI7P9EQNebQD2kKXficM0kKV5lRRVtW5024PftWoQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3amkyWWlxSTJkZEdMZFhL\nU2t5OGFIa25TRmdFM0ZNcUhFRHk0eDJQN2tjCm9UcUs2V0lEZ0hyNU9uaDVrckpj\nZ1JSQlhNeExjOER2aFJTM2NDS25PN2MKLS0tIFhmM21rT0Z4aUI5TUZyNnNBQ3Jy\nSDAxejhhZDZNQTVCNjNUSTBsZncra1kKFFQrFxNMyg0AEMb1wpKBc7LOVtEHyFZW\n/o7L52fTNa0GFJ3SVEdqg0PpnRzTyA8F5L77FBGKtx6auCVVHyZZ9g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2025-07-09T10:02:48Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:HooesDb1S24Cfb7H0lVTA8fAjM2QAN9MaJFvOSHniR6ICJAX8t8X0xfWIFRFuwPjAxi4kpBYSjW0420Yz9lZ2m4Fxswo1TV3lzHDVN2u9hdrsfpKXg5fW+2oZihuvCRStDagT3l2fKv+C+gBnGs1qyCM60BStvrEiQxTxTTHfho=,iv:kL8N0qBj4q+ZJbNJ8Y8RcV1KpUUMvNCpdwKbTPGpG6k=,tag:o2PmRsSkqTP5Idq7veGDOw==,type:str]",
|
||||||
|
"unencrypted_suffix": "_unencrypted",
|
||||||
|
"version": "3.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MCowBQYDK2VwAyEA/MuamRX6ZLcJunm7lZvlai0OZh++YuqMa56GiTwO68A=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
manifest.name = "clan-core/emergency-access";
|
manifest.name = "clan-core/emergency-access";
|
||||||
manifest.description = "Set recovery password for emergency access to machine";
|
manifest.description = "Set recovery password for emergency access to machine";
|
||||||
manifest.categories = [ "System" ];
|
manifest.categories = [ "System" ];
|
||||||
|
manifest.readme = builtins.readFile ./README.md;
|
||||||
|
|
||||||
roles.default.perInstance = {
|
roles.default.perInstance = {
|
||||||
nixosModule =
|
nixosModule =
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"System"
|
"System"
|
||||||
"Network"
|
"Network"
|
||||||
];
|
];
|
||||||
|
manifest.readme = builtins.readFile ./README.md;
|
||||||
|
|
||||||
roles.client = {
|
roles.client = {
|
||||||
interface =
|
interface =
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
manifest.name = "clan-core/state-version";
|
manifest.name = "clan-core/state-version";
|
||||||
manifest.description = "Automatically generate the state version of the nixos installation.";
|
manifest.description = "Automatically generate the state version of the nixos installation.";
|
||||||
manifest.categories = [ "System" ];
|
manifest.categories = [ "System" ];
|
||||||
|
manifest.readme = builtins.readFile ./README.md;
|
||||||
|
|
||||||
roles.default = {
|
roles.default = {
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
warnings = [
|
warnings = [
|
||||||
''
|
''
|
||||||
The clan.state-version service is deprecated and will be
|
The clan.state-version service is deprecated and will be
|
||||||
removed on 2025-07-15 in favor of a nix option.
|
removed on 2025-07-15 in favor of a nix option.
|
||||||
|
|
||||||
Please migrate your configuration to use `clan.core.settings.state-version.enable = true` instead.
|
Please migrate your configuration to use `clan.core.settings.state-version.enable = true` instead.
|
||||||
''
|
''
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
manifest.name = "clan-core/trusted-nix-caches";
|
manifest.name = "clan-core/trusted-nix-caches";
|
||||||
manifest.description = "This module sets the `clan.lol` and `nix-community` cache up as a trusted cache.";
|
manifest.description = "This module sets the `clan.lol` and `nix-community` cache up as a trusted cache.";
|
||||||
manifest.categories = [ "System" ];
|
manifest.categories = [ "System" ];
|
||||||
|
manifest.readme = builtins.readFile ./README.md;
|
||||||
|
|
||||||
roles.default = {
|
roles.default = {
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
_class = "clan.service";
|
_class = "clan.service";
|
||||||
manifest.name = "clan-core/users";
|
manifest.name = "clan-core/user";
|
||||||
manifest.description = "Automatically generates and configures a password for the specified user account.";
|
manifest.description = ''
|
||||||
|
An instance of this module will create a user account on the added machines,
|
||||||
|
along with a generated password that is constant across machines and user settings.
|
||||||
|
'';
|
||||||
manifest.categories = [ "System" ];
|
manifest.categories = [ "System" ];
|
||||||
|
manifest.readme = builtins.readFile ./README.md;
|
||||||
|
|
||||||
roles.default = {
|
roles.default = {
|
||||||
interface =
|
interface =
|
||||||
{ lib, ... }:
|
{ config, lib, ... }:
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
user = lib.mkOption {
|
user = lib.mkOption {
|
||||||
@@ -19,7 +23,58 @@
|
|||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
example = false;
|
example = false;
|
||||||
description = "Whether the user should be prompted.";
|
description = ''
|
||||||
|
Whether the user should be prompted for a password.
|
||||||
|
|
||||||
|
Effects:
|
||||||
|
|
||||||
|
- *enabled* (`true`) - Prompt for a password during the machine installation or update workflow.
|
||||||
|
- *disabled* (`false`) - Generate a password during the machine installation or update workflow.
|
||||||
|
|
||||||
|
The password can be shown in two steps:
|
||||||
|
|
||||||
|
- `clan vars list <machine-name>`
|
||||||
|
- `clan vars get <machine-name> <name-of-password-variable>`
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
regularUser = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = config.user != "root";
|
||||||
|
defaultText = lib.literalExpression "config.user != \"root\"";
|
||||||
|
example = false;
|
||||||
|
description = ''
|
||||||
|
Whether the user should be a regular user or a system user.
|
||||||
|
|
||||||
|
Regular users are normal users that can log in and have a home directory.
|
||||||
|
|
||||||
|
System users are used for system services and do not have a home directory.
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
`root` cannot be a regular user.
|
||||||
|
You must set this to `false` for `root`
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
groups = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [
|
||||||
|
"wheel"
|
||||||
|
"networkmanager"
|
||||||
|
"video"
|
||||||
|
"input"
|
||||||
|
];
|
||||||
|
description = ''
|
||||||
|
Additional groups the user should be added to.
|
||||||
|
You can add any group that exists on your system.
|
||||||
|
Make sure these group exists on all machines where the user is enabled.
|
||||||
|
|
||||||
|
Commonly used groups:
|
||||||
|
|
||||||
|
- "wheel" - Allows the user to run commands as root using `sudo`.
|
||||||
|
- "networkmanager" - Allows the user to manage network connections.
|
||||||
|
- "video" - Allows the user to access video devices.
|
||||||
|
- "input" - Allows the user to access input devices.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -35,9 +90,13 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
users.mutableUsers = false;
|
users.users.${settings.user} = {
|
||||||
users.users.${settings.user}.hashedPasswordFile =
|
isNormalUser = settings.regularUser;
|
||||||
config.clan.core.vars.generators."user-password-${settings.user}".files.user-password-hash.path;
|
extraGroups = settings.groups;
|
||||||
|
|
||||||
|
hashedPasswordFile =
|
||||||
|
config.clan.core.vars.generators."user-password-${settings.user}".files.user-password-hash.path;
|
||||||
|
};
|
||||||
|
|
||||||
clan.core.vars.generators."user-password-${settings.user}" = {
|
clan.core.vars.generators."user-password-${settings.user}" = {
|
||||||
|
|
||||||
@@ -80,4 +139,11 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
perMachine = {
|
||||||
|
nixosModule = {
|
||||||
|
# Immutable users to ensure that this module has exclusive control over the users.
|
||||||
|
users.mutableUsers = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
server = {
|
server = {
|
||||||
users.users.testuser.group = "testuser";
|
users.users.testuser.group = "testuser";
|
||||||
users.groups.testuser = { };
|
users.groups.testuser = { };
|
||||||
users.users.testuser.isNormalUser = true;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
sha256-pFUj3KhQ4FkzZT19t+FHBru8u8Lspax0rS2cv7nXIgM=
|
sha256-LdjcFZLL8WNldUO2LbdqFlss/ERiGeXVqMee0IxV2z0=
|
||||||
|
|||||||
12
devFlake/private/flake.lock
generated
12
devFlake/private/flake.lock
generated
@@ -66,11 +66,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-dev": {
|
"nixpkgs-dev": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751867001,
|
"lastModified": 1752467518,
|
||||||
"narHash": "sha256-3I49W0s3WVEDBO5S1RxYr74E2LLG7X8Wuvj9AmU0RDk=",
|
"narHash": "sha256-7SSvjNlM5ZsFZMP7Nw2uUa7EKYhB6Ny9iNtxtPPhWYY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "73feb5e20ec7259e280ca6f424ba165059b3bb6b",
|
"rev": "2f21cef1d1dc734a2dd89f535427cf291aebc8ef",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -146,11 +146,11 @@
|
|||||||
"nixpkgs": []
|
"nixpkgs": []
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1750931469,
|
"lastModified": 1752055615,
|
||||||
"narHash": "sha256-0IEdQB1nS+uViQw4k3VGUXntjkDp7aAlqcxdewb/hAc=",
|
"narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "ac8e6f32e11e9c7f153823abc3ab007f2a65d3e1",
|
"rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ nav:
|
|||||||
- 🚀 Creating Your First Clan: guides/getting-started/index.md
|
- 🚀 Creating Your First Clan: guides/getting-started/index.md
|
||||||
- 📀 Create USB Installer (optional): guides/getting-started/installer.md
|
- 📀 Create USB Installer (optional): guides/getting-started/installer.md
|
||||||
- ⚙️ Add Machines: guides/getting-started/add-machines.md
|
- ⚙️ Add Machines: guides/getting-started/add-machines.md
|
||||||
|
- ⚙️ Add User: guides/getting-started/add-user.md
|
||||||
- ⚙️ Add Services: guides/getting-started/add-services.md
|
- ⚙️ Add Services: guides/getting-started/add-services.md
|
||||||
- 🔐 Secrets & Facts: guides/getting-started/secrets.md
|
- 🔐 Secrets & Facts: guides/getting-started/secrets.md
|
||||||
- 🚢 Deploy Machine: guides/getting-started/deploy.md
|
- 🚢 Deploy Machine: guides/getting-started/deploy.md
|
||||||
@@ -79,6 +80,7 @@ nav:
|
|||||||
- Migrate existing Flakes: guides/migrations/migration-guide.md
|
- Migrate existing Flakes: guides/migrations/migration-guide.md
|
||||||
- Migrate inventory Services: guides/migrations/migrate-inventory-services.md
|
- Migrate inventory Services: guides/migrations/migrate-inventory-services.md
|
||||||
- Facts Vars Migration: guides/migrations/migration-facts-vars.md
|
- Facts Vars Migration: guides/migrations/migration-facts-vars.md
|
||||||
|
- Disk id: guides/migrations/disk-id.md
|
||||||
- macOS: guides/macos.md
|
- macOS: guides/macos.md
|
||||||
- Reference:
|
- Reference:
|
||||||
- Overview: reference/index.md
|
- Overview: reference/index.md
|
||||||
@@ -86,6 +88,7 @@ nav:
|
|||||||
- Overview: reference/clanServices/index.md
|
- Overview: reference/clanServices/index.md
|
||||||
- reference/clanServices/admin.md
|
- reference/clanServices/admin.md
|
||||||
- reference/clanServices/borgbackup.md
|
- reference/clanServices/borgbackup.md
|
||||||
|
- reference/clanServices/data-mesher.md
|
||||||
- reference/clanServices/emergency-access.md
|
- reference/clanServices/emergency-access.md
|
||||||
- reference/clanServices/garage.md
|
- reference/clanServices/garage.md
|
||||||
- reference/clanServices/hello-world.md
|
- reference/clanServices/hello-world.md
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
# Frontmatter for clanModules
|
# Frontmatter for clanModules
|
||||||
clanModulesFrontmatter =
|
clanModulesFrontmatter =
|
||||||
let
|
let
|
||||||
docs = pkgs.nixosOptionsDoc { options = self.clanLib.modules.frontmatterOptions; };
|
docs = pkgs.nixosOptionsDoc {
|
||||||
|
options = self.clanLib.modules.frontmatterOptions;
|
||||||
|
transformOptions = self.clanLib.docs.stripStorePathsFromDeclarations;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
docs.optionsJSON;
|
docs.optionsJSON;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
pkgs,
|
pkgs,
|
||||||
clan-core,
|
clan-core,
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
inherit (clan-core.clanLib.docs) stripStorePathsFromDeclarations;
|
||||||
|
transformOptions = stripStorePathsFromDeclarations;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
# clanModules docs
|
# clanModules docs
|
||||||
clanModulesViaNix = lib.mapAttrs (
|
clanModulesViaNix = lib.mapAttrs (
|
||||||
@@ -20,6 +24,7 @@
|
|||||||
}).options
|
}).options
|
||||||
).clan.${name} or { };
|
).clan.${name} or { };
|
||||||
warningsAreErrors = true;
|
warningsAreErrors = true;
|
||||||
|
inherit transformOptions;
|
||||||
}).optionsJSON
|
}).optionsJSON
|
||||||
else
|
else
|
||||||
{ }
|
{ }
|
||||||
@@ -32,6 +37,7 @@
|
|||||||
(nixosOptionsDoc {
|
(nixosOptionsDoc {
|
||||||
inherit options;
|
inherit options;
|
||||||
warningsAreErrors = true;
|
warningsAreErrors = true;
|
||||||
|
inherit transformOptions;
|
||||||
}).optionsJSON
|
}).optionsJSON
|
||||||
) rolesOptions
|
) rolesOptions
|
||||||
) modulesRolesOptions;
|
) modulesRolesOptions;
|
||||||
@@ -52,7 +58,15 @@
|
|||||||
|
|
||||||
(nixosOptionsDoc {
|
(nixosOptionsDoc {
|
||||||
transformOptions =
|
transformOptions =
|
||||||
opt: if lib.strings.hasPrefix "_" opt.name then opt // { visible = false; } else opt;
|
opt:
|
||||||
|
let
|
||||||
|
# Apply store path stripping first
|
||||||
|
transformed = transformOptions opt;
|
||||||
|
in
|
||||||
|
if lib.strings.hasPrefix "_" transformed.name then
|
||||||
|
transformed // { visible = false; }
|
||||||
|
else
|
||||||
|
transformed;
|
||||||
options = (lib.evalModules { modules = [ role.interface ]; }).options;
|
options = (lib.evalModules { modules = [ role.interface ]; }).options;
|
||||||
warningsAreErrors = true;
|
warningsAreErrors = true;
|
||||||
}).optionsJSON
|
}).optionsJSON
|
||||||
@@ -72,5 +86,6 @@
|
|||||||
}).options
|
}).options
|
||||||
).clan.core or { };
|
).clan.core or { };
|
||||||
warningsAreErrors = true;
|
warningsAreErrors = true;
|
||||||
|
inherit transformOptions;
|
||||||
}).optionsJSON;
|
}).optionsJSON;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ from dataclasses import dataclass, field
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from clan_lib.api.modules import (
|
from clan_lib.errors import ClanError
|
||||||
|
from clan_lib.services.modules import (
|
||||||
CategoryInfo,
|
CategoryInfo,
|
||||||
Frontmatter,
|
Frontmatter,
|
||||||
extract_frontmatter,
|
extract_frontmatter,
|
||||||
get_roles,
|
get_roles,
|
||||||
)
|
)
|
||||||
from clan_lib.errors import ClanError
|
|
||||||
|
|
||||||
# Get environment variables
|
# Get environment variables
|
||||||
CLAN_CORE_PATH = Path(os.environ["CLAN_CORE_PATH"])
|
CLAN_CORE_PATH = Path(os.environ["CLAN_CORE_PATH"])
|
||||||
@@ -62,14 +62,11 @@ def sanitize(text: str) -> str:
|
|||||||
return text.replace(">", "\\>")
|
return text.replace(">", "\\>")
|
||||||
|
|
||||||
|
|
||||||
def replace_store_path(text: str) -> tuple[str, str]:
|
def replace_git_url(text: str) -> tuple[str, str]:
|
||||||
res = text
|
res = text
|
||||||
if text.startswith("/nix/store/"):
|
name = Path(res).name
|
||||||
res = "https://git.clan.lol/clan/clan-core/src/branch/main/" + str(
|
if text.startswith("https://git.clan.lol/clan/clan-core/src/branch/main/"):
|
||||||
Path(*Path(text).parts[4:])
|
name = str(Path(*Path(text).parts[7:]))
|
||||||
)
|
|
||||||
# name = Path(res).name
|
|
||||||
name = str(Path(*Path(text).parts[4:]))
|
|
||||||
return (res, name)
|
return (res, name)
|
||||||
|
|
||||||
|
|
||||||
@@ -159,7 +156,7 @@ def render_option(
|
|||||||
|
|
||||||
decls = option.get("declarations", [])
|
decls = option.get("declarations", [])
|
||||||
if decls:
|
if decls:
|
||||||
source_path, name = replace_store_path(decls[0])
|
source_path, name = replace_git_url(decls[0])
|
||||||
|
|
||||||
name = name.split(",")[0]
|
name = name.split(",")[0]
|
||||||
source_path = source_path.split(",")[0]
|
source_path = source_path.split(",")[0]
|
||||||
|
|||||||
@@ -55,9 +55,37 @@ If you're using VSCode, it has a handy feature that makes paths to source code f
|
|||||||
|
|
||||||
## Finding Print Messages
|
## Finding Print Messages
|
||||||
|
|
||||||
To identify where a specific print message comes from, you can enable a helpful feature. Simply set the environment variable `export TRACE_PRINT=1`. When you run commands with `--debug` mode, each print message will include information about its source location.
|
To trace the origin of print messages in `clan-cli`, you can enable special debugging features using environment variables:
|
||||||
|
|
||||||
|
- Set `TRACE_PRINT=1` to include the source location with each print message:
|
||||||
|
```bash
|
||||||
|
export TRACE_PRINT=1
|
||||||
|
```
|
||||||
|
When running commands with `--debug`, every print will show where it was triggered in the code.
|
||||||
|
|
||||||
|
- To see a deeper stack trace for each print, set `TRACE_DEPTH` to the desired number of stack frames (e.g., 3):
|
||||||
|
```bash
|
||||||
|
export TRACE_DEPTH=3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Debug Logging
|
||||||
|
|
||||||
|
You can enable more detailed logging for specific components by setting these environment variables:
|
||||||
|
|
||||||
|
- `CLAN_DEBUG_NIX_SELECTORS=1` — verbose logs for flake.select operations
|
||||||
|
- `CLAN_DEBUG_NIX_PREFETCH=1` — verbose logs for flake.prefetch operations
|
||||||
|
- `CLAN_DEBUG_COMMANDS=1` — print the diffed environment of executed commands
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```bash
|
||||||
|
export CLAN_DEBUG_NIX_SELECTORS=1
|
||||||
|
export CLAN_DEBUG_NIX_PREFETCH=1
|
||||||
|
export CLAN_DEBUG_COMMANDS=1
|
||||||
|
```
|
||||||
|
|
||||||
|
These options help you pinpoint the source and context of print messages and debug logs during development.
|
||||||
|
|
||||||
|
|
||||||
If you need more details, you can expand the stack trace information that appears with each print by setting the environment variable `export TRACE_DEPTH=3`.
|
|
||||||
|
|
||||||
## Analyzing Performance
|
## Analyzing Performance
|
||||||
|
|
||||||
|
|||||||
@@ -10,64 +10,23 @@ See the complete [list](../../guides/more-machines.md#automatic-registration) of
|
|||||||
|
|
||||||
## Create a machine
|
## Create a machine
|
||||||
|
|
||||||
=== "flake.nix (flake-parts)"
|
=== "clan.nix (declarative)"
|
||||||
|
|
||||||
```{.nix hl_lines=12-15}
|
```{.nix hl_lines="3-4"}
|
||||||
{
|
{
|
||||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
inventory.machines = {
|
||||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
# Define a machine
|
||||||
inputs.flake-parts.follows = "clan-core/flake-parts";
|
jon = { };
|
||||||
inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
inputs@{ flake-parts, ... }:
|
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
|
||||||
imports = [ inputs.clan-core.flakeModules.default ];
|
|
||||||
clan = {
|
|
||||||
inventory.machines = {
|
|
||||||
# Define a machine
|
|
||||||
jon = { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "flake.nix (classic)"
|
# Additional NixOS configuration can be added here.
|
||||||
|
# machines/jon/configuration.nix will be automatically imported.
|
||||||
```{.nix hl_lines=11-14}
|
# See: https://docs.clan.lol/guides/more-machines/#automatic-registration
|
||||||
{
|
machines = {
|
||||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
# jon = { config, ... }: {
|
||||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
# environment.systemPackages = [ pkgs.asciinema ];
|
||||||
|
# };
|
||||||
outputs =
|
};
|
||||||
{ self, clan-core, ... }:
|
|
||||||
let
|
|
||||||
clan = clan-core.lib.clan {
|
|
||||||
inherit self;
|
|
||||||
|
|
||||||
inventory.machines = {
|
|
||||||
# Define a machine
|
|
||||||
jon = { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit (clan.config)
|
|
||||||
nixosConfigurations
|
|
||||||
nixosModules
|
|
||||||
clanInternals
|
|
||||||
darwinConfigurations
|
|
||||||
darwinModules
|
|
||||||
;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,16 +48,15 @@ See the complete [list](../../guides/more-machines.md#automatic-registration) of
|
|||||||
|
|
||||||
The option: `machines.<name>` is used to add extra *nixosConfiguration* to a machine
|
The option: `machines.<name>` is used to add extra *nixosConfiguration* to a machine
|
||||||
|
|
||||||
```{.nix .annotate title="flake.nix" hl_lines="3-13 18-22"}
|
Add the following to your `clan.nix` file for each machine.
|
||||||
# Sometimes this attribute set is defined in clan.nix
|
This example demonstrates what is needed based on a machine called `jon`:
|
||||||
clan = {
|
|
||||||
|
```{.nix .annotate title="clan.nix" hl_lines="3-6 15-19"}
|
||||||
|
{
|
||||||
inventory.machines = {
|
inventory.machines = {
|
||||||
jon = {
|
jon = {
|
||||||
# Define targetHost here
|
# Define tags here (optional)
|
||||||
# Required before deployment
|
tags = [ ]; # (1)
|
||||||
deploy.targetHost = "root@jon"; # (1)
|
|
||||||
# Define tags here
|
|
||||||
tags = [ ];
|
|
||||||
};
|
};
|
||||||
sara = {
|
sara = {
|
||||||
deploy.targetHost = "root@sara";
|
deploy.targetHost = "root@sara";
|
||||||
@@ -117,9 +75,24 @@ clan = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
1. It is required to define a *targetHost* for each machine before deploying. Best practice has been, to use the zerotier ip/hostname or the ip from the from overlay network you decided to use.
|
1. Tags can be used to automatically add this machine to services later on. - You dont need to set this now.
|
||||||
2. Add your *ssh key* here - That will ensure you can always login to your machine via *ssh* in case something goes wrong.
|
2. Add your *ssh key* here - That will ensure you can always login to your machine via *ssh* in case something goes wrong.
|
||||||
|
|
||||||
|
### (Optional) Create a `configuration.nix`
|
||||||
|
|
||||||
|
```nix title="./machines/jon/configuration.nix"
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
# enables GNOME desktop (optional)
|
||||||
|
../../modules/gnome.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# Set nixosOptions here
|
||||||
|
# Or import your own modules via 'imports'
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### (Optional) Renaming a Machine
|
### (Optional) Renaming a Machine
|
||||||
|
|
||||||
Older templates included static machine folders like `jon` and `sara`.
|
Older templates included static machine folders like `jon` and `sara`.
|
||||||
|
|||||||
@@ -17,104 +17,61 @@ To learn more: [Guide about clanService](../clanServices.md)
|
|||||||
|
|
||||||
## Configure a Zerotier Network (recommended)
|
## Configure a Zerotier Network (recommended)
|
||||||
|
|
||||||
```{.nix title="flake.nix" hl_lines="20-28"}
|
```{.nix title="clan.nix" hl_lines="8-16"}
|
||||||
{
|
{
|
||||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
inventory.machines = {
|
||||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
jon = { };
|
||||||
inputs.flake-parts.follows = "clan-core/flake-parts";
|
sara = { };
|
||||||
inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
inputs@{ flake-parts, ... }:
|
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
|
||||||
imports = [ inputs.clan-core.flakeModules.default ];
|
|
||||||
# Sometimes this attribute set is defined in clan.nix
|
|
||||||
clan = {
|
|
||||||
inventory.machines = {
|
|
||||||
jon = {
|
|
||||||
targetHost = "root@jon";
|
|
||||||
};
|
|
||||||
sara = {
|
|
||||||
targetHost = "root@jon";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
inventory.instances = {
|
|
||||||
zerotier = { # (1)
|
|
||||||
# Defines 'jon' as the controller
|
|
||||||
roles.controller.machines.jon = {};
|
|
||||||
# Defines all machines as networking peer.
|
|
||||||
# The 'all' tag is a clan builtin.
|
|
||||||
roles.peer.tags.all = {};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
systems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inventory.instances = {
|
||||||
|
zerotier = { # (1)
|
||||||
|
# Replace with the name (string) of your machine that you will use as zerotier-controller
|
||||||
|
# See: https://docs.zerotier.com/controller/
|
||||||
|
# Deploy this machine first to create the network secrets
|
||||||
|
roles.controller.machines."jon" = { }; # (2)
|
||||||
|
# Peers of the network
|
||||||
|
# this line means 'all' clan machines will be 'peers'
|
||||||
|
roles.peer.tags.all = { }; # (3)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# ...
|
||||||
|
# elided
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
1. See [reference/clanServices](../../reference/clanServices/index.md) for all available services and how to configure them.
|
1. See [reference/clanServices](../../reference/clanServices/index.md) for all available services and how to configure them.
|
||||||
Or read [authoring/clanServices](../authoring/clanServices/index.md) if you want to bring your own
|
Or read [authoring/clanServices](../authoring/clanServices/index.md) if you want to bring your own
|
||||||
|
|
||||||
|
2. Replace `__YOUR_CONTROLLER_` with the *name* of your machine.
|
||||||
|
|
||||||
|
3. This line will add all machines of your clan as `peer` to zerotier
|
||||||
|
|
||||||
## Adding more recommended defaults
|
## Adding more recommended defaults
|
||||||
|
|
||||||
Adding the following services is recommended for most users:
|
Adding the following services is recommended for most users:
|
||||||
|
|
||||||
```{.nix title="flake.nix" hl_lines="25-35"}
|
```{.nix title="clan.nix" hl_lines="7-14"}
|
||||||
{
|
{
|
||||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
inventory.machines = {
|
||||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
jon = { };
|
||||||
inputs.flake-parts.follows = "clan-core/flake-parts";
|
sara = { };
|
||||||
inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
|
};
|
||||||
|
inventory.instances = {
|
||||||
outputs =
|
admin = { # (1)
|
||||||
inputs@{ flake-parts, ... }:
|
roles.default.tags.all = { };
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
roles.default.settings = {
|
||||||
imports = [ inputs.clan-core.flakeModules.default ];
|
allowedKeys = {
|
||||||
# Sometimes this attribute set is defined in clan.nix
|
"my-user" = "ssh-ed25519 AAAAC3N..."; # (2)
|
||||||
clan = {
|
|
||||||
inventory.machines = {
|
|
||||||
jon = {
|
|
||||||
targetHost = "root@jon";
|
|
||||||
};
|
|
||||||
sara = {
|
|
||||||
targetHost = "root@jon";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
inventory.instances = {
|
|
||||||
zerotier = {
|
|
||||||
roles.controller.machines.jon = {};
|
|
||||||
roles.peer.tags.all = {};
|
|
||||||
};
|
|
||||||
admin = { # (1)
|
|
||||||
roles.default.tags.all = { };
|
|
||||||
roles.default.settings = {
|
|
||||||
allowedKeys = {
|
|
||||||
"my-user" = "ssh-ed25519 AAAAC3N..."; # elided
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
state-version = { # (2)
|
|
||||||
roles.default.tags.all = { };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
systems = [
|
};
|
||||||
"x86_64-linux"
|
# ...
|
||||||
"aarch64-linux"
|
# elided
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
1. The `admin` service will generate a **root-password** and **add your ssh-key** that allows for convienient administration.
|
1. The `admin` service will generate a **root-password** and **add your ssh-key** that allows for convienient administration.
|
||||||
|
2. Equivalent to directly setting `authorizedKeys` like in [configuring a machine](./add-machines.md#configuring-a-machine)
|
||||||
2. The `state-version` service will generate a [nixos state version](https://wiki.nixos.org/wiki/FAQ/When_do_I_update_stateVersion) for each system once it is deployed.
|
3. Adds `user = jon` as a user on all machines. Will create a `home` directory, and prompt for a password before deployment.
|
||||||
|
|||||||
127
docs/site/guides/getting-started/add-user.md
Normal file
127
docs/site/guides/getting-started/add-user.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# How to add users
|
||||||
|
|
||||||
|
!!! Note "Under construction"
|
||||||
|
|
||||||
|
The users concept of clan is not done yet. This guide outlines some solutions from our community.
|
||||||
|
Defining users can be done in many different ways. We want to highlight two approaches:
|
||||||
|
|
||||||
|
- Using clan's [users](../../reference/clanServices/users.md) service.
|
||||||
|
- Using a custom approach.
|
||||||
|
|
||||||
|
## Adding Users using the [users](../../reference/clanServices/users.md) service
|
||||||
|
|
||||||
|
To add a first *user* this guide will be leveraging two things:
|
||||||
|
|
||||||
|
- [clanServices](../../reference/clanServices/index.md): Allows to bind arbitrary logic to something we call an `ìnstance`.
|
||||||
|
- [clanServices/users](../../reference/clanServices/users.md): Implements logic for adding a single user perInstance.
|
||||||
|
|
||||||
|
The example shows how to add a user called `jon`:
|
||||||
|
|
||||||
|
```{.nix title="clan.nix" hl_lines="7-21"}
|
||||||
|
{
|
||||||
|
inventory.machines = {
|
||||||
|
jon = { };
|
||||||
|
sara = { };
|
||||||
|
};
|
||||||
|
inventory.instances = {
|
||||||
|
jon-user = { # (1)
|
||||||
|
module.name = "users";
|
||||||
|
|
||||||
|
roles.default.tags.all = { }; # (2)
|
||||||
|
|
||||||
|
roles.default.settings = {
|
||||||
|
user = "jon"; # (3)
|
||||||
|
groups = [
|
||||||
|
"wheel" # Allow using 'sudo'
|
||||||
|
"networkmanager" # Allows to manage network connections.
|
||||||
|
"video" # Allows to access video devices.
|
||||||
|
"input" # Allows to access input devices.
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# ...
|
||||||
|
# elided
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Add `user = jon` as a user on all machines. Will create a `home` directory, and prompt for a password before deployment.
|
||||||
|
2. Add this user to `all` machines
|
||||||
|
3. Define the `name` of the user to be `jon`
|
||||||
|
|
||||||
|
The `users` service creates a `/home/jon` directory, allows `jon` to sign in and will take care of the users password as part of [deployment](./deploy.md).
|
||||||
|
|
||||||
|
For more information see [clanService/users](../../reference/clanServices/users.md)
|
||||||
|
|
||||||
|
## Using a custom approach
|
||||||
|
|
||||||
|
Some people like to define a `users` folder in their repository root.
|
||||||
|
That allows to bind all user specific logic to a single place (`default.nix`)
|
||||||
|
Which can be imported into individual machines to make the user avilable on that machine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.
|
||||||
|
├── machines
|
||||||
|
│ ├── jon
|
||||||
|
# ......
|
||||||
|
├── users
|
||||||
|
│ ├── jon
|
||||||
|
│ │ └── default.nix # <- a NixOS module; sets some options
|
||||||
|
# ... ... ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## using [home-manager](https://github.com/nix-community/home-manager)
|
||||||
|
|
||||||
|
When using clan's `users` service it is possible to define extraModules.
|
||||||
|
In fact this is always possible when using clan's services.
|
||||||
|
|
||||||
|
We can use this property of clan services to bind a nixosModule to the user, which configures home-manager.
|
||||||
|
|
||||||
|
```{.nix title="clan.nix" hl_lines="22"}
|
||||||
|
{
|
||||||
|
inventory.machines = {
|
||||||
|
jon = { };
|
||||||
|
sara = { };
|
||||||
|
};
|
||||||
|
inventory.instances = {
|
||||||
|
jon-user = {
|
||||||
|
module.name = "users";
|
||||||
|
|
||||||
|
roles.default.tags.all = { };
|
||||||
|
|
||||||
|
roles.default.settings = {
|
||||||
|
user = "jon",
|
||||||
|
groups = [
|
||||||
|
"wheel"
|
||||||
|
"networkmanager"
|
||||||
|
"video"
|
||||||
|
"input"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
roles.default.extraModules = [ ./users/jon/home.nix ]; # (1)
|
||||||
|
};
|
||||||
|
# ...
|
||||||
|
# elided
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Type `path` or `string`: Must point to a seperate file. Inlining a module is not possible
|
||||||
|
|
||||||
|
!!! Note "This is inspiration"
|
||||||
|
Our community might come up with better solutions soon.
|
||||||
|
We are seeking contributions to improve this pattern if you have a nicer solution in mind.
|
||||||
|
|
||||||
|
```nix title="users/jon/home.nix"
|
||||||
|
# NixOS module to import home-manager and the home-manager configuration of 'jon'
|
||||||
|
{ self, ...}:
|
||||||
|
{
|
||||||
|
imports = [ self.inputs.home-manager.nixosModules.default ];
|
||||||
|
home-manager.users.jon = {
|
||||||
|
imports = [
|
||||||
|
./home-configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Deploy a machine
|
# Deploy a machine
|
||||||
|
|
||||||
Now that you have created a new machine, we will walk through how to install it.
|
Now that you have created a machines, added some services and setup secrets. This guide will walk through how to deploy it.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -10,256 +10,212 @@ Now that you have created a new machine, we will walk through how to install it.
|
|||||||
- [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md)
|
- [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md)
|
||||||
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
|
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
|
||||||
|
|
||||||
=== "**Physical Hardware**"
|
## Physical Hardware
|
||||||
|
|
||||||
- [x] **USB Flash Drive**: See [Clan Installer](installer.md)
|
!!! note "skip this if using a cloud VM"
|
||||||
|
|
||||||
!!! Steps
|
Steps:
|
||||||
|
|
||||||
1. Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
|
- Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
|
||||||
|
- Boot the target machine and connect it to a network that makes it reachable from your setup computer.
|
||||||
|
- Note down a reachable ip adress (*ipv4*, *ipv6* or *tor*)
|
||||||
|
|
||||||
2. Boot the target machine and connect it to a network that makes it reachable from your setup computer.
|
---
|
||||||
|
|
||||||
=== "**Cloud VMs**"
|
The installer will generate a password and local addresses on boot, then run ssh with these preconfigured.
|
||||||
|
The installer shows it's deployment relevant information in two formats, a text form, as well as a QR code.
|
||||||
|
|
||||||
- [x] Any cloud machine if it is reachable via SSH and supports `kexec`.
|
Sample boot screen shows:
|
||||||
|
|
||||||
!!! Warning "NixOS can cause strange issues when booting in certain cloud environments."
|
- Root password
|
||||||
If on Linode: Make sure that the system uses Direct Disk boot kernel (found in the configuration pannel)
|
- IP address
|
||||||
|
- Optional Tor and mDNS details
|
||||||
|
|
||||||
|
```{ .bash .annotate .no-copy .nohighlight}
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ┌───────────────────────────┐ │
|
||||||
|
│ │███████████████████████████│ # This is the QR Code (1) │
|
||||||
|
│ │██ ▄▄▄▄▄ █▀▄█▀█▀▄█ ▄▄▄▄▄ ██│ │
|
||||||
|
│ │██ █ █ █▀▄▄▄█ ▀█ █ █ ██│ │
|
||||||
|
│ │██ █▄▄▄█ █▀▄ ▀▄▄▄█ █▄▄▄█ ██│ │
|
||||||
|
│ │██▄▄▄▄▄▄▄█▄▀ ▀▄▀▄█▄▄▄▄▄▄▄██│ │
|
||||||
|
│ │███▀▀▀ █▄▄█ ▀▄ ▄▀▄█ ███│ │
|
||||||
|
│ │██▄██▄▄█▄▄▀▀██▄▀ ▄▄▄ ▄▀█▀██│ │
|
||||||
|
│ │██ ▄▄▄▄▄ █▄▄▄▄ █ █▄█ █▀ ███│ │
|
||||||
|
│ │██ █ █ █ █ █ ▄▄▄ ▄▀▀ ██│ │
|
||||||
|
│ │██ █▄▄▄█ █ ▄ ▄ ▄ ▀█ ▄███│ │
|
||||||
|
│ │██▄▄▄▄▄▄▄█▄▄▄▄▄▄█▄▄▄▄▄█▄███│ │
|
||||||
|
│ │███████████████████████████│ │
|
||||||
|
│ └───────────────────────────┘ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │Root password: cheesy-capital-unwell # password (2) │ │
|
||||||
|
│ │Local network addresses: │ │
|
||||||
|
│ │enp1s0 UP 192.168.178.169/24 metric 1024 fe80::21e:6ff:fe45:3c92/64 │ │
|
||||||
|
│ │enp2s0 DOWN │ │
|
||||||
|
│ │wlan0 DOWN # connect to wlan (3) │ │
|
||||||
|
│ │Onion address: 6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion │ │
|
||||||
|
│ │Multicast DNS: nixos-installer.local │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ Press 'Ctrl-C' for console access │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. This is not an actual QR code, because it is displayed rather poorly on text sites.
|
||||||
|
This would be the actual content of this specific QR code prettified:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pass": "cheesy-capital-unwell",
|
||||||
|
"tor": "6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion",
|
||||||
|
"addrs": [
|
||||||
|
"2001:9e8:347:ca00:21e:6ff:fe45:3c92"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To generate the actual QR code, that would be displayed use:
|
||||||
|
```shellSession
|
||||||
|
echo '{"pass":"cheesy-capital-unwell","tor":"6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion","addrs":["2001:9e8:347:ca00:21e:6ff:fe45:3c92"]}' | nix run nixpkgs#qrencode -- -s 2 -m 2 -t utf8
|
||||||
|
```
|
||||||
|
2. The root password for the installer medium.
|
||||||
|
This password is autogenerated and meant to be easily typeable.
|
||||||
|
3. See [how to connect to wlan](./installer.md#optional-connect-to-wifi-manually).
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
Use [KDE Connect](https://apps.kde.org/de/kdeconnect/) for easyily sharing QR codes from phone to desktop
|
||||||
|
|
||||||
|
## Cloud VMs
|
||||||
|
|
||||||
|
!!! note "skip this if using a physical machine"
|
||||||
|
|
||||||
|
Clan supports any cloud machine if it is reachable via SSH and supports `kexec`.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
- Go to the configuration panel and note down how to connect to the machine via ssh.
|
||||||
|
|
||||||
|
!!! tip "NixOS can cause strange issues when booting in certain cloud environments."
|
||||||
|
If on Linode: Make sure that the system uses "Direct Disk boot kernel" (found in the configuration panel)
|
||||||
|
|
||||||
## Setting `targetHost`
|
## Setting `targetHost`
|
||||||
|
|
||||||
=== "flake.nix (flake-parts)"
|
In your nix files set the targetHost (reachable ip) that you retrieved in the previous step.
|
||||||
|
|
||||||
```{.nix hl_lines="22"}
|
```{.nix title="clan.nix" hl_lines="9"}
|
||||||
{
|
{
|
||||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
# Ensure this is unique among all clans you want to use.
|
||||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
meta.name = "my-clan";
|
||||||
inputs.flake-parts.follows = "clan-core/flake-parts";
|
|
||||||
inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
|
|
||||||
|
|
||||||
outputs =
|
inventory.machines = {
|
||||||
inputs@{ flake-parts, ... }:
|
# Define machines here.
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
# The machine name will be used as the hostname.
|
||||||
systems = [
|
jon = {
|
||||||
"x86_64-linux"
|
deploy.targetHost = "root@192.168.192.4"; # (1)
|
||||||
"aarch64-linux"
|
};
|
||||||
"x86_64-darwin"
|
};
|
||||||
"aarch64-darwin"
|
# ...
|
||||||
];
|
# elided
|
||||||
imports = [ inputs.clan-core.flakeModules.default ];
|
}
|
||||||
|
```
|
||||||
|
|
||||||
clan = {
|
1. Use the ip address of your targetMachine that you want to deploy. If using the [flash-installer](./installer.md) it should display its local ip-address when booted.
|
||||||
inventory.machines = {
|
|
||||||
jon = {
|
|
||||||
# targetHost will get picked up by cli commands
|
|
||||||
deploy.targetHost = "root@jon";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "flake.nix (classic)"
|
|
||||||
|
|
||||||
```{.nix hl_lines="14"}
|
|
||||||
{
|
|
||||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
|
||||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{ self, clan-core, ... }:
|
|
||||||
let
|
|
||||||
clan = clan-core.lib.clan {
|
|
||||||
inherit self;
|
|
||||||
|
|
||||||
inventory.machines = {
|
|
||||||
jon = {
|
|
||||||
# targetHost will get picked up by cli commands
|
|
||||||
deploy.targetHost = "root@jon";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit (clan.config)
|
|
||||||
nixosConfigurations
|
|
||||||
nixosModules
|
|
||||||
clanInternals
|
|
||||||
darwinConfigurations
|
|
||||||
darwinModules
|
|
||||||
;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
The use of `root@` in the target address implies SSH access as the `root` user.
|
The use of `root@` in the target address implies SSH access as the `root` user.
|
||||||
Ensure that the root login is secured and only used when necessary.
|
Ensure that the root login is secured and only used when necessary.
|
||||||
|
|
||||||
## Identify the Target Disk
|
See also [how to set TargetHost](../target-host.md) for other methods.
|
||||||
|
|
||||||
On the setup computer, SSH into the target:
|
## Retrieve the hardware report
|
||||||
|
|
||||||
```bash title="setup computer"
|
By default clan uses [nixos-facter](https://github.com/nix-community/nixos-facter) which captures detailed information about the machine or virtual environment.
|
||||||
ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
|
|
||||||
|
To generate the hardware-report (`facter.json`) run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clan machines update-hardware-config <machineName>
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `<IP>` with the machine's IP or hostname if mDNS (i.e. Avahi) is available.
|
Example output:
|
||||||
|
|
||||||
Which should show something like:
|
```shell-session
|
||||||
|
$ clan machines update-hardware-config jon
|
||||||
```{.shellSession hl_lines="6" .no-copy}
|
[jon] $ nixos-facter
|
||||||
NAME ID-LINK FSTYPE SIZE MOUNTPOINT
|
Successfully generated: ./machines/jon/facter.json
|
||||||
sda usb-ST_16GB_AA6271026J1000000509-0:0 14.9G
|
|
||||||
├─sda1 usb-ST_16GB_AA6271026J1000000509-0:0-part1 1M
|
|
||||||
├─sda2 usb-ST_16GB_AA6271026J1000000509-0:0-part2 vfat 100M /boot
|
|
||||||
└─sda3 usb-ST_16GB_AA6271026J1000000509-0:0-part3 ext4 2.9G /
|
|
||||||
nvme0n1 nvme-eui.e8238fa6bf530001001b448b4aec2929 476.9G
|
|
||||||
├─nvme0n1p1 nvme-eui.e8238fa6bf530001001b448b4aec2929-part1 vfat 512M
|
|
||||||
├─nvme0n1p2 nvme-eui.e8238fa6bf530001001b448b4aec2929-part2 ext4 459.6G
|
|
||||||
└─nvme0n1p3 nvme-eui.e8238fa6bf530001001b448b4aec2929-part3 swap 16.8G
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Look for the top-level disk device (e.g., nvme0n1 or sda) and copy its `ID-LINK`. Avoid using partition IDs like `nvme0n1p1`.
|
See [update-hardware-config cli reference](../../reference/cli/machines.md#machines-update-hardware-config) for further configuration possibilities if needed.
|
||||||
|
|
||||||
In this example we would copy `nvme-eui.e8238fa6bf530001001b448b4aec2929`
|
## Configure your disk schema
|
||||||
|
|
||||||
|
By default clan uses [disko](https://github.com/nix-community/disko) which allows for declarative disk partitioning.
|
||||||
|
|
||||||
|
To setup a disk schema for a machine run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clan templates apply disk single-disk jon --set mainDisk ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Which should fail and give the valid options for the specific hardware:
|
||||||
|
|
||||||
|
```shellSession
|
||||||
|
Invalid value for placeholder mainDisk - Valid options:
|
||||||
|
/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-run the command with the correct disk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
|
||||||
|
```
|
||||||
|
|
||||||
|
Should now be succesfull
|
||||||
|
|
||||||
|
```shellSession
|
||||||
|
Applied disk template 'single-disk' to machine 'jon'
|
||||||
|
```
|
||||||
|
|
||||||
|
A disko.nix file should be created in `machines/jon`
|
||||||
|
You can have a look and customize it if needed.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
For advanced partitioning, see [Disko templates](https://github.com/nix-community/disko-templates) or [Disko examples](https://github.com/nix-community/disko/tree/master/example).
|
For advanced partitioning, see [Disko templates](https://github.com/nix-community/disko-templates) or [Disko examples](https://github.com/nix-community/disko/tree/master/example).
|
||||||
|
|
||||||
## Fill in hardware specific machine configuration
|
!!! Danger
|
||||||
|
Don't change the `disko.nix` after the machine is installed for the first time.
|
||||||
|
|
||||||
Edit the following fields inside the `./machines/<machine_name>/configuration.nix`
|
Changing disko configuration requires wiping and reinstalling the machine.
|
||||||
|
|
||||||
<!-- Note: Use "jon" instead of "<machine>" as "<" is not supported in title tag -->
|
Unless you really know what you are doing.
|
||||||
|
|
||||||
```nix title="./machines/jon/configuration.nix" hl_lines="12 15 19"
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
# contains your disk format and partitioning configuration.
|
|
||||||
../../modules/disko.nix
|
|
||||||
# this file is shared among all machines
|
|
||||||
../../modules/shared.nix
|
|
||||||
# enables GNOME desktop (optional)
|
|
||||||
../../modules/gnome.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
# Put your username here for login
|
|
||||||
users.users.user.name = "__YOUR_USERNAME__";
|
|
||||||
|
|
||||||
# Replace this __CHANGE_ME__ with the copied result of the lsblk command
|
|
||||||
disko.devices.disk.main.device = "/dev/disk/by-id/__CHANGE_ME__";
|
|
||||||
|
|
||||||
# IMPORTANT! Add your SSH key here
|
|
||||||
# e.g. > cat ~/.ssh/id_ed25519.pub
|
|
||||||
users.users.root.openssh.authorizedKeys.keys = [ "__YOUR_SSH_KEY__" ];
|
|
||||||
|
|
||||||
# ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Info "Replace `__YOUR_USERNAME__` with the ip of your machine, if you use avahi you can also use your hostname"
|
|
||||||
!!! Info "Replace `__CHANGE_ME__` with the appropriate `ID-LINK` identifier, such as `nvme-eui.e8238fa6bf530001001b448b4aec2929`"
|
|
||||||
!!! Info "Replace `__YOUR_SSH_KEY__` with your personal key, like `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoMI0NC5eT9pHlQExrvR5ASV3iW9+BXwhfchq0smXUJ jon@jon-desktop`"
|
|
||||||
|
|
||||||
## Deploy the machine
|
## Deploy the machine
|
||||||
|
|
||||||
**Finally deployment time!** Use the following command to build and deploy the image via SSH onto your machine.
|
**Finally deployment time!** Use one of the following commands to build and deploy the image via SSH onto your machine.
|
||||||
|
|
||||||
=== "**Image Installer**"
|
|
||||||
|
|
||||||
The installer will generate a password and local addresses on boot, then run ssh with these preconfigured.
|
|
||||||
The installer shows it's deployment relevant information in two formats, a text form, as well as a QR code.
|
|
||||||
|
|
||||||
Sample boot screen shows:
|
|
||||||
|
|
||||||
- Root password
|
|
||||||
- IP address
|
|
||||||
- Optional Tor and mDNS details
|
|
||||||
|
|
||||||
```{ .bash .annotate .no-copy .nohighlight}
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ ┌───────────────────────────┐ │
|
|
||||||
│ │███████████████████████████│ # This is the QR Code (1) │
|
|
||||||
│ │██ ▄▄▄▄▄ █▀▄█▀█▀▄█ ▄▄▄▄▄ ██│ │
|
|
||||||
│ │██ █ █ █▀▄▄▄█ ▀█ █ █ ██│ │
|
|
||||||
│ │██ █▄▄▄█ █▀▄ ▀▄▄▄█ █▄▄▄█ ██│ │
|
|
||||||
│ │██▄▄▄▄▄▄▄█▄▀ ▀▄▀▄█▄▄▄▄▄▄▄██│ │
|
|
||||||
│ │███▀▀▀ █▄▄█ ▀▄ ▄▀▄█ ███│ │
|
|
||||||
│ │██▄██▄▄█▄▄▀▀██▄▀ ▄▄▄ ▄▀█▀██│ │
|
|
||||||
│ │██ ▄▄▄▄▄ █▄▄▄▄ █ █▄█ █▀ ███│ │
|
|
||||||
│ │██ █ █ █ █ █ ▄▄▄ ▄▀▀ ██│ │
|
|
||||||
│ │██ █▄▄▄█ █ ▄ ▄ ▄ ▀█ ▄███│ │
|
|
||||||
│ │██▄▄▄▄▄▄▄█▄▄▄▄▄▄█▄▄▄▄▄█▄███│ │
|
|
||||||
│ │███████████████████████████│ │
|
|
||||||
│ └───────────────────────────┘ │
|
|
||||||
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │Root password: cheesy-capital-unwell # password (2) │ │
|
|
||||||
│ │Local network addresses: │ │
|
|
||||||
│ │enp1s0 UP 192.168.178.169/24 metric 1024 fe80::21e:6ff:fe45:3c92/64 │ │
|
|
||||||
│ │enp2s0 DOWN │ │
|
|
||||||
│ │wlan0 DOWN # connect to wlan (3) │ │
|
|
||||||
│ │Onion address: 6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion │ │
|
|
||||||
│ │Multicast DNS: nixos-installer.local │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
|
|
||||||
│ Press 'Ctrl-C' for console access │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
1. This is not an actual QR code, because it is displayed rather poorly on text sites.
|
|
||||||
This would be the actual content of this specific QR code prettified:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"pass": "cheesy-capital-unwell",
|
|
||||||
"tor": "6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion",
|
|
||||||
"addrs": [
|
|
||||||
"2001:9e8:347:ca00:21e:6ff:fe45:3c92"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To generate the actual QR code, that would be displayed use:
|
|
||||||
```shellSession
|
|
||||||
echo '{"pass":"cheesy-capital-unwell","tor":"6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion","addrs":["2001:9e8:347:ca00:21e:6ff:fe45:3c92"]}' | nix run nixpkgs#qrencode -- -s 2 -m 2 -t utf8
|
|
||||||
```
|
|
||||||
2. The root password for the installer medium.
|
|
||||||
This password is autogenerated and meant to be easily typeable.
|
|
||||||
3. See [how to connect to wlan](./installer.md#optional-connect-to-wifi-manually).
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Use [KDE Connect](https://apps.kde.org/de/kdeconnect/) for easyily sharing QR codes from phone to desktop
|
|
||||||
|
|
||||||
=== "**Cloud VM**"
|
|
||||||
|
|
||||||
Just run the command **Option B: Cloud VM** below
|
|
||||||
|
|
||||||
### Deployment Commands
|
### Deployment Commands
|
||||||
|
|
||||||
#### Using password auth
|
#### Using password auth
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clan machines install [MACHINE] --target-host <IP> --update-hardware-config nixos-facter
|
clan machines install [MACHINE] --target-host <IP>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using QR JSON
|
#### Using QR JSON
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clan machines install [MACHINE] --json "[JSON]" --update-hardware-config nixos-facter
|
clan machines install [MACHINE] --json "[JSON]"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using QR image file
|
#### Using QR image file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clan machines install [MACHINE] --png [PATH] --update-hardware-config nixos-facter
|
clan machines install [MACHINE] --png [PATH]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Option B: Cloud VM
|
#### Option B: Cloud VM
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clan machines install [MACHINE] --target-host <IP> --update-hardware-config nixos-facter
|
clan machines install [MACHINE] --target-host <IP>
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! success
|
!!! success
|
||||||
@@ -318,4 +274,3 @@ clan {
|
|||||||
```
|
```
|
||||||
|
|
||||||
This is useful for machines that are not always online or are not part of the regular update cycle.
|
This is useful for machines that are not always online or are not part of the regular update cycle.
|
||||||
|
|
||||||
|
|||||||
@@ -38,31 +38,24 @@ By the end of this guide, you'll have a fresh NixOS configuration ready to push
|
|||||||
|
|
||||||
## Add Clan CLI to Your Shell
|
## Add Clan CLI to Your Shell
|
||||||
|
|
||||||
Add the Clan CLI into your environment:
|
Create a new clan
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix shell git+https://git.clan.lol/clan/clan-core#clan-cli --refresh
|
nix run git+https://git.clan.lol/clan/clan-core#clan-cli --refresh -- flakes create
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This should prompt for a *name*:
|
||||||
|
|
||||||
```terminalSession
|
```terminalSession
|
||||||
clan --help
|
Enter a name for the new clan: my-clan
|
||||||
```
|
```
|
||||||
|
|
||||||
Should print the avilable commands.
|
Enter a *name*, confirm with *enter*. A directory with that name will be created and initialized.
|
||||||
|
|
||||||
Also checkout the [cli-reference documentation](../../reference/cli/index.md).
|
!!! Note
|
||||||
|
This command uses the `default` template
|
||||||
|
|
||||||
## Initialize Your Project
|
See `clan templates list` and the `--help` reference for how to use other templates.
|
||||||
|
|
||||||
If you want to migrate an existing project, follow this [guide](../migrations/migration-guide.md).
|
|
||||||
|
|
||||||
Set the foundation of your Clan project by initializing it by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clan flakes create my-clan
|
|
||||||
```
|
|
||||||
|
|
||||||
This command creates a `flake.nix` and some other files for your project.
|
|
||||||
|
|
||||||
## Explore the Project Structure
|
## Explore the Project Structure
|
||||||
|
|
||||||
@@ -83,36 +76,48 @@ For example, you might see something like:
|
|||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Don’t worry if your output looks different—the template evolves over time.
|
Don’t worry if your output looks different — Clan templates evolve over time.
|
||||||
|
|
||||||
??? info "Recommended way of sourcing the `clan` CLI tool"
|
To interact with your newly created clan the you need to load the `clan` cli-package it into your environment by running:
|
||||||
|
|
||||||
The default template adds the `clan` CLI tool to the development shell.
|
=== "Automatic (direnv, recommended)"
|
||||||
This means that you can access the `clan` CLI tool directly from the folder
|
- prerequisite: [install nix-direnv](https://github.com/nix-community/nix-direnv)
|
||||||
you are in right now.
|
|
||||||
|
|
||||||
In the `my-clan` directory, run the following command:
|
```
|
||||||
|
direnv allow
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Manual (nix develop)"
|
||||||
|
|
||||||
```
|
```
|
||||||
nix develop
|
nix develop
|
||||||
```
|
```
|
||||||
|
|
||||||
This will ensure the `clan` CLI tool is available in your shell environment.
|
verify that you can run `clan` commands:
|
||||||
|
|
||||||
To automatically add the `clan` CLI tool to your environment without having to
|
```bash
|
||||||
run `nix develop` every time, we recommend setting up [direnv](https://direnv.net/).
|
|
||||||
|
|
||||||
```
|
|
||||||
clan show
|
clan show
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see something like this:
|
You should see something like this:
|
||||||
|
|
||||||
```terminal-session
|
```shellSession
|
||||||
Name: my-clan
|
Name: __CHANGE_ME__
|
||||||
Description: None
|
Description: None
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To change the name of your clan edit `meta.name` in the `clan.nix` or `flake.nix` file
|
||||||
|
|
||||||
|
```{.nix title="clan.nix" hl_lines="3"}
|
||||||
|
{
|
||||||
|
# Ensure this is unique among all clans you want to use.
|
||||||
|
meta.name = "__CHANGE_ME__";
|
||||||
|
|
||||||
|
# ...
|
||||||
|
# elided
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
@@ -123,6 +128,7 @@ You can continue with **any** of the following steps at your own pace:
|
|||||||
- [x] [Initialize Clan](./index.md#initialize-your-project)
|
- [x] [Initialize Clan](./index.md#initialize-your-project)
|
||||||
- [ ] [Create USB Installer (optional)](./installer.md)
|
- [ ] [Create USB Installer (optional)](./installer.md)
|
||||||
- [ ] [Add Machines](./add-machines.md)
|
- [ ] [Add Machines](./add-machines.md)
|
||||||
|
- [ ] [Add a User](./add-user.md)
|
||||||
- [ ] [Add Services](./add-services.md)
|
- [ ] [Add Services](./add-services.md)
|
||||||
- [ ] [Configure Secrets](./secrets.md)
|
- [ ] [Configure Secrets](./secrets.md)
|
||||||
- [ ] [Deploy](./deploy.md) - Requires configured secrets
|
- [ ] [Deploy](./deploy.md) - Requires configured secrets
|
||||||
|
|||||||
98
docs/site/guides/migrations/disk-id.md
Normal file
98
docs/site/guides/migrations/disk-id.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Migrate disko config from `clanModules.disk-id`
|
||||||
|
|
||||||
|
If you previously bootstrapped a machine's disk using `clanModules.disk-id`, you should now migrate to a standalone, self-contained disko configuration. This ensures long-term stability and avoids reliance on dynamic values from Clan.
|
||||||
|
|
||||||
|
If your `disko.nix` currently looks something like this:
|
||||||
|
|
||||||
|
```nix title="disko.nix"
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
clan-core,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
clan-core.clanModules.disk-id
|
||||||
|
];
|
||||||
|
|
||||||
|
# DO NOT EDIT THIS FILE AFTER INSTALLATION of a machine
|
||||||
|
# Otherwise your system might not boot because of missing partitions / filesystems
|
||||||
|
boot.loader.grub.efiSupport = lib.mkDefault true;
|
||||||
|
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
|
||||||
|
disko.devices = {
|
||||||
|
disk = {
|
||||||
|
"main" = {
|
||||||
|
# suffix is to prevent disk name collisions
|
||||||
|
name = "main-" + suffix;
|
||||||
|
type = "disk";
|
||||||
|
# Set the following in flake.nix for each maschine:
|
||||||
|
# device = <uuid>;
|
||||||
|
content = {
|
||||||
|
# edlied
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Retrieve your `disk-id`
|
||||||
|
|
||||||
|
Run the following command to retrieve the generated disk ID for your machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clan vars list <machineName>
|
||||||
|
```
|
||||||
|
|
||||||
|
Which should print the generated `disk-id/diskId` value in clear text
|
||||||
|
You should see output like:
|
||||||
|
|
||||||
|
```shellSession
|
||||||
|
disk-id/diskId: fcef30a749f8451d8f60c46e1ead726f
|
||||||
|
# ...
|
||||||
|
# elided
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy this value — you'll need it in the next step.
|
||||||
|
|
||||||
|
## ✍️ Step 2: Replace Dynamic Configuration with Static Values
|
||||||
|
|
||||||
|
✅ Goal: Make your disko.nix file standalone.
|
||||||
|
|
||||||
|
We are going to make three changes:
|
||||||
|
|
||||||
|
- Remove `let in, imports, {lib,clan-core,config, ...}:` to isolate the file.
|
||||||
|
- Replace `suffix` with the actual disk-id
|
||||||
|
- Move `disko.devices.disk.main.device` from `flake.nix` or `configuration.nix` into this file.
|
||||||
|
|
||||||
|
```{.nix title="disko.nix" hl_lines="7-9 11-14"}
|
||||||
|
{
|
||||||
|
boot.loader.grub.efiSupport = lib.mkDefault true;
|
||||||
|
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
|
||||||
|
disko.devices = {
|
||||||
|
disk = {
|
||||||
|
"main" = {
|
||||||
|
# ↓ Copy the disk-id into place
|
||||||
|
name = "main-fcef30a749f8451d8f60c46e1ead726f";
|
||||||
|
type = "disk";
|
||||||
|
|
||||||
|
# Some earlier guides had this line in a flake.nix
|
||||||
|
# disko.devices.disk.main.device = "/dev/disk/by-id/__CHANGE_ME__";
|
||||||
|
# ↓ Copy the '/dev/disk/by-id' into here instead
|
||||||
|
device = "/dev/disk/by-id/nvme-eui.e8238fa6bf530001001b448b4aec2929";
|
||||||
|
|
||||||
|
# edlied;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These steps are only needed for existing configurations that depend on the `diskId` module.
|
||||||
|
|
||||||
|
For newer machines clan offers simple *disk templates* via its [templates cli](../../reference/cli/templates.md)
|
||||||
20
flake.lock
generated
20
flake.lock
generated
@@ -16,11 +16,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751846468,
|
"lastModified": 1752451292,
|
||||||
"narHash": "sha256-h0mpWZIOIAKj4fmLNyI2HDG+c0YOkbYmyJXSj/bQ9s0=",
|
"narHash": "sha256-jvLbfYFvcS5f0AEpUlFS2xZRnK770r9TRM2smpUFFaU=",
|
||||||
"rev": "a2166c13b0cb3febdaf36391cd2019aa2ccf4366",
|
"rev": "309e06fbc9a6d133ab6dd1c7d8e4876526e058bb",
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/a2166c13b0cb3febdaf36391cd2019aa2ccf4366.tar.gz"
|
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/309e06fbc9a6d133ab6dd1c7d8e4876526e058bb.tar.gz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
@@ -34,11 +34,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751854533,
|
"lastModified": 1752113600,
|
||||||
"narHash": "sha256-U/OQFplExOR1jazZY4KkaQkJqOl59xlh21HP9mI79Vc=",
|
"narHash": "sha256-7LYDxKxZgBQ8LZUuolAQ8UkIB+jb4A2UmiR+kzY9CLI=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "16b74a1e304197248a1bc663280f2548dbfcae3c",
|
"rev": "79264292b7e3482e5702932949de9cbb69fedf6d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -184,11 +184,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1750931469,
|
"lastModified": 1752055615,
|
||||||
"narHash": "sha256-0IEdQB1nS+uViQw4k3VGUXntjkDp7aAlqcxdewb/hAc=",
|
"narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "ac8e6f32e11e9c7f153823abc3ab007f2a65d3e1",
|
"rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ lib.fix (
|
|||||||
introspection = import ./introspection { inherit lib; };
|
introspection = import ./introspection { inherit lib; };
|
||||||
jsonschema = import ./jsonschema { inherit lib; };
|
jsonschema = import ./jsonschema { inherit lib; };
|
||||||
facts = import ./facts.nix { inherit lib; };
|
facts = import ./facts.nix { inherit lib; };
|
||||||
|
docs = import ./docs.nix { inherit lib; };
|
||||||
|
|
||||||
# flakes
|
# flakes
|
||||||
flakes = clanLib.callLib ./flakes.nix { };
|
flakes = clanLib.callLib ./flakes.nix { };
|
||||||
|
|||||||
25
lib/docs.nix
Normal file
25
lib/docs.nix
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
rec {
|
||||||
|
prefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
||||||
|
# Strip store paths from option declarations to make docs more stable
|
||||||
|
# This prevents documentation from rebuilding when store paths change
|
||||||
|
# but the actual content remains the same
|
||||||
|
stripStorePathsFromDeclarations =
|
||||||
|
opt:
|
||||||
|
opt
|
||||||
|
// {
|
||||||
|
declarations = map (
|
||||||
|
decl:
|
||||||
|
if lib.isString decl && lib.hasPrefix "/nix/store/" decl then
|
||||||
|
let
|
||||||
|
parts = lib.splitString "/" decl;
|
||||||
|
in
|
||||||
|
if builtins.length parts > 4 then
|
||||||
|
(prefix + "/" + lib.concatStringsSep "/" (lib.drop 4 parts))
|
||||||
|
else
|
||||||
|
decl
|
||||||
|
else
|
||||||
|
decl
|
||||||
|
) opt.declarations;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,9 +9,11 @@ let
|
|||||||
clan-core.modules.clan.default
|
clan-core.modules.clan.default
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
evalDocs = pkgs.nixosOptionsDoc {
|
evalDocs = pkgs.nixosOptionsDoc {
|
||||||
options = eval.options;
|
options = eval.options;
|
||||||
warningsAreErrors = false;
|
warningsAreErrors = false;
|
||||||
|
transformOptions = clan-core.clanLib.docs.stripStorePathsFromDeclarations;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ in
|
|||||||
type = attrsWith {
|
type = attrsWith {
|
||||||
placeholder = "mappedServiceName";
|
placeholder = "mappedServiceName";
|
||||||
elemType = submoduleWith {
|
elemType = submoduleWith {
|
||||||
|
class = "clan.service";
|
||||||
modules = [
|
modules = [
|
||||||
(
|
(
|
||||||
{ name, ... }:
|
{ name, ... }:
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ in
|
|||||||
evalServices =
|
evalServices =
|
||||||
{ modules, prefix }:
|
{ modules, prefix }:
|
||||||
lib.evalModules {
|
lib.evalModules {
|
||||||
|
class = "clan";
|
||||||
specialArgs = {
|
specialArgs = {
|
||||||
inherit clanLib;
|
inherit clanLib;
|
||||||
_ctx = prefix;
|
_ctx = prefix;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ in
|
|||||||
prefix = [ ];
|
prefix = [ ];
|
||||||
}).options;
|
}).options;
|
||||||
warningsAreErrors = true;
|
warningsAreErrors = true;
|
||||||
|
transformOptions = self.clanLib.docs.stripStorePathsFromDeclarations;
|
||||||
}).optionsJSON;
|
}).optionsJSON;
|
||||||
|
|
||||||
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
|
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
|
||||||
|
|||||||
@@ -35,10 +35,20 @@ in
|
|||||||
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
|
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
|
||||||
) inputsWithModules;
|
) inputsWithModules;
|
||||||
};
|
};
|
||||||
options.localModules = lib.mkOption {
|
options.moduleSchemas = lib.mkOption {
|
||||||
|
# { sourceName :: { moduleName :: { roleName :: Schema }}}
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
type = lib.types.raw;
|
type = lib.types.raw;
|
||||||
default = config.modulesPerSource.self;
|
default = lib.mapAttrs (
|
||||||
|
_inputName: moduleSet:
|
||||||
|
lib.mapAttrs (
|
||||||
|
_moduleName: module:
|
||||||
|
(clanLib.evalService {
|
||||||
|
modules = [ module ];
|
||||||
|
prefix = [ ];
|
||||||
|
}).config.result.api.schema
|
||||||
|
) moduleSet
|
||||||
|
) config.modulesPerSource;
|
||||||
};
|
};
|
||||||
options.templatesPerSource = lib.mkOption {
|
options.templatesPerSource = lib.mkOption {
|
||||||
# { sourceName :: { moduleName :: {} }}
|
# { sourceName :: { moduleName :: {} }}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ let
|
|||||||
package
|
package
|
||||||
path
|
path
|
||||||
str
|
str
|
||||||
|
strMatching
|
||||||
submoduleWith
|
submoduleWith
|
||||||
;
|
;
|
||||||
# the original types.submodule has strange behavior
|
# the original types.submodule has strange behavior
|
||||||
@@ -47,7 +48,7 @@ in
|
|||||||
imports = [ ./generator.nix ];
|
imports = [ ./generator.nix ];
|
||||||
options = {
|
options = {
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = str;
|
||||||
description = ''
|
description = ''
|
||||||
The name of the generator.
|
The name of the generator.
|
||||||
This name will be used to refer to the generator in other generators.
|
This name will be used to refer to the generator in other generators.
|
||||||
@@ -153,7 +154,7 @@ in
|
|||||||
options =
|
options =
|
||||||
{
|
{
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = str;
|
||||||
description = ''
|
description = ''
|
||||||
name of the public fact
|
name of the public fact
|
||||||
'';
|
'';
|
||||||
@@ -162,7 +163,7 @@ in
|
|||||||
defaultText = "Name of the file";
|
defaultText = "Name of the file";
|
||||||
};
|
};
|
||||||
generatorName = lib.mkOption {
|
generatorName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = str;
|
||||||
description = ''
|
description = ''
|
||||||
name of the generator
|
name of the generator
|
||||||
'';
|
'';
|
||||||
@@ -171,7 +172,7 @@ in
|
|||||||
defaultText = "Name of the generator that generates this file";
|
defaultText = "Name of the generator that generates this file";
|
||||||
};
|
};
|
||||||
share = lib.mkOption {
|
share = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = bool;
|
||||||
description = ''
|
description = ''
|
||||||
Whether the generated vars should be shared between machines.
|
Whether the generated vars should be shared between machines.
|
||||||
Shared vars are only generated once, when the first machine using it is deployed.
|
Shared vars are only generated once, when the first machine using it is deployed.
|
||||||
@@ -233,7 +234,7 @@ in
|
|||||||
By setting this to `user`, the secret will be deployed prior to users and groups are created, allowing
|
By setting this to `user`, the secret will be deployed prior to users and groups are created, allowing
|
||||||
users' passwords to be managed by vars. The secret will be stored in `/run/secrets-for-users` and `owner` and `group` must be `root`.
|
users' passwords to be managed by vars. The secret will be stored in `/run/secrets-for-users` and `owner` and `group` must be `root`.
|
||||||
'';
|
'';
|
||||||
type = lib.types.enum [
|
type = enum [
|
||||||
"partitioning"
|
"partitioning"
|
||||||
"activation"
|
"activation"
|
||||||
"users"
|
"users"
|
||||||
@@ -251,7 +252,7 @@ in
|
|||||||
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
|
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
|
||||||
};
|
};
|
||||||
mode = lib.mkOption {
|
mode = lib.mkOption {
|
||||||
type = lib.types.strMatching "^[0-7]{4}$";
|
type = strMatching "^[0-7]{4}$";
|
||||||
description = "The unix file mode of the file. Must be a 4-digit octal number.";
|
description = "The unix file mode of the file. Must be a 4-digit octal number.";
|
||||||
default = "0400";
|
default = "0400";
|
||||||
};
|
};
|
||||||
@@ -375,7 +376,7 @@ in
|
|||||||
- all required programs are in PATH
|
- all required programs are in PATH
|
||||||
- sandbox is set up correctly
|
- sandbox is set up correctly
|
||||||
'';
|
'';
|
||||||
type = lib.types.path;
|
type = path;
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
internal = true;
|
internal = true;
|
||||||
};
|
};
|
||||||
|
|||||||
37
nixosModules/clanCore/vars/secret/sops/collectFiles.nix
Normal file
37
nixosModules/clanCore/vars/secret/sops/collectFiles.nix
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# collectFiles helper function
|
||||||
|
{
|
||||||
|
lib ? import <nixpkgs/lib>,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
inherit (lib)
|
||||||
|
filterAttrs
|
||||||
|
flatten
|
||||||
|
mapAttrsToList
|
||||||
|
;
|
||||||
|
in
|
||||||
|
generators:
|
||||||
|
let
|
||||||
|
relevantFiles =
|
||||||
|
generator:
|
||||||
|
filterAttrs (
|
||||||
|
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
|
||||||
|
) generator.files;
|
||||||
|
allFiles = flatten (
|
||||||
|
mapAttrsToList (
|
||||||
|
gen_name: generator:
|
||||||
|
mapAttrsToList (fname: file: {
|
||||||
|
name = fname;
|
||||||
|
generator = gen_name;
|
||||||
|
neededForUsers = file.neededFor == "users";
|
||||||
|
inherit (generator) share;
|
||||||
|
inherit (file)
|
||||||
|
owner
|
||||||
|
group
|
||||||
|
mode
|
||||||
|
restartUnits
|
||||||
|
;
|
||||||
|
}) (relevantFiles generator)
|
||||||
|
) generators
|
||||||
|
);
|
||||||
|
in
|
||||||
|
allFiles
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
|
||||||
inherit (import ./funcs.nix { inherit lib; }) collectFiles;
|
collectFiles = import ./collectFiles.nix { inherit lib; };
|
||||||
|
|
||||||
machineName = config.clan.core.settings.machine.name;
|
machineName = config.clan.core.settings.machine.name;
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
lib ? import <nixpkgs/lib>,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
|
|
||||||
inherit (lib)
|
|
||||||
filterAttrs
|
|
||||||
flatten
|
|
||||||
mapAttrsToList
|
|
||||||
;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
|
|
||||||
collectFiles =
|
|
||||||
generators:
|
|
||||||
let
|
|
||||||
relevantFiles =
|
|
||||||
generator:
|
|
||||||
filterAttrs (
|
|
||||||
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
|
|
||||||
) generator.files;
|
|
||||||
allFiles = flatten (
|
|
||||||
mapAttrsToList (
|
|
||||||
gen_name: generator:
|
|
||||||
mapAttrsToList (fname: file: {
|
|
||||||
name = fname;
|
|
||||||
generator = gen_name;
|
|
||||||
neededForUsers = file.neededFor == "users";
|
|
||||||
inherit (generator) share;
|
|
||||||
inherit (file)
|
|
||||||
owner
|
|
||||||
group
|
|
||||||
mode
|
|
||||||
restartUnits
|
|
||||||
;
|
|
||||||
}) (relevantFiles generator)
|
|
||||||
) generators
|
|
||||||
);
|
|
||||||
in
|
|
||||||
allFiles;
|
|
||||||
}
|
|
||||||
@@ -23,9 +23,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../clan-cli/clan_lib"
|
"path": "../clan-cli/clan_lib"
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "ui-2d"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@@ -16,9 +16,32 @@ def main(argv: list[str] = sys.argv) -> int:
|
|||||||
"--content-uri", type=str, help="The URI of the content to display"
|
"--content-uri", type=str, help="The URI of the content to display"
|
||||||
)
|
)
|
||||||
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
||||||
|
parser.add_argument(
|
||||||
|
"--http-api",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable HTTP API mode (default: False)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--http-host",
|
||||||
|
type=str,
|
||||||
|
default="localhost",
|
||||||
|
help="The host for the HTTP API server (default: localhost)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--http-port",
|
||||||
|
type=int,
|
||||||
|
default=8080,
|
||||||
|
help="The host and port for the HTTP API server (default: 8080)",
|
||||||
|
)
|
||||||
args = parser.parse_args(argv[1:])
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
app_opts = ClanAppOptions(content_uri=args.content_uri, debug=args.debug)
|
app_opts = ClanAppOptions(
|
||||||
|
content_uri=args.content_uri,
|
||||||
|
http_api=args.http_api,
|
||||||
|
http_host=args.http_host,
|
||||||
|
http_port=args.http_port,
|
||||||
|
debug=args.debug,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
app_run(app_opts)
|
app_run(app_opts)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from . import main
|
from clan_app import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from clan_lib.api import ApiResponse
|
||||||
|
from clan_lib.api.tasks import WebThread
|
||||||
|
from clan_lib.async_run import set_should_cancel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .middleware import Middleware
|
from .middleware import Middleware
|
||||||
|
|
||||||
@@ -15,12 +20,12 @@ class BackendRequest:
|
|||||||
method_name: str
|
method_name: str
|
||||||
args: dict[str, Any]
|
args: dict[str, Any]
|
||||||
header: dict[str, Any]
|
header: dict[str, Any]
|
||||||
op_key: str
|
op_key: str | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BackendResponse:
|
class BackendResponse:
|
||||||
body: Any
|
body: ApiResponse
|
||||||
header: dict[str, Any]
|
header: dict[str, Any]
|
||||||
_op_key: str
|
_op_key: str
|
||||||
|
|
||||||
@@ -30,9 +35,10 @@ class ApiBridge(ABC):
|
|||||||
"""Generic interface for API bridges that can handle method calls from different sources."""
|
"""Generic interface for API bridges that can handle method calls from different sources."""
|
||||||
|
|
||||||
middleware_chain: tuple["Middleware", ...]
|
middleware_chain: tuple["Middleware", ...]
|
||||||
|
threads: dict[str, WebThread] = field(default_factory=dict)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def send_response(self, response: BackendResponse) -> None:
|
def send_api_response(self, response: BackendResponse) -> None:
|
||||||
"""Send response back to the client."""
|
"""Send response back to the client."""
|
||||||
|
|
||||||
def process_request(self, request: BackendRequest) -> None:
|
def process_request(self, request: BackendRequest) -> None:
|
||||||
@@ -55,12 +61,12 @@ class ApiBridge(ABC):
|
|||||||
middleware.process(context)
|
middleware.process(context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If middleware fails, handle error
|
# If middleware fails, handle error
|
||||||
self.send_error_response(
|
self.send_api_error_response(
|
||||||
request.op_key, str(e), ["middleware_error"]
|
request.op_key or "unknown", str(e), ["middleware_error"]
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
def send_error_response(
|
def send_api_error_response(
|
||||||
self, op_key: str, error_message: str, location: list[str]
|
self, op_key: str, error_message: str, location: list[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send an error response."""
|
"""Send an error response."""
|
||||||
@@ -84,4 +90,52 @@ class ApiBridge(ABC):
|
|||||||
_op_key=op_key,
|
_op_key=op_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.send_response(response)
|
self.send_api_response(response)
|
||||||
|
|
||||||
|
def process_request_in_thread(
|
||||||
|
self,
|
||||||
|
request: BackendRequest,
|
||||||
|
*,
|
||||||
|
thread_name: str = "ApiBridgeThread",
|
||||||
|
wait_for_completion: bool = False,
|
||||||
|
timeout: float = 60.0,
|
||||||
|
) -> None:
|
||||||
|
"""Process an API request in a separate thread with cancellation support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The API request to process
|
||||||
|
thread_name: Name for the thread (for debugging)
|
||||||
|
wait_for_completion: Whether to wait for the thread to complete
|
||||||
|
timeout: Timeout in seconds when waiting for completion
|
||||||
|
"""
|
||||||
|
op_key = request.op_key or "unknown"
|
||||||
|
|
||||||
|
def thread_task(stop_event: threading.Event) -> None:
|
||||||
|
set_should_cancel(lambda: stop_event.is_set())
|
||||||
|
try:
|
||||||
|
log.debug(
|
||||||
|
f"Processing {request.method_name} with args {request.args} "
|
||||||
|
f"and header {request.header} in thread {thread_name}"
|
||||||
|
)
|
||||||
|
self.process_request(request)
|
||||||
|
finally:
|
||||||
|
self.threads.pop(op_key, None)
|
||||||
|
|
||||||
|
stop_event = threading.Event()
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=thread_task, args=(stop_event,), name=thread_name
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
self.threads[op_key] = WebThread(thread=thread, stop_event=stop_event)
|
||||||
|
|
||||||
|
if wait_for_completion:
|
||||||
|
# Wait for the thread to complete (this blocks until response is sent)
|
||||||
|
thread.join(timeout=timeout)
|
||||||
|
|
||||||
|
# Handle timeout
|
||||||
|
if thread.is_alive():
|
||||||
|
stop_event.set() # Cancel the thread
|
||||||
|
self.send_api_error_response(
|
||||||
|
op_key, "Request timeout", ["api_bridge", request.method_name]
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ gi.require_version("Gtk", "4.0")
|
|||||||
|
|
||||||
from clan_lib.api import ApiError, ErrorDataClass, SuccessDataClass
|
from clan_lib.api import ApiError, ErrorDataClass, SuccessDataClass
|
||||||
from clan_lib.api.directory import FileRequest
|
from clan_lib.api.directory import FileRequest
|
||||||
|
from clan_lib.clan.check import check_clan_valid
|
||||||
|
from clan_lib.flake import Flake
|
||||||
from gi.repository import Gio, GLib, Gtk
|
from gi.repository import Gio, GLib, Gtk
|
||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
@@ -22,13 +24,58 @@ def remove_none(_list: list) -> list:
|
|||||||
RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
|
RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
|
||||||
|
|
||||||
|
|
||||||
def open_file(
|
def get_clan_folder(*, op_key: str) -> SuccessDataClass[Flake] | ErrorDataClass:
|
||||||
|
"""
|
||||||
|
Opens the clan folder using the GTK file dialog.
|
||||||
|
Returns the path to the clan folder or an error if it fails.
|
||||||
|
"""
|
||||||
|
file_request = FileRequest(
|
||||||
|
mode="select_folder",
|
||||||
|
title="Select Clan Folder",
|
||||||
|
initial_folder=str(Path.home()),
|
||||||
|
)
|
||||||
|
response = get_system_file(file_request, op_key=op_key)
|
||||||
|
|
||||||
|
if isinstance(response, ErrorDataClass):
|
||||||
|
return response
|
||||||
|
|
||||||
|
if not response.data or len(response.data) == 0:
|
||||||
|
return ErrorDataClass(
|
||||||
|
op_key=op_key,
|
||||||
|
status="error",
|
||||||
|
errors=[
|
||||||
|
ApiError(
|
||||||
|
message="No folder selected",
|
||||||
|
description="You must select a folder to open.",
|
||||||
|
location=["get_clan_folder"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
clan_folder = Flake(response.data[0])
|
||||||
|
if not check_clan_valid(clan_folder):
|
||||||
|
return ErrorDataClass(
|
||||||
|
op_key=op_key,
|
||||||
|
status="error",
|
||||||
|
errors=[
|
||||||
|
ApiError(
|
||||||
|
message="Invalid clan folder",
|
||||||
|
description=f"The selected folder '{clan_folder}' is not a valid clan folder.",
|
||||||
|
location=["get_clan_folder"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return SuccessDataClass(op_key=op_key, data=clan_folder, status="success")
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_file(
|
||||||
file_request: FileRequest, *, op_key: str
|
file_request: FileRequest, *, op_key: str
|
||||||
) -> SuccessDataClass[list[str] | None] | ErrorDataClass:
|
) -> SuccessDataClass[list[str] | None] | ErrorDataClass:
|
||||||
GLib.idle_add(gtk_open_file, file_request, op_key)
|
GLib.idle_add(gtk_open_file, file_request, op_key)
|
||||||
|
|
||||||
while RESULT.get(op_key) is None:
|
while RESULT.get(op_key) is None:
|
||||||
time.sleep(0.2)
|
time.sleep(0.1)
|
||||||
response = RESULT[op_key]
|
response = RESULT[op_key]
|
||||||
del RESULT[op_key]
|
del RESULT[op_key]
|
||||||
return response
|
return response
|
||||||
@@ -59,7 +106,7 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
|||||||
ApiError(
|
ApiError(
|
||||||
message=e.__class__.__name__,
|
message=e.__class__.__name__,
|
||||||
description=str(e),
|
description=str(e),
|
||||||
location=["open_file"],
|
location=["get_system_file"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -87,7 +134,7 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
|||||||
ApiError(
|
ApiError(
|
||||||
message=e.__class__.__name__,
|
message=e.__class__.__name__,
|
||||||
description=str(e),
|
description=str(e),
|
||||||
location=["open_file"],
|
location=["get_system_file"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -115,7 +162,7 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
|||||||
ApiError(
|
ApiError(
|
||||||
message=e.__class__.__name__,
|
message=e.__class__.__name__,
|
||||||
description=str(e),
|
description=str(e),
|
||||||
location=["open_file"],
|
location=["get_system_file"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -143,7 +190,7 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
|||||||
ApiError(
|
ApiError(
|
||||||
message=e.__class__.__name__,
|
message=e.__class__.__name__,
|
||||||
description=str(e),
|
description=str(e),
|
||||||
location=["open_file"],
|
location=["get_system_file"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -192,7 +239,7 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
|||||||
dialog.select_folder(callback=on_folder_select)
|
dialog.select_folder(callback=on_folder_select)
|
||||||
if file_request.mode == "open_multiple_files":
|
if file_request.mode == "open_multiple_files":
|
||||||
dialog.open_multiple(callback=on_file_select_multiple)
|
dialog.open_multiple(callback=on_file_select_multiple)
|
||||||
elif file_request.mode == "open_file":
|
elif file_request.mode == "get_system_file":
|
||||||
dialog.open(callback=on_file_select)
|
dialog.open(callback=on_file_select)
|
||||||
elif file_request.mode == "save":
|
elif file_request.mode == "save":
|
||||||
dialog.save(callback=on_save_finish)
|
dialog.save(callback=on_save_finish)
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ class ArgumentParsingMiddleware(Middleware):
|
|||||||
log.exception(
|
log.exception(
|
||||||
f"Error while parsing arguments for {context.request.method_name}"
|
f"Error while parsing arguments for {context.request.method_name}"
|
||||||
)
|
)
|
||||||
context.bridge.send_error_response(
|
context.bridge.send_api_error_response(
|
||||||
context.request.op_key,
|
context.request.op_key or "unknown",
|
||||||
str(e),
|
str(e),
|
||||||
["argument_parsing", context.request.method_name],
|
["argument_parsing", context.request.method_name],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,15 +36,15 @@ class LoggingMiddleware(Middleware):
|
|||||||
)
|
)
|
||||||
# Create log file
|
# Create log file
|
||||||
log_file = self.log_manager.create_log_file(
|
log_file = self.log_manager.create_log_file(
|
||||||
method, op_key=context.request.op_key, group_path=log_group
|
method, op_key=context.request.op_key or "unknown", group_path=log_group
|
||||||
).get_file_path()
|
).get_file_path()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
log.exception(
|
||||||
f"Error while handling request header of {context.request.method_name}"
|
f"Error while handling request header of {context.request.method_name}"
|
||||||
)
|
)
|
||||||
context.bridge.send_error_response(
|
context.bridge.send_api_error_response(
|
||||||
context.request.op_key,
|
context.request.op_key or "unknown",
|
||||||
str(e),
|
str(e),
|
||||||
["header_middleware", context.request.method_name],
|
["header_middleware", context.request.method_name],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ class MethodExecutionMiddleware(Middleware):
|
|||||||
response = BackendResponse(
|
response = BackendResponse(
|
||||||
body=result,
|
body=result,
|
||||||
header={},
|
header={},
|
||||||
_op_key=context.request.op_key,
|
_op_key=context.request.op_key or "unknown",
|
||||||
)
|
)
|
||||||
context.bridge.send_response(response)
|
context.bridge.send_api_response(response)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
log.exception(
|
||||||
f"Error while handling result of {context.request.method_name}"
|
f"Error while handling result of {context.request.method_name}"
|
||||||
)
|
)
|
||||||
context.bridge.send_error_response(
|
context.bridge.send_api_error_response(
|
||||||
context.request.op_key,
|
context.request.op_key or "unknown",
|
||||||
str(e),
|
str(e),
|
||||||
["method_execution", context.request.method_name],
|
["method_execution", context.request.method_name],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from clan_lib.dirs import user_data_dir
|
|||||||
from clan_lib.log_manager import LogGroupConfig, LogManager
|
from clan_lib.log_manager import LogGroupConfig, LogManager
|
||||||
from clan_lib.log_manager import api as log_manager_api
|
from clan_lib.log_manager import api as log_manager_api
|
||||||
|
|
||||||
from clan_app.api.file_gtk import open_file
|
from clan_app.api.file_gtk import get_clan_folder, get_system_file
|
||||||
from clan_app.api.middleware import (
|
from clan_app.api.middleware import (
|
||||||
ArgumentParsingMiddleware,
|
ArgumentParsingMiddleware,
|
||||||
LoggingMiddleware,
|
LoggingMiddleware,
|
||||||
@@ -25,6 +25,9 @@ log = logging.getLogger(__name__)
|
|||||||
class ClanAppOptions:
|
class ClanAppOptions:
|
||||||
content_uri: str
|
content_uri: str
|
||||||
debug: bool
|
debug: bool
|
||||||
|
http_api: bool = False
|
||||||
|
http_host: str = "127.0.0.1"
|
||||||
|
http_port: int = 8080
|
||||||
|
|
||||||
|
|
||||||
@profile
|
@profile
|
||||||
@@ -53,21 +56,73 @@ def app_run(app_opts: ClanAppOptions) -> int:
|
|||||||
|
|
||||||
# Populate the API global with all functions
|
# Populate the API global with all functions
|
||||||
load_in_all_api_functions()
|
load_in_all_api_functions()
|
||||||
API.overwrite_fn(open_file)
|
|
||||||
|
|
||||||
webview = Webview(
|
# Create a shared threads dictionary for both HTTP and Webview modes
|
||||||
debug=app_opts.debug, title="Clan App", size=Size(1280, 1024, SizeHint.NONE)
|
shared_threads: dict[str, tasks.WebThread] = {}
|
||||||
)
|
tasks.BAKEND_THREADS = shared_threads
|
||||||
|
|
||||||
# Add middleware to the webview
|
# Start HTTP API server if requested
|
||||||
webview.add_middleware(ArgumentParsingMiddleware(api=API))
|
http_server = None
|
||||||
webview.add_middleware(LoggingMiddleware(log_manager=log_manager))
|
if app_opts.http_api:
|
||||||
webview.add_middleware(MethodExecutionMiddleware(api=API))
|
from clan_app.deps.http.http_server import HttpApiServer
|
||||||
|
|
||||||
# Init BAKEND_THREADS global in tasks module
|
openapi_file = os.getenv("OPENAPI_FILE", None)
|
||||||
tasks.BAKEND_THREADS = webview.threads
|
swagger_dist = os.getenv("SWAGGER_UI_DIST", None)
|
||||||
|
|
||||||
|
http_server = HttpApiServer(
|
||||||
|
api=API,
|
||||||
|
openapi_file=Path(openapi_file) if openapi_file else None,
|
||||||
|
swagger_dist=Path(swagger_dist) if swagger_dist else None,
|
||||||
|
host=app_opts.http_host,
|
||||||
|
port=app_opts.http_port,
|
||||||
|
shared_threads=shared_threads,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add middleware to HTTP server
|
||||||
|
http_server.add_middleware(ArgumentParsingMiddleware(api=API))
|
||||||
|
http_server.add_middleware(LoggingMiddleware(log_manager=log_manager))
|
||||||
|
http_server.add_middleware(MethodExecutionMiddleware(api=API))
|
||||||
|
|
||||||
|
# Start the server (bridge will be created automatically)
|
||||||
|
http_server.start()
|
||||||
|
|
||||||
|
# HTTP-only mode - keep the server running
|
||||||
|
log.info("HTTP API server running...")
|
||||||
|
log.info(
|
||||||
|
f"Swagger: http://{app_opts.http_host}:{app_opts.http_port}/api/swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info("Press Ctrl+C to stop the server")
|
||||||
|
try:
|
||||||
|
# Keep the main thread alive
|
||||||
|
import time
|
||||||
|
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("Shutting down HTTP API server...")
|
||||||
|
if http_server:
|
||||||
|
http_server.stop()
|
||||||
|
|
||||||
|
# Create webview if not running in HTTP-only mode
|
||||||
|
if not app_opts.http_api:
|
||||||
|
webview = Webview(
|
||||||
|
debug=app_opts.debug,
|
||||||
|
title="Clan App",
|
||||||
|
size=Size(1280, 1024, SizeHint.NONE),
|
||||||
|
shared_threads=shared_threads,
|
||||||
|
)
|
||||||
|
|
||||||
|
API.overwrite_fn(get_system_file)
|
||||||
|
API.overwrite_fn(get_clan_folder)
|
||||||
|
|
||||||
|
# Add middleware to the webview
|
||||||
|
webview.add_middleware(ArgumentParsingMiddleware(api=API))
|
||||||
|
webview.add_middleware(LoggingMiddleware(log_manager=log_manager))
|
||||||
|
webview.add_middleware(MethodExecutionMiddleware(api=API))
|
||||||
|
|
||||||
|
webview.bind_jsonschema_api(API, log_manager=log_manager)
|
||||||
|
webview.navigate(content_uri)
|
||||||
|
webview.run()
|
||||||
|
|
||||||
webview.bind_jsonschema_api(API, log_manager=log_manager)
|
|
||||||
webview.navigate(content_uri)
|
|
||||||
webview.run()
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
0
pkgs/clan-app/clan_app/deps/http/__init__.py
Normal file
0
pkgs/clan-app/clan_app/deps/http/__init__.py
Normal file
341
pkgs/clan-app/clan_app/deps/http/http_bridge.py
Normal file
341
pkgs/clan-app/clan_app/deps/http/http_bridge.py
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from http.server import BaseHTTPRequestHandler
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from clan_lib.api import MethodRegistry, SuccessDataClass, dataclass_to_dict
|
||||||
|
from clan_lib.api.tasks import WebThread
|
||||||
|
|
||||||
|
from clan_app.api.api_bridge import ApiBridge, BackendRequest, BackendResponse
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from clan_app.api.middleware import Middleware
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||||
|
"""HTTP-specific implementation of the API bridge that handles HTTP requests directly.
|
||||||
|
|
||||||
|
This bridge combines the API bridge functionality with HTTP request handling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api: MethodRegistry,
|
||||||
|
middleware_chain: tuple["Middleware", ...],
|
||||||
|
request: Any,
|
||||||
|
client_address: Any,
|
||||||
|
server: Any,
|
||||||
|
*,
|
||||||
|
openapi_file: Path | None = None,
|
||||||
|
swagger_dist: Path | None = None,
|
||||||
|
shared_threads: dict[str, WebThread] | None = None,
|
||||||
|
) -> None:
|
||||||
|
# Initialize API bridge fields
|
||||||
|
self.api = api
|
||||||
|
self.middleware_chain = middleware_chain
|
||||||
|
self.threads = shared_threads if shared_threads is not None else {}
|
||||||
|
|
||||||
|
# Initialize OpenAPI/Swagger fields
|
||||||
|
self.openapi_file = openapi_file
|
||||||
|
self.swagger_dist = swagger_dist
|
||||||
|
|
||||||
|
# Initialize HTTP handler
|
||||||
|
super(BaseHTTPRequestHandler, self).__init__(request, client_address, server)
|
||||||
|
|
||||||
|
def _send_cors_headers(self) -> None:
|
||||||
|
"""Send CORS headers for cross-origin requests."""
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
def _send_json_response_with_status(
|
||||||
|
self, data: dict[str, Any], status_code: int = 200
|
||||||
|
) -> None:
|
||||||
|
"""Send a JSON response with the given status code."""
|
||||||
|
try:
|
||||||
|
self.send_response_only(status_code)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self._send_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
response_data = json.dumps(data, indent=2, ensure_ascii=False)
|
||||||
|
self.wfile.write(response_data.encode("utf-8"))
|
||||||
|
except BrokenPipeError as e:
|
||||||
|
log.warning(f"Client disconnected before we could send a response: {e!s}")
|
||||||
|
|
||||||
|
def send_api_response(self, response: BackendResponse) -> None:
|
||||||
|
"""Send HTTP response directly to the client."""
|
||||||
|
response_dict = dataclass_to_dict(response)
|
||||||
|
self._send_json_response_with_status(response_dict, 200)
|
||||||
|
log.debug(
|
||||||
|
f"HTTP response for {response._op_key}: {json.dumps(response_dict, indent=2)}" # noqa: SLF001
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_success_response(
|
||||||
|
self, op_key: str, data: dict[str, Any]
|
||||||
|
) -> BackendResponse:
|
||||||
|
"""Create a successful API response."""
|
||||||
|
return BackendResponse(
|
||||||
|
body=SuccessDataClass(op_key=op_key, status="success", data=data),
|
||||||
|
header={},
|
||||||
|
_op_key=op_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send_info_response(self) -> None:
|
||||||
|
"""Send server information response."""
|
||||||
|
response = self._create_success_response(
|
||||||
|
"info", {"message": "Clan API Server", "version": "1.0.0"}
|
||||||
|
)
|
||||||
|
self.send_api_response(response)
|
||||||
|
|
||||||
|
def _send_methods_response(self) -> None:
|
||||||
|
"""Send available API methods response."""
|
||||||
|
response = self._create_success_response(
|
||||||
|
"methods", {"methods": list(self.api.functions.keys())}
|
||||||
|
)
|
||||||
|
self.send_api_response(response)
|
||||||
|
|
||||||
|
def _handle_swagger_request(self, parsed_url: Any) -> None:
|
||||||
|
"""Handle Swagger UI related requests."""
|
||||||
|
if not self.swagger_dist or not self.swagger_dist.exists():
|
||||||
|
self.send_error(404, "Swagger file not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
rel_path = parsed_url.path[len("/api/swagger") :].lstrip("/")
|
||||||
|
|
||||||
|
# Redirect /api/swagger to /api/swagger/index.html
|
||||||
|
if rel_path == "":
|
||||||
|
self.send_response(302)
|
||||||
|
self.send_header("Location", "/api/swagger/index.html")
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._serve_swagger_file(rel_path)
|
||||||
|
|
||||||
|
def _serve_swagger_file(self, rel_path: str) -> None:
|
||||||
|
"""Serve a specific Swagger UI file."""
|
||||||
|
file_path = self._get_swagger_file_path(rel_path)
|
||||||
|
|
||||||
|
if not file_path.exists() or not file_path.is_file():
|
||||||
|
self.send_error(404, "Swagger file not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_type = self._get_content_type(file_path)
|
||||||
|
file_data = self._read_and_process_file(file_path, rel_path)
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", content_type)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(file_data)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error reading Swagger file: {e!s}")
|
||||||
|
self.send_error(500, "Internal Server Error")
|
||||||
|
|
||||||
|
def _get_swagger_file_path(self, rel_path: str) -> Path:
|
||||||
|
"""Get the file path for a Swagger resource."""
|
||||||
|
if rel_path == "index.html":
|
||||||
|
return Path(__file__).parent / "swagger.html"
|
||||||
|
if rel_path == "openapi.json":
|
||||||
|
if not self.openapi_file:
|
||||||
|
return Path("/nonexistent") # Will fail exists() check
|
||||||
|
return self.openapi_file
|
||||||
|
return (
|
||||||
|
self.swagger_dist / rel_path if self.swagger_dist else Path("/nonexistent")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_content_type(self, file_path: Path) -> str:
|
||||||
|
"""Get the content type for a file based on its extension."""
|
||||||
|
content_types = {
|
||||||
|
".html": "text/html",
|
||||||
|
".js": "application/javascript",
|
||||||
|
".css": "text/css",
|
||||||
|
".json": "application/json",
|
||||||
|
".png": "image/png",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
}
|
||||||
|
return content_types.get(file_path.suffix, "application/octet-stream")
|
||||||
|
|
||||||
|
def _read_and_process_file(self, file_path: Path, rel_path: str) -> bytes:
|
||||||
|
"""Read and optionally process a file (e.g., inject server URL into openapi.json)."""
|
||||||
|
with file_path.open("rb") as f:
|
||||||
|
file_data = f.read()
|
||||||
|
|
||||||
|
if rel_path == "openapi.json":
|
||||||
|
json_data = json.loads(file_data.decode("utf-8"))
|
||||||
|
server_address = getattr(self.server, "server_address", ("localhost", 80))
|
||||||
|
json_data["servers"] = [
|
||||||
|
{"url": f"http://{server_address[0]}:{server_address[1]}/api/v1/"}
|
||||||
|
]
|
||||||
|
file_data = json.dumps(json_data, indent=2).encode("utf-8")
|
||||||
|
|
||||||
|
return file_data
|
||||||
|
|
||||||
|
def do_OPTIONS(self) -> None: # noqa: N802
|
||||||
|
"""Handle CORS preflight requests."""
|
||||||
|
self.send_response_only(200)
|
||||||
|
self._send_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_GET(self) -> None: # noqa: N802
|
||||||
|
"""Handle GET requests."""
|
||||||
|
parsed_url = urlparse(self.path)
|
||||||
|
path = parsed_url.path
|
||||||
|
|
||||||
|
if path == "/":
|
||||||
|
self._send_info_response()
|
||||||
|
elif path.startswith("/api/swagger"):
|
||||||
|
self._handle_swagger_request(parsed_url)
|
||||||
|
elif path == "/api/methods":
|
||||||
|
self._send_methods_response()
|
||||||
|
else:
|
||||||
|
self.send_api_error_response("info", "Not Found", ["http_bridge", "GET"])
|
||||||
|
|
||||||
|
def do_POST(self) -> None: # noqa: N802
|
||||||
|
"""Handle POST requests."""
|
||||||
|
parsed_url = urlparse(self.path)
|
||||||
|
path = parsed_url.path
|
||||||
|
|
||||||
|
# Validate API path
|
||||||
|
if not path.startswith("/api/v1/"):
|
||||||
|
self.send_api_error_response(
|
||||||
|
"post", f"Path not found: {path}", ["http_bridge", "POST"]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Extract and validate method name
|
||||||
|
method_name = path[len("/api/v1/") :]
|
||||||
|
if not method_name:
|
||||||
|
self.send_api_error_response(
|
||||||
|
"post", "Method name required", ["http_bridge", "POST"]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if method_name not in self.api.functions:
|
||||||
|
self.send_api_error_response(
|
||||||
|
"post",
|
||||||
|
f"Method '{method_name}' not found",
|
||||||
|
["http_bridge", "POST", method_name],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read and parse request body
|
||||||
|
request_data = self._read_request_body(method_name)
|
||||||
|
if request_data is None:
|
||||||
|
return # Error already sent
|
||||||
|
|
||||||
|
# Generate operation key and handle request
|
||||||
|
gen_op_key = str(uuid.uuid4())
|
||||||
|
try:
|
||||||
|
self._handle_api_request(method_name, request_data, gen_op_key)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error processing API request {method_name}")
|
||||||
|
self.send_api_error_response(
|
||||||
|
gen_op_key,
|
||||||
|
f"Internal server error: {e!s}",
|
||||||
|
["http_bridge", "POST", method_name],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _read_request_body(self, method_name: str) -> dict[str, Any] | None:
|
||||||
|
"""Read and parse the request body. Returns None if there was an error."""
|
||||||
|
try:
|
||||||
|
content_length = int(self.headers.get("Content-Length", 0))
|
||||||
|
if content_length > 0:
|
||||||
|
body = self.rfile.read(content_length)
|
||||||
|
return json.loads(body.decode("utf-8"))
|
||||||
|
return {}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.send_api_error_response(
|
||||||
|
"post",
|
||||||
|
"Invalid JSON in request body",
|
||||||
|
["http_bridge", "POST", method_name],
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
self.send_api_error_response(
|
||||||
|
"post",
|
||||||
|
f"Error reading request: {e!s}",
|
||||||
|
["http_bridge", "POST", method_name],
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle_api_request(
|
||||||
|
self,
|
||||||
|
method_name: str,
|
||||||
|
request_data: dict[str, Any],
|
||||||
|
gen_op_key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Handle an API request by processing it through middleware."""
|
||||||
|
try:
|
||||||
|
# Validate and parse request data
|
||||||
|
header, body, op_key = self._parse_request_data(request_data, gen_op_key)
|
||||||
|
|
||||||
|
# Validate operation key
|
||||||
|
self._validate_operation_key(op_key)
|
||||||
|
|
||||||
|
# Create API request
|
||||||
|
api_request = BackendRequest(
|
||||||
|
method_name=method_name, args=body, header=header, op_key=op_key
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_api_error_response(
|
||||||
|
gen_op_key, str(e), ["http_bridge", method_name]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._process_api_request_in_thread(api_request, method_name)
|
||||||
|
|
||||||
|
def _parse_request_data(
|
||||||
|
self, request_data: dict[str, Any], gen_op_key: str
|
||||||
|
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
||||||
|
"""Parse and validate request data components."""
|
||||||
|
header = request_data.get("header", {})
|
||||||
|
if not isinstance(header, dict):
|
||||||
|
msg = f"Expected header to be a dict, got {type(header)}"
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
body = request_data.get("body", {})
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
msg = f"Expected body to be a dict, got {type(body)}"
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
op_key = header.get("op_key", gen_op_key)
|
||||||
|
if not isinstance(op_key, str):
|
||||||
|
msg = f"Expected op_key to be a string, got {type(op_key)}"
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
return header, body, op_key
|
||||||
|
|
||||||
|
def _validate_operation_key(self, op_key: str) -> None:
|
||||||
|
"""Validate that the operation key is valid and not in use."""
|
||||||
|
try:
|
||||||
|
uuid.UUID(op_key)
|
||||||
|
except ValueError as e:
|
||||||
|
msg = f"op_key '{op_key}' is not a valid UUID"
|
||||||
|
raise TypeError(msg) from e
|
||||||
|
|
||||||
|
if op_key in self.threads:
|
||||||
|
msg = f"Operation key '{op_key}' is already in use. Please try again."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
def _process_api_request_in_thread(
|
||||||
|
self, api_request: BackendRequest, method_name: str
|
||||||
|
) -> None:
|
||||||
|
"""Process the API request in a separate thread."""
|
||||||
|
# Use the inherited thread processing method
|
||||||
|
self.process_request_in_thread(
|
||||||
|
api_request,
|
||||||
|
thread_name="HttpThread",
|
||||||
|
wait_for_completion=True,
|
||||||
|
timeout=60.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
|
||||||
|
"""Override default logging to use our logger."""
|
||||||
|
log.info(f"{self.address_string()} - {format % args}")
|
||||||
114
pkgs/clan-app/clan_app/deps/http/http_server.py
Normal file
114
pkgs/clan-app/clan_app/deps/http/http_server.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from http.server import HTTPServer, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from clan_lib.api import MethodRegistry
|
||||||
|
from clan_lib.api.tasks import WebThread
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from clan_app.api.middleware import Middleware
|
||||||
|
|
||||||
|
from .http_bridge import HttpBridge
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HttpApiServer:
|
||||||
|
"""HTTP server for the Clan API using Python's built-in HTTP server."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api: MethodRegistry,
|
||||||
|
host: str = "127.0.0.1",
|
||||||
|
port: int = 8080,
|
||||||
|
openapi_file: Path | None = None,
|
||||||
|
swagger_dist: Path | None = None,
|
||||||
|
shared_threads: dict[str, WebThread] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.api = api
|
||||||
|
self.openapi = openapi_file
|
||||||
|
self.swagger_dist = swagger_dist
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self._server: HTTPServer | None = None
|
||||||
|
self._server_thread: threading.Thread | None = None
|
||||||
|
# Bridge is now the request handler itself, no separate instance needed
|
||||||
|
self._middleware: list[Middleware] = []
|
||||||
|
self.shared_threads = shared_threads if shared_threads is not None else {}
|
||||||
|
|
||||||
|
def add_middleware(self, middleware: "Middleware") -> None:
|
||||||
|
"""Add middleware to the middleware chain."""
|
||||||
|
if self._server is not None:
|
||||||
|
msg = "Cannot add middleware after server is started"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
self._middleware.append(middleware)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server(self) -> HTTPServer | None:
|
||||||
|
"""Get the HTTP server instance."""
|
||||||
|
return self._server
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_thread(self) -> threading.Thread | None:
|
||||||
|
"""Get the server thread."""
|
||||||
|
return self._server_thread
|
||||||
|
|
||||||
|
def _create_request_handler(self) -> type[HttpBridge]:
|
||||||
|
"""Create a request handler class with injected dependencies."""
|
||||||
|
api = self.api
|
||||||
|
middleware_chain = tuple(self._middleware)
|
||||||
|
openapi_file = self.openapi
|
||||||
|
swagger_dist = self.swagger_dist
|
||||||
|
shared_threads = self.shared_threads
|
||||||
|
|
||||||
|
class RequestHandler(HttpBridge):
|
||||||
|
def __init__(self, request: Any, client_address: Any, server: Any) -> None:
|
||||||
|
super().__init__(
|
||||||
|
api=api,
|
||||||
|
middleware_chain=middleware_chain,
|
||||||
|
request=request,
|
||||||
|
client_address=client_address,
|
||||||
|
server=server,
|
||||||
|
openapi_file=openapi_file,
|
||||||
|
swagger_dist=swagger_dist,
|
||||||
|
shared_threads=shared_threads,
|
||||||
|
)
|
||||||
|
|
||||||
|
return RequestHandler
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Start the HTTP server in a separate thread."""
|
||||||
|
if self._server_thread is not None:
|
||||||
|
log.warning("HTTP server is already running")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create the server using ThreadingHTTPServer for concurrent request handling
|
||||||
|
handler_class = self._create_request_handler()
|
||||||
|
self._server = ThreadingHTTPServer((self.host, self.port), handler_class)
|
||||||
|
|
||||||
|
def run_server() -> None:
|
||||||
|
if self._server:
|
||||||
|
log.info(f"HTTP API server started on http://{self.host}:{self.port}")
|
||||||
|
self._server.serve_forever()
|
||||||
|
|
||||||
|
self._server_thread = threading.Thread(target=run_server, daemon=True)
|
||||||
|
self._server_thread.start()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stop the HTTP server."""
|
||||||
|
if self._server:
|
||||||
|
self._server.shutdown()
|
||||||
|
self._server.server_close()
|
||||||
|
self._server = None
|
||||||
|
|
||||||
|
if self._server_thread:
|
||||||
|
self._server_thread.join(timeout=5)
|
||||||
|
self._server_thread = None
|
||||||
|
|
||||||
|
log.info("HTTP API server stopped")
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""Check if the server is running."""
|
||||||
|
return self._server_thread is not None and self._server_thread.is_alive()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user