Merge remote-tracking branch 'origin/main' into rework-installation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
name: deploy
|
name: deploy
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
@@ -10,4 +10,4 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- run: nix run .#deploy-docs
|
- run: nix run .#deploy-docs
|
||||||
env:
|
env:
|
||||||
SSH_HOMEPAGE_KEY: ${{ secrets.SSH_HOMEPAGE_KEY }}
|
SSH_HOMEPAGE_KEY: ${{ secrets.SSH_HOMEPAGE_KEY }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.direnv
|
.direnv
|
||||||
|
**/.nixos-test-history
|
||||||
***/.hypothesis
|
***/.hypothesis
|
||||||
out.log
|
out.log
|
||||||
.coverage.*
|
.coverage.*
|
||||||
|
|||||||
22
checks/devshell/flake-module.nix
Normal file
22
checks/devshell/flake-module.nix
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
perSystem =
|
||||||
|
{ self', pkgs, ... }:
|
||||||
|
{
|
||||||
|
checks.devshell =
|
||||||
|
pkgs.runCommand "check-devshell-not-depends-on-clan-cli"
|
||||||
|
{
|
||||||
|
exportReferencesGraph = [
|
||||||
|
"graph"
|
||||||
|
self'.devShells.default
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
if grep -q "${self'.packages.clan-cli}" ./graph; then
|
||||||
|
echo "devshell depends on clan-cli, which is not allowed";
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
mkdir $out
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{ self, ... }:
|
{ self, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./impure/flake-module.nix
|
|
||||||
./backups/flake-module.nix
|
./backups/flake-module.nix
|
||||||
./installation/flake-module.nix
|
./devshell/flake-module.nix
|
||||||
./flash/flake-module.nix
|
./flash/flake-module.nix
|
||||||
|
./impure/flake-module.nix
|
||||||
|
./installation/flake-module.nix
|
||||||
];
|
];
|
||||||
perSystem =
|
perSystem =
|
||||||
{
|
{
|
||||||
@@ -40,10 +41,11 @@
|
|||||||
secrets = import ./secrets nixosTestArgs;
|
secrets = import ./secrets nixosTestArgs;
|
||||||
container = import ./container nixosTestArgs;
|
container = import ./container nixosTestArgs;
|
||||||
deltachat = import ./deltachat nixosTestArgs;
|
deltachat = import ./deltachat nixosTestArgs;
|
||||||
matrix-synapse = import ./matrix-synapse nixosTestArgs;
|
|
||||||
zt-tcp-relay = import ./zt-tcp-relay nixosTestArgs;
|
|
||||||
borgbackup = import ./borgbackup nixosTestArgs;
|
borgbackup = import ./borgbackup nixosTestArgs;
|
||||||
|
matrix-synapse = import ./matrix-synapse nixosTestArgs;
|
||||||
|
mumble = import ./mumble nixosTestArgs;
|
||||||
syncthing = import ./syncthing nixosTestArgs;
|
syncthing = import ./syncthing nixosTestArgs;
|
||||||
|
zt-tcp-relay = import ./zt-tcp-relay nixosTestArgs;
|
||||||
postgresql = import ./postgresql nixosTestArgs;
|
postgresql = import ./postgresql nixosTestArgs;
|
||||||
wayland-proxy-virtwl = import ./wayland-proxy-virtwl nixosTestArgs;
|
wayland-proxy-virtwl = import ./wayland-proxy-virtwl nixosTestArgs;
|
||||||
};
|
};
|
||||||
|
|||||||
146
checks/mumble/default.nix
Normal file
146
checks/mumble/default.nix
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
(import ../lib/test-base.nix) (
|
||||||
|
{ ... }:
|
||||||
|
let
|
||||||
|
common =
|
||||||
|
{ self, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
self.clanModules.mumble
|
||||||
|
self.nixosModules.clanCore
|
||||||
|
(self.inputs.nixpkgs + "/nixos/tests/common/x11.nix")
|
||||||
|
{
|
||||||
|
clan.core.clanDir = ./.;
|
||||||
|
environment.systemPackages = [ pkgs.killall ];
|
||||||
|
services.murmur.sslKey = "/etc/mumble-key";
|
||||||
|
services.murmur.sslCert = "/etc/mumble-cert";
|
||||||
|
clan.core.facts.services.mumble.secret."mumble-key".path = "/etc/mumble-key";
|
||||||
|
clan.core.facts.services.mumble.public."mumble-cert".path = "/etc/mumble-cert";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
name = "mumble";
|
||||||
|
|
||||||
|
enableOCR = true;
|
||||||
|
|
||||||
|
nodes.peer1 =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
common
|
||||||
|
{
|
||||||
|
clan.core.machineName = "peer1";
|
||||||
|
environment.etc = {
|
||||||
|
"mumble-key".source = ./peer_1/peer_1_test_key;
|
||||||
|
"mumble-cert".source = ./peer_1/peer_1_test_cert;
|
||||||
|
};
|
||||||
|
systemd.tmpfiles.settings."vmsecrets" = {
|
||||||
|
"/etc/secrets/mumble-key" = {
|
||||||
|
C.argument = "${./peer_1/peer_1_test_key}";
|
||||||
|
z = {
|
||||||
|
mode = "0400";
|
||||||
|
user = "murmur";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"/etc/secrets/mumble-cert" = {
|
||||||
|
C.argument = "${./peer_1/peer_1_test_cert}";
|
||||||
|
z = {
|
||||||
|
mode = "0400";
|
||||||
|
user = "murmur";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services.murmur.sslKey = "/etc/mumble-key";
|
||||||
|
services.murmur.sslCert = "/etc/mumble-cert";
|
||||||
|
clan.core.facts.services.mumble.secret."mumble-key".path = "/etc/mumble-key";
|
||||||
|
clan.core.facts.services.mumble.public."mumble-cert".path = "/etc/mumble-cert";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
nodes.peer2 =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
common
|
||||||
|
{
|
||||||
|
clan.core.machineName = "peer2";
|
||||||
|
environment.etc = {
|
||||||
|
"mumble-key".source = ./peer_2/peer_2_test_key;
|
||||||
|
"mumble-cert".source = ./peer_2/peer_2_test_cert;
|
||||||
|
};
|
||||||
|
systemd.tmpfiles.settings."vmsecrets" = {
|
||||||
|
"/etc/secrets/mumble-key" = {
|
||||||
|
C.argument = "${./peer_2/peer_2_test_key}";
|
||||||
|
z = {
|
||||||
|
mode = "0400";
|
||||||
|
user = "murmur";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"/etc/secrets/mumble-cert" = {
|
||||||
|
C.argument = "${./peer_2/peer_2_test_cert}";
|
||||||
|
z = {
|
||||||
|
mode = "0400";
|
||||||
|
user = "murmur";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
|
||||||
|
with subtest("Waiting for x"):
|
||||||
|
peer1.wait_for_x()
|
||||||
|
peer2.wait_for_x()
|
||||||
|
|
||||||
|
with subtest("Waiting for murmur"):
|
||||||
|
peer1.wait_for_unit("murmur.service")
|
||||||
|
peer2.wait_for_unit("murmur.service")
|
||||||
|
|
||||||
|
with subtest("Starting Mumble"):
|
||||||
|
# starting mumble is blocking
|
||||||
|
peer1.execute("mumble >&2 &")
|
||||||
|
peer2.execute("mumble >&2 &")
|
||||||
|
|
||||||
|
with subtest("Wait for Mumble"):
|
||||||
|
peer1.wait_for_window(r"^Mumble$")
|
||||||
|
peer2.wait_for_window(r"^Mumble$")
|
||||||
|
|
||||||
|
with subtest("Wait for certificate creation"):
|
||||||
|
peer1.wait_for_window(r"^Mumble$")
|
||||||
|
peer1.sleep(3) # mumble is slow to register handlers
|
||||||
|
peer1.send_chars("\n")
|
||||||
|
peer1.send_chars("\n")
|
||||||
|
peer2.wait_for_window(r"^Mumble$")
|
||||||
|
peer2.sleep(3) # mumble is slow to register handlers
|
||||||
|
peer2.send_chars("\n")
|
||||||
|
peer2.send_chars("\n")
|
||||||
|
|
||||||
|
with subtest("Wait for server connect"):
|
||||||
|
peer1.wait_for_window(r"^Mumble Server Connect$")
|
||||||
|
peer2.wait_for_window(r"^Mumble Server Connect$")
|
||||||
|
|
||||||
|
with subtest("Check validity of server certificates"):
|
||||||
|
peer1.execute("killall .mumble-wrapped")
|
||||||
|
peer1.sleep(1)
|
||||||
|
peer1.execute("mumble mumble://peer2 >&2 &")
|
||||||
|
peer1.wait_for_window(r"^Mumble$")
|
||||||
|
peer1.sleep(3) # mumble is slow to register handlers
|
||||||
|
peer1.send_chars("\n")
|
||||||
|
peer1.send_chars("\n")
|
||||||
|
peer1.wait_for_text("Connected.")
|
||||||
|
|
||||||
|
peer2.execute("killall .mumble-wrapped")
|
||||||
|
peer2.sleep(1)
|
||||||
|
peer2.execute("mumble mumble://peer1 >&2 &")
|
||||||
|
peer2.wait_for_window(r"^Mumble$")
|
||||||
|
peer2.sleep(3) # mumble is slow to register handlers
|
||||||
|
peer2.send_chars("\n")
|
||||||
|
peer2.send_chars("\n")
|
||||||
|
peer2.wait_for_text("Connected.")
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
)
|
||||||
22
checks/mumble/machines/peer1/facts/mumble-cert
Normal file
22
checks/mumble/machines/peer1/facts/mumble-cert
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDazCCAlOgAwIBAgIUCUjfNkF0CDhTKbO3nNczcsCW4qEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA2MjcwOTM2NDZaFw0yNDA3
|
||||||
|
MjcwOTM2NDZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQDCcdZEJvXJIeOKO5pF5XUFvUeJtCCiwfWvWS662bxc
|
||||||
|
R/5MZucRLqfTNYo9aBv4NITw5kxZsTaaubmS4zSGQoTEAVzqzVdi3a/gNvsdVLb+
|
||||||
|
7CivpmweLllX/OGsTL0kHPEI+74AYiTBjXfdWV1Y5T1tuwc3G8ATrguQ33Uo5vvF
|
||||||
|
vcqsbTKcRZC0pB9O/nn4q03GsRdvlpaKakIhjMpRG/uZ3u7wtbyZ+WqjsjxZNfnY
|
||||||
|
aMyPoaipFqX1v+L7GKlOj2NpyEZFVVwa2ZqhVSYXyDfpAWQFznwKGzD5mjtcyKym
|
||||||
|
gnv/5LwrpH4Xj+JMt48hN+rPnu5vfXT8Y4KnID30OQW7AgMBAAGjUzBRMB0GA1Ud
|
||||||
|
DgQWBBQBBO8Wp975pAGioMjkaxANAVInfzAfBgNVHSMEGDAWgBQBBO8Wp975pAGi
|
||||||
|
oMjkaxANAVInfzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAg
|
||||||
|
F40MszTZXpR/A1z9B1CcXH47tNK67f8bCMR2dhvXODbpatwSihyxhQjtLb5R6kYH
|
||||||
|
5Yq/B4yrh303j0CXaobCQ4nQH7zI7fhViww+TzW7vDhgM7ueEyyXrqCXt6JY8avg
|
||||||
|
TuvIRtJSeWSQJ5aLNaYqmiwMf/tj9W3BMDpctGyLqu1WTSrbpYa9mA5Vudud70Yz
|
||||||
|
DgZ/aqHilB07cVNqzVYZzRZ56WJlTjGzVevRgnHZqPiZNVrU13H6gtWa3r8aV4Gj
|
||||||
|
i4F663eRAttj166cRgfl1QqpSG2IprNyV9UfuS2LlUaVNT3y0idawiJ4HhaA8pGB
|
||||||
|
ZqMUUkA4DSucb6xxEcTK
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
1
checks/mumble/machines/peer1/key.age
Normal file
1
checks/mumble/machines/peer1/key.age
Normal file
@@ -0,0 +1 @@
|
|||||||
|
AGE-SECRET-KEY-1UCXEUJH6JXF8LFKWFHDM4N9AQE2CCGQZGXLUNV4TKR5KY0KC8FDQ2TY4NX
|
||||||
14
checks/mumble/machines/peer1/peer_1_test_cert
Normal file
14
checks/mumble/machines/peer1/peer_1_test_cert
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICHTCCAaKgAwIBAgIIT2gZuvqVFP0wCgYIKoZIzj0EAwIwSjESMBAGA1UEChMJ
|
||||||
|
U3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdlbmVyYXRlZDESMBAG
|
||||||
|
A1UEAxMJc3luY3RoaW5nMB4XDTIzMTIwNjAwMDAwMFoXDTQzMTIwMTAwMDAwMFow
|
||||||
|
SjESMBAGA1UEChMJU3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdl
|
||||||
|
bmVyYXRlZDESMBAGA1UEAxMJc3luY3RoaW5nMHYwEAYHKoZIzj0CAQYFK4EEACID
|
||||||
|
YgAEBAr1CsciwCa0vi7eC6xxuSGijY3txbjtsyFanec/fge4oJBD3rVpaLKFETb3
|
||||||
|
TvHHsuvblzElcP483MEVq6FMUoxwuL9CzTtpJrRhtwSmAs8AHLFu8irVn8sZjgkL
|
||||||
|
sXMho1UwUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
|
||||||
|
AQUFBwMCMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJc3luY3RoaW5nMAoGCCqG
|
||||||
|
SM49BAMCA2kAMGYCMQDbrtLgfcyMMIkNQn+PJe9DHYAqj8C47LQcWuIY/nekhOu0
|
||||||
|
aUfKctEAwyBtI60Y5zcCMQCEdgD/6CNBh7Qqq3z3CKPhlrpxHtCO5tNw17k0jfdH
|
||||||
|
haCwJInHZvZgclHk4EtFpTw=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
6
checks/mumble/machines/peer1/peer_1_test_key
Normal file
6
checks/mumble/machines/peer1/peer_1_test_key
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MIGkAgEBBDA14Nqo17Xs/xRLGH2KLuyzjKp4eW9iWFobVNM93RZZbECT++W3XcQc
|
||||||
|
cEc5WVtiPmWgBwYFK4EEACKhZANiAAQECvUKxyLAJrS+Lt4LrHG5IaKNje3FuO2z
|
||||||
|
IVqd5z9+B7igkEPetWlosoURNvdO8cey69uXMSVw/jzcwRWroUxSjHC4v0LNO2km
|
||||||
|
tGG3BKYCzwAcsW7yKtWfyxmOCQuxcyE=
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
22
checks/mumble/machines/peer2/facts/mumble-cert
Normal file
22
checks/mumble/machines/peer2/facts/mumble-cert
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDazCCAlOgAwIBAgIUfENbTtH5nr7giuawwQpDYqUpWJswDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA2MjcwOTQxNDNaFw0yNDA3
|
||||||
|
MjcwOTQxNDNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQCfP6cZhCs9jOnWqyQP12vrOOxlBrWofYZFf9amUA24
|
||||||
|
AfE7oGcSfkylanmkxzvGqQkhgLAvkHZj/GEvHujKyy8PgcEGP+pwmsfWNQMvU0Dz
|
||||||
|
j3syjWOTi3eIC/3DoUnHlWCT2qCil/bjqxgU1l7fO/OXUlq5kyvIjln7Za4sUHun
|
||||||
|
ixe/m96Er6l8a4Mh2pxh2C5pkLCvulkQhjjGG+R6MccH8wwQwmLg5oVBkFEZrnRE
|
||||||
|
pnRKBI0DvA+wk1aJFAPOI4d8Q5T7o/MyxH3f8TYGHqbeMQFCKwusnlWPRtrNdaIc
|
||||||
|
gaLvSpR0LVlroXGu8tYmRpvHPByoKGDbgVvO0Bwx8fmRAgMBAAGjUzBRMB0GA1Ud
|
||||||
|
DgQWBBR7r+mQWNUZ0TpQNwrwjgxgngvOjTAfBgNVHSMEGDAWgBR7r+mQWNUZ0TpQ
|
||||||
|
NwrwjgxgngvOjTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCO
|
||||||
|
7B4s6uQEGE8jg3CQgy76oU/D8sazGcP8+/E4JLHSc0Nj49w4ztSpkOVk2HyEtzbm
|
||||||
|
uR3TreIw+SfqpbiOI/ivVNDbEBsb/vEeq7qPzDH1Bi72plHZNRVhNGGV5rd7ibga
|
||||||
|
TkfXHKPM9yt8ffffHHiu1ROvb8gg2B6JbQwboU4hvvmmorW7onyTFSYEzZVdNSpv
|
||||||
|
pUtKPldxYjTnLlbsJdXC4xyCC4PrJt2CC0n0jsWfICJ77LMxIxTODh8oZNjbPg6r
|
||||||
|
RdI7U/DsD+R072DjbIcrivvigotJM+jihzz5inZwbO8o0WQOHAbJLIG3C3BnRW3A
|
||||||
|
Ek4u3+HXZMl5a0LGJ76u
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
14
checks/mumble/machines/peer2/peer_2_test_cert
Normal file
14
checks/mumble/machines/peer2/peer_2_test_cert
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICHjCCAaOgAwIBAgIJAKbMWefkf1rVMAoGCCqGSM49BAMCMEoxEjAQBgNVBAoT
|
||||||
|
CVN5bmN0aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBHZW5lcmF0ZWQxEjAQ
|
||||||
|
BgNVBAMTCXN5bmN0aGluZzAeFw0yMzEyMDYwMDAwMDBaFw00MzEyMDEwMDAwMDBa
|
||||||
|
MEoxEjAQBgNVBAoTCVN5bmN0aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBH
|
||||||
|
ZW5lcmF0ZWQxEjAQBgNVBAMTCXN5bmN0aGluZzB2MBAGByqGSM49AgEGBSuBBAAi
|
||||||
|
A2IABFZTMt4RfsfBue0va7QuNdjfXMI4HfZzJCEcG+b9MtV7FlDmwMKX5fgGykD9
|
||||||
|
FBbC7yiza3+xCobdMb5bakz1qYJ7nUFCv1mwSDo2eNM+/XE+rJmlre8NwkwGmvzl
|
||||||
|
h1uhyqNVMFMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr
|
||||||
|
BgEFBQcDAjAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCXN5bmN0aGluZzAKBggq
|
||||||
|
hkjOPQQDAgNpADBmAjEAwzhsroN6R4/quWeXj6dO5gt5CfSTLkLee6vrcuIP5i1U
|
||||||
|
rZvJ3OKQVmmGG6IWYe7iAjEAyuq3X2wznaqiw2YK3IDI4qVeYWpCUap0fwRNq7/x
|
||||||
|
4dC4k+BOzHcuJOwNBIY/bEuK
|
||||||
|
-----END CERTIFICATE-----
|
||||||
6
checks/mumble/machines/peer2/peer_2_test_key
Normal file
6
checks/mumble/machines/peer2/peer_2_test_key
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MIGkAgEBBDCXHGpvumKjjDRxB6SsjZOb7duw3w+rdlGQCJTIvRThLjD6zwjnyImi
|
||||||
|
7c3PD5nWtLqgBwYFK4EEACKhZANiAARWUzLeEX7HwbntL2u0LjXY31zCOB32cyQh
|
||||||
|
HBvm/TLVexZQ5sDCl+X4BspA/RQWwu8os2t/sQqG3TG+W2pM9amCe51BQr9ZsEg6
|
||||||
|
NnjTPv1xPqyZpa3vDcJMBpr85Ydboco=
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
1
checks/mumble/peer_1/key.age
Normal file
1
checks/mumble/peer_1/key.age
Normal file
@@ -0,0 +1 @@
|
|||||||
|
AGE-SECRET-KEY-1UCXEUJH6JXF8LFKWFHDM4N9AQE2CCGQZGXLUNV4TKR5KY0KC8FDQ2TY4NX
|
||||||
22
checks/mumble/peer_1/peer_1_test_cert
Normal file
22
checks/mumble/peer_1/peer_1_test_cert
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDazCCAlOgAwIBAgIUCUjfNkF0CDhTKbO3nNczcsCW4qEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA2MjcwOTM2NDZaFw0yNDA3
|
||||||
|
MjcwOTM2NDZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQDCcdZEJvXJIeOKO5pF5XUFvUeJtCCiwfWvWS662bxc
|
||||||
|
R/5MZucRLqfTNYo9aBv4NITw5kxZsTaaubmS4zSGQoTEAVzqzVdi3a/gNvsdVLb+
|
||||||
|
7CivpmweLllX/OGsTL0kHPEI+74AYiTBjXfdWV1Y5T1tuwc3G8ATrguQ33Uo5vvF
|
||||||
|
vcqsbTKcRZC0pB9O/nn4q03GsRdvlpaKakIhjMpRG/uZ3u7wtbyZ+WqjsjxZNfnY
|
||||||
|
aMyPoaipFqX1v+L7GKlOj2NpyEZFVVwa2ZqhVSYXyDfpAWQFznwKGzD5mjtcyKym
|
||||||
|
gnv/5LwrpH4Xj+JMt48hN+rPnu5vfXT8Y4KnID30OQW7AgMBAAGjUzBRMB0GA1Ud
|
||||||
|
DgQWBBQBBO8Wp975pAGioMjkaxANAVInfzAfBgNVHSMEGDAWgBQBBO8Wp975pAGi
|
||||||
|
oMjkaxANAVInfzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAg
|
||||||
|
F40MszTZXpR/A1z9B1CcXH47tNK67f8bCMR2dhvXODbpatwSihyxhQjtLb5R6kYH
|
||||||
|
5Yq/B4yrh303j0CXaobCQ4nQH7zI7fhViww+TzW7vDhgM7ueEyyXrqCXt6JY8avg
|
||||||
|
TuvIRtJSeWSQJ5aLNaYqmiwMf/tj9W3BMDpctGyLqu1WTSrbpYa9mA5Vudud70Yz
|
||||||
|
DgZ/aqHilB07cVNqzVYZzRZ56WJlTjGzVevRgnHZqPiZNVrU13H6gtWa3r8aV4Gj
|
||||||
|
i4F663eRAttj166cRgfl1QqpSG2IprNyV9UfuS2LlUaVNT3y0idawiJ4HhaA8pGB
|
||||||
|
ZqMUUkA4DSucb6xxEcTK
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
28
checks/mumble/peer_1/peer_1_test_key
Normal file
28
checks/mumble/peer_1/peer_1_test_key
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCcdZEJvXJIeOK
|
||||||
|
O5pF5XUFvUeJtCCiwfWvWS662bxcR/5MZucRLqfTNYo9aBv4NITw5kxZsTaaubmS
|
||||||
|
4zSGQoTEAVzqzVdi3a/gNvsdVLb+7CivpmweLllX/OGsTL0kHPEI+74AYiTBjXfd
|
||||||
|
WV1Y5T1tuwc3G8ATrguQ33Uo5vvFvcqsbTKcRZC0pB9O/nn4q03GsRdvlpaKakIh
|
||||||
|
jMpRG/uZ3u7wtbyZ+WqjsjxZNfnYaMyPoaipFqX1v+L7GKlOj2NpyEZFVVwa2Zqh
|
||||||
|
VSYXyDfpAWQFznwKGzD5mjtcyKymgnv/5LwrpH4Xj+JMt48hN+rPnu5vfXT8Y4Kn
|
||||||
|
ID30OQW7AgMBAAECggEAGVKn+/Iy+kG+l2cRvV6XseqnoWhjA69M5swviMgIfuAl
|
||||||
|
Xx/boeI4mwoS+dJQKi/0zEbB1MB+gwIDB/0s/vs0vS4MQswBQG/skr+2TmiU+Hgb
|
||||||
|
CF0dIYUZv5rAbScFTumx/mCCqxwc+1QIMzyLKqOYL203EFc92ZJGEVT4th321haZ
|
||||||
|
8Wd+dllcYAb7BbEeBhCrTqRe9T3zt5reZgtZTquTF5hGm8EAyBp6rLjZK7dyZ9dd
|
||||||
|
gyIsDbWgPC9vkRc6x/eANn70hgDbYOuoXwAP/qIFnWLL1Zzy8LKUyOsSgQ91S3S3
|
||||||
|
Il4Lt6lEyU3+61MsCYss7jDoP/7REEjz5h6gfxlFSQKBgQD9u8nhHuwte4/d9VNU
|
||||||
|
rhSBW9h8IJzwPif/eS8vh9VaS2SjR2dDCcHg6rGYKnexeEzUcx56aQMA+p3nRJwy
|
||||||
|
Uwnx5BfEWs9FO6yPR8VEI0a2sBp+hoWKJX/Lvat+QCs6IFuGmlQpczD7/RYAkhG4
|
||||||
|
mwyt/ymqzjukb9mFaeYIltOfPwKBgQDELnkH1ChTUH5u3HgDoelFbzR18okz6dxH
|
||||||
|
urMbfZMAl8W5h2zAvHsAX5qxyHHankOUsiH2y3BrAgqQtTuIA2a5W7j+yHBkYiEZ
|
||||||
|
EUNeI9YNA0KU+wwZpVVvRGUsRB5SUBo5LlcSYmX/V32f0oU5Np44i0vjl3Ju8esx
|
||||||
|
2MLfj1A2hQKBgQDCxtZZZ0h8Pb8Z7wpSFfQNvXi5CLwQvFYuClQLk6VXVErkAJsn
|
||||||
|
XiUjyGYeXnNVm/i2mcyKwXQZ20k90HBrPU2ED8mi5Ob5ya5Uqw6mmMHe2d7sw81d
|
||||||
|
WB37RBWSrCXC0DYSZQQ4cYHn3sd2Fqtd4EBijV7qDLjCKU582OdKLqYzNwKBgH31
|
||||||
|
UKQkJZgIkIThbPT4GewI0GgCRvFb76DmUGUQJTg2Oi86siq1WUwOFiabie5RuxZX
|
||||||
|
oNLyH8W008/BbO2RMX1FVOvRCciJ8LJFkTl6TM6iDzfUUBqPOuFryoG3Yrh60btw
|
||||||
|
81rMbqyZIgFhi0QGu2OWnC0Oadyt2tJwV/5t55R5AoGBAPspZttDmOzVkAJDSn9Z
|
||||||
|
iByYt1KmwBQ6l7LpFg33a7ds9zWqW4+i6r0PzXvSewf/z69L0cAywSk5CaJJjDso
|
||||||
|
dTlNMqwux01wd6V+nQGR871xnsOg+qzgJ565TJZelWgRmNRUooi4DMp5POJA33xp
|
||||||
|
rqAISUfW0w2S+q7/5Lm0QiJE
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
22
checks/mumble/peer_2/peer_2_test_cert
Normal file
22
checks/mumble/peer_2/peer_2_test_cert
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDazCCAlOgAwIBAgIUfENbTtH5nr7giuawwQpDYqUpWJswDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||||
|
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA2MjcwOTQxNDNaFw0yNDA3
|
||||||
|
MjcwOTQxNDNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQCfP6cZhCs9jOnWqyQP12vrOOxlBrWofYZFf9amUA24
|
||||||
|
AfE7oGcSfkylanmkxzvGqQkhgLAvkHZj/GEvHujKyy8PgcEGP+pwmsfWNQMvU0Dz
|
||||||
|
j3syjWOTi3eIC/3DoUnHlWCT2qCil/bjqxgU1l7fO/OXUlq5kyvIjln7Za4sUHun
|
||||||
|
ixe/m96Er6l8a4Mh2pxh2C5pkLCvulkQhjjGG+R6MccH8wwQwmLg5oVBkFEZrnRE
|
||||||
|
pnRKBI0DvA+wk1aJFAPOI4d8Q5T7o/MyxH3f8TYGHqbeMQFCKwusnlWPRtrNdaIc
|
||||||
|
gaLvSpR0LVlroXGu8tYmRpvHPByoKGDbgVvO0Bwx8fmRAgMBAAGjUzBRMB0GA1Ud
|
||||||
|
DgQWBBR7r+mQWNUZ0TpQNwrwjgxgngvOjTAfBgNVHSMEGDAWgBR7r+mQWNUZ0TpQ
|
||||||
|
NwrwjgxgngvOjTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCO
|
||||||
|
7B4s6uQEGE8jg3CQgy76oU/D8sazGcP8+/E4JLHSc0Nj49w4ztSpkOVk2HyEtzbm
|
||||||
|
uR3TreIw+SfqpbiOI/ivVNDbEBsb/vEeq7qPzDH1Bi72plHZNRVhNGGV5rd7ibga
|
||||||
|
TkfXHKPM9yt8ffffHHiu1ROvb8gg2B6JbQwboU4hvvmmorW7onyTFSYEzZVdNSpv
|
||||||
|
pUtKPldxYjTnLlbsJdXC4xyCC4PrJt2CC0n0jsWfICJ77LMxIxTODh8oZNjbPg6r
|
||||||
|
RdI7U/DsD+R072DjbIcrivvigotJM+jihzz5inZwbO8o0WQOHAbJLIG3C3BnRW3A
|
||||||
|
Ek4u3+HXZMl5a0LGJ76u
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
28
checks/mumble/peer_2/peer_2_test_key
Normal file
28
checks/mumble/peer_2/peer_2_test_key
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCfP6cZhCs9jOnW
|
||||||
|
qyQP12vrOOxlBrWofYZFf9amUA24AfE7oGcSfkylanmkxzvGqQkhgLAvkHZj/GEv
|
||||||
|
HujKyy8PgcEGP+pwmsfWNQMvU0Dzj3syjWOTi3eIC/3DoUnHlWCT2qCil/bjqxgU
|
||||||
|
1l7fO/OXUlq5kyvIjln7Za4sUHunixe/m96Er6l8a4Mh2pxh2C5pkLCvulkQhjjG
|
||||||
|
G+R6MccH8wwQwmLg5oVBkFEZrnREpnRKBI0DvA+wk1aJFAPOI4d8Q5T7o/MyxH3f
|
||||||
|
8TYGHqbeMQFCKwusnlWPRtrNdaIcgaLvSpR0LVlroXGu8tYmRpvHPByoKGDbgVvO
|
||||||
|
0Bwx8fmRAgMBAAECggEACAkjOnNj5zA0IIP0RuRc6rqtmw9ynTTwUJN51lyVxKI8
|
||||||
|
dQDMEq/S2En+J2VyS7z92/XtbgkBIFx83u7VWl5UWpj2j4UsJFB7IwD7zyiJT4D+
|
||||||
|
+3cM/kX8Wx4XyQZbfbm47N0MXAgFCkn45hxHH0acLReXwmN9wxoDyl7AIjZRdwvG
|
||||||
|
Qq0rnOnIc8kkkew7L6AiFwQS8b77eyzua3d6moKXN9hU/kfiJ6YUFG/WLe0pmQA1
|
||||||
|
HbF27YghfeLnYUt50oDuX6jF6CzQhflchWVq/wn8/cxEpg/RMicWE8ulrTk7o27l
|
||||||
|
JwCrHrhYEBsPuZO4mxX/DHrAMmhTeFjLaV5bQlz0PQKBgQDgRPSOEixYnKz9iPs/
|
||||||
|
EDTlji5LA3Rm6TytRCNsjYY6Trw60KcvYqwyDUCiEjruvOQ9mqgBiQm1VHSalrG3
|
||||||
|
RcbVfpEMouyZbEwmTjS8KdOi5x4Z6AX+4yWDN31jX3b8sktgbxV/HRdg3sA3q7MJ
|
||||||
|
vExTUuoXg57W+FepIZ+XlhSoQwKBgQC1x6UMAlAeW45/yUUm/LFRcCgb/bdCQx+e
|
||||||
|
hSb8w3jdvVoNWgx1j7RsjjFKaZUnseK3qQvVfCm4Qjvlz6MpKDxslaUYuR162Ku0
|
||||||
|
e153z/xc7XRoXyPyPLdGZFlWii30jirB7ZqPdyz6mwlWwqdImNerbUqdFt9R8bId
|
||||||
|
pYsyHB5zmwKBgBjYCq9iW/9E+/TqI8sMpI95fK9app5v4AThs3rnAqOa7Ucmrh6V
|
||||||
|
s7Wnui06D8U6r54Tb+EbqTOpM3Gcl/tRg4FLEA5yTfuA/76Ok1D04Tj+mVsNVPyz
|
||||||
|
dQhgMUe835WGusroA12df2V/x5NjNeYyMdJZMQ2ByyrNQAjAbMmCGq+5AoGBAIj8
|
||||||
|
ERFysMOfxUvg9b7CkDFJrsAhOzew86P2vYGfIHchGTqUkG0LRTDFGrnzxNXsBGjY
|
||||||
|
+DUB40Kajx7IkTETxC0jvA1ceq23l/VjPrZVQt0YiC+a+rCyNn7SYkyHxsfTVr9b
|
||||||
|
ea0BZyDXMntyJrPbkjL6Ik8tDE9pLwuOU84ISJ5fAoGAZ2+Ams/VhdZj/wpRpMky
|
||||||
|
K4jtS4nzbCmJzzTa6vdVV7Kjer5kFxSFFqMrS/FtJ/RxHeHvxdze9dfGu9jIdTKK
|
||||||
|
vSzbyQdHFfZgRkmAKfcoN9u567z7Oc74AQ9UgFEGdEVFQUbfWOevmr8KIPt8nDQK
|
||||||
|
J9HuVfILi1kH0jzDd/64TvA=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"publickey": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00",
|
"publickey": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00",
|
||||||
"type": "age"
|
"type": "age"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"publickey": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00",
|
"publickey": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00",
|
||||||
"type": "age"
|
"type": "age"
|
||||||
}
|
}
|
||||||
|
|||||||
6
clanModules/dyndns/README.md
Normal file
6
clanModules/dyndns/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
description = "A dynamic DNS service to update domain IPs"
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
265
clanModules/dyndns/default.nix
Normal file
265
clanModules/dyndns/default.nix
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
name = "dyndns";
|
||||||
|
cfg = config.clan.${name};
|
||||||
|
|
||||||
|
# We dedup secrets if they have the same provider + base domain
|
||||||
|
secret_id = opt: "${name}-${opt.provider}-${opt.domain}";
|
||||||
|
secret_path =
|
||||||
|
opt: config.clan.core.facts.services."${secret_id opt}".secret."${secret_id opt}".path;
|
||||||
|
|
||||||
|
# We check that a secret has not been set in extraSettings.
|
||||||
|
extraSettingsSafe =
|
||||||
|
opt:
|
||||||
|
if (builtins.hasAttr opt.secret_field_name opt.extraSettings) then
|
||||||
|
throw "Please do not set ${opt.secret_field_name} in extraSettings, it is automatically set by the dyndns module."
|
||||||
|
else
|
||||||
|
opt.extraSettings;
|
||||||
|
/*
|
||||||
|
We go from:
|
||||||
|
{home.example.com:{value:{domain:example.com,host:home, provider:namecheap}}}
|
||||||
|
To:
|
||||||
|
{settings: [{domain: example.com, host: home, provider: namecheap, password: dyndns-namecheap-example.com}]}
|
||||||
|
*/
|
||||||
|
service_config = {
|
||||||
|
settings = builtins.catAttrs "value" (
|
||||||
|
builtins.attrValues (
|
||||||
|
lib.mapAttrs (_: opt: {
|
||||||
|
value =
|
||||||
|
(extraSettingsSafe opt)
|
||||||
|
// {
|
||||||
|
domain = opt.domain;
|
||||||
|
provider = opt.provider;
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
"${opt.secret_field_name}" = secret_id opt;
|
||||||
|
};
|
||||||
|
}) cfg.settings
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
secret_generator = _: opt: {
|
||||||
|
name = secret_id opt;
|
||||||
|
value = {
|
||||||
|
secret.${secret_id opt} = { };
|
||||||
|
generator.prompt = "Dyndns passphrase for ${secret_id opt}";
|
||||||
|
generator.script = ''
|
||||||
|
echo "$prompt_value" > $secrets/${secret_id opt}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.clan.${name} = {
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = name;
|
||||||
|
description = "User to run the service as";
|
||||||
|
};
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = name;
|
||||||
|
description = "Group to run the service as";
|
||||||
|
};
|
||||||
|
|
||||||
|
server = {
|
||||||
|
enable = lib.mkEnableOption "dyndns webserver";
|
||||||
|
domain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Domain to serve the webservice on";
|
||||||
|
};
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 54805;
|
||||||
|
description = "Port to listen on";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
period = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 5;
|
||||||
|
description = "Domain update period in minutes";
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule (
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
provider = lib.mkOption {
|
||||||
|
example = "namecheap";
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The dyndns provider to use";
|
||||||
|
};
|
||||||
|
domain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "example.com";
|
||||||
|
description = "The top level domain to update.";
|
||||||
|
};
|
||||||
|
secret_field_name = lib.mkOption {
|
||||||
|
example = [
|
||||||
|
"password"
|
||||||
|
"api_key"
|
||||||
|
];
|
||||||
|
type = lib.types.enum [
|
||||||
|
"password"
|
||||||
|
"token"
|
||||||
|
"api_key"
|
||||||
|
];
|
||||||
|
default = "password";
|
||||||
|
description = "The field name for the secret";
|
||||||
|
};
|
||||||
|
# TODO: Ideally we would create a gigantic list of all possible settings / types
|
||||||
|
# optimally we would have a way to generate the options from the source code
|
||||||
|
extraSettings = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf lib.types.str;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Extra settings for the provider.
|
||||||
|
Provider specific settings: https://github.com/qdm12/ddns-updater#configuration
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
default = [ ];
|
||||||
|
description = "Configuration for which domains to update";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
(lib.mkRemovedOptionModule [
|
||||||
|
"clan"
|
||||||
|
"dyndns"
|
||||||
|
"enable"
|
||||||
|
] "Just define clan.dyndns.settings to enable it")
|
||||||
|
];
|
||||||
|
|
||||||
|
config = lib.mkMerge [
|
||||||
|
(lib.mkIf (cfg.settings != { }) {
|
||||||
|
clan.core.facts.services = lib.mapAttrs' secret_generator cfg.settings;
|
||||||
|
|
||||||
|
users.groups.${cfg.group} = { };
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
group = cfg.group;
|
||||||
|
isSystemUser = true;
|
||||||
|
description = "User for ${name} service";
|
||||||
|
home = "/var/lib/${name}";
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = lib.mkIf cfg.server.enable [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
|
||||||
|
services.nginx = lib.mkIf cfg.server.enable {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts = {
|
||||||
|
"${cfg.server.domain}" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://localhost:${toString cfg.server.port}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.${name} = {
|
||||||
|
path = [ ];
|
||||||
|
description = "Dynamic DNS updater";
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
environment = {
|
||||||
|
MYCONFIG = "${builtins.toJSON service_config}";
|
||||||
|
SERVER_ENABLED = if cfg.server.enable then "yes" else "no";
|
||||||
|
PERIOD = "${toString cfg.period}m";
|
||||||
|
LISTENING_ADDRESS = ":${toString cfg.server.port}";
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig =
|
||||||
|
let
|
||||||
|
pyscript = pkgs.writers.writePyPy3Bin "test.py" { libraries = [ ]; } ''
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
cred_dir = Path(os.getenv("CREDENTIALS_DIRECTORY"))
|
||||||
|
config_str = os.getenv("MYCONFIG")
|
||||||
|
|
||||||
|
|
||||||
|
def get_credential(name):
|
||||||
|
secret_p = cred_dir / name
|
||||||
|
with open(secret_p, 'r') as f:
|
||||||
|
return f.read().strip()
|
||||||
|
|
||||||
|
|
||||||
|
config = json.loads(config_str)
|
||||||
|
print(f"Config: {config}")
|
||||||
|
for attrset in config["settings"]:
|
||||||
|
if "password" in attrset:
|
||||||
|
attrset['password'] = get_credential(attrset['password'])
|
||||||
|
elif "token" in attrset:
|
||||||
|
attrset['token'] = get_credential(attrset['token'])
|
||||||
|
elif "api_key" in attrset:
|
||||||
|
attrset['api_key'] = get_credential(attrset['api_key'])
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Missing secret field in {attrset}")
|
||||||
|
|
||||||
|
# create directory data if it does not exist
|
||||||
|
data_dir = Path('data')
|
||||||
|
data_dir.mkdir(mode=0o770, exist_ok=True)
|
||||||
|
|
||||||
|
# Write the config with secrets back
|
||||||
|
config_path = data_dir / 'config.json'
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
f.write(json.dumps(config, indent=4))
|
||||||
|
|
||||||
|
# Set file permissions to read and write
|
||||||
|
# only by the user and group
|
||||||
|
config_path.chmod(0o660)
|
||||||
|
|
||||||
|
# Set file permissions to read
|
||||||
|
# and write only by the user and group
|
||||||
|
for file in data_dir.iterdir():
|
||||||
|
file.chmod(0o660)
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
ExecStartPre = lib.getExe pyscript;
|
||||||
|
ExecStart = lib.getExe pkgs.ddns-updater;
|
||||||
|
LoadCredential = lib.mapAttrsToList (_: opt: "${secret_id opt}:${secret_path opt}") cfg.settings;
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ReadOnlyPaths = "/";
|
||||||
|
PrivateDevices = "yes";
|
||||||
|
ProtectKernelModules = "yes";
|
||||||
|
ProtectKernelTunables = "yes";
|
||||||
|
|
||||||
|
WorkingDirectory = "/var/lib/${name}";
|
||||||
|
ReadWritePaths = [
|
||||||
|
"/proc/self"
|
||||||
|
"/var/lib/${name}"
|
||||||
|
];
|
||||||
|
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = 60;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -4,18 +4,23 @@
|
|||||||
borgbackup = ./borgbackup;
|
borgbackup = ./borgbackup;
|
||||||
borgbackup-static = ./borgbackup-static;
|
borgbackup-static = ./borgbackup-static;
|
||||||
deltachat = ./deltachat;
|
deltachat = ./deltachat;
|
||||||
|
dyndns = ./dyndns;
|
||||||
ergochat = ./ergochat;
|
ergochat = ./ergochat;
|
||||||
|
garage = ./garage;
|
||||||
|
golem-provider = ./golem-provider;
|
||||||
|
iwd = ./iwd;
|
||||||
localbackup = ./localbackup;
|
localbackup = ./localbackup;
|
||||||
localsend = ./localsend;
|
localsend = ./localsend;
|
||||||
single-disk = ./single-disk;
|
|
||||||
matrix-synapse = ./matrix-synapse;
|
matrix-synapse = ./matrix-synapse;
|
||||||
moonlight = ./moonlight;
|
moonlight = ./moonlight;
|
||||||
|
mumble = ./mumble;
|
||||||
packages = ./packages;
|
packages = ./packages;
|
||||||
postgresql = ./postgresql;
|
postgresql = ./postgresql;
|
||||||
root-password = ./root-password;
|
root-password = ./root-password;
|
||||||
|
single-disk = ./single-disk;
|
||||||
sshd = ./sshd;
|
sshd = ./sshd;
|
||||||
sunshine = ./sunshine;
|
|
||||||
static-hosts = ./static-hosts;
|
static-hosts = ./static-hosts;
|
||||||
|
sunshine = ./sunshine;
|
||||||
syncthing = ./syncthing;
|
syncthing = ./syncthing;
|
||||||
syncthing-static-peers = ./syncthing-static-peers;
|
syncthing-static-peers = ./syncthing-static-peers;
|
||||||
thelounge = ./thelounge;
|
thelounge = ./thelounge;
|
||||||
|
|||||||
10
clanModules/garage/README.md
Normal file
10
clanModules/garage/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description = "S3-compatible object store for small self-hosted geo-distributed deployments"
|
||||||
|
---
|
||||||
|
|
||||||
|
This module generates garage specific keys automatically.
|
||||||
|
When using garage in a distributed deployment the `rpc_key` between connected instances must be shared.
|
||||||
|
This is currently still a manual process.
|
||||||
|
|
||||||
|
Options: [NixosModuleOptions](https://search.nixos.org/options?channel=unstable&size=50&sort=relevance&type=packages&query=garage)
|
||||||
|
Documentation: https://garagehq.deuxfleurs.fr/
|
||||||
33
clanModules/garage/default.nix
Normal file
33
clanModules/garage/default.nix
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
systemd.services.garage.serviceConfig = {
|
||||||
|
LoadCredential = [
|
||||||
|
"rpc_secret_path:${config.clan.core.vars.generators.garage.files.rpc_secret.path}"
|
||||||
|
"admin_token_path:${config.clan.core.vars.generators.garage.files.admin_token.path}"
|
||||||
|
"metrics_token_path:${config.clan.core.vars.generators.garage.files.metrics_token.path}"
|
||||||
|
];
|
||||||
|
Environment = [
|
||||||
|
"GARAGE_ALLOW_WORLD_READABLE_SECRETS=true"
|
||||||
|
"GARAGE_RPC_SECRET_FILE=%d/rpc_secret_path"
|
||||||
|
"GARAGE_ADMIN_TOKEN_FILE=%d/admin_token_path"
|
||||||
|
"GARAGE_METRICS_TOKEN_FILE=%d/metrics_token_path"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
clan.core.vars.generators.garage = {
|
||||||
|
files.rpc_secret = { };
|
||||||
|
files.admin_token = { };
|
||||||
|
files.metrics_token = { };
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.openssl
|
||||||
|
];
|
||||||
|
script = ''
|
||||||
|
openssl rand -hex -out $out/rpc_secret 32
|
||||||
|
openssl rand -base64 -out $out/admin_token 32
|
||||||
|
openssl rand -base64 -out $out/metrics_token 32
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
clan.core.state.garage.folders = [ config.services.garage.settings.metadata_dir ];
|
||||||
|
}
|
||||||
7
clanModules/golem-provider/README.md
Normal file
7
clanModules/golem-provider/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
description = "Golem Provider for the Golem Network, an open-source and decentralized platform where everyone can use and share each other's computing power without relying on centralized entities like cloud computing corporations"
|
||||||
|
---
|
||||||
|
|
||||||
|
By running a golem provider your machine's compute resources are offered via the golem network which will allow other members to execute compute tasks on your machine. If this happens, you will be compensated with GLM, an ERC20 token.
|
||||||
|
|
||||||
|
More about golem providers: https://docs.golem.network/docs/golem/overview
|
||||||
34
clanModules/golem-provider/default.nix
Normal file
34
clanModules/golem-provider/default.nix
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.clan.golem-provider;
|
||||||
|
yagna = pkgs.callPackage ../../pkgs/yagna { };
|
||||||
|
accountFlag = if cfg.account != null then "--account ${cfg.account}" else "";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [ ./interface.nix ];
|
||||||
|
|
||||||
|
users.users.golem = {
|
||||||
|
isSystemUser = true;
|
||||||
|
home = "/var/lib/golem";
|
||||||
|
group = "golem";
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.golem = { };
|
||||||
|
|
||||||
|
environment.systemPackages = [ yagna ];
|
||||||
|
|
||||||
|
systemd.services.golem-provider = {
|
||||||
|
description = "Golem Provider";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = "${yagna}/bin/golemsp run --no-interactive ${accountFlag}";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "5";
|
||||||
|
User = "golem";
|
||||||
|
Group = "golem";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
20
clanModules/golem-provider/interface.nix
Normal file
20
clanModules/golem-provider/interface.nix
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) mkOption;
|
||||||
|
|
||||||
|
inherit (lib.types) nullOr str;
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.clan.golem-provider = {
|
||||||
|
account = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
description = ''
|
||||||
|
Ethereum address for payouts.
|
||||||
|
|
||||||
|
Leave empty to automatically generate a new address upon first start.
|
||||||
|
'';
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
4
clanModules/golem-provider/test/vm.nix
Normal file
4
clanModules/golem-provider/test/vm.nix
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [ ../. ];
|
||||||
|
}
|
||||||
6
clanModules/iwd/README.md
Normal file
6
clanModules/iwd/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
description = "Automatically provisions wifi credentials"
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
83
clanModules/iwd/default.nix
Normal file
83
clanModules/iwd/default.nix
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{ lib, config, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.clan.iwd;
|
||||||
|
secret_path = ssid: config.clan.core.facts.services."iwd.${ssid}".secret."iwd.${ssid}".path;
|
||||||
|
secret_generator = name: value: {
|
||||||
|
name = "iwd.${value.ssid}";
|
||||||
|
value =
|
||||||
|
let
|
||||||
|
secret_name = "iwd.${value.ssid}";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
secret.${secret_name} = { };
|
||||||
|
generator.prompt = "Wifi password for '${value.ssid}'";
|
||||||
|
generator.script = ''
|
||||||
|
config="
|
||||||
|
[Security]
|
||||||
|
Passphrase=$prompt_value
|
||||||
|
"
|
||||||
|
echo "$config" > $secrets/${secret_name}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.clan.iwd = {
|
||||||
|
networks = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule (
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
ssid = lib.mkOption {
|
||||||
|
type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
|
||||||
|
default = name;
|
||||||
|
description = "The name of the wifi network";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
description = "Wifi networks to predefine";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
(lib.mkRemovedOptionModule [
|
||||||
|
"clan"
|
||||||
|
"iwd"
|
||||||
|
"enable"
|
||||||
|
] "Just define clan.iwd.networks to enable it")
|
||||||
|
];
|
||||||
|
|
||||||
|
config = lib.mkMerge [
|
||||||
|
(lib.mkIf (cfg.networks != { }) {
|
||||||
|
# Systemd tmpfiles rule to create /var/lib/iwd/example.psk file
|
||||||
|
systemd.tmpfiles.rules = lib.mapAttrsToList (
|
||||||
|
_: value: "C /var/lib/iwd/${value.ssid}.psk 0600 root root - ${secret_path value.ssid}"
|
||||||
|
) cfg.networks;
|
||||||
|
|
||||||
|
clan.core.facts.services = lib.mapAttrs' secret_generator cfg.networks;
|
||||||
|
|
||||||
|
# TODO: restart the iwd.service if something changes
|
||||||
|
})
|
||||||
|
{
|
||||||
|
# disable wpa supplicant
|
||||||
|
networking.wireless.enable = false;
|
||||||
|
|
||||||
|
# Use iwd instead of wpa_supplicant. It has a user friendly CLI
|
||||||
|
networking.wireless.iwd = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
Network = {
|
||||||
|
EnableIPv6 = true;
|
||||||
|
RoutePriorityOffset = 300;
|
||||||
|
};
|
||||||
|
Settings.AutoConnect = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -169,6 +169,11 @@ in
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
enable = true;
|
enable = true;
|
||||||
virtualHosts = {
|
virtualHosts = {
|
||||||
|
|||||||
14
clanModules/mumble/README.md
Normal file
14
clanModules/mumble/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
description = "Open Source, Low Latency, High Quality Voice Chat."
|
||||||
|
categories = ["chat", "voice"]
|
||||||
|
---
|
||||||
|
The mumble clan module gives you:
|
||||||
|
|
||||||
|
- True low latency voice communication.
|
||||||
|
- Secure, authenticated encryption.
|
||||||
|
- Free software.
|
||||||
|
- Backed by a large and active open-source community.
|
||||||
|
|
||||||
|
This all set up in a way that allows peer-to-peer hosting.
|
||||||
|
Every machine inside the clan can be a host for mumble,
|
||||||
|
and thus it doesn't matter who in the network is online - as long as two people are online they are able to chat with each other.
|
||||||
104
clanModules/mumble/default.nix
Normal file
104
clanModules/mumble/default.nix
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
clanDir = config.clan.core.clanDir;
|
||||||
|
machineDir = clanDir + "/machines/";
|
||||||
|
machinesFileSet = builtins.readDir machineDir;
|
||||||
|
machines = lib.mapAttrsToList (name: _: name) machinesFileSet;
|
||||||
|
machineJson = builtins.toJSON machines;
|
||||||
|
certificateMachinePath = machines: machineDir + "/${machines}" + "/facts/mumble-cert";
|
||||||
|
certificatesUnchecked = builtins.map (
|
||||||
|
machine:
|
||||||
|
let
|
||||||
|
fullPath = certificateMachinePath machine;
|
||||||
|
in
|
||||||
|
if builtins.pathExists fullPath then machine else null
|
||||||
|
) machines;
|
||||||
|
certificate = lib.filter (machine: machine != null) certificatesUnchecked;
|
||||||
|
machineCert = builtins.map (
|
||||||
|
machine: (lib.nameValuePair machine (builtins.readFile (certificateMachinePath machine)))
|
||||||
|
) certificate;
|
||||||
|
machineCertJson = builtins.toJSON machineCert;
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.clan.services.mumble = {
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "alice";
|
||||||
|
description = "The user mumble should be set up for.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
services.murmur = {
|
||||||
|
enable = true;
|
||||||
|
logDays = -1;
|
||||||
|
registerName = config.clan.core.machineName;
|
||||||
|
openFirewall = true;
|
||||||
|
bonjour = true;
|
||||||
|
sslKey = config.clan.core.facts.services.mumble.secret.mumble-key.path;
|
||||||
|
sslCert = config.clan.core.facts.services.mumble.public.mumble-cert.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
clan.core.state.mumble.folders = [
|
||||||
|
"/var/lib/mumble"
|
||||||
|
"/var/lib/murmur"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '/var/lib/mumble' 0770 '${config.clan.services.mumble.user}' 'users' - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages =
|
||||||
|
let
|
||||||
|
mumbleCfgDir = "/var/lib/mumble";
|
||||||
|
mumbleDatabasePath = "${mumbleCfgDir}/mumble.sqlite";
|
||||||
|
mumbleCfgPath = "/var/lib/mumble/mumble_settings.json";
|
||||||
|
populate-channels = pkgs.writers.writePython3 "mumble-populate-channels" {
|
||||||
|
libraries = [
|
||||||
|
pkgs.python3Packages.cryptography
|
||||||
|
pkgs.python3Packages.pyopenssl
|
||||||
|
];
|
||||||
|
flakeIgnore = [
|
||||||
|
# We don't live in the dark ages anymore.
|
||||||
|
# Languages like Python that are whitespace heavy will overrun
|
||||||
|
# 79 characters..
|
||||||
|
"E501"
|
||||||
|
];
|
||||||
|
} (builtins.readFile ./mumble-populate-channels.py);
|
||||||
|
mumble = pkgs.writeShellScriptBin "mumble" ''
|
||||||
|
set -xeu
|
||||||
|
mkdir -p ${mumbleCfgDir}
|
||||||
|
pushd "${mumbleCfgDir}"
|
||||||
|
XDG_DATA_HOME=${mumbleCfgDir}
|
||||||
|
XDG_DATA_DIR=${mumbleCfgDir}
|
||||||
|
${populate-channels} --ensure-config '${mumbleCfgPath}' --db-location ${mumbleDatabasePath}
|
||||||
|
echo ${machineCertJson}
|
||||||
|
${populate-channels} --machines '${machineJson}' --username ${config.clan.core.machineName} --db-location ${mumbleDatabasePath}
|
||||||
|
${populate-channels} --servers '${machineCertJson}' --username ${config.clan.core.machineName} --db-location ${mumbleDatabasePath} --cert True
|
||||||
|
${pkgs.mumble}/bin/mumble --config ${mumbleCfgPath} "$@"
|
||||||
|
popd
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
[ mumble ];
|
||||||
|
|
||||||
|
clan.core.facts.services.mumble = {
|
||||||
|
secret.mumble-key = { };
|
||||||
|
public.mumble-cert = { };
|
||||||
|
generator.path = [
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.openssl
|
||||||
|
];
|
||||||
|
generator.script = ''
|
||||||
|
openssl genrsa -out $secrets/mumble-key 2048
|
||||||
|
openssl req -new -x509 -key $secrets/mumble-key -out $facts/mumble-cert
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
249
clanModules/mumble/mumble-populate-channels.py
Normal file
249
clanModules/mumble/mumble-populate-channels.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_config(path: str, db_path: str) -> None:
|
||||||
|
# Default JSON structure if the file doesn't exist
|
||||||
|
default_json = {
|
||||||
|
"misc": {
|
||||||
|
"audio_wizard_has_been_shown": True,
|
||||||
|
"database_location": db_path,
|
||||||
|
"viewed_server_ping_consent_message": True,
|
||||||
|
},
|
||||||
|
"settings_version": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the file exists
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path) as file:
|
||||||
|
data = json.load(file)
|
||||||
|
else:
|
||||||
|
data = default_json
|
||||||
|
# Create the file with default JSON structure
|
||||||
|
with open(path, "w") as file:
|
||||||
|
json.dump(data, file, indent=4)
|
||||||
|
|
||||||
|
# TODO: make sure to only update the diff
|
||||||
|
updated_data = {**default_json, **data}
|
||||||
|
|
||||||
|
# Write the modified JSON object back to the file
|
||||||
|
with open(path, "w") as file:
|
||||||
|
json.dump(updated_data, file, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_database(db_location: str) -> None:
|
||||||
|
"""
|
||||||
|
Initializes the database. If the database or the servers table does not exist, it creates them.
|
||||||
|
|
||||||
|
:param db_location: The path to the SQLite database
|
||||||
|
"""
|
||||||
|
conn = sqlite3.connect(db_location)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create the servers table if it doesn't exist
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS servers (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
hostname TEXT NOT NULL,
|
||||||
|
port INTEGER NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
url TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Commit the changes
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"An error occurred while initializing the database: {e}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_certificates(
|
||||||
|
db_location: str, hostname: str, port: str, digest: str
|
||||||
|
) -> None:
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect(db_location)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create a cursor object
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# TODO: check if cert already there
|
||||||
|
# if server_check(cursor, name, hostname):
|
||||||
|
# print(
|
||||||
|
# f"Server with name '{name}' and hostname '{hostname}' already exists."
|
||||||
|
# )
|
||||||
|
# return
|
||||||
|
|
||||||
|
# SQL command to insert data into the servers table
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO cert (hostname, port, digest)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Data to be inserted
|
||||||
|
data = (hostname, port, digest)
|
||||||
|
|
||||||
|
# Execute the insert command with the provided data
|
||||||
|
cursor.execute(insert_query, data)
|
||||||
|
|
||||||
|
# Commit the changes
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print("Data has been successfully inserted.")
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
# Close the connection
|
||||||
|
conn.close()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_digest(cert: str) -> str:
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
|
||||||
|
cert = cert.strip()
|
||||||
|
cert = cert.encode("utf-8")
|
||||||
|
cert = x509.load_pem_x509_certificate(cert, default_backend())
|
||||||
|
digest = cert.fingerprint(hashes.SHA1()).hex()
|
||||||
|
return digest
|
||||||
|
|
||||||
|
|
||||||
|
def server_check(cursor: str, name: str, hostname: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a server with the given name and hostname already exists.
|
||||||
|
|
||||||
|
:param cursor: The database cursor
|
||||||
|
:param name: The name of the server
|
||||||
|
:param hostname: The hostname of the server
|
||||||
|
:return: True if the server exists, False otherwise
|
||||||
|
"""
|
||||||
|
check_query = """
|
||||||
|
SELECT 1 FROM servers WHERE name = ? AND hostname = ?
|
||||||
|
"""
|
||||||
|
cursor.execute(check_query, (name, hostname))
|
||||||
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def insert_server(
|
||||||
|
name: str,
|
||||||
|
hostname: str,
|
||||||
|
port: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
url: str,
|
||||||
|
db_location: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Inserts a new server record into the servers table.
|
||||||
|
|
||||||
|
:param name: The name of the server
|
||||||
|
:param hostname: The hostname of the server
|
||||||
|
:param port: The port number
|
||||||
|
:param username: The username
|
||||||
|
:param password: The password
|
||||||
|
:param url: The URL
|
||||||
|
"""
|
||||||
|
# Connect to the SQLite database
|
||||||
|
conn = sqlite3.connect(db_location)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create a cursor object
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
if server_check(cursor, name, hostname):
|
||||||
|
print(
|
||||||
|
f"Server with name '{name}' and hostname '{hostname}' already exists."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# SQL command to insert data into the servers table
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO servers (name, hostname, port, username, password, url)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Data to be inserted
|
||||||
|
data = (name, hostname, port, username, password, url)
|
||||||
|
|
||||||
|
# Execute the insert command with the provided data
|
||||||
|
cursor.execute(insert_query, data)
|
||||||
|
|
||||||
|
# Commit the changes
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print("Data has been successfully inserted.")
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
# Close the connection
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
port = 64738
|
||||||
|
password = ""
|
||||||
|
url = None
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="initialize_mumble",
|
||||||
|
)
|
||||||
|
|
||||||
|
subparser = parser.add_subparsers(dest="certificates")
|
||||||
|
# cert_parser = subparser.add_parser("certificates")
|
||||||
|
|
||||||
|
parser.add_argument("--cert")
|
||||||
|
parser.add_argument("--digest")
|
||||||
|
parser.add_argument("--machines")
|
||||||
|
parser.add_argument("--servers")
|
||||||
|
parser.add_argument("--username")
|
||||||
|
parser.add_argument("--db-location")
|
||||||
|
parser.add_argument("--ensure-config")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(args)
|
||||||
|
|
||||||
|
if args.ensure_config:
|
||||||
|
ensure_config(args.ensure_config, args.db_location)
|
||||||
|
print("Initialized config")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if args.servers:
|
||||||
|
print(args.servers)
|
||||||
|
servers = json.loads(f"{args.servers}")
|
||||||
|
db_location = args.db_location
|
||||||
|
for server in servers:
|
||||||
|
digest = calculate_digest(server.get("value"))
|
||||||
|
name = server.get("name")
|
||||||
|
initialize_certificates(db_location, name, port, digest)
|
||||||
|
print("Initialized certificates")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
initialize_database(args.db_location)
|
||||||
|
|
||||||
|
# Insert the server into the database
|
||||||
|
print(args.machines)
|
||||||
|
machines = json.loads(f"{args.machines}")
|
||||||
|
print(machines)
|
||||||
|
print(list(machines))
|
||||||
|
|
||||||
|
for machine in list(machines):
|
||||||
|
print(f"Inserting {machine}.")
|
||||||
|
insert_server(
|
||||||
|
machine,
|
||||||
|
machine,
|
||||||
|
port,
|
||||||
|
args.username,
|
||||||
|
password,
|
||||||
|
url,
|
||||||
|
args.db_location,
|
||||||
|
)
|
||||||
42
clanModules/mumble/test.nix
Normal file
42
clanModules/mumble/test.nix
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{ pkgs, self, ... }:
|
||||||
|
pkgs.nixosTest {
|
||||||
|
name = "mumble";
|
||||||
|
nodes.peer1 =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.mumble
|
||||||
|
self.inputs.clan-core.nixosModules.clanCore
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
clan.core.machineName = "peer1";
|
||||||
|
clan.core.clanDir = ./.;
|
||||||
|
|
||||||
|
documentation.enable = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
nodes.peer2 =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.mumble
|
||||||
|
self.inputs.clan-core.nixosModules.clanCore
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
|
||||||
|
clan.core.machineName = "peer2";
|
||||||
|
clan.core.clanDir = ./.;
|
||||||
|
|
||||||
|
documentation.enable = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
'';
|
||||||
|
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
echo -e "${ansiEscapes.green}switch to another dev-shell using: select-shell${ansiEscapes.reset}"
|
echo -e "${ansiEscapes.green}switch to another dev-shell using: select-shell${ansiEscapes.reset}"
|
||||||
|
export PROJECT_ROOT=$(git rev-parse --show-toplevel)
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ nav:
|
|||||||
- Configure: getting-started/configure.md
|
- Configure: getting-started/configure.md
|
||||||
- Secrets & Facts: getting-started/secrets.md
|
- Secrets & Facts: getting-started/secrets.md
|
||||||
- Deploy Machine: getting-started/deploy.md
|
- Deploy Machine: getting-started/deploy.md
|
||||||
|
- Disk Encryption: getting-started/disk-encryption.md
|
||||||
- Mesh VPN: getting-started/mesh-vpn.md
|
- Mesh VPN: getting-started/mesh-vpn.md
|
||||||
- Backup & Restore: getting-started/backups.md
|
- Backup & Restore: getting-started/backups.md
|
||||||
- Flake-parts: getting-started/flake-parts.md
|
- Flake-parts: getting-started/flake-parts.md
|
||||||
@@ -54,15 +55,20 @@ nav:
|
|||||||
- Reference:
|
- Reference:
|
||||||
- reference/index.md
|
- reference/index.md
|
||||||
- Clan Modules:
|
- Clan Modules:
|
||||||
- reference/clanModules/index.md
|
|
||||||
- reference/clanModules/borgbackup-static.md
|
- reference/clanModules/borgbackup-static.md
|
||||||
- reference/clanModules/borgbackup.md
|
- reference/clanModules/borgbackup.md
|
||||||
- reference/clanModules/deltachat.md
|
- reference/clanModules/deltachat.md
|
||||||
|
- reference/clanModules/dyndns.md
|
||||||
- reference/clanModules/ergochat.md
|
- reference/clanModules/ergochat.md
|
||||||
|
- reference/clanModules/garage.md
|
||||||
|
- reference/clanModules/golem-provider.md
|
||||||
|
- reference/clanModules/index.md
|
||||||
|
- reference/clanModules/iwd.md
|
||||||
- reference/clanModules/localbackup.md
|
- reference/clanModules/localbackup.md
|
||||||
- reference/clanModules/localsend.md
|
- reference/clanModules/localsend.md
|
||||||
- reference/clanModules/matrix-synapse.md
|
- reference/clanModules/matrix-synapse.md
|
||||||
- reference/clanModules/moonlight.md
|
- reference/clanModules/moonlight.md
|
||||||
|
- reference/clanModules/mumble.md
|
||||||
- reference/clanModules/packages.md
|
- reference/clanModules/packages.md
|
||||||
- reference/clanModules/postgresql.md
|
- reference/clanModules/postgresql.md
|
||||||
- reference/clanModules/root-password.md
|
- reference/clanModules/root-password.md
|
||||||
@@ -81,7 +87,6 @@ nav:
|
|||||||
- CLI:
|
- CLI:
|
||||||
- reference/cli/index.md
|
- reference/cli/index.md
|
||||||
- reference/cli/backups.md
|
- reference/cli/backups.md
|
||||||
- reference/cli/config.md
|
|
||||||
- reference/cli/facts.md
|
- reference/cli/facts.md
|
||||||
- reference/cli/flakes.md
|
- reference/cli/flakes.md
|
||||||
- reference/cli/flash.md
|
- reference/cli/flash.md
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block extrahead %}
|
||||||
|
<meta
|
||||||
{% block extrahead %}
|
property="og:title"
|
||||||
<meta property="og:title" content="Clan - Documentation, Blog & Getting Started Guide" />
|
content="Clan - Documentation, Blog & Getting Started Guide"
|
||||||
<meta property="og:description" content="Documentation for Clan. The peer-to-peer machine deployment framework." />
|
/>
|
||||||
<meta property="og:image" content="https://clan.lol/static/dark-favicon/128x128.png" />
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Documentation for Clan. The peer-to-peer machine deployment framework."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://clan.lol/static/dark-favicon/128x128.png"
|
||||||
|
/>
|
||||||
<meta property="og:url" content="https://docs.clan.lol" />
|
<meta property="og:url" content="https://docs.clan.lol" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:site_name" content="Clan" />
|
<meta property="og:site_name" content="Clan" />
|
||||||
<meta property="og:locale" content="en_US" />
|
<meta property="og:locale" content="en_US" />
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ This is useful for machines that are not always online or are not part of the re
|
|||||||
|
|
||||||
## What's next ?
|
## What's next ?
|
||||||
|
|
||||||
|
- [**Disk Encryption**](./disk-encryption.md): Configure disk encryption with remote decryption
|
||||||
- [**Mesh VPN**](./mesh-vpn.md): Configuring a secure mesh network.
|
- [**Mesh VPN**](./mesh-vpn.md): Configuring a secure mesh network.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
301
docs/site/getting-started/disk-encryption.md
Normal file
301
docs/site/getting-started/disk-encryption.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
## Setting up Encryption with Remote Decryption in NixOS
|
||||||
|
|
||||||
|
This guide provides an example setup for a single-disk ZFS system with native encryption, accessible for decryption remotely. This configuration only applies to `systemd-boot` enabled systems and requires UEFI booting.
|
||||||
|
|
||||||
|
For a mirrored disk setup, add `mode = "mirror";` to `zroot`. Under the `disk` option, provide the additional disk identifier, e.g., `y = mirrorBoot /dev/disk/by-id/<second_disk_id>`.
|
||||||
|
|
||||||
|
Replace the disk `nvme-eui.002538b931b59865` with your own.
|
||||||
|
|
||||||
|
Below is the configuration for `disko.nix`
|
||||||
|
```nix
|
||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
mirrorBoot = idx: {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/disk/by-id/${idx}";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
boot = {
|
||||||
|
size = "1M";
|
||||||
|
type = "EF02"; # for grub MBR
|
||||||
|
priority = 1;
|
||||||
|
};
|
||||||
|
ESP = lib.mkIf (idx == "nvme-eui.002538b931b59865") {
|
||||||
|
size = "1G";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
mountOptions = [ "nofail" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
zfs = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "zfs";
|
||||||
|
pool = "zroot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
boot.loader.systemd-boot.enable = true;
|
||||||
|
|
||||||
|
disko.devices = {
|
||||||
|
disk = {
|
||||||
|
x = mirrorBoot "nvme-eui.002538b931b59865";
|
||||||
|
};
|
||||||
|
zpool = {
|
||||||
|
zroot = {
|
||||||
|
type = "zpool";
|
||||||
|
rootFsOptions = {
|
||||||
|
compression = "lz4";
|
||||||
|
acltype = "posixacl";
|
||||||
|
xattr = "sa";
|
||||||
|
"com.sun:auto-snapshot" = "true";
|
||||||
|
mountpoint = "none";
|
||||||
|
};
|
||||||
|
datasets = {
|
||||||
|
"root" = {
|
||||||
|
type = "zfs_fs";
|
||||||
|
options = {
|
||||||
|
mountpoint = "none";
|
||||||
|
encryption = "aes-256-gcm";
|
||||||
|
keyformat = "passphrase";
|
||||||
|
keylocation = "file:///tmp/secret.key";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"root/nixos" = {
|
||||||
|
type = "zfs_fs";
|
||||||
|
options.mountpoint = "/";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
"root/home" = {
|
||||||
|
type = "zfs_fs";
|
||||||
|
options.mountpoint = "/home";
|
||||||
|
mountpoint = "/home";
|
||||||
|
};
|
||||||
|
"root/tmp" = {
|
||||||
|
type = "zfs_fs";
|
||||||
|
mountpoint = "/tmp";
|
||||||
|
options = {
|
||||||
|
mountpoint = "/tmp";
|
||||||
|
sync = "disabled";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this to networking.nix and **replace** the `default` values as well as the name `gchq-local` and `networking.hostId` with your own.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ config, lib, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
networking.gchq-local.ipv4.address = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "192.168.178.177";
|
||||||
|
};
|
||||||
|
networking.gchq-local.ipv4.cidr = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "24";
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.gchq-local.ipv4.gateway = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "192.168.178.1";
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.gchq-local.ipv6.address = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "2003:100:6701:d500:fbbc:40fb:cff3:3b87";
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.gchq-local.ipv6.cidr = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "64";
|
||||||
|
};
|
||||||
|
networking.gchq-local.ipv6.gateway = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "fe80::3ea6:2fff:feef:3435";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
networking.dhcpcd.enable = false;
|
||||||
|
networking.nameservers = [ "127.0.0.1" ];
|
||||||
|
networking.hostId = "a76ebcca"; # Needs to be unique for each host
|
||||||
|
|
||||||
|
# The '10' in the network name is the priority, so this will be the first network to be configured
|
||||||
|
systemd.network.networks."10-eth" = {
|
||||||
|
matchConfig.Type = "ether";
|
||||||
|
addresses = [
|
||||||
|
{
|
||||||
|
Address=config.networking.gchq-local.ipv4.address + "/" + config.networking.gchq-local.ipv4.cidr;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Address=config.networking.gchq-local.ipv6.address + "/" + config.networking.gchq-local.ipv6.cidr;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
DHCP = "yes";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Put this into initrd.nix and add your pubkey to `authorizedKeys`.
|
||||||
|
Replace `kernelModules` with the ethernet module loaded one on your system.
|
||||||
|
```nix
|
||||||
|
{config, pkgs, ...}:
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
boot.initrd.systemd = {
|
||||||
|
enable = true;
|
||||||
|
network.networks."10-eth" = config.systemd.network.networks."10-eth";
|
||||||
|
};
|
||||||
|
boot.initrd.network = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
ssh = {
|
||||||
|
enable = true;
|
||||||
|
port = 7172;
|
||||||
|
authorizedKeys = [ "<yourkey>" ];
|
||||||
|
hostKeys = [
|
||||||
|
"/var/lib/initrd-ssh-key"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"xhci_pci"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Check the network card by running `lspci -k` on the target machine
|
||||||
|
boot.initrd.kernelModules = [ "r8169" ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Step 1: Copying SSH Public Key
|
||||||
|
|
||||||
|
Before starting the installation process, ensure that the SSH public key is copied to the NixOS installer.
|
||||||
|
|
||||||
|
1. Copy your public SSH key to the installer, if it has not been copied already:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-copy-id -o PreferredAuthentications=password -o PubkeyAuthentication=no root@nixos-installer.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1.5: Prepare Secret Key and Clear Disk Data
|
||||||
|
|
||||||
|
1. Access the installer using SSH:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@nixos-installer.local
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a `secret.key` file in `/tmp` using `nano` or another text editor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /tmp/secret.key
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Discard the old disk partition data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
blkdiscard /dev/disk/by-id/nvme-eui.002538b931b59865
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the `clan` machine installation with the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clan machines install gchq-local root@nixos-installer --yes --no-reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: ZFS Pool Import and System Installation
|
||||||
|
|
||||||
|
1. SSH into the installer once again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@nixos-installer.local
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Perform the following commands on the remote installation environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zpool import zroot
|
||||||
|
zfs set keystate=prompt zroot/root
|
||||||
|
zfs load-key zroot/root
|
||||||
|
zfs set mountpoint=/mnt zroot/root/nixos
|
||||||
|
mount /dev/nvme0n1p2 /mnt/boot
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Disconnect from the SSH session:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CTRL+D
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Securely copy your local `initrd_rsa_key` to the installer's `/mnt` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp ~/.ssh/initrd_rsa_key root@nixos-installer.local:/mnt/var/lib/initrd-ssh-key
|
||||||
|
```
|
||||||
|
|
||||||
|
5. SSH back into the installer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@nixos-installer.local
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Navigate to the `/mnt` directory, enter the `nixos-enter` environment, and then exit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt
|
||||||
|
nixos-enter
|
||||||
|
realpath /run/current-system
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Run the `nixos-install` command with the appropriate system path `<SYS_PATH>`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nixos-install --no-root-passwd --no-channel-copy --root /mnt --system <SYS_PATH>
|
||||||
|
```
|
||||||
|
|
||||||
|
8. After the installation process, unmount `/mnt/boot`, change the ZFS mountpoint, and reboot the system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
umount /mnt/boot
|
||||||
|
cd /
|
||||||
|
zfs set mountpoint=/ zroot/root/nixos
|
||||||
|
reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
9. Perform a hard reboot of the machine and remove the USB stick.
|
||||||
|
|
||||||
|
### Step 3: Accessing the Initial Ramdisk (initrd) Environment
|
||||||
|
|
||||||
|
1. SSH into the initrd environment using the `initrd_rsa_key` and provided port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -p 7172 root@192.168.178.141
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the `systemd-tty-ask-password-agent` utility to query a password:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemd-tty-ask-password-agent --query
|
||||||
|
```
|
||||||
|
|
||||||
|
After completing these steps, your NixOS should be successfully installed and ready for use.
|
||||||
|
|
||||||
|
**Note:** Replace `root@nixos-installer.local` and `192.168.178.141` with the appropriate user and IP addresses for your setup. Also, adjust `<SYS_PATH>` to reflect the correct system path for your environment.
|
||||||
@@ -49,7 +49,7 @@ sudo umount /dev/sdb1
|
|||||||
clan flash --flake git+https://git.clan.lol/clan/clan-core \
|
clan flash --flake git+https://git.clan.lol/clan/clan-core \
|
||||||
--ssh-pubkey $HOME/.ssh/id_ed25519.pub \
|
--ssh-pubkey $HOME/.ssh/id_ed25519.pub \
|
||||||
--keymap us \
|
--keymap us \
|
||||||
--language en_US.utf-8 \
|
--language en_US.UTF-8 \
|
||||||
--disk main /dev/sd<X> \
|
--disk main /dev/sd<X> \
|
||||||
flash-installer
|
flash-installer
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roboto";
|
font-family: "Roboto";
|
||||||
src: url(./Roboto-Regular.ttf) format('truetype');
|
src: url(./Roboto-Regular.ttf) format("truetype");
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Fira Code";
|
font-family: "Fira Code";
|
||||||
src: url(./FiraCode-VF.ttf) format('truetype');
|
src: url(./FiraCode-VF.ttf) format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--md-text-font: "Roboto";
|
--md-text-font: "Roboto";
|
||||||
--md-code-font: "Fira Code";
|
--md-code-font: "Fira Code";
|
||||||
}
|
}
|
||||||
|
|||||||
52
flake.lock
generated
52
flake.lock
generated
@@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1721417620,
|
"lastModified": 1723080788,
|
||||||
"narHash": "sha256-6q9b1h8fI3hXg2DG6/vrKWCeG8c5Wj2Kvv22RCgedzg=",
|
"narHash": "sha256-C5LbM5VMdcolt9zHeLQ0bYMRjUL+N+AL5pK7/tVTdes=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "bec6e3cde912b8acb915fecdc509eda7c973fb42",
|
"rev": "ffc1f95f6c28e1c6d1e587b51a2147027a3e45ed",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1719994518,
|
"lastModified": 1722555600,
|
||||||
"narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=",
|
"narHash": "sha256-XOQkdLafnb/p9ij77byFQjDf5m5QYl9b2REiVClC+x4=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7",
|
"rev": "8471fe90ad337a8074e957b69ca4d0089218391d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -48,11 +48,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1721571445,
|
"lastModified": 1724028934,
|
||||||
"narHash": "sha256-2MnlPVcNJZ9Nbu90kFyo7+lng366gswErP4FExfrUbc=",
|
"narHash": "sha256-2M5dqS7UbAKfrO+1U+P/t5S2QIGbuGIsTNMYJzwB17g=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixos-images",
|
"repo": "nixos-images",
|
||||||
"rev": "accee005735844d57b411d9969c5d0aabc6a55f6",
|
"rev": "b733f0680a42cc01d6ad53896fb5ca40a66d5e79",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -63,11 +63,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1721571961,
|
"lastModified": 1724137240,
|
||||||
"narHash": "sha256-jfF4gpRUpTBY2OxDB0FRySsgNGOiuDckEtu7YDQom3Y=",
|
"narHash": "sha256-VjbV/91spoYpl+fD7cK1asDhQIjJduP0lT+SgeXtcIc=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4cc8b29327bed3d52b40041f810f49734298af46",
|
"rev": "d2fa2514f041934a6aa261c66dc44829251cffd3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -84,6 +84,7 @@
|
|||||||
"nixos-images": "nixos-images",
|
"nixos-images": "nixos-images",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"sops-nix": "sops-nix",
|
"sops-nix": "sops-nix",
|
||||||
|
"systems": "systems",
|
||||||
"treefmt-nix": "treefmt-nix"
|
"treefmt-nix": "treefmt-nix"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -95,11 +96,11 @@
|
|||||||
"nixpkgs-stable": []
|
"nixpkgs-stable": []
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1721531171,
|
"lastModified": 1723501126,
|
||||||
"narHash": "sha256-AsvPw7T0tBLb53xZGcUC3YPqlIpdxoSx56u8vPCr6gU=",
|
"narHash": "sha256-N9IcHgj/p1+2Pvk8P4Zc1bfrMwld5PcosVA0nL6IGdE=",
|
||||||
"owner": "Mic92",
|
"owner": "Mic92",
|
||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"rev": "909e8cfb60d83321d85c8d17209d733658a21c95",
|
"rev": "be0eec2d27563590194a9206f551a6f73d52fa34",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -108,6 +109,21 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"treefmt-nix": {
|
"treefmt-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -115,11 +131,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1721458737,
|
"lastModified": 1723808491,
|
||||||
"narHash": "sha256-wNXLQ/ATs1S4Opg1PmuNoJ+Wamqj93rgZYV3Di7kxkg=",
|
"narHash": "sha256-rhis3qNuGmJmYC/okT7Dkc4M8CeUuRCSvW6kC2f3hBc=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "888bfb10a9b091d9ed2f5f8064de8d488f7b7c97",
|
"rev": "1d07739554fdc4f8481068f1b11d6ab4c1a4167a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
14
flake.nix
14
flake.nix
@@ -14,12 +14,18 @@
|
|||||||
nixos-images.inputs.nixos-stable.follows = "";
|
nixos-images.inputs.nixos-stable.follows = "";
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
|
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
|
||||||
|
systems.url = "github:nix-systems/default";
|
||||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||||
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
inputs@{ flake-parts, self, ... }:
|
inputs@{
|
||||||
|
flake-parts,
|
||||||
|
self,
|
||||||
|
systems,
|
||||||
|
...
|
||||||
|
}:
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } (
|
flake-parts.lib.mkFlake { inherit inputs; } (
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
@@ -27,11 +33,7 @@
|
|||||||
meta.name = "clan-core";
|
meta.name = "clan-core";
|
||||||
directory = self;
|
directory = self;
|
||||||
};
|
};
|
||||||
systems = [
|
systems = import systems;
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
imports = [
|
imports = [
|
||||||
./checks/flake-module.nix
|
./checks/flake-module.nix
|
||||||
./clanModules/flake-module.nix
|
./clanModules/flake-module.nix
|
||||||
|
|||||||
@@ -3,126 +3,34 @@ clan-core:
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
flake-parts-lib,
|
flake-parts-lib,
|
||||||
inputs,
|
|
||||||
self,
|
self,
|
||||||
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib) mkOption types;
|
inherit (lib) types;
|
||||||
buildClan = import ../lib/build-clan {
|
|
||||||
inherit lib clan-core;
|
|
||||||
inherit (inputs) nixpkgs;
|
|
||||||
};
|
|
||||||
cfg = config.clan;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
|
||||||
# TODO: figure out how to print the deprecation warning
|
|
||||||
# "${inputs.nixpkgs}/nixos/modules/misc/assertions.nix"
|
|
||||||
(lib.mkRenamedOptionModule
|
|
||||||
[
|
|
||||||
"clan"
|
|
||||||
"clanName"
|
|
||||||
]
|
|
||||||
[
|
|
||||||
"clan"
|
|
||||||
"meta"
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
(lib.mkRenamedOptionModule
|
|
||||||
[
|
|
||||||
"clan"
|
|
||||||
"clanIcon"
|
|
||||||
]
|
|
||||||
[
|
|
||||||
"clan"
|
|
||||||
"meta"
|
|
||||||
"icon"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
options.clan = {
|
options.clan = lib.mkOption {
|
||||||
directory = mkOption {
|
type = types.submoduleWith {
|
||||||
type = types.path;
|
specialArgs = {
|
||||||
description = "The directory containing the clan subdirectory";
|
inherit clan-core self;
|
||||||
default = self; # default to the directory of the flake
|
inherit (inputs) nixpkgs;
|
||||||
};
|
|
||||||
specialArgs = mkOption {
|
|
||||||
type = types.attrsOf types.raw;
|
|
||||||
default = { };
|
|
||||||
description = "Extra arguments to pass to nixosSystem i.e. useful to make self available";
|
|
||||||
};
|
|
||||||
machines = mkOption {
|
|
||||||
type = types.attrsOf types.raw;
|
|
||||||
default = { };
|
|
||||||
description = "Allows to include machine-specific modules i.e. machines.\${name} = { ... }";
|
|
||||||
};
|
|
||||||
inventory = mkOption {
|
|
||||||
#type = types.submodule { imports = [ ../lib/inventory/build-inventory/interface.nix ]; };
|
|
||||||
type = types.attrsOf types.raw;
|
|
||||||
default = { };
|
|
||||||
description = ''
|
|
||||||
An abstract service layer for consistently configuring distributed services across machine boundaries.
|
|
||||||
See https://docs.clan.lol/concepts/inventory/ for more details.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Checks are performed in 'buildClan'
|
|
||||||
# Not everyone uses flake-parts
|
|
||||||
meta = {
|
|
||||||
name = lib.mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.";
|
|
||||||
};
|
};
|
||||||
icon = mkOption {
|
modules = [
|
||||||
type = types.nullOr types.path;
|
../lib/build-clan/interface.nix
|
||||||
default = null;
|
../lib/build-clan/module.nix
|
||||||
description = "A path to an icon to be used for the clan in the GUI";
|
];
|
||||||
};
|
|
||||||
description = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "A short description of the clan";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pkgsForSystem = mkOption {
|
|
||||||
type = types.functionTo types.raw;
|
|
||||||
default = _system: null;
|
|
||||||
description = "A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
options.flake = flake-parts-lib.mkSubmoduleOptions {
|
|
||||||
clanInternals = lib.mkOption {
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
inventory = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
|
||||||
inventoryFile = lib.mkOption { type = lib.types.unspecified; };
|
|
||||||
|
|
||||||
clanModules = lib.mkOption { type = lib.types.attrsOf lib.types.path; };
|
options.flake = flake-parts-lib.mkSubmoduleOptions {
|
||||||
source = lib.mkOption { type = lib.types.path; };
|
clanInternals = lib.mkOption { type = types.raw; };
|
||||||
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
|
||||||
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
|
||||||
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
|
|
||||||
machinesFunc = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
config = {
|
config = {
|
||||||
flake = buildClan {
|
flake.clanInternals = config.clan.clanInternals;
|
||||||
inherit (cfg)
|
flake.nixosConfigurations = config.clan.nixosConfigurations;
|
||||||
directory
|
|
||||||
specialArgs
|
|
||||||
machines
|
|
||||||
pkgsForSystem
|
|
||||||
meta
|
|
||||||
inventory
|
|
||||||
;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
_file = __curPos.file;
|
_file = __curPos.file;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,42 @@
|
|||||||
treefmt.programs.nixfmt.enable = true;
|
treefmt.programs.nixfmt.enable = true;
|
||||||
treefmt.programs.nixfmt.package = pkgs.nixfmt-rfc-style;
|
treefmt.programs.nixfmt.package = pkgs.nixfmt-rfc-style;
|
||||||
treefmt.programs.deadnix.enable = true;
|
treefmt.programs.deadnix.enable = true;
|
||||||
|
treefmt.settings.global.excludes = [
|
||||||
|
"*.png"
|
||||||
|
"*.jpeg"
|
||||||
|
"*.gitignore"
|
||||||
|
".vscode/*"
|
||||||
|
"*.toml"
|
||||||
|
"*.clan-flake"
|
||||||
|
"*.code-workspace"
|
||||||
|
"*.pub"
|
||||||
|
"*.typed"
|
||||||
|
"*.age"
|
||||||
|
"*.list"
|
||||||
|
"*.desktop"
|
||||||
|
];
|
||||||
|
treefmt.programs.prettier = {
|
||||||
|
enable = true;
|
||||||
|
includes = [
|
||||||
|
"*.cjs"
|
||||||
|
"*.css"
|
||||||
|
"*.html"
|
||||||
|
"*.js"
|
||||||
|
"*.json5"
|
||||||
|
"*.jsx"
|
||||||
|
"*.mdx"
|
||||||
|
"*.mjs"
|
||||||
|
"*.scss"
|
||||||
|
"*.ts"
|
||||||
|
"*.tsx"
|
||||||
|
"*.vue"
|
||||||
|
"*.yaml"
|
||||||
|
"*.yml"
|
||||||
|
];
|
||||||
|
# plugins = [
|
||||||
|
# "${self'.packages.prettier-plugin-tailwindcss}/lib/node_modules/prettier-plugin-tailwindcss/dist/index.mjs"
|
||||||
|
# ];
|
||||||
|
};
|
||||||
treefmt.programs.mypy.directories =
|
treefmt.programs.mypy.directories =
|
||||||
{
|
{
|
||||||
"pkgs/clan-cli" = {
|
"pkgs/clan-cli" = {
|
||||||
@@ -20,8 +55,8 @@
|
|||||||
};
|
};
|
||||||
"pkgs/clan-app" = {
|
"pkgs/clan-app" = {
|
||||||
extraPythonPackages =
|
extraPythonPackages =
|
||||||
# clan-app currently only exists on linux
|
|
||||||
(self'.packages.clan-app.externalTestDeps or [ ]) ++ self'.packages.clan-cli.testDependencies;
|
(self'.packages.clan-app.externalTestDeps or [ ]) ++ self'.packages.clan-cli.testDependencies;
|
||||||
|
extraPythonPaths = [ "../clan-cli" ];
|
||||||
modules = [ "clan_app" ];
|
modules = [ "clan_app" ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -30,8 +65,8 @@
|
|||||||
{
|
{
|
||||||
"pkgs/clan-vm-manager" = {
|
"pkgs/clan-vm-manager" = {
|
||||||
extraPythonPackages =
|
extraPythonPackages =
|
||||||
# # clan-app currently only exists on linux
|
self'.packages.clan-vm-manager.externalTestDeps ++ self'.packages.clan-cli.testDependencies;
|
||||||
self'.packages.clan-vm-manager.testDependencies;
|
extraPythonPaths = [ "../clan-cli" ];
|
||||||
modules = [ "clan_vm_manager" ];
|
modules = [ "clan_vm_manager" ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -41,53 +76,5 @@
|
|||||||
treefmt.programs.ruff.check = true;
|
treefmt.programs.ruff.check = true;
|
||||||
treefmt.programs.ruff.format = true;
|
treefmt.programs.ruff.format = true;
|
||||||
|
|
||||||
# FIXME: currently broken in CI
|
|
||||||
#treefmt.settings.formatter.vale =
|
|
||||||
# let
|
|
||||||
# vocab = "cLAN";
|
|
||||||
# style = "Docs";
|
|
||||||
# config = pkgs.writeText "vale.ini" ''
|
|
||||||
# StylesPath = ${styles}
|
|
||||||
# Vocab = ${vocab}
|
|
||||||
|
|
||||||
# [*.md]
|
|
||||||
# BasedOnStyles = Vale, ${style}
|
|
||||||
# Vale.Terms = No
|
|
||||||
# '';
|
|
||||||
# styles = pkgs.symlinkJoin {
|
|
||||||
# name = "vale-style";
|
|
||||||
# paths = [
|
|
||||||
# accept
|
|
||||||
# headings
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
# accept = pkgs.writeTextDir "config/vocabularies/${vocab}/accept.txt" ''
|
|
||||||
# Nix
|
|
||||||
# NixOS
|
|
||||||
# Nixpkgs
|
|
||||||
# clan.lol
|
|
||||||
# Clan
|
|
||||||
# monorepo
|
|
||||||
# '';
|
|
||||||
# headings = pkgs.writeTextDir "${style}/headings.yml" ''
|
|
||||||
# extends: capitalization
|
|
||||||
# message: "'%s' should be in sentence case"
|
|
||||||
# level: error
|
|
||||||
# scope: heading
|
|
||||||
# # $title, $sentence, $lower, $upper, or a pattern.
|
|
||||||
# match: $sentence
|
|
||||||
# '';
|
|
||||||
# in
|
|
||||||
# {
|
|
||||||
# command = "${pkgs.vale}/bin/vale";
|
|
||||||
# options = [ "--config=${config}" ];
|
|
||||||
# includes = [ "*.md" ];
|
|
||||||
# # TODO: too much at once, fix piecemeal
|
|
||||||
# excludes = [
|
|
||||||
# "docs/*"
|
|
||||||
# "clanModules/*"
|
|
||||||
# "pkgs/*"
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,306 +1,39 @@
|
|||||||
|
## WARNING: Do not add core logic here.
|
||||||
|
## This is only a wrapper such that buildClan can be called as a function.
|
||||||
|
## Add any logic to ./module.nix
|
||||||
{
|
{
|
||||||
clan-core,
|
|
||||||
nixpkgs,
|
|
||||||
lib,
|
lib,
|
||||||
|
nixpkgs,
|
||||||
|
clan-core,
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
directory, # The directory containing the machines subdirectory
|
## Inputs
|
||||||
specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available
|
directory, # The directory containing the machines subdirectory # allows to include machine-specific modules i.e. machines.${name} = { ... }
|
||||||
machines ? { }, # allows to include machine-specific modules i.e. machines.${name} = { ... }
|
|
||||||
# DEPRECATED: use meta.name instead
|
|
||||||
clanName ? null, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
|
|
||||||
# DEPRECATED: use meta.icon instead
|
|
||||||
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
|
|
||||||
meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string
|
|
||||||
# A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
|
# A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
|
||||||
# This improves performance, but all nipxkgs.* options will be ignored.
|
# This improves performance, but all nipxkgs.* options will be ignored.
|
||||||
pkgsForSystem ? (_system: null),
|
# deadnix: skip
|
||||||
/*
|
|
||||||
Low level inventory configuration.
|
|
||||||
Overrides the services configuration.
|
|
||||||
*/
|
|
||||||
inventory ? { },
|
inventory ? { },
|
||||||
}:
|
## Sepcial inputs (not passed to the module system as config)
|
||||||
|
specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available # A set containing clan meta: name :: string, icon :: string, description :: string
|
||||||
|
##
|
||||||
|
...
|
||||||
|
}@attrs:
|
||||||
let
|
let
|
||||||
# Internal inventory, this is the result of merging all potential inventory sources:
|
eval = import ./eval.nix {
|
||||||
# - Default instances configured via 'services'
|
inherit
|
||||||
# - The inventory overrides
|
lib
|
||||||
# - Machines that exist in inventory.machines
|
nixpkgs
|
||||||
# - Machines explicitly configured via 'machines' argument
|
specialArgs
|
||||||
# - Machines that exist in the machines directory
|
clan-core
|
||||||
# Checks on the module level:
|
;
|
||||||
# - Each service role must reference a valid machine after all machines are merged
|
self = directory;
|
||||||
|
|
||||||
clanToInventory =
|
|
||||||
config:
|
|
||||||
{ clanPath, inventoryPath }:
|
|
||||||
let
|
|
||||||
v = lib.attrByPath clanPath null config;
|
|
||||||
in
|
|
||||||
lib.optionalAttrs (v != null) (lib.setAttrByPath inventoryPath v);
|
|
||||||
|
|
||||||
mergedInventory =
|
|
||||||
(lib.evalModules {
|
|
||||||
modules = [
|
|
||||||
clan-core.lib.inventory.interface
|
|
||||||
{ inherit meta; }
|
|
||||||
(
|
|
||||||
if
|
|
||||||
builtins.pathExists "${directory}/inventory.json"
|
|
||||||
# Is recursively applied. Any explicit nix will override.
|
|
||||||
then
|
|
||||||
(builtins.fromJSON (builtins.readFile "${directory}/inventory.json"))
|
|
||||||
else
|
|
||||||
{ }
|
|
||||||
)
|
|
||||||
inventory
|
|
||||||
# Machines explicitly configured via 'machines' argument
|
|
||||||
{
|
|
||||||
# { ${name} :: meta // { name, tags } }
|
|
||||||
machines = lib.mapAttrs (
|
|
||||||
name: machineConfig:
|
|
||||||
(lib.attrByPath [
|
|
||||||
"clan"
|
|
||||||
"meta"
|
|
||||||
] { } machineConfig)
|
|
||||||
// {
|
|
||||||
# meta.name default is the attribute name of the machine
|
|
||||||
name = lib.mkDefault (
|
|
||||||
lib.attrByPath [
|
|
||||||
"clan"
|
|
||||||
"meta"
|
|
||||||
"name"
|
|
||||||
] name machineConfig
|
|
||||||
);
|
|
||||||
}
|
|
||||||
# tags
|
|
||||||
// (clanToInventory machineConfig {
|
|
||||||
clanPath = [
|
|
||||||
"clan"
|
|
||||||
"tags"
|
|
||||||
];
|
|
||||||
inventoryPath = [ "tags" ];
|
|
||||||
})
|
|
||||||
# system
|
|
||||||
// (clanToInventory machineConfig {
|
|
||||||
clanPath = [
|
|
||||||
"nixpkgs"
|
|
||||||
"hostPlatform"
|
|
||||||
];
|
|
||||||
inventoryPath = [ "system" ];
|
|
||||||
})
|
|
||||||
# deploy.targetHost
|
|
||||||
// (clanToInventory machineConfig {
|
|
||||||
clanPath = [
|
|
||||||
"clan"
|
|
||||||
"core"
|
|
||||||
"networking"
|
|
||||||
"targetHost"
|
|
||||||
];
|
|
||||||
inventoryPath = [
|
|
||||||
"deploy"
|
|
||||||
"targetHost"
|
|
||||||
];
|
|
||||||
})
|
|
||||||
) machines;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Will be deprecated
|
|
||||||
{
|
|
||||||
machines =
|
|
||||||
lib.mapAttrs
|
|
||||||
(
|
|
||||||
name: _:
|
|
||||||
# Use mkForce to make sure users migrate to the inventory system.
|
|
||||||
# When the settings.json exists the evaluation will print the deprecation warning.
|
|
||||||
lib.mkForce {
|
|
||||||
inherit name;
|
|
||||||
system = (machineSettings name).nixpkgs.hostSystem or null;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(
|
|
||||||
lib.filterAttrs (
|
|
||||||
machineName: _: builtins.pathExists "${directory}/machines/${machineName}/settings.json"
|
|
||||||
) machinesDirs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deprecated interface
|
|
||||||
(if clanName != null then { meta.name = clanName; } else { })
|
|
||||||
(if clanIcon != null then { meta.icon = clanIcon; } else { })
|
|
||||||
];
|
|
||||||
}).config;
|
|
||||||
|
|
||||||
inherit (clan-core.lib.inventory) buildInventory;
|
|
||||||
|
|
||||||
# map from machine name to service configuration
|
|
||||||
# { ${machineName} :: Config }
|
|
||||||
serviceConfigs = buildInventory {
|
|
||||||
inventory = mergedInventory;
|
|
||||||
inherit directory;
|
|
||||||
};
|
};
|
||||||
|
rest = builtins.removeAttrs attrs [ "specialArgs" ];
|
||||||
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
|
|
||||||
builtins.readDir (directory + /machines)
|
|
||||||
);
|
|
||||||
|
|
||||||
machineSettings =
|
|
||||||
machineName:
|
|
||||||
let
|
|
||||||
warn = lib.warn ''
|
|
||||||
The use of ./machines/<machine>/settings.json is deprecated.
|
|
||||||
If your settings.json is empty, you can safely remove it.
|
|
||||||
!!! Consider using the inventory system. !!!
|
|
||||||
|
|
||||||
File: ${directory + /machines/${machineName}/settings.json}
|
|
||||||
|
|
||||||
If there are still features missing in the inventory system, please open an issue on the clan-core repository.
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
|
|
||||||
# This is useful for doing a dry-run before writing changes into the settings.json
|
|
||||||
# Using CLAN_MACHINE_SETTINGS_FILE requires passing --impure to nix eval
|
|
||||||
if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then
|
|
||||||
warn (builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")))
|
|
||||||
else
|
|
||||||
lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json") (
|
|
||||||
warn (builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json)))
|
|
||||||
);
|
|
||||||
|
|
||||||
machineImports =
|
|
||||||
machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]);
|
|
||||||
|
|
||||||
deprecationWarnings = [
|
|
||||||
(lib.warnIf (
|
|
||||||
clanName != null
|
|
||||||
) "clanName in buildClan is deprecated, please use meta.name instead." null)
|
|
||||||
(lib.warnIf (clanIcon != null) "clanIcon is deprecated, please use meta.icon instead" null)
|
|
||||||
];
|
|
||||||
|
|
||||||
# TODO: remove default system once we have a hardware-config mechanism
|
|
||||||
nixosConfiguration =
|
|
||||||
{
|
|
||||||
system ? "x86_64-linux",
|
|
||||||
name,
|
|
||||||
pkgs ? null,
|
|
||||||
extraConfig ? { },
|
|
||||||
}:
|
|
||||||
nixpkgs.lib.nixosSystem {
|
|
||||||
modules =
|
|
||||||
let
|
|
||||||
settings = machineSettings name;
|
|
||||||
in
|
|
||||||
(machineImports settings)
|
|
||||||
++ [
|
|
||||||
{
|
|
||||||
# Autoinclude configuration.nix and hardware-configuration.nix
|
|
||||||
imports = builtins.filter (p: builtins.pathExists p) [
|
|
||||||
"${directory}/machines/${name}/configuration.nix"
|
|
||||||
"${directory}/machines/${name}/hardware-configuration.nix"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
settings
|
|
||||||
clan-core.nixosModules.clanCore
|
|
||||||
extraConfig
|
|
||||||
(machines.${name} or { })
|
|
||||||
# Inherit the inventory assertions ?
|
|
||||||
{ inherit (mergedInventory) assertions; }
|
|
||||||
{ imports = serviceConfigs.${name} or { }; }
|
|
||||||
(
|
|
||||||
{
|
|
||||||
# Settings
|
|
||||||
clan.core.clanDir = directory;
|
|
||||||
# Inherited from clan wide settings
|
|
||||||
clan.core.clanName = meta.name or clanName;
|
|
||||||
clan.core.clanIcon = meta.icon or clanIcon;
|
|
||||||
|
|
||||||
# Machine specific settings
|
|
||||||
clan.core.machineName = name;
|
|
||||||
networking.hostName = lib.mkDefault name;
|
|
||||||
nixpkgs.hostPlatform = lib.mkDefault system;
|
|
||||||
|
|
||||||
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
|
|
||||||
nix.registry.nixpkgs.to = {
|
|
||||||
type = "path";
|
|
||||||
path = lib.mkDefault nixpkgs;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (pkgs != null) { nixpkgs.pkgs = lib.mkForce pkgs; }
|
|
||||||
)
|
|
||||||
];
|
|
||||||
specialArgs = {
|
|
||||||
inherit clan-core;
|
|
||||||
} // specialArgs;
|
|
||||||
};
|
|
||||||
|
|
||||||
allMachines = mergedInventory.machines or { };
|
|
||||||
|
|
||||||
supportedSystems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"riscv64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
|
|
||||||
nixosConfigurations = lib.mapAttrs (name: _: nixosConfiguration { inherit name; }) allMachines;
|
|
||||||
|
|
||||||
# This instantiates nixos for each system that we support:
|
|
||||||
# configPerSystem = <system>.<machine>.nixosConfiguration
|
|
||||||
# We need this to build nixos secret generators for each system
|
|
||||||
configsPerSystem = builtins.listToAttrs (
|
|
||||||
builtins.map (
|
|
||||||
system:
|
|
||||||
lib.nameValuePair system (
|
|
||||||
lib.mapAttrs (
|
|
||||||
name: _:
|
|
||||||
nixosConfiguration {
|
|
||||||
inherit name system;
|
|
||||||
pkgs = pkgsForSystem system;
|
|
||||||
}
|
|
||||||
) allMachines
|
|
||||||
)
|
|
||||||
) supportedSystems
|
|
||||||
);
|
|
||||||
|
|
||||||
configsFuncPerSystem = builtins.listToAttrs (
|
|
||||||
builtins.map (
|
|
||||||
system:
|
|
||||||
lib.nameValuePair system (
|
|
||||||
lib.mapAttrs (
|
|
||||||
name: _: args:
|
|
||||||
nixosConfiguration (
|
|
||||||
args
|
|
||||||
// {
|
|
||||||
inherit name system;
|
|
||||||
pkgs = pkgsForSystem system;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) allMachines
|
|
||||||
)
|
|
||||||
) supportedSystems
|
|
||||||
);
|
|
||||||
in
|
in
|
||||||
builtins.deepSeq deprecationWarnings {
|
eval {
|
||||||
inherit nixosConfigurations;
|
imports = [
|
||||||
|
rest
|
||||||
clanInternals = {
|
# implementation
|
||||||
inherit (clan-core) clanModules;
|
./module.nix
|
||||||
source = "${clan-core}";
|
];
|
||||||
|
|
||||||
meta = mergedInventory.meta;
|
|
||||||
inventory = mergedInventory;
|
|
||||||
|
|
||||||
inventoryFile = "${directory}/inventory.json";
|
|
||||||
|
|
||||||
# machine specifics
|
|
||||||
machines = configsPerSystem;
|
|
||||||
machinesFunc = configsFuncPerSystem;
|
|
||||||
all-machines-json = lib.mapAttrs (
|
|
||||||
system: configs:
|
|
||||||
nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (
|
|
||||||
lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs
|
|
||||||
)
|
|
||||||
) configsPerSystem;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
19
lib/build-clan/eval.nix
Normal file
19
lib/build-clan/eval.nix
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
nixpkgs,
|
||||||
|
clan-core,
|
||||||
|
specialArgs ? { },
|
||||||
|
self,
|
||||||
|
}:
|
||||||
|
# Returns a function that takes self, which should point to the directory of the flake
|
||||||
|
module:
|
||||||
|
(lib.evalModules {
|
||||||
|
specialArgs = {
|
||||||
|
inherit self clan-core nixpkgs;
|
||||||
|
};
|
||||||
|
modules = [
|
||||||
|
./interface.nix
|
||||||
|
module
|
||||||
|
{ inherit specialArgs; }
|
||||||
|
];
|
||||||
|
}).config
|
||||||
41
lib/build-clan/flake-module.nix
Normal file
41
lib/build-clan/flake-module.nix
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{ self, inputs, ... }:
|
||||||
|
let
|
||||||
|
inputOverrides = builtins.concatStringsSep " " (
|
||||||
|
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
|
perSystem =
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
system,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# let
|
||||||
|
|
||||||
|
# in
|
||||||
|
{
|
||||||
|
|
||||||
|
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
|
||||||
|
legacyPackages.evalTests-build-clan = import ./tests.nix {
|
||||||
|
inherit lib;
|
||||||
|
inherit (inputs) nixpkgs;
|
||||||
|
clan-core = self;
|
||||||
|
buildClan = self.lib.buildClan;
|
||||||
|
};
|
||||||
|
checks = {
|
||||||
|
lib-build-clan-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
||||||
|
export HOME="$(realpath .)"
|
||||||
|
|
||||||
|
nix-unit --eval-store "$HOME" \
|
||||||
|
--extra-experimental-features flakes \
|
||||||
|
${inputOverrides} \
|
||||||
|
--flake ${self}#legacyPackages.${system}.evalTests-build-clan
|
||||||
|
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
74
lib/build-clan/interface.nix
Normal file
74
lib/build-clan/interface.nix
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{ lib, self, ... }:
|
||||||
|
let
|
||||||
|
types = lib.types;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
# Required options
|
||||||
|
directory = lib.mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = self;
|
||||||
|
description = "The directory containing the clan subdirectory";
|
||||||
|
};
|
||||||
|
|
||||||
|
specialArgs = lib.mkOption {
|
||||||
|
type = types.attrsOf types.raw;
|
||||||
|
default = { };
|
||||||
|
description = "Extra arguments to pass to nixosSystem i.e. useful to make self available";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
machines = lib.mkOption {
|
||||||
|
type = types.attrsOf types.deferredModule;
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
inventory = lib.mkOption {
|
||||||
|
type = types.submodule { imports = [ ../inventory/build-inventory/interface.nix ]; };
|
||||||
|
};
|
||||||
|
|
||||||
|
# Meta
|
||||||
|
meta = lib.mkOption {
|
||||||
|
type = types.nullOr (
|
||||||
|
types.submodule {
|
||||||
|
options = {
|
||||||
|
name = lib.mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
pkgsForSystem = lib.mkOption {
|
||||||
|
type = types.functionTo (types.nullOr types.attrs);
|
||||||
|
default = _: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Outputs
|
||||||
|
nixosConfigurations = lib.mkOption {
|
||||||
|
type = types.lazyAttrsOf types.raw;
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
# flake.clanInternals
|
||||||
|
clanInternals = lib.mkOption {
|
||||||
|
# type = types.raw;
|
||||||
|
# ClanInternals
|
||||||
|
type = types.submodule {
|
||||||
|
options = {
|
||||||
|
# Those options are interfaced by the CLI
|
||||||
|
# We don't speficy the type here, for better performance.
|
||||||
|
inventory = lib.mkOption { type = lib.types.raw; };
|
||||||
|
inventoryFile = lib.mkOption { type = lib.types.raw; };
|
||||||
|
clanModules = lib.mkOption { type = lib.types.raw; };
|
||||||
|
source = lib.mkOption { type = lib.types.raw; };
|
||||||
|
meta = lib.mkOption { type = lib.types.raw; };
|
||||||
|
all-machines-json = lib.mkOption { type = lib.types.raw; };
|
||||||
|
machines = lib.mkOption { type = lib.types.raw; };
|
||||||
|
machinesFunc = lib.mkOption { type = lib.types.raw; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
228
lib/build-clan/module.nix
Normal file
228
lib/build-clan/module.nix
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
clan-core,
|
||||||
|
nixpkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
inherit (config)
|
||||||
|
directory
|
||||||
|
machines
|
||||||
|
pkgsForSystem
|
||||||
|
specialArgs
|
||||||
|
;
|
||||||
|
|
||||||
|
inherit (config.clanInternals) inventory;
|
||||||
|
|
||||||
|
inherit (clan-core.lib.inventory) buildInventory;
|
||||||
|
|
||||||
|
# map from machine name to service configuration
|
||||||
|
# { ${machineName} :: Config }
|
||||||
|
serviceConfigs = (
|
||||||
|
buildInventory {
|
||||||
|
inherit inventory;
|
||||||
|
inherit directory;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
machineSettings =
|
||||||
|
machineName:
|
||||||
|
let
|
||||||
|
warn = lib.warn ''
|
||||||
|
The use of ./machines/<machine>/settings.json is deprecated.
|
||||||
|
If your settings.json is empty, you can safely remove it.
|
||||||
|
!!! Consider using the inventory system. !!!
|
||||||
|
|
||||||
|
File: ${directory + /machines/${machineName}/settings.json}
|
||||||
|
|
||||||
|
If there are still features missing in the inventory system, please open an issue on the clan-core repository.
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
|
||||||
|
# This is useful for doing a dry-run before writing changes into the settings.json
|
||||||
|
# Using CLAN_MACHINE_SETTINGS_FILE requires passing --impure to nix eval
|
||||||
|
if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then
|
||||||
|
warn (builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")))
|
||||||
|
else
|
||||||
|
lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json") (
|
||||||
|
warn (builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json)))
|
||||||
|
);
|
||||||
|
|
||||||
|
machineImports =
|
||||||
|
machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]);
|
||||||
|
|
||||||
|
# TODO: remove default system once we have a hardware-config mechanism
|
||||||
|
nixosConfiguration =
|
||||||
|
{
|
||||||
|
system ? "x86_64-linux",
|
||||||
|
name,
|
||||||
|
pkgs ? null,
|
||||||
|
extraConfig ? { },
|
||||||
|
}:
|
||||||
|
nixpkgs.lib.nixosSystem {
|
||||||
|
modules =
|
||||||
|
let
|
||||||
|
settings = machineSettings name;
|
||||||
|
in
|
||||||
|
(machineImports settings)
|
||||||
|
++ [
|
||||||
|
{
|
||||||
|
# Autoinclude configuration.nix and hardware-configuration.nix
|
||||||
|
imports = builtins.filter builtins.pathExists [
|
||||||
|
"${directory}/machines/${name}/configuration.nix"
|
||||||
|
"${directory}/machines/${name}/hardware-configuration.nix"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
settings
|
||||||
|
clan-core.nixosModules.clanCore
|
||||||
|
extraConfig
|
||||||
|
(machines.${name} or { })
|
||||||
|
# Inherit the inventory assertions ?
|
||||||
|
# { inherit (mergedInventory) assertions; }
|
||||||
|
{ imports = serviceConfigs.${name} or [ ]; }
|
||||||
|
(
|
||||||
|
{
|
||||||
|
# Settings
|
||||||
|
clan.core.clanDir = directory;
|
||||||
|
# Inherited from clan wide settings
|
||||||
|
# TODO: remove these
|
||||||
|
clan.core.name = config.inventory.meta.name;
|
||||||
|
clan.core.icon = config.inventory.meta.icon;
|
||||||
|
|
||||||
|
# Machine specific settings
|
||||||
|
clan.core.machineName = name;
|
||||||
|
networking.hostName = lib.mkDefault name;
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault system;
|
||||||
|
|
||||||
|
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
|
||||||
|
nix.registry.nixpkgs.to = {
|
||||||
|
type = "path";
|
||||||
|
path = lib.mkDefault nixpkgs;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (pkgs != null) { nixpkgs.pkgs = lib.mkForce pkgs; }
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
specialArgs = {
|
||||||
|
inherit clan-core;
|
||||||
|
} // specialArgs;
|
||||||
|
};
|
||||||
|
|
||||||
|
# TODO: Will be deprecated
|
||||||
|
# We must migrate the tests, that create a settings.json to add a machine.
|
||||||
|
##################################################
|
||||||
|
testMachines =
|
||||||
|
lib.mapAttrs
|
||||||
|
(name: _: {
|
||||||
|
inherit name;
|
||||||
|
system = (machineSettings name).nixpkgs.hostSystem or null;
|
||||||
|
})
|
||||||
|
(
|
||||||
|
lib.filterAttrs (
|
||||||
|
machineName: _:
|
||||||
|
if builtins.pathExists "${directory}/machines/${machineName}/settings.json" then
|
||||||
|
lib.warn ''
|
||||||
|
The use of ./machines/<machine>/settings.json is deprecated.
|
||||||
|
If your settings.json is empty, you can safely remove it.
|
||||||
|
!!! Consider using the inventory system. !!!
|
||||||
|
|
||||||
|
File: ${directory + /machines/${machineName}/settings.json}
|
||||||
|
|
||||||
|
If there are still features missing in the inventory system, please open an issue on the clan-core repository.
|
||||||
|
'' true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
) machinesDirs
|
||||||
|
);
|
||||||
|
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
|
||||||
|
builtins.readDir (directory + /machines)
|
||||||
|
);
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
allMachines = inventory.machines or { } // config.machines or { } // testMachines;
|
||||||
|
|
||||||
|
supportedSystems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"riscv64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
|
||||||
|
nixosConfigurations = lib.mapAttrs (name: _: nixosConfiguration { inherit name; }) allMachines;
|
||||||
|
|
||||||
|
# This instantiates nixos for each system that we support:
|
||||||
|
# configPerSystem = <system>.<machine>.nixosConfiguration
|
||||||
|
# We need this to build nixos secret generators for each system
|
||||||
|
configsPerSystem = builtins.listToAttrs (
|
||||||
|
builtins.map (
|
||||||
|
system:
|
||||||
|
lib.nameValuePair system (
|
||||||
|
lib.mapAttrs (
|
||||||
|
name: _:
|
||||||
|
nixosConfiguration {
|
||||||
|
inherit name system;
|
||||||
|
pkgs = pkgsForSystem system;
|
||||||
|
}
|
||||||
|
) allMachines
|
||||||
|
)
|
||||||
|
) supportedSystems
|
||||||
|
);
|
||||||
|
|
||||||
|
configsFuncPerSystem = builtins.listToAttrs (
|
||||||
|
builtins.map (
|
||||||
|
system:
|
||||||
|
lib.nameValuePair system (
|
||||||
|
lib.mapAttrs (
|
||||||
|
name: _: args:
|
||||||
|
nixosConfiguration (
|
||||||
|
args
|
||||||
|
// {
|
||||||
|
inherit name system;
|
||||||
|
pkgs = pkgsForSystem system;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) allMachines
|
||||||
|
)
|
||||||
|
) supportedSystems
|
||||||
|
);
|
||||||
|
|
||||||
|
inventoryFile = "${directory}/inventory.json";
|
||||||
|
|
||||||
|
inventoryLoaded =
|
||||||
|
if builtins.pathExists inventoryFile then
|
||||||
|
(builtins.fromJSON (builtins.readFile inventoryFile))
|
||||||
|
else
|
||||||
|
{ };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
# Merge the inventory file
|
||||||
|
{ inventory = inventoryLoaded; }
|
||||||
|
# Merge the meta attributes from the buildClan function
|
||||||
|
{ inventory.meta = if config.meta != null then config.meta else { }; }
|
||||||
|
];
|
||||||
|
|
||||||
|
inherit nixosConfigurations;
|
||||||
|
|
||||||
|
clanInternals = {
|
||||||
|
inherit (clan-core) clanModules;
|
||||||
|
inherit inventoryFile;
|
||||||
|
inventory = config.inventory;
|
||||||
|
meta = config.inventory.meta;
|
||||||
|
|
||||||
|
source = "${clan-core}";
|
||||||
|
|
||||||
|
# machine specifics
|
||||||
|
machines = configsPerSystem;
|
||||||
|
machinesFunc = configsFuncPerSystem;
|
||||||
|
all-machines-json = lib.mapAttrs (
|
||||||
|
system: configs:
|
||||||
|
nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (
|
||||||
|
lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs
|
||||||
|
)
|
||||||
|
) configsPerSystem;
|
||||||
|
};
|
||||||
|
}
|
||||||
153
lib/build-clan/tests.nix
Normal file
153
lib/build-clan/tests.nix
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
nixpkgs,
|
||||||
|
clan-core,
|
||||||
|
buildClan,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
evalClan = import ./eval.nix {
|
||||||
|
inherit lib nixpkgs clan-core;
|
||||||
|
self = ./.;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
#######
|
||||||
|
{
|
||||||
|
test_only_required =
|
||||||
|
let
|
||||||
|
config = evalClan {
|
||||||
|
meta.name = "test";
|
||||||
|
imports = [ ./module.nix ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
expr = config.inventory ? meta;
|
||||||
|
expected = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
test_all_simple =
|
||||||
|
let
|
||||||
|
config = evalClan {
|
||||||
|
directory = ./.;
|
||||||
|
machines = { };
|
||||||
|
inventory = {
|
||||||
|
meta.name = "test";
|
||||||
|
};
|
||||||
|
pkgsForSystem = _system: { };
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
expr = config ? inventory;
|
||||||
|
expected = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
test_outputs_clanInternals =
|
||||||
|
let
|
||||||
|
config = evalClan {
|
||||||
|
imports = [
|
||||||
|
# What the user needs to specif
|
||||||
|
{
|
||||||
|
directory = ./.;
|
||||||
|
inventory.meta.name = "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
./module.nix
|
||||||
|
# Explicit output, usually defined by flake-parts
|
||||||
|
{ options.nixosConfigurations = lib.mkOption { type = lib.types.raw; }; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
expr = config.clanInternals.meta;
|
||||||
|
expected = {
|
||||||
|
description = null;
|
||||||
|
icon = null;
|
||||||
|
name = "test";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test_fn_simple =
|
||||||
|
let
|
||||||
|
result = buildClan {
|
||||||
|
directory = ./.;
|
||||||
|
meta.name = "test";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
expr = result.clanInternals.meta;
|
||||||
|
expected = {
|
||||||
|
description = null;
|
||||||
|
icon = null;
|
||||||
|
name = "test";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test_fn_extensiv_meta =
|
||||||
|
let
|
||||||
|
result = buildClan {
|
||||||
|
directory = ./.;
|
||||||
|
meta.name = "test";
|
||||||
|
meta.description = "test";
|
||||||
|
meta.icon = "test";
|
||||||
|
inventory.meta.name = "superclan";
|
||||||
|
inventory.meta.description = "description";
|
||||||
|
inventory.meta.icon = "icon";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
expr = result.clanInternals.meta;
|
||||||
|
expectedError = {
|
||||||
|
type = "ThrownError";
|
||||||
|
msg = "";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test_fn_clan_core =
|
||||||
|
let
|
||||||
|
result = buildClan {
|
||||||
|
directory = ../../.;
|
||||||
|
meta.name = "test-clan-core";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
expr = builtins.attrNames result.nixosConfigurations;
|
||||||
|
expected = [ "test-inventory-machine" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
test_buildClan_all_machines =
|
||||||
|
let
|
||||||
|
result = buildClan {
|
||||||
|
directory = ./.;
|
||||||
|
meta.name = "test";
|
||||||
|
inventory.machines.machine1.meta.name = "machine1";
|
||||||
|
|
||||||
|
machines.machine2 = { };
|
||||||
|
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
expr = builtins.attrNames result.nixosConfigurations;
|
||||||
|
expected = [
|
||||||
|
"machine1"
|
||||||
|
"machine2"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
test_buildClan_specialArgs =
|
||||||
|
let
|
||||||
|
result = buildClan {
|
||||||
|
directory = ./.;
|
||||||
|
meta.name = "test";
|
||||||
|
specialArgs.foo = "dream2nix";
|
||||||
|
machines.machine2 =
|
||||||
|
{ foo, ... }:
|
||||||
|
{
|
||||||
|
networking.hostName = foo;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
expr = result.nixosConfigurations.machine2.config.networking.hostName;
|
||||||
|
expected = "dream2nix";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
evalClanModules = import ./eval-clan-modules { inherit clan-core nixpkgs lib; };
|
evalClanModules = import ./eval-clan-modules { inherit clan-core nixpkgs lib; };
|
||||||
buildClan = import ./build-clan { inherit clan-core lib nixpkgs; };
|
buildClan = import ./build-clan { inherit lib nixpkgs clan-core; };
|
||||||
facts = import ./facts.nix { inherit lib; };
|
facts = import ./facts.nix { inherit lib; };
|
||||||
inventory = import ./inventory { inherit lib clan-core; };
|
inventory = import ./inventory { inherit lib clan-core; };
|
||||||
jsonschema = import ./jsonschema { inherit lib; };
|
jsonschema = import ./jsonschema { inherit lib; };
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ let
|
|||||||
imports = (import (pkgs.path + "/nixos/modules/module-list.nix")) ++ [
|
imports = (import (pkgs.path + "/nixos/modules/module-list.nix")) ++ [
|
||||||
{
|
{
|
||||||
nixpkgs.hostPlatform = "x86_64-linux";
|
nixpkgs.hostPlatform = "x86_64-linux";
|
||||||
clan.core.clanName = "dummy";
|
clan.core.name = "dummy";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
imports = [
|
imports = [
|
||||||
./jsonschema/flake-module.nix
|
./jsonschema/flake-module.nix
|
||||||
./inventory/flake-module.nix
|
./inventory/flake-module.nix
|
||||||
|
./build-clan/flake-module.nix
|
||||||
];
|
];
|
||||||
flake.lib = import ./default.nix {
|
flake.lib = import ./default.nix {
|
||||||
inherit lib inputs;
|
inherit lib inputs;
|
||||||
|
|||||||
77
lib/inventory/build-inventory/assertions.nix
Normal file
77
lib/inventory/build-inventory/assertions.nix
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Integrity validation of the inventory
|
||||||
|
{ config, lib, ... }:
|
||||||
|
{
|
||||||
|
# Assertion must be of type
|
||||||
|
# { assertion :: bool, message :: string, severity :: "error" | "warning" }
|
||||||
|
imports = [
|
||||||
|
# Check that each machine used in a service is defined in the top-level machines
|
||||||
|
{
|
||||||
|
assertions = lib.foldlAttrs (
|
||||||
|
ass1: serviceName: c:
|
||||||
|
ass1
|
||||||
|
++ lib.foldlAttrs (
|
||||||
|
ass2: instanceName: instanceConfig:
|
||||||
|
let
|
||||||
|
topLevelMachines = lib.attrNames config.machines;
|
||||||
|
# All machines must be defined in the top-level machines
|
||||||
|
assertions = lib.foldlAttrs (
|
||||||
|
assertions: roleName: role:
|
||||||
|
assertions
|
||||||
|
++ builtins.filter (a: !a.assertion) (
|
||||||
|
builtins.map (m: {
|
||||||
|
assertion = builtins.elem m topLevelMachines;
|
||||||
|
message = ''
|
||||||
|
Machine '${m}' is not defined in the inventory. This might still work, if the machine is defined via nix.
|
||||||
|
|
||||||
|
Defined in service: '${serviceName}' instance: '${instanceName}' role: '${roleName}'.
|
||||||
|
|
||||||
|
Inventory machines:
|
||||||
|
${builtins.concatStringsSep "\n" (map (n: "'${n}'") topLevelMachines)}
|
||||||
|
'';
|
||||||
|
severity = "warning";
|
||||||
|
}) role.machines
|
||||||
|
)
|
||||||
|
) [ ] instanceConfig.roles;
|
||||||
|
in
|
||||||
|
ass2 ++ assertions
|
||||||
|
) [ ] c
|
||||||
|
) [ ] config.services;
|
||||||
|
}
|
||||||
|
# Check that each tag used in a role is defined in at least one machines tags
|
||||||
|
{
|
||||||
|
assertions = lib.foldlAttrs (
|
||||||
|
ass1: serviceName: c:
|
||||||
|
ass1
|
||||||
|
++ lib.foldlAttrs (
|
||||||
|
ass2: instanceName: instanceConfig:
|
||||||
|
let
|
||||||
|
allTags = lib.foldlAttrs (
|
||||||
|
tags: _machineName: machine:
|
||||||
|
tags ++ machine.tags
|
||||||
|
) [ ] config.machines;
|
||||||
|
# All machines must be defined in the top-level machines
|
||||||
|
assertions = lib.foldlAttrs (
|
||||||
|
assertions: roleName: role:
|
||||||
|
assertions
|
||||||
|
++ builtins.filter (a: !a.assertion) (
|
||||||
|
builtins.map (m: {
|
||||||
|
assertion = builtins.elem m allTags;
|
||||||
|
message = ''
|
||||||
|
Tag '${m}' is not defined in the inventory.
|
||||||
|
|
||||||
|
Defined in service: '${serviceName}' instance: '${instanceName}' role: '${roleName}'.
|
||||||
|
|
||||||
|
Available tags:
|
||||||
|
${builtins.concatStringsSep "\n" (map (n: "'${n}'") allTags)}
|
||||||
|
'';
|
||||||
|
severity = "error";
|
||||||
|
}) role.tags
|
||||||
|
)
|
||||||
|
) [ ] instanceConfig.roles;
|
||||||
|
in
|
||||||
|
ass2 ++ assertions
|
||||||
|
) [ ] c
|
||||||
|
) [ ] config.services;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
# Generate partial NixOS configurations for every machine in the inventory
|
# Generate partial NixOS configurations for every machine in the inventory
|
||||||
# This function is responsible for generating the module configuration for every machine in the inventory.
|
# This function is responsible for generating the module configuration for every machine in the inventory.
|
||||||
{ lib, clan-core }:
|
{ lib, clan-core }:
|
||||||
{ inventory, directory }:
|
|
||||||
let
|
let
|
||||||
machines = machinesFromInventory inventory;
|
|
||||||
|
|
||||||
resolveTags =
|
resolveTags =
|
||||||
# Inventory, { machines :: [string], tags :: [string] }
|
# Inventory, { machines :: [string], tags :: [string] }
|
||||||
{
|
{
|
||||||
@@ -45,8 +42,41 @@ let
|
|||||||
|
|
||||||
machinesFromInventory :: Inventory -> { ${machine_name} :: NixOSConfiguration }
|
machinesFromInventory :: Inventory -> { ${machine_name} :: NixOSConfiguration }
|
||||||
*/
|
*/
|
||||||
machinesFromInventory =
|
|
||||||
|
# { client_1_machine = { tags = [ "backup" ]; }; client_2_machine = { tags = [ "backup" ]; }; not_used_machine = { }; }
|
||||||
|
getAllMachines =
|
||||||
inventory:
|
inventory:
|
||||||
|
lib.foldlAttrs (
|
||||||
|
res: serviceName: serviceConfigs:
|
||||||
|
(lib.foldlAttrs (
|
||||||
|
res: instanceName: serviceConfig:
|
||||||
|
lib.foldlAttrs (
|
||||||
|
res: roleName: members:
|
||||||
|
let
|
||||||
|
resolved = resolveTags {
|
||||||
|
inherit
|
||||||
|
serviceName
|
||||||
|
instanceName
|
||||||
|
roleName
|
||||||
|
inventory
|
||||||
|
members
|
||||||
|
;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
res
|
||||||
|
// builtins.listToAttrs (
|
||||||
|
builtins.map (m: {
|
||||||
|
name = m;
|
||||||
|
value = { };
|
||||||
|
}) resolved.machines
|
||||||
|
)
|
||||||
|
) res serviceConfig.roles
|
||||||
|
) res serviceConfigs)
|
||||||
|
) { } (inventory.services or { })
|
||||||
|
// inventory.machines or { };
|
||||||
|
|
||||||
|
buildInventory =
|
||||||
|
{ inventory, directory }:
|
||||||
# For every machine in the inventory, build a NixOS configuration
|
# For every machine in the inventory, build a NixOS configuration
|
||||||
# For each machine generate config, forEach service, if the machine is used.
|
# For each machine generate config, forEach service, if the machine is used.
|
||||||
builtins.mapAttrs (
|
builtins.mapAttrs (
|
||||||
@@ -152,6 +182,8 @@ let
|
|||||||
config.clan.core.networking.targetHost = machineConfig.deploy.targetHost;
|
config.clan.core.networking.targetHost = machineConfig.deploy.targetHost;
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
) inventory.machines or { };
|
) (getAllMachines inventory);
|
||||||
in
|
in
|
||||||
machines
|
{
|
||||||
|
inherit buildInventory getAllMachines;
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
imports = [ ./assertions.nix ];
|
||||||
options = {
|
options = {
|
||||||
assertions = lib.mkOption {
|
assertions = lib.mkOption {
|
||||||
type = types.listOf types.unspecified;
|
type = types.listOf types.unspecified;
|
||||||
@@ -126,39 +127,4 @@ in
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Smoke validation of the inventory
|
|
||||||
config.assertions =
|
|
||||||
let
|
|
||||||
# Inventory assertions
|
|
||||||
# - All referenced machines must exist in the top-level machines
|
|
||||||
serviceAssertions = lib.foldlAttrs (
|
|
||||||
ass1: serviceName: c:
|
|
||||||
ass1
|
|
||||||
++ lib.foldlAttrs (
|
|
||||||
ass2: instanceName: instanceConfig:
|
|
||||||
let
|
|
||||||
serviceMachineNames = lib.attrNames instanceConfig.machines;
|
|
||||||
topLevelMachines = lib.attrNames config.machines;
|
|
||||||
# All machines must be defined in the top-level machines
|
|
||||||
assertions = builtins.map (m: {
|
|
||||||
assertion = builtins.elem m topLevelMachines;
|
|
||||||
message = "${serviceName}.${instanceName}.machines.${m}. Should be one of [ ${builtins.concatStringsSep " | " topLevelMachines} ]";
|
|
||||||
}) serviceMachineNames;
|
|
||||||
in
|
|
||||||
ass2 ++ assertions
|
|
||||||
) [ ] c
|
|
||||||
) [ ] config.services;
|
|
||||||
|
|
||||||
# Machine assertions
|
|
||||||
# - A machine must define their host system
|
|
||||||
machineAssertions = map (
|
|
||||||
{ name, ... }:
|
|
||||||
{
|
|
||||||
assertion = true;
|
|
||||||
message = "Machine ${name} should define its host system in the inventory. ()";
|
|
||||||
}
|
|
||||||
) (lib.attrsToList (lib.filterAttrs (_n: v: v.system or null == null) config.machines));
|
|
||||||
in
|
|
||||||
machineAssertions ++ serviceAssertions;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{ lib, clan-core }:
|
{ lib, clan-core }:
|
||||||
{
|
{
|
||||||
buildInventory = import ./build-inventory { inherit lib clan-core; };
|
inherit (import ./build-inventory { inherit lib clan-core; }) buildInventory;
|
||||||
interface = ./build-inventory/interface.nix;
|
interface = ./build-inventory/interface.nix;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ in
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
buildInventory = import ./build-inventory {
|
inventory = (
|
||||||
clan-core = self;
|
import ./build-inventory {
|
||||||
inherit lib;
|
clan-core = self;
|
||||||
};
|
inherit lib;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
getSchema = import ./interface-to-schema.nix { inherit lib self; };
|
getSchema = import ./interface-to-schema.nix { inherit lib self; };
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ in
|
|||||||
|
|
||||||
# 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
|
||||||
legacyPackages.evalTests-inventory = import ./tests {
|
legacyPackages.evalTests-inventory = import ./tests {
|
||||||
inherit buildInventory;
|
inherit inventory;
|
||||||
clan-core = self;
|
clan-core = self;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
{ buildInventory, clan-core, ... }:
|
{ inventory, clan-core, ... }:
|
||||||
|
let
|
||||||
|
inherit (inventory) buildInventory getAllMachines;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
|
test_get_all_used_machines = {
|
||||||
|
# Test that all machines are returned
|
||||||
|
expr = getAllMachines {
|
||||||
|
machines = {
|
||||||
|
machine_3 = {
|
||||||
|
tags = [ "tag_3" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services = {
|
||||||
|
borgbackup.instance_1 = {
|
||||||
|
roles.server.machines = [ "backup_server" ];
|
||||||
|
roles.client.machines = [
|
||||||
|
"client_1_machine"
|
||||||
|
"client_2_machine"
|
||||||
|
];
|
||||||
|
roles.client.tags = [ "tag_3" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
expected = {
|
||||||
|
backup_server = { };
|
||||||
|
client_1_machine = { };
|
||||||
|
client_2_machine = { };
|
||||||
|
machine_3 = {
|
||||||
|
tags = [ "tag_3" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
test_inventory_empty = {
|
test_inventory_empty = {
|
||||||
# Empty inventory should return an empty module
|
# Empty inventory should return an empty module
|
||||||
expr = buildInventory {
|
expr = buildInventory {
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
{ lib, pkgs, ... }:
|
|
||||||
|
|
||||||
{
|
{
|
||||||
# use latest kernel we can support to get more hardware support
|
lib,
|
||||||
boot.kernelPackages =
|
pkgs,
|
||||||
lib.mkForce
|
config,
|
||||||
(pkgs.zfs.override { removeLinuxDRM = pkgs.hostPlatform.isAarch64; }).latestCompatibleLinuxPackages;
|
...
|
||||||
boot.zfs.removeLinuxDRM = lib.mkDefault pkgs.hostPlatform.isAarch64;
|
}:
|
||||||
|
{
|
||||||
|
# If we also need zfs, we can use the unstable version as we otherwise don't have a new enough kernel version
|
||||||
|
boot.zfs.package = pkgs.zfsUnstable;
|
||||||
|
boot.kernelPackages = lib.mkIf config.boot.zfs.enabled (
|
||||||
|
lib.mkForce config.boot.zfs.package.latestCompatibleLinuxPackages
|
||||||
|
);
|
||||||
|
|
||||||
# Enable bcachefs support
|
# Enable bcachefs support
|
||||||
boot.supportedFilesystems.bcachefs = lib.mkDefault true;
|
boot.supportedFilesystems.bcachefs = lib.mkDefault true;
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
|
||||||
bcachefs-tools
|
|
||||||
keyutils
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
Each service can have a generator script which generates the secrets and facts.
|
Each service can have a generator script which generates the secrets and facts.
|
||||||
The generator script is expected to generate all secrets and facts defined for this service.
|
The generator script is expected to generate all secrets and facts defined for this service.
|
||||||
|
|
||||||
A `service` does not need to ba analogous to a systemd service, it can be any group of facts and secrets that need to be generated together.
|
A `service` does not need to be analogous to a systemd service, it can be any group of facts and secrets that need to be generated together.
|
||||||
'';
|
'';
|
||||||
default = { };
|
default = { };
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf (
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ in
|
|||||||
options.clan.meta.name = lib.mkOption { type = lib.types.str; };
|
options.clan.meta.name = lib.mkOption { type = lib.types.str; };
|
||||||
options.clan.meta.description = lib.mkOption { type = optStr; };
|
options.clan.meta.description = lib.mkOption { type = optStr; };
|
||||||
options.clan.meta.icon = lib.mkOption { type = optStr; };
|
options.clan.meta.icon = lib.mkOption { type = optStr; };
|
||||||
options.clan.tags = lib.mkOption { type = lib.types.listOf lib.types.str; };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
{ lib, pkgs, ... }:
|
{ lib, pkgs, ... }:
|
||||||
{
|
{
|
||||||
|
imports = [
|
||||||
|
(lib.mkRemovedOptionModule [
|
||||||
|
"clan"
|
||||||
|
"core"
|
||||||
|
"clanName"
|
||||||
|
] "clanName has been removed. Use clan.core.name instead.")
|
||||||
|
(lib.mkRemovedOptionModule [
|
||||||
|
"clan"
|
||||||
|
"core"
|
||||||
|
"clanIcon"
|
||||||
|
] "clanIcon has been removed. Use clan.core.icon instead.")
|
||||||
|
];
|
||||||
options.clan.core = {
|
options.clan.core = {
|
||||||
clanName = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = ''
|
description = ''
|
||||||
the name of the clan
|
the name of the clan
|
||||||
'';
|
'';
|
||||||
|
# Set by the flake, so it's read-only in the maschine
|
||||||
|
readOnly = true;
|
||||||
|
};
|
||||||
|
icon = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
description = ''
|
||||||
|
the location of the clan icon
|
||||||
|
'';
|
||||||
|
# Set by the flake, so it's read-only in the maschine
|
||||||
|
readOnly = true;
|
||||||
};
|
};
|
||||||
machineIcon = lib.mkOption {
|
machineIcon = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
@@ -28,12 +50,6 @@
|
|||||||
the location of the flake repo, used to calculate the location of facts and secrets
|
the location of the flake repo, used to calculate the location of facts and secrets
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
clanIcon = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.path;
|
|
||||||
description = ''
|
|
||||||
the location of the clan icon
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
machineName = lib.mkOption {
|
machineName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "nixos";
|
default = "nixos";
|
||||||
|
|||||||
@@ -106,8 +106,6 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
# defaults
|
|
||||||
config.clan.core.state.HOME.folders = [ "/home" ];
|
|
||||||
config.environment.systemPackages = lib.optional (config.clan.core.state != { }) (
|
config.environment.systemPackages = lib.optional (config.clan.core.state != { }) (
|
||||||
pkgs.runCommand "state-commands" { } ''
|
pkgs.runCommand "state-commands" { } ''
|
||||||
${builtins.concatStringsSep "\n" (
|
${builtins.concatStringsSep "\n" (
|
||||||
|
|||||||
@@ -39,7 +39,12 @@ in
|
|||||||
vars = {
|
vars = {
|
||||||
generators = lib.flip lib.mapAttrs config.clan.core.vars.generators (
|
generators = lib.flip lib.mapAttrs config.clan.core.vars.generators (
|
||||||
_name: generator: {
|
_name: generator: {
|
||||||
inherit (generator) dependencies finalScript prompts;
|
inherit (generator)
|
||||||
|
dependencies
|
||||||
|
finalScript
|
||||||
|
prompts
|
||||||
|
share
|
||||||
|
;
|
||||||
files = lib.flip lib.mapAttrs generator.files (_name: file: { inherit (file) secret; });
|
files = lib.flip lib.mapAttrs generator.files (_name: file: { inherit (file) secret; });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -168,6 +168,15 @@ in
|
|||||||
internal = true;
|
internal = true;
|
||||||
visible = false;
|
visible = false;
|
||||||
};
|
};
|
||||||
|
share = {
|
||||||
|
description = ''
|
||||||
|
Whether the generated vars should be shared between machines.
|
||||||
|
Shared vars are only generated once, when the first machine using it is deployed.
|
||||||
|
Subsequent machines will re-use the already generated values.
|
||||||
|
'';
|
||||||
|
type = bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,9 +10,17 @@ let
|
|||||||
|
|
||||||
inherit (import ./funcs.nix { inherit lib; }) listVars;
|
inherit (import ./funcs.nix { inherit lib; }) listVars;
|
||||||
|
|
||||||
varsDir = config.clan.core.clanDir + "/sops/vars";
|
varsDirMachines = config.clan.core.clanDir + "/sops/vars/per-machine";
|
||||||
|
varsDirShared = config.clan.core.clanDir + "/sops/vars/shared";
|
||||||
|
|
||||||
vars = listVars varsDir;
|
varsUnfiltered = (listVars varsDirMachines) ++ (listVars varsDirShared);
|
||||||
|
filterVars =
|
||||||
|
vars:
|
||||||
|
builtins.elem vars.machine [
|
||||||
|
config.clan.core.machineName
|
||||||
|
"shared"
|
||||||
|
];
|
||||||
|
vars = lib.filter filterVars varsUnfiltered;
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -33,7 +41,7 @@ in
|
|||||||
flip map vars (secret: {
|
flip map vars (secret: {
|
||||||
name = secret.id;
|
name = secret.id;
|
||||||
value = {
|
value = {
|
||||||
sopsFile = config.clan.core.clanDir + "/sops/vars/${secret.id}/secret";
|
sopsFile = secret.sopsFile;
|
||||||
format = "binary";
|
format = "binary";
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ rec {
|
|||||||
generator = generator_name;
|
generator = generator_name;
|
||||||
name = secret_name;
|
name = secret_name;
|
||||||
id = "${machine_name}/${generator_name}/${secret_name}";
|
id = "${machine_name}/${generator_name}/${secret_name}";
|
||||||
|
sopsFile = "${varsDir}/${machine_name}/${generator_name}/${secret_name}/secret";
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ let
|
|||||||
./waypipe.nix
|
./waypipe.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
clan.core.state.HOME.folders = [ "/home" ];
|
||||||
|
|
||||||
clan.services.waypipe = {
|
clan.services.waypipe = {
|
||||||
inherit (config.clan.core.vm.inspect.waypipe) enable command;
|
inherit (config.clan.core.vm.inspect.waypipe) enable command;
|
||||||
};
|
};
|
||||||
@@ -39,18 +41,14 @@ let
|
|||||||
|
|
||||||
boot.initrd.systemd.enable = true;
|
boot.initrd.systemd.enable = true;
|
||||||
|
|
||||||
# currently needed for system.etc.overlay.enable
|
|
||||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
|
||||||
|
|
||||||
boot.initrd.systemd.storePaths = [
|
boot.initrd.systemd.storePaths = [
|
||||||
pkgs.util-linux
|
pkgs.util-linux
|
||||||
pkgs.e2fsprogs
|
pkgs.e2fsprogs
|
||||||
];
|
];
|
||||||
boot.initrd.systemd.emergencyAccess = true;
|
boot.initrd.systemd.emergencyAccess = true;
|
||||||
|
|
||||||
# sysusers is faster than nixos's perl scripts
|
# sysusers would be faster because it doesn't need perl, but it cannot create normal users
|
||||||
# and doesn't require state.
|
systemd.sysusers.enable = false;
|
||||||
systemd.sysusers.enable = true;
|
|
||||||
users.mutableUsers = false;
|
users.mutableUsers = false;
|
||||||
users.allowNoPasswordLogin = true;
|
users.allowNoPasswordLogin = true;
|
||||||
|
|
||||||
@@ -252,8 +250,8 @@ in
|
|||||||
config = {
|
config = {
|
||||||
# for clan vm inspect
|
# for clan vm inspect
|
||||||
clan.core.vm.inspect = {
|
clan.core.vm.inspect = {
|
||||||
clan_name = config.clan.core.clanName;
|
clan_name = config.clan.core.name;
|
||||||
machine_icon = config.clan.core.machineIcon or config.clan.core.clanIcon;
|
machine_icon = config.clan.core.machineIcon or config.clan.core.icon;
|
||||||
machine_name = config.clan.core.machineName;
|
machine_name = config.clan.core.machineName;
|
||||||
machine_description = config.clan.core.machineDescription;
|
machine_description = config.clan.core.machineDescription;
|
||||||
memory_size = config.clan.virtualisation.memorySize;
|
memory_size = config.clan.virtualisation.memorySize;
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ in
|
|||||||
};
|
};
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = config.clan.core.clanName;
|
default = config.clan.core.name;
|
||||||
defaultText = "config.clan.core.clanName";
|
defaultText = "config.clan.core.name";
|
||||||
description = ''
|
description = ''
|
||||||
zerotier network name
|
zerotier network name
|
||||||
'';
|
'';
|
||||||
@@ -89,11 +89,7 @@ in
|
|||||||
({
|
({
|
||||||
# Override license so that we can build zerotierone without
|
# Override license so that we can build zerotierone without
|
||||||
# having to re-import nixpkgs.
|
# having to re-import nixpkgs.
|
||||||
services.zerotierone.package = lib.mkDefault (
|
services.zerotierone.package = lib.mkDefault (pkgs.callPackage ../../../pkgs/zerotierone { });
|
||||||
pkgs.zerotierone.overrideAttrs (_old: {
|
|
||||||
meta = { };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
(lib.mkIf ((facts.zerotier-ip.value or null) != null) {
|
(lib.mkIf ((facts.zerotier-ip.value or null) != null) {
|
||||||
environment.etc."zerotier/ip".text = facts.zerotier-ip.value;
|
environment.etc."zerotier/ip".text = facts.zerotier-ip.value;
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
flake.nixosModules = {
|
flake.nixosModules = {
|
||||||
hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ];
|
hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ];
|
||||||
bcachefs.imports = [ ./bcachefs.nix ];
|
bcachefs.imports = [ ./bcachefs.nix ];
|
||||||
|
zfs.imports = [ ./zfs.nix ];
|
||||||
installer.imports = [
|
installer.imports = [
|
||||||
./installer
|
./installer
|
||||||
self.nixosModules.hidden-ssh-announce
|
self.nixosModules.hidden-ssh-announce
|
||||||
self.nixosModules.bcachefs
|
self.nixosModules.bcachefs
|
||||||
|
self.nixosModules.zfs
|
||||||
];
|
];
|
||||||
clanCore.imports = [
|
clanCore.imports = [
|
||||||
inputs.sops-nix.nixosModules.sops
|
inputs.sops-nix.nixosModules.sops
|
||||||
|
|||||||
16
nixosModules/zfs.nix
Normal file
16
nixosModules/zfs.nix
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{ lib, config, ... }:
|
||||||
|
{
|
||||||
|
# Use the same default hostID as the NixOS install ISO and nixos-anywhere.
|
||||||
|
# This allows us to import zfs pool without using a force import.
|
||||||
|
# ZFS has this as a safety mechanism for networked block storage (ISCSI), but
|
||||||
|
# in practice we found it causes more breakages like unbootable machines,
|
||||||
|
# while people using ZFS on ISCSI is quite rare.
|
||||||
|
networking.hostId = lib.mkDefault "8425e349";
|
||||||
|
|
||||||
|
services.zfs = lib.mkIf (config.boot.zfs.enabled) {
|
||||||
|
autoSnapshot.enable = true;
|
||||||
|
# defaults to 12, which is a bit much given how much data is written
|
||||||
|
autoSnapshot.monthly = lib.mkDefault 1;
|
||||||
|
autoScrub.enable = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -60,6 +60,10 @@ class ImplFunc(GObject.Object, Generic[P, B]):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Reimplement this such that it uses a multiprocessing.Array of type ctypes.c_char
|
||||||
|
# all fn arguments are serialized to json and passed to the new process over the Array
|
||||||
|
# the new process deserializes the json and calls the function
|
||||||
|
# the result is serialized to json and passed back to the main process over another Array
|
||||||
class MethodExecutor(threading.Thread):
|
class MethodExecutor(threading.Thread):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, function: Callable[..., Any], *args: Any, **kwargs: dict[str, Any]
|
self, function: Callable[..., Any], *args: Any, **kwargs: dict[str, Any]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import gi
|
|||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from clan_cli.api import ErrorDataClass, SuccessDataClass
|
from clan_cli.api import ErrorDataClass, SuccessDataClass
|
||||||
from clan_cli.api.directory import FileRequest
|
from clan_cli.api.directory import FileRequest
|
||||||
@@ -14,10 +16,14 @@ from clan_app.api import ImplFunc
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_none(_list: list) -> list:
|
||||||
|
return [i for i in _list if i is not None]
|
||||||
|
|
||||||
|
|
||||||
# This implements the abstract function open_file with one argument, file_request,
|
# This implements the abstract function open_file with one argument, file_request,
|
||||||
# which is a FileRequest object and returns a string or None.
|
# which is a FileRequest object and returns a string or None.
|
||||||
class open_file(
|
class open_file(
|
||||||
ImplFunc[[FileRequest, str], SuccessDataClass[str | None] | ErrorDataClass]
|
ImplFunc[[FileRequest, str], SuccessDataClass[list[str] | None] | ErrorDataClass]
|
||||||
):
|
):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -27,7 +33,7 @@ class open_file(
|
|||||||
try:
|
try:
|
||||||
gfile = file_dialog.open_finish(task)
|
gfile = file_dialog.open_finish(task)
|
||||||
if gfile:
|
if gfile:
|
||||||
selected_path = gfile.get_path()
|
selected_path = remove_none([gfile.get_path()])
|
||||||
self.returns(
|
self.returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key, data=selected_path, status="success"
|
||||||
@@ -36,16 +42,39 @@ class open_file(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting selected file or directory: {e}")
|
print(f"Error getting selected file or directory: {e}")
|
||||||
|
|
||||||
|
def on_file_select_multiple(
|
||||||
|
file_dialog: Gtk.FileDialog, task: Gio.Task
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
gfiles: Any = file_dialog.open_multiple_finish(task)
|
||||||
|
if gfiles:
|
||||||
|
selected_paths = remove_none([gfile.get_path() for gfile in gfiles])
|
||||||
|
self.returns(
|
||||||
|
SuccessDataClass(
|
||||||
|
op_key=op_key, data=selected_paths, status="success"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.returns(
|
||||||
|
SuccessDataClass(op_key=op_key, data=None, status="success")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting selected files: {e}")
|
||||||
|
|
||||||
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||||
try:
|
try:
|
||||||
gfile = file_dialog.select_folder_finish(task)
|
gfile = file_dialog.select_folder_finish(task)
|
||||||
if gfile:
|
if gfile:
|
||||||
selected_path = gfile.get_path()
|
selected_path = remove_none([gfile.get_path()])
|
||||||
self.returns(
|
self.returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key, data=selected_path, status="success"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.returns(
|
||||||
|
SuccessDataClass(op_key=op_key, data=None, status="success")
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting selected directory: {e}")
|
print(f"Error getting selected directory: {e}")
|
||||||
|
|
||||||
@@ -53,12 +82,16 @@ class open_file(
|
|||||||
try:
|
try:
|
||||||
gfile = file_dialog.save_finish(task)
|
gfile = file_dialog.save_finish(task)
|
||||||
if gfile:
|
if gfile:
|
||||||
selected_path = gfile.get_path()
|
selected_path = remove_none([gfile.get_path()])
|
||||||
self.returns(
|
self.returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key, data=selected_path, status="success"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.returns(
|
||||||
|
SuccessDataClass(op_key=op_key, data=None, status="success")
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting selected file: {e}")
|
print(f"Error getting selected file: {e}")
|
||||||
|
|
||||||
@@ -90,9 +123,21 @@ class open_file(
|
|||||||
filters.append(file_filters)
|
filters.append(file_filters)
|
||||||
dialog.set_filters(filters)
|
dialog.set_filters(filters)
|
||||||
|
|
||||||
|
if file_request.initial_file:
|
||||||
|
p = Path(file_request.initial_file).expanduser()
|
||||||
|
f = Gio.File.new_for_path(str(p))
|
||||||
|
dialog.set_initial_file(f)
|
||||||
|
|
||||||
|
if file_request.initial_folder:
|
||||||
|
p = Path(file_request.initial_folder).expanduser()
|
||||||
|
f = Gio.File.new_for_path(str(p))
|
||||||
|
dialog.set_initial_folder(f)
|
||||||
|
|
||||||
# if select_folder
|
# if select_folder
|
||||||
if file_request.mode == "select_folder":
|
if file_request.mode == "select_folder":
|
||||||
dialog.select_folder(callback=on_folder_select)
|
dialog.select_folder(callback=on_folder_select)
|
||||||
|
if file_request.mode == "open_multiple_files":
|
||||||
|
dialog.open_multiple(callback=on_file_select_multiple)
|
||||||
elif file_request.mode == "open_file":
|
elif file_request.mode == "open_file":
|
||||||
dialog.open(callback=on_file_select)
|
dialog.open(callback=on_file_select)
|
||||||
elif file_request.mode == "save":
|
elif file_request.mode == "save":
|
||||||
|
|||||||
@@ -1,66 +1,63 @@
|
|||||||
/* Insert custom styles here */
|
/* Insert custom styles here */
|
||||||
|
|
||||||
navigation-view {
|
navigation-view {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
/* padding-left: 5px;
|
/* padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
padding-bottom: 5px; */
|
padding-bottom: 5px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
avatar {
|
avatar {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trust {
|
.trust {
|
||||||
padding-top: 25px;
|
padding-top: 25px;
|
||||||
padding-bottom: 25px;
|
padding-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-list {
|
.join-list {
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
margin-right: 25px;
|
margin-right: 25px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-list {
|
.group-list {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
.group-list > .activatable:hover {
|
.group-list > .activatable:hover {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-list > row {
|
.group-list > row {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
border-bottom: unset;
|
border-bottom: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.vm-list {
|
.vm-list {
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-shadow {
|
.no-shadow {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-entry {
|
.search-entry {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
searchbar {
|
searchbar {
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.log-view {
|
.log-view {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
127
pkgs/clan-app/clan_app/components/executor.py
Normal file
127
pkgs/clan-app/clan_app/components/executor.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import dataclasses
|
||||||
|
import logging
|
||||||
|
import multiprocessing as mp
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Kill the new process and all its children by sending a SIGTERM signal to the process group
|
||||||
|
def _kill_group(proc: mp.Process) -> None:
|
||||||
|
pid = proc.pid
|
||||||
|
if proc.is_alive() and pid:
|
||||||
|
os.killpg(pid, signal.SIGTERM)
|
||||||
|
else:
|
||||||
|
log.warning(f"Process '{proc.name}' with pid '{pid}' is already dead")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class MPProcess:
|
||||||
|
name: str
|
||||||
|
proc: mp.Process
|
||||||
|
out_file: Path
|
||||||
|
|
||||||
|
# Kill the new process and all its children by sending a SIGTERM signal to the process group
|
||||||
|
def kill_group(self) -> None:
|
||||||
|
_kill_group(proc=self.proc)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_proc_name(name: str) -> None:
|
||||||
|
if sys.platform != "linux":
|
||||||
|
return
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
# Define the prctl function with the appropriate arguments and return type
|
||||||
|
libc = ctypes.CDLL("libc.so.6")
|
||||||
|
prctl = libc.prctl
|
||||||
|
prctl.argtypes = [
|
||||||
|
ctypes.c_int,
|
||||||
|
ctypes.c_char_p,
|
||||||
|
ctypes.c_ulong,
|
||||||
|
ctypes.c_ulong,
|
||||||
|
ctypes.c_ulong,
|
||||||
|
]
|
||||||
|
prctl.restype = ctypes.c_int
|
||||||
|
|
||||||
|
# Set the process name to "my_process"
|
||||||
|
prctl(15, name.encode(), 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _init_proc(
|
||||||
|
func: Callable,
|
||||||
|
out_file: Path,
|
||||||
|
proc_name: str,
|
||||||
|
on_except: Callable[[Exception, mp.process.BaseProcess], None] | None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
# Create a new process group
|
||||||
|
os.setsid()
|
||||||
|
|
||||||
|
# Open stdout and stderr
|
||||||
|
with open(out_file, "w") as out_fd:
|
||||||
|
os.dup2(out_fd.fileno(), sys.stdout.fileno())
|
||||||
|
os.dup2(out_fd.fileno(), sys.stderr.fileno())
|
||||||
|
|
||||||
|
# Print some information
|
||||||
|
pid = os.getpid()
|
||||||
|
gpid = os.getpgid(pid=pid)
|
||||||
|
|
||||||
|
# Set the process name
|
||||||
|
_set_proc_name(proc_name)
|
||||||
|
|
||||||
|
# Close stdin
|
||||||
|
sys.stdin.close()
|
||||||
|
|
||||||
|
linebreak = "=" * 5
|
||||||
|
# Execute the main function
|
||||||
|
print(linebreak + f" {func.__name__}:{pid} " + linebreak, file=sys.stderr)
|
||||||
|
try:
|
||||||
|
func(**kwargs)
|
||||||
|
except Exception as ex:
|
||||||
|
traceback.print_exc()
|
||||||
|
if on_except is not None:
|
||||||
|
on_except(ex, mp.current_process())
|
||||||
|
|
||||||
|
# Kill the new process and all its children by sending a SIGTERM signal to the process group
|
||||||
|
pid = os.getpid()
|
||||||
|
gpid = os.getpgid(pid=pid)
|
||||||
|
print(f"Killing process group pid={pid} gpid={gpid}", file=sys.stderr)
|
||||||
|
os.killpg(gpid, signal.SIGTERM)
|
||||||
|
sys.exit(1)
|
||||||
|
# Don't use a finally block here, because we want the exitcode to be set to
|
||||||
|
# 0 if the function returns normally
|
||||||
|
|
||||||
|
|
||||||
|
def spawn(
|
||||||
|
*,
|
||||||
|
out_file: Path,
|
||||||
|
on_except: Callable[[Exception, mp.process.BaseProcess], None] | None,
|
||||||
|
func: Callable,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> MPProcess:
|
||||||
|
# Decouple the process from the parent
|
||||||
|
if mp.get_start_method(allow_none=True) is None:
|
||||||
|
mp.set_start_method(method="forkserver")
|
||||||
|
|
||||||
|
# Set names
|
||||||
|
proc_name = f"MPExec:{func.__name__}"
|
||||||
|
|
||||||
|
# Start the process
|
||||||
|
proc = mp.Process(
|
||||||
|
target=_init_proc,
|
||||||
|
args=(func, out_file, proc_name, on_except),
|
||||||
|
name=proc_name,
|
||||||
|
kwargs=kwargs,
|
||||||
|
)
|
||||||
|
proc.start()
|
||||||
|
|
||||||
|
# Return the process
|
||||||
|
mp_proc = MPProcess(name=proc_name, proc=proc, out_file=out_file)
|
||||||
|
|
||||||
|
return mp_proc
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import dataclasses
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -116,10 +115,10 @@ class WebExecutor(GObject.Object):
|
|||||||
# Introspect the function and create the expected dataclass from dict dynamically
|
# Introspect the function and create the expected dataclass from dict dynamically
|
||||||
# Depending on the introspected argument_type
|
# Depending on the introspected argument_type
|
||||||
arg_class = self.jschema_api.get_method_argtype(method_name, k)
|
arg_class = self.jschema_api.get_method_argtype(method_name, k)
|
||||||
if dataclasses.is_dataclass(arg_class):
|
|
||||||
reconciled_arguments[k] = from_dict(arg_class, v)
|
# TODO: rename from_dict into something like construct_checked_value
|
||||||
else:
|
# from_dict really takes Anything and returns an instance of the type/class
|
||||||
reconciled_arguments[k] = v
|
reconciled_arguments[k] = from_dict(arg_class, v)
|
||||||
|
|
||||||
GLib.idle_add(fn_instance._async_run, reconciled_arguments)
|
GLib.idle_add(fn_instance._async_run, reconciled_arguments)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
@@ -18,7 +19,7 @@ log = logging.getLogger(__name__)
|
|||||||
class MainWindow(Adw.ApplicationWindow):
|
class MainWindow(Adw.ApplicationWindow):
|
||||||
def __init__(self, config: ClanConfig) -> None:
|
def __init__(self, config: ClanConfig) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.set_title("Clan Manager")
|
self.set_title("Clan App")
|
||||||
self.set_default_size(980, 850)
|
self.set_default_size(980, 850)
|
||||||
|
|
||||||
# Overlay for GTK side exclusive toasts
|
# Overlay for GTK side exclusive toasts
|
||||||
@@ -47,3 +48,4 @@ class MainWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
def on_destroy(self, source: "Adw.ApplicationWindow") -> None:
|
def on_destroy(self, source: "Adw.ApplicationWindow") -> None:
|
||||||
log.debug("Destroying Adw.ApplicationWindow")
|
log.debug("Destroying Adw.ApplicationWindow")
|
||||||
|
os._exit(0)
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ disallow_untyped_calls = true
|
|||||||
disallow_untyped_defs = true
|
disallow_untyped_defs = true
|
||||||
no_implicit_optional = true
|
no_implicit_optional = true
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
|
||||||
module = "argcomplete.*"
|
|
||||||
ignore_missing_imports = true
|
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "clan_cli.*"
|
module = "clan_cli.*"
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|||||||
@@ -65,6 +65,5 @@ mkShell {
|
|||||||
|
|
||||||
export XDG_DATA_DIRS=${gtk4}/share/gsettings-schemas/gtk4-4.14.4:$XDG_DATA_DIRS
|
export XDG_DATA_DIRS=${gtk4}/share/gsettings-schemas/gtk4-4.14.4:$XDG_DATA_DIRS
|
||||||
export XDG_DATA_DIRS=${gsettings-desktop-schemas}/share/gsettings-schemas/gsettings-desktop-schemas-46.0:$XDG_DATA_DIRS
|
export XDG_DATA_DIRS=${gsettings-desktop-schemas}/share/gsettings-schemas/gsettings-desktop-schemas-46.0:$XDG_DATA_DIRS
|
||||||
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import pytest
|
|||||||
from helpers import cli
|
from helpers import cli
|
||||||
|
|
||||||
|
|
||||||
def test_help(capfd: pytest.CaptureFixture) -> None:
|
def test_help() -> None:
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
cli.run(["clan-app", "--help"])
|
cli.run(["clan-app", "--help"])
|
||||||
|
|||||||
48
pkgs/clan-cli/.vscode/launch.json
vendored
48
pkgs/clan-cli/.vscode/launch.json
vendored
@@ -1,26 +1,24 @@
|
|||||||
{
|
{
|
||||||
// Use IntelliSense to learn about possible attributes.
|
// Use IntelliSense to learn about possible attributes.
|
||||||
// Hover to view descriptions of existing attributes.
|
// Hover to view descriptions of existing attributes.
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Clan Webui",
|
"name": "Clan Webui",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "clan_cli.webui",
|
"module": "clan_cli.webui",
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"args": [ "--reload", "--no-open", "--log-level", "debug" ],
|
"args": ["--reload", "--no-open", "--log-level", "debug"]
|
||||||
|
},
|
||||||
},
|
{
|
||||||
{
|
"name": "Clan Cli VMs",
|
||||||
"name": "Clan Cli VMs",
|
"type": "python",
|
||||||
"type": "python",
|
"request": "launch",
|
||||||
"request": "launch",
|
"module": "clan_cli",
|
||||||
"module": "clan_cli",
|
"justMyCode": false,
|
||||||
"justMyCode": false,
|
"args": ["vms"]
|
||||||
"args": [ "vms" ],
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
42
pkgs/clan-cli/.vscode/settings.json
vendored
42
pkgs/clan-cli/.vscode/settings.json
vendored
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"python.testing.pytestArgs": [
|
"python.testing.pytestArgs": [
|
||||||
// Coverage is not supported by vscode:
|
// Coverage is not supported by vscode:
|
||||||
// https://github.com/Microsoft/vscode-python/issues/693
|
// https://github.com/Microsoft/vscode-python/issues/693
|
||||||
// Note that this will make pytest fail if pytest-cov is not installed,
|
// Note that this will make pytest fail if pytest-cov is not installed,
|
||||||
// if that's the case, then this option needs to be be removed (overrides
|
// if that's the case, then this option needs to be be removed (overrides
|
||||||
// can be set at a workspace level, it's up to you to decide what's the
|
// can be set at a workspace level, it's up to you to decide what's the
|
||||||
// best approach). You might also prefer to only set this option
|
// best approach). You might also prefer to only set this option
|
||||||
// per-workspace (wherever coverage is used).
|
// per-workspace (wherever coverage is used).
|
||||||
"--no-cov",
|
"--no-cov",
|
||||||
"tests"
|
"tests"
|
||||||
],
|
],
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.direnv": true
|
"**/.direnv": true
|
||||||
},
|
},
|
||||||
"python.linting.mypyPath": "mypy",
|
"python.linting.mypyPath": "mypy",
|
||||||
"python.linting.mypyEnabled": true,
|
"python.linting.mypyEnabled": true,
|
||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.defaultInterpreterPath": "python"
|
"python.defaultInterpreterPath": "python"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ from pathlib import Path
|
|||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
# These imports are unused, but necessary for @API.register to run once.
|
# These imports are unused, but necessary for @API.register to run once.
|
||||||
from clan_cli.api import directory, mdns_discovery, modules
|
from clan_cli.api import directory, disk, mdns_discovery, modules
|
||||||
from clan_cli.arg_actions import AppendOptionAction
|
from clan_cli.arg_actions import AppendOptionAction
|
||||||
from clan_cli.clan import show, update
|
from clan_cli.clan import show, update
|
||||||
|
|
||||||
# API endpoints that are not used in the cli.
|
# API endpoints that are not used in the cli.
|
||||||
__all__ = ["directory", "mdns_discovery", "modules", "update"]
|
__all__ = ["directory", "mdns_discovery", "modules", "update", "disk"]
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
backups,
|
backups,
|
||||||
clan,
|
clan,
|
||||||
config,
|
|
||||||
facts,
|
facts,
|
||||||
flash,
|
flash,
|
||||||
flatpak,
|
|
||||||
history,
|
history,
|
||||||
machines,
|
machines,
|
||||||
secrets,
|
secrets,
|
||||||
@@ -178,18 +176,6 @@ For more detailed information, visit: {help_hyperlink("getting-started", "https:
|
|||||||
|
|
||||||
clan.register_parser(parser_flake)
|
clan.register_parser(parser_flake)
|
||||||
|
|
||||||
parser_config = subparsers.add_parser(
|
|
||||||
"config",
|
|
||||||
help="read a nixos configuration option",
|
|
||||||
description="read a nixos configuration option",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
config.register_parser(parser_config)
|
|
||||||
|
|
||||||
parser_ssh = subparsers.add_parser(
|
parser_ssh = subparsers.add_parser(
|
||||||
"ssh",
|
"ssh",
|
||||||
help="ssh to a remote machine",
|
help="ssh to a remote machine",
|
||||||
@@ -408,8 +394,6 @@ def main() -> None:
|
|||||||
if getattr(args, "debug", False):
|
if getattr(args, "debug", False):
|
||||||
setup_logging(logging.DEBUG, root_log_name=__name__.split(".")[0])
|
setup_logging(logging.DEBUG, root_log_name=__name__.split(".")[0])
|
||||||
log.debug("Debug log activated")
|
log.debug("Debug log activated")
|
||||||
if flatpak.is_flatpak():
|
|
||||||
log.debug("Running inside a flatpak sandbox")
|
|
||||||
else:
|
else:
|
||||||
setup_logging(logging.INFO, root_log_name=__name__.split(".")[0])
|
setup_logging(logging.INFO, root_log_name=__name__.split(".")[0])
|
||||||
|
|
||||||
|
|||||||
13
pkgs/clan-cli/clan_cli/api/cli.py
Executable file
13
pkgs/clan-cli/clan_cli/api/cli.py
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
|
||||||
|
from clan_cli.api import API
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Debug the API.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
schema = API.to_json_schema()
|
||||||
|
print(json.dumps(schema, indent=4))
|
||||||
@@ -12,24 +12,26 @@ from . import API
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileFilter:
|
class FileFilter:
|
||||||
title: str | None
|
title: str | None = field(default=None)
|
||||||
mime_types: list[str] | None
|
mime_types: list[str] | None = field(default=None)
|
||||||
patterns: list[str] | None
|
patterns: list[str] | None = field(default=None)
|
||||||
suffixes: list[str] | None
|
suffixes: list[str] | None = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileRequest:
|
class FileRequest:
|
||||||
# Mode of the os dialog window
|
# Mode of the os dialog window
|
||||||
mode: Literal["open_file", "select_folder", "save"]
|
mode: Literal["open_file", "select_folder", "save", "open_multiple_files"]
|
||||||
# Title of the os dialog window
|
# Title of the os dialog window
|
||||||
title: str | None = None
|
title: str | None = field(default=None)
|
||||||
# Pre-applied filters for the file dialog
|
# Pre-applied filters for the file dialog
|
||||||
filters: FileFilter | None = None
|
filters: FileFilter | None = field(default=None)
|
||||||
|
initial_file: str | None = field(default=None)
|
||||||
|
initial_folder: str | None = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
@API.register_abstract
|
@API.register_abstract
|
||||||
def open_file(file_request: FileRequest) -> str | None:
|
def open_file(file_request: FileRequest) -> list[str] | None:
|
||||||
"""
|
"""
|
||||||
Abstract api method to open a file dialog window.
|
Abstract api method to open a file dialog window.
|
||||||
It must return the name of the selected file or None if no file was selected.
|
It must return the name of the selected file or None if no file was selected.
|
||||||
@@ -88,6 +90,8 @@ def get_directory(current_path: str) -> Directory:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class BlkInfo:
|
class BlkInfo:
|
||||||
name: str
|
name: str
|
||||||
|
id_link: str
|
||||||
|
path: str
|
||||||
rm: str
|
rm: str
|
||||||
size: str
|
size: str
|
||||||
ro: bool
|
ro: bool
|
||||||
@@ -103,21 +107,53 @@ class Blockdevices:
|
|||||||
def blk_from_dict(data: dict) -> BlkInfo:
|
def blk_from_dict(data: dict) -> BlkInfo:
|
||||||
return BlkInfo(
|
return BlkInfo(
|
||||||
name=data["name"],
|
name=data["name"],
|
||||||
|
path=data["path"],
|
||||||
rm=data["rm"],
|
rm=data["rm"],
|
||||||
size=data["size"],
|
size=data["size"],
|
||||||
ro=data["ro"],
|
ro=data["ro"],
|
||||||
mountpoints=data["mountpoints"],
|
mountpoints=data["mountpoints"],
|
||||||
type_=data["type"], # renamed here
|
type_=data["type"], # renamed
|
||||||
|
id_link=data["id-link"], # renamed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlockDeviceOptions:
|
||||||
|
hostname: str | None = None
|
||||||
|
keyfile: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def show_block_devices() -> Blockdevices:
|
def show_block_devices(options: BlockDeviceOptions) -> Blockdevices:
|
||||||
"""
|
"""
|
||||||
Abstract api method to show block devices.
|
Abstract api method to show block devices.
|
||||||
It must return a list of block devices.
|
It must return a list of block devices.
|
||||||
"""
|
"""
|
||||||
cmd = nix_shell(["nixpkgs#util-linux"], ["lsblk", "--json"])
|
keyfile = options.keyfile
|
||||||
|
remote = (
|
||||||
|
[
|
||||||
|
"ssh",
|
||||||
|
*(["-i", f"{keyfile}"] if keyfile else []),
|
||||||
|
# Disable strict host key checking
|
||||||
|
"-o StrictHostKeyChecking=no",
|
||||||
|
# Disable known hosts file
|
||||||
|
"-o UserKnownHostsFile=/dev/null",
|
||||||
|
f"{options.hostname}",
|
||||||
|
]
|
||||||
|
if options.hostname
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = nix_shell(
|
||||||
|
["nixpkgs#util-linux", *(["nixpkgs#openssh"] if options.hostname else [])],
|
||||||
|
[
|
||||||
|
*remote,
|
||||||
|
"lsblk",
|
||||||
|
"--json",
|
||||||
|
"--output",
|
||||||
|
"PATH,NAME,RM,SIZE,RO,MOUNTPOINTS,TYPE,ID-LINK",
|
||||||
|
],
|
||||||
|
)
|
||||||
proc = run_no_stdout(cmd)
|
proc = run_no_stdout(cmd)
|
||||||
res = proc.stdout.strip()
|
res = proc.stdout.strip()
|
||||||
|
|
||||||
|
|||||||
65
pkgs/clan-cli/clan_cli/api/disk.py
Normal file
65
pkgs/clan-cli/clan_cli/api/disk.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from clan_cli.api import API
|
||||||
|
from clan_cli.inventory import (
|
||||||
|
ServiceMeta,
|
||||||
|
ServiceSingleDisk,
|
||||||
|
ServiceSingleDiskRole,
|
||||||
|
ServiceSingleDiskRoleDefault,
|
||||||
|
SingleDiskConfig,
|
||||||
|
load_inventory_eval,
|
||||||
|
load_inventory_json,
|
||||||
|
save_inventory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance_name(machine_name: str) -> str:
|
||||||
|
return f"{machine_name}-single-disk"
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def set_single_disk_uuid(
|
||||||
|
base_path: str,
|
||||||
|
machine_name: str,
|
||||||
|
disk_uuid: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set the disk UUID of single disk machine
|
||||||
|
"""
|
||||||
|
inventory = load_inventory_json(base_path)
|
||||||
|
|
||||||
|
instance_name = get_instance_name(machine_name)
|
||||||
|
|
||||||
|
single_disk_config: ServiceSingleDisk = ServiceSingleDisk(
|
||||||
|
meta=ServiceMeta(name=instance_name),
|
||||||
|
roles=ServiceSingleDiskRole(
|
||||||
|
default=ServiceSingleDiskRoleDefault(
|
||||||
|
config=SingleDiskConfig(device=disk_uuid)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
inventory.services.single_disk[instance_name] = single_disk_config
|
||||||
|
|
||||||
|
save_inventory(
|
||||||
|
inventory,
|
||||||
|
base_path,
|
||||||
|
f"Set disk UUID: '{disk_uuid}' on machine: '{machine_name}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def get_single_disk_uuid(
|
||||||
|
base_path: str,
|
||||||
|
machine_name: str,
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
Get the disk UUID of single disk machine
|
||||||
|
"""
|
||||||
|
inventory = load_inventory_eval(base_path)
|
||||||
|
|
||||||
|
instance_name = get_instance_name(machine_name)
|
||||||
|
|
||||||
|
single_disk_config: ServiceSingleDisk = inventory.services.single_disk[
|
||||||
|
instance_name
|
||||||
|
]
|
||||||
|
|
||||||
|
return single_disk_config.roles.default.config.device
|
||||||
@@ -3,13 +3,16 @@ import re
|
|||||||
import tomllib
|
import tomllib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, get_args, get_type_hints
|
||||||
|
|
||||||
from clan_cli.cmd import run_no_stdout
|
from clan_cli.cmd import run_no_stdout
|
||||||
from clan_cli.errors import ClanCmdError, ClanError
|
from clan_cli.errors import ClanCmdError, ClanError
|
||||||
from clan_cli.inventory import Inventory, load_inventory_json
|
from clan_cli.inventory import Inventory, load_inventory_json, save_inventory
|
||||||
|
from clan_cli.inventory.classes import Service
|
||||||
from clan_cli.nix import nix_eval
|
from clan_cli.nix import nix_eval
|
||||||
|
|
||||||
from . import API
|
from . import API
|
||||||
|
from .serde import from_dict
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -153,3 +156,35 @@ def get_module_info(
|
|||||||
@API.register
|
@API.register
|
||||||
def get_inventory(base_path: str) -> Inventory:
|
def get_inventory(base_path: str) -> Inventory:
|
||||||
return load_inventory_json(base_path)
|
return load_inventory_json(base_path)
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def set_service_instance(
|
||||||
|
base_path: str, module_name: str, instance_name: str, config: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
A function that allows to set any service instance in the inventory.
|
||||||
|
Takes any untyped dict. The dict is then checked and converted into the correct type using the type hints of the service.
|
||||||
|
If any conversion error occurs, the function will raise an error.
|
||||||
|
"""
|
||||||
|
service_keys = get_type_hints(Service).keys()
|
||||||
|
|
||||||
|
if module_name not in service_keys:
|
||||||
|
raise ValueError(
|
||||||
|
f"{module_name} is not a valid Service attribute. Expected one of {', '.join(service_keys)}."
|
||||||
|
)
|
||||||
|
|
||||||
|
inventory = load_inventory_json(base_path)
|
||||||
|
target_type = get_args(get_type_hints(Service)[module_name])[1]
|
||||||
|
|
||||||
|
module_instance_map: dict[str, Any] = getattr(inventory.services, module_name, {})
|
||||||
|
|
||||||
|
module_instance_map[instance_name] = from_dict(target_type, config)
|
||||||
|
|
||||||
|
setattr(inventory.services, module_name, module_instance_map)
|
||||||
|
|
||||||
|
save_inventory(
|
||||||
|
inventory, base_path, f"Update {module_name} instance {instance_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Add a check that rolls back the inventory if the service config is not valid or causes conflicts.
|
||||||
|
|||||||
@@ -29,17 +29,21 @@ Dependencies:
|
|||||||
Note: This module assumes the presence of other modules and classes such as `ClanError` and `ErrorDetails` from the `clan_cli.errors` module.
|
Note: This module assumes the presence of other modules and classes such as `ClanError` and `ErrorDetails` from the `clan_cli.errors` module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass, fields, is_dataclass
|
from dataclasses import dataclass, fields, is_dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import UnionType
|
||||||
from typing import (
|
from typing import (
|
||||||
|
Annotated,
|
||||||
Any,
|
Any,
|
||||||
|
Literal,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pydantic import TypeAdapter, ValidationError
|
|
||||||
from pydantic_core import ErrorDetails
|
|
||||||
|
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +68,8 @@ def dataclass_to_dict(obj: Any, *, use_alias: bool = True) -> Any:
|
|||||||
field.metadata.get("alias", field.name) if use_alias else field.name
|
field.metadata.get("alias", field.name) if use_alias else field.name
|
||||||
): _to_dict(getattr(obj, field.name))
|
): _to_dict(getattr(obj, field.name))
|
||||||
for field in fields(obj)
|
for field in fields(obj)
|
||||||
if not field.name.startswith("_") # type: ignore
|
if not field.name.startswith("_")
|
||||||
|
and getattr(obj, field.name) is not None # type: ignore
|
||||||
}
|
}
|
||||||
elif isinstance(obj, list | tuple):
|
elif isinstance(obj, list | tuple):
|
||||||
return [_to_dict(item) for item in obj]
|
return [_to_dict(item) for item in obj]
|
||||||
@@ -81,26 +86,169 @@ def dataclass_to_dict(obj: Any, *, use_alias: bool = True) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", bound=dataclass) # type: ignore
|
T = TypeVar("T", bound=dataclass) # type: ignore
|
||||||
|
G = TypeVar("G") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def from_dict(t: type[T], data: Any) -> T:
|
def is_union_type(type_hint: type | UnionType) -> bool:
|
||||||
|
return (
|
||||||
|
type(type_hint) is UnionType
|
||||||
|
or isinstance(type_hint, UnionType)
|
||||||
|
or get_origin(type_hint) is Union
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_type_in_union(union_type: type | UnionType, target_type: type) -> bool:
|
||||||
|
if get_origin(union_type) is UnionType:
|
||||||
|
return any(issubclass(arg, target_type) for arg in get_args(union_type))
|
||||||
|
return union_type == target_type
|
||||||
|
|
||||||
|
|
||||||
|
def unwrap_none_type(type_hint: type | UnionType) -> type:
|
||||||
"""
|
"""
|
||||||
|
Takes a type union and returns the first non-None type.
|
||||||
|
None | str
|
||||||
|
=>
|
||||||
|
str
|
||||||
|
"""
|
||||||
|
|
||||||
|
if is_union_type(type_hint):
|
||||||
|
# Return the first non-None type
|
||||||
|
return next(t for t in get_args(type_hint) if t is not type(None))
|
||||||
|
|
||||||
|
return type_hint # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
JsonValue = str | float | dict[str, Any] | list[Any] | None
|
||||||
|
|
||||||
|
|
||||||
|
def construct_value(t: type, field_value: JsonValue, loc: list[str] = []) -> Any:
|
||||||
|
"""
|
||||||
|
Construct a field value from a type hint and a field value.
|
||||||
|
"""
|
||||||
|
if t is None and field_value:
|
||||||
|
raise ClanError(f"Expected None but got: {field_value}", location=f"{loc}")
|
||||||
|
# If the field is another dataclass
|
||||||
|
# Field_value must be a dictionary
|
||||||
|
if is_dataclass(t) and isinstance(field_value, dict):
|
||||||
|
return construct_dataclass(t, field_value)
|
||||||
|
|
||||||
|
# If the field expects a path
|
||||||
|
# Field_value must be a string
|
||||||
|
elif is_type_in_union(t, Path):
|
||||||
|
if not isinstance(field_value, str):
|
||||||
|
raise ClanError(
|
||||||
|
f"Expected string, cannot construct pathlib.Path() from: {field_value} ",
|
||||||
|
location=f"{loc}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return Path(field_value)
|
||||||
|
|
||||||
|
# Trivial values
|
||||||
|
elif t is str:
|
||||||
|
if not isinstance(field_value, str):
|
||||||
|
raise ClanError(f"Expected string, got {field_value}", location=f"{loc}")
|
||||||
|
|
||||||
|
return field_value
|
||||||
|
|
||||||
|
elif t is int and not isinstance(field_value, str):
|
||||||
|
return int(field_value) # type: ignore
|
||||||
|
elif t is float and not isinstance(field_value, str):
|
||||||
|
return float(field_value) # type: ignore
|
||||||
|
elif t is bool and isinstance(field_value, bool):
|
||||||
|
return field_value # type: ignore
|
||||||
|
|
||||||
|
# Union types construct the first non-None type
|
||||||
|
elif is_union_type(t):
|
||||||
|
# Unwrap the union type
|
||||||
|
t = unwrap_none_type(t)
|
||||||
|
# Construct the field value
|
||||||
|
return construct_value(t, field_value)
|
||||||
|
|
||||||
|
# Nested types
|
||||||
|
# list
|
||||||
|
# dict
|
||||||
|
elif get_origin(t) is list:
|
||||||
|
if not isinstance(field_value, list):
|
||||||
|
raise ClanError(f"Expected list, got {field_value}", location=f"{loc}")
|
||||||
|
|
||||||
|
return [construct_value(get_args(t)[0], item) for item in field_value]
|
||||||
|
elif get_origin(t) is dict and isinstance(field_value, dict):
|
||||||
|
return {
|
||||||
|
key: construct_value(get_args(t)[1], value)
|
||||||
|
for key, value in field_value.items()
|
||||||
|
}
|
||||||
|
elif get_origin(t) is Literal:
|
||||||
|
valid_values = get_args(t)
|
||||||
|
if field_value not in valid_values:
|
||||||
|
raise ClanError(
|
||||||
|
f"Expected one of {valid_values}, got {field_value}", location=f"{loc}"
|
||||||
|
)
|
||||||
|
return field_value
|
||||||
|
|
||||||
|
elif get_origin(t) is Annotated:
|
||||||
|
(base_type,) = get_args(t)
|
||||||
|
return construct_value(base_type, field_value)
|
||||||
|
|
||||||
|
# elif get_origin(t) is Union:
|
||||||
|
|
||||||
|
# Unhandled
|
||||||
|
else:
|
||||||
|
raise ClanError(f"Unhandled field type {t} with value {field_value}")
|
||||||
|
|
||||||
|
|
||||||
|
def construct_dataclass(t: type[T], data: dict[str, Any], path: list[str] = []) -> T:
|
||||||
|
"""
|
||||||
|
type t MUST be a dataclass
|
||||||
Dynamically instantiate a data class from a dictionary, handling nested data classes.
|
Dynamically instantiate a data class from a dictionary, handling nested data classes.
|
||||||
We use dataclasses. But the deserialization logic of pydantic takes a lot of complexity.
|
|
||||||
"""
|
"""
|
||||||
adapter = TypeAdapter(t)
|
if not is_dataclass(t):
|
||||||
try:
|
raise ClanError(f"{t.__name__} is not a dataclass")
|
||||||
return adapter.validate_python(
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
except ValidationError as e:
|
|
||||||
fst_error: ErrorDetails = e.errors()[0]
|
|
||||||
if not fst_error:
|
|
||||||
raise ClanError(msg=str(e))
|
|
||||||
|
|
||||||
msg = fst_error.get("msg")
|
# Attempt to create an instance of the data_class#
|
||||||
loc = fst_error.get("loc")
|
field_values: dict[str, Any] = {}
|
||||||
field_path = "Unknown"
|
required: list[str] = []
|
||||||
if loc:
|
|
||||||
field_path = str(loc)
|
for field in fields(t):
|
||||||
raise ClanError(msg=msg, location=f"{t!s}: {field_path}", description=str(e))
|
if field.name.startswith("_"):
|
||||||
|
continue
|
||||||
|
# The first type in a Union
|
||||||
|
# str <- None | str | Path
|
||||||
|
field_type: type[Any] = unwrap_none_type(field.type) # type: ignore
|
||||||
|
data_field_name = field.metadata.get("alias", field.name)
|
||||||
|
|
||||||
|
if (
|
||||||
|
field.default is dataclasses.MISSING
|
||||||
|
and field.default_factory is dataclasses.MISSING
|
||||||
|
):
|
||||||
|
required.append(field.name)
|
||||||
|
|
||||||
|
# Populate the field_values dictionary with the field value
|
||||||
|
# if present in the data
|
||||||
|
if data_field_name in data:
|
||||||
|
field_value = data.get(data_field_name)
|
||||||
|
|
||||||
|
if field_value is None and (
|
||||||
|
field.type is None or is_type_in_union(field.type, type(None))
|
||||||
|
):
|
||||||
|
field_values[field.name] = None
|
||||||
|
else:
|
||||||
|
field_values[field.name] = construct_value(field_type, field_value)
|
||||||
|
|
||||||
|
# Check that all required field are present.
|
||||||
|
for field_name in required:
|
||||||
|
if field_name not in field_values:
|
||||||
|
formatted_path = " ".join(path)
|
||||||
|
raise ClanError(
|
||||||
|
f"Default value missing for: '{field_name}' in {t} {formatted_path}, got Value: {data}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return t(**field_values) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def from_dict(t: type[G], data: dict[str, Any] | Any, path: list[str] = []) -> G:
|
||||||
|
if is_dataclass(t):
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ClanError(f"{data} is not a dict. Expected {t}")
|
||||||
|
return construct_dataclass(t, data, path) # type: ignore
|
||||||
|
else:
|
||||||
|
return construct_value(t, data, path)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from ..clan_uri import FlakeId
|
|||||||
from ..cmd import run
|
from ..cmd import run
|
||||||
from ..dirs import machine_gcroot
|
from ..dirs import machine_gcroot
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..machines.list import list_machines
|
from ..machines.list import list_nixos_machines
|
||||||
from ..machines.machines import Machine
|
from ..machines.machines import Machine
|
||||||
from ..nix import nix_add_to_gcroots, nix_build, nix_config, nix_eval, nix_metadata
|
from ..nix import nix_add_to_gcroots, nix_build, nix_config, nix_eval, nix_metadata
|
||||||
from ..vms.inspect import VmConfig, inspect_vm
|
from ..vms.inspect import VmConfig, inspect_vm
|
||||||
@@ -40,7 +40,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
|
|||||||
system = config["system"]
|
system = config["system"]
|
||||||
|
|
||||||
# Check if the machine exists
|
# Check if the machine exists
|
||||||
machines = list_machines(flake_url, False)
|
machines: list[str] = list_nixos_machines(flake_url)
|
||||||
if machine_name not in machines:
|
if machine_name not in machines:
|
||||||
raise ClanError(
|
raise ClanError(
|
||||||
f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}"
|
f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}"
|
||||||
@@ -57,7 +57,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
|
|||||||
# Get the Clan name
|
# Get the Clan name
|
||||||
cmd = nix_eval(
|
cmd = nix_eval(
|
||||||
[
|
[
|
||||||
f'{flake_url}#clanInternals.machines."{system}"."{machine_name}".config.clan.core.clanName'
|
f'{flake_url}#clanInternals.machines."{system}"."{machine_name}".config.clan.core.name'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
res = run_cmd(cmd)
|
res = run_cmd(cmd)
|
||||||
@@ -66,7 +66,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
|
|||||||
# Get the clan icon path
|
# Get the clan icon path
|
||||||
cmd = nix_eval(
|
cmd = nix_eval(
|
||||||
[
|
[
|
||||||
f'{flake_url}#clanInternals.machines."{system}"."{machine_name}".config.clan.core.clanIcon'
|
f'{flake_url}#clanInternals.machines."{system}"."{machine_name}".config.clan.core.icon'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
res = run_cmd(cmd)
|
res = run_cmd(cmd)
|
||||||
@@ -79,9 +79,9 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
|
|||||||
|
|
||||||
cmd = nix_build(
|
cmd = nix_build(
|
||||||
[
|
[
|
||||||
f'{flake_url}#clanInternals.machines."{system}"."{machine_name}".config.clan.core.clanIcon'
|
f'{flake_url}#clanInternals.machines."{system}"."{machine_name}".config.clan.core.icon'
|
||||||
],
|
],
|
||||||
machine_gcroot(flake_url=str(flake_url)) / "clanIcon",
|
machine_gcroot(flake_url=str(flake_url)) / "icon",
|
||||||
)
|
)
|
||||||
run_cmd(cmd)
|
run_cmd(cmd)
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, get_origin
|
from typing import Any, get_origin
|
||||||
|
|
||||||
from clan_cli.cmd import run
|
from clan_cli.cmd import run
|
||||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
|
||||||
from clan_cli.dirs import machine_settings_file
|
from clan_cli.dirs import machine_settings_file
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
from clan_cli.git import commit_file
|
from clan_cli.git import commit_file
|
||||||
@@ -305,65 +303,3 @@ def set_option(
|
|||||||
repo_dir=flake_dir,
|
repo_dir=flake_dir,
|
||||||
commit_message=f"Set option {option_description}",
|
commit_message=f"Set option {option_description}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# takes a (sub)parser and configures it
|
|
||||||
def register_parser(
|
|
||||||
parser: argparse.ArgumentParser | None,
|
|
||||||
) -> None:
|
|
||||||
if parser is None:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Set or show NixOS options",
|
|
||||||
)
|
|
||||||
|
|
||||||
# inject callback function to process the input later
|
|
||||||
parser.set_defaults(func=get_option)
|
|
||||||
set_machine_action = parser.add_argument(
|
|
||||||
"--machine",
|
|
||||||
"-m",
|
|
||||||
help="Machine to configure",
|
|
||||||
type=str,
|
|
||||||
default="default",
|
|
||||||
)
|
|
||||||
add_dynamic_completer(set_machine_action, complete_machines)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--show-trace",
|
|
||||||
help="Show nix trace on evaluation error",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--options-file",
|
|
||||||
help="JSON file with options",
|
|
||||||
type=Path,
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--settings-file",
|
|
||||||
help="JSON file with settings",
|
|
||||||
type=Path,
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--quiet",
|
|
||||||
help="Do not print the value",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"option",
|
|
||||||
help="Option to read or set (e.g. foo.bar)",
|
|
||||||
type=str,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> None:
|
|
||||||
if argv is None:
|
|
||||||
argv = sys.argv
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
register_parser(parser)
|
|
||||||
parser.parse_args(argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def verify_machine_config(
|
|||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Verify that the machine evaluates successfully
|
Verify that the machine evaluates successfully
|
||||||
Returns a tuple of (success, error_message)
|
Returns None, in case of success, or a String containing the error_message
|
||||||
"""
|
"""
|
||||||
if config is None:
|
if config is None:
|
||||||
config = config_for_machine(flake_dir, machine_name)
|
config = config_for_machine(flake_dir, machine_name)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from clan_cli.errors import ClanError, ClanHttpError
|
|||||||
from clan_cli.nix import nix_eval
|
from clan_cli.nix import nix_eval
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: When moving the api to `clan-app`, the whole config module should be
|
||||||
|
# ported to the `clan-app`, because it is not used by the cli at all.
|
||||||
@API.register
|
@API.register
|
||||||
def machine_schema(
|
def machine_schema(
|
||||||
flake_dir: Path,
|
flake_dir: Path,
|
||||||
@@ -86,9 +88,9 @@ def machine_schema(
|
|||||||
[
|
[
|
||||||
clan-core.nixosModules.clanCore
|
clan-core.nixosModules.clanCore
|
||||||
# potentially the config might affect submodule options,
|
# potentially the config might affect submodule options,
|
||||||
# therefore we need to import it
|
# therefore we need to import it
|
||||||
config
|
config
|
||||||
{{ clan.core.clanName = "fakeClan"; }}
|
{{ clan.core.name = "fakeClan"; }}
|
||||||
]
|
]
|
||||||
# add all clan modules specified via clanImports
|
# add all clan modules specified via clanImports
|
||||||
++ (map (name: clan-core.clanModules.${{name}}) config.clanImports or []);
|
++ (map (name: clan-core.clanModules.${{name}}) config.clanImports or []);
|
||||||
|
|||||||
@@ -105,14 +105,8 @@ def generate_service_facts(
|
|||||||
)
|
)
|
||||||
files_to_commit = []
|
files_to_commit = []
|
||||||
# store secrets
|
# store secrets
|
||||||
for secret in machine.facts_data[service]["secret"]:
|
for secret_name, secret in machine.facts_data[service]["secret"].items():
|
||||||
if isinstance(secret, str):
|
groups = secret.get("groups", [])
|
||||||
# TODO: This is the old NixOS module, can be dropped everyone has updated.
|
|
||||||
secret_name = secret
|
|
||||||
groups = []
|
|
||||||
else:
|
|
||||||
secret_name = secret["name"]
|
|
||||||
groups = secret.get("groups", [])
|
|
||||||
|
|
||||||
secret_file = secrets_dir / secret_name
|
secret_file = secrets_dir / secret_name
|
||||||
if not secret_file.is_file():
|
if not secret_file.is_file():
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ def upload_secrets(machine: Machine) -> None:
|
|||||||
"rsync",
|
"rsync",
|
||||||
"-e",
|
"-e",
|
||||||
" ".join(["ssh"] + ssh_cmd[2:]),
|
" ".join(["ssh"] + ssh_cmd[2:]),
|
||||||
"-az",
|
"--recursive",
|
||||||
|
"--links",
|
||||||
|
"--times",
|
||||||
|
"--compress",
|
||||||
"--delete",
|
"--delete",
|
||||||
"--chown=root:root",
|
|
||||||
"--chmod=D700,F600",
|
"--chmod=D700,F600",
|
||||||
f"{tempdir!s}/",
|
f"{tempdir!s}/",
|
||||||
f"{host.user}@{host.host}:{machine.secrets_upload_directory}/",
|
f"{host.user}@{host.host}:{machine.secrets_upload_directory}/",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -19,23 +19,118 @@ from .completions import add_dynamic_completer, complete_machines
|
|||||||
from .errors import ClanError
|
from .errors import ClanError
|
||||||
from .facts.secret_modules import SecretStoreBase
|
from .facts.secret_modules import SecretStoreBase
|
||||||
from .machines.machines import Machine
|
from .machines.machines import Machine
|
||||||
from .nix import nix_shell
|
from .nix import nix_build, nix_shell
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WifiConfig:
|
||||||
|
ssid: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SystemConfig:
|
||||||
|
language: str | None = field(default=None)
|
||||||
|
keymap: str | None = field(default=None)
|
||||||
|
ssh_keys_path: list[str] | None = field(default=None)
|
||||||
|
wifi_settings: list[WifiConfig] | None = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def list_possible_keymaps() -> list[str]:
|
||||||
|
cmd = nix_build(["nixpkgs#kbd"])
|
||||||
|
result = run(cmd, log=Log.STDERR, error_msg="Failed to find kbdinfo")
|
||||||
|
keymaps_dir = Path(result.stdout.strip()) / "share" / "keymaps"
|
||||||
|
|
||||||
|
if not keymaps_dir.exists():
|
||||||
|
raise FileNotFoundError(f"Keymaps directory '{keymaps_dir}' does not exist.")
|
||||||
|
|
||||||
|
keymap_files = []
|
||||||
|
|
||||||
|
for root, _, files in os.walk(keymaps_dir):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(".map.gz"):
|
||||||
|
# Remove '.map.gz' ending
|
||||||
|
name_without_ext = file[:-7]
|
||||||
|
keymap_files.append(name_without_ext)
|
||||||
|
|
||||||
|
return keymap_files
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def list_possible_languages() -> list[str]:
|
||||||
|
cmd = nix_build(["nixpkgs#glibcLocales"])
|
||||||
|
result = run(cmd, log=Log.STDERR, error_msg="Failed to find glibc locales")
|
||||||
|
locale_file = Path(result.stdout.strip()) / "share" / "i18n" / "SUPPORTED"
|
||||||
|
|
||||||
|
if not locale_file.exists():
|
||||||
|
raise FileNotFoundError(f"Locale file '{locale_file}' does not exist.")
|
||||||
|
|
||||||
|
with locale_file.open() as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
languages = []
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "SUPPORTED-LOCALES" in line:
|
||||||
|
continue
|
||||||
|
# Split by '/' and take the first part
|
||||||
|
language = line.split("/")[0].strip()
|
||||||
|
languages.append(language)
|
||||||
|
|
||||||
|
return languages
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def flash_machine(
|
def flash_machine(
|
||||||
machine: Machine,
|
machine: Machine,
|
||||||
*,
|
*,
|
||||||
mode: str,
|
mode: str,
|
||||||
disks: dict[str, str],
|
disks: dict[str, str],
|
||||||
system_config: dict[str, Any],
|
system_config: SystemConfig,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
write_efi_boot_entries: bool,
|
write_efi_boot_entries: bool,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
extra_args: list[str] = [],
|
extra_args: list[str] = [],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
system_config_nix: dict[str, Any] = {}
|
||||||
|
|
||||||
|
if system_config.wifi_settings:
|
||||||
|
wifi_settings = {}
|
||||||
|
for wifi in system_config.wifi_settings:
|
||||||
|
wifi_settings[wifi.ssid] = {"password": wifi.password}
|
||||||
|
system_config_nix["clan"] = {"iwd": {"networks": wifi_settings}}
|
||||||
|
|
||||||
|
if system_config.language:
|
||||||
|
if system_config.language not in list_possible_languages():
|
||||||
|
raise ClanError(
|
||||||
|
f"Language '{system_config.language}' is not a valid language. "
|
||||||
|
f"Run 'clan flash --list-languages' to see a list of possible languages."
|
||||||
|
)
|
||||||
|
system_config_nix["i18n"] = {"defaultLocale": system_config.language}
|
||||||
|
|
||||||
|
if system_config.keymap:
|
||||||
|
if system_config.keymap not in list_possible_keymaps():
|
||||||
|
raise ClanError(
|
||||||
|
f"Keymap '{system_config.keymap}' is not a valid keymap. "
|
||||||
|
f"Run 'clan flash --list-keymaps' to see a list of possible keymaps."
|
||||||
|
)
|
||||||
|
system_config_nix["console"] = {"keyMap": system_config.keymap}
|
||||||
|
|
||||||
|
if system_config.ssh_keys_path:
|
||||||
|
root_keys = []
|
||||||
|
for key_path in map(lambda x: Path(x), system_config.ssh_keys_path):
|
||||||
|
try:
|
||||||
|
root_keys.append(key_path.read_text())
|
||||||
|
except OSError as e:
|
||||||
|
raise ClanError(f"Cannot read SSH public key file: {key_path}: {e}")
|
||||||
|
system_config_nix["users"] = {
|
||||||
|
"users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}}
|
||||||
|
}
|
||||||
|
|
||||||
secret_facts_module = importlib.import_module(machine.secret_facts_module)
|
secret_facts_module = importlib.import_module(machine.secret_facts_module)
|
||||||
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
|
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
|
||||||
machine=machine
|
machine=machine
|
||||||
@@ -58,7 +153,8 @@ def flash_machine(
|
|||||||
raise ClanError(
|
raise ClanError(
|
||||||
"sudo is required to run disko-install as a non-root user"
|
"sudo is required to run disko-install as a non-root user"
|
||||||
)
|
)
|
||||||
disko_install.append("sudo")
|
wrapper = 'set -x; disko_install=$(command -v disko-install); exec sudo "$disko_install" "$@"'
|
||||||
|
disko_install.extend(["bash", "-c", wrapper])
|
||||||
|
|
||||||
disko_install.append("disko-install")
|
disko_install.append("disko-install")
|
||||||
if write_efi_boot_entries:
|
if write_efi_boot_entries:
|
||||||
@@ -76,7 +172,7 @@ def flash_machine(
|
|||||||
disko_install.extend(
|
disko_install.extend(
|
||||||
[
|
[
|
||||||
"--system-config",
|
"--system-config",
|
||||||
json.dumps(system_config),
|
json.dumps(system_config_nix),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
disko_install.extend(["--option", "dry-run", "true"])
|
disko_install.extend(["--option", "dry-run", "true"])
|
||||||
@@ -94,15 +190,13 @@ class FlashOptions:
|
|||||||
flake: FlakeId
|
flake: FlakeId
|
||||||
machine: str
|
machine: str
|
||||||
disks: dict[str, str]
|
disks: dict[str, str]
|
||||||
ssh_keys_path: list[Path]
|
|
||||||
dry_run: bool
|
dry_run: bool
|
||||||
confirm: bool
|
confirm: bool
|
||||||
debug: bool
|
debug: bool
|
||||||
mode: str
|
mode: str
|
||||||
language: str
|
|
||||||
keymap: str
|
|
||||||
write_efi_boot_entries: bool
|
write_efi_boot_entries: bool
|
||||||
nix_options: list[str]
|
nix_options: list[str]
|
||||||
|
system_config: SystemConfig
|
||||||
|
|
||||||
|
|
||||||
class AppendDiskAction(argparse.Action):
|
class AppendDiskAction(argparse.Action):
|
||||||
@@ -126,17 +220,36 @@ def flash_command(args: argparse.Namespace) -> None:
|
|||||||
flake=args.flake,
|
flake=args.flake,
|
||||||
machine=args.machine,
|
machine=args.machine,
|
||||||
disks=args.disk,
|
disks=args.disk,
|
||||||
ssh_keys_path=args.ssh_pubkey,
|
|
||||||
dry_run=args.dry_run,
|
dry_run=args.dry_run,
|
||||||
confirm=not args.yes,
|
confirm=not args.yes,
|
||||||
debug=args.debug,
|
debug=args.debug,
|
||||||
mode=args.mode,
|
mode=args.mode,
|
||||||
language=args.language,
|
system_config=SystemConfig(
|
||||||
keymap=args.keymap,
|
language=args.language,
|
||||||
|
keymap=args.keymap,
|
||||||
|
ssh_keys_path=args.ssh_pubkey,
|
||||||
|
wifi_settings=None,
|
||||||
|
),
|
||||||
write_efi_boot_entries=args.write_efi_boot_entries,
|
write_efi_boot_entries=args.write_efi_boot_entries,
|
||||||
nix_options=args.option,
|
nix_options=args.option,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if args.list_languages:
|
||||||
|
for language in list_possible_languages():
|
||||||
|
print(language)
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.list_keymaps:
|
||||||
|
for keymap in list_possible_keymaps():
|
||||||
|
print(keymap)
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.wifi:
|
||||||
|
opts.system_config.wifi_settings = [
|
||||||
|
WifiConfig(ssid=ssid, password=password)
|
||||||
|
for ssid, password in args.wifi.items()
|
||||||
|
]
|
||||||
|
|
||||||
machine = Machine(opts.machine, flake=opts.flake)
|
machine = Machine(opts.machine, flake=opts.flake)
|
||||||
if opts.confirm and not opts.dry_run:
|
if opts.confirm and not opts.dry_run:
|
||||||
disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items())
|
disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items())
|
||||||
@@ -148,28 +261,11 @@ def flash_command(args: argparse.Namespace) -> None:
|
|||||||
if ask != "y":
|
if ask != "y":
|
||||||
return
|
return
|
||||||
|
|
||||||
extra_config: dict[str, Any] = {}
|
|
||||||
if opts.ssh_keys_path:
|
|
||||||
root_keys = []
|
|
||||||
for key_path in opts.ssh_keys_path:
|
|
||||||
try:
|
|
||||||
root_keys.append(key_path.read_text())
|
|
||||||
except OSError as e:
|
|
||||||
raise ClanError(f"Cannot read SSH public key file: {key_path}: {e}")
|
|
||||||
extra_config["users"] = {
|
|
||||||
"users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}}
|
|
||||||
}
|
|
||||||
if opts.keymap:
|
|
||||||
extra_config["console"] = {"keyMap": opts.keymap}
|
|
||||||
|
|
||||||
if opts.language:
|
|
||||||
extra_config["i18n"] = {"defaultLocale": opts.language}
|
|
||||||
|
|
||||||
flash_machine(
|
flash_machine(
|
||||||
machine,
|
machine,
|
||||||
mode=opts.mode,
|
mode=opts.mode,
|
||||||
disks=opts.disks,
|
disks=opts.disks,
|
||||||
system_config=extra_config,
|
system_config=opts.system_config,
|
||||||
dry_run=opts.dry_run,
|
dry_run=opts.dry_run,
|
||||||
debug=opts.debug,
|
debug=opts.debug,
|
||||||
write_efi_boot_entries=opts.write_efi_boot_entries,
|
write_efi_boot_entries=opts.write_efi_boot_entries,
|
||||||
@@ -202,6 +298,15 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
Mount is useful for updating an existing system without losing data.
|
Mount is useful for updating an existing system without losing data.
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--wifi",
|
||||||
|
type=str,
|
||||||
|
nargs=2,
|
||||||
|
metavar=("ssid", "password"),
|
||||||
|
action=AppendDiskAction,
|
||||||
|
help="wifi network to connect to",
|
||||||
|
default={},
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--mode",
|
"--mode",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -221,6 +326,18 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
type=str,
|
type=str,
|
||||||
help="system language",
|
help="system language",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list-languages",
|
||||||
|
help="List possible languages",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list-keymaps",
|
||||||
|
help="List possible keymaps",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--keymap",
|
"--keymap",
|
||||||
type=str,
|
type=str,
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def is_flatpak() -> bool:
|
|
||||||
"""Check if the current process is running inside a flatpak sandbox."""
|
|
||||||
# FLATPAK_ID environment variable check
|
|
||||||
flatpak_env = "FLATPAK_ID" in os.environ
|
|
||||||
|
|
||||||
flatpak_file = False
|
|
||||||
try:
|
|
||||||
with open("/.flatpak-info"):
|
|
||||||
flatpak_file = True
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return flatpak_env and flatpak_file
|
|
||||||
@@ -7,7 +7,7 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from clan_cli.clan.inspect import FlakeConfig, inspect_flake
|
from clan_cli.clan.inspect import FlakeConfig, inspect_flake
|
||||||
from clan_cli.machines.list import list_machines
|
from clan_cli.machines.list import list_nixos_machines
|
||||||
|
|
||||||
from ..clan_uri import ClanURI
|
from ..clan_uri import ClanURI
|
||||||
from ..dirs import user_history_file
|
from ..dirs import user_history_file
|
||||||
@@ -72,7 +72,7 @@ def new_history_entry(url: str, machine: str) -> HistoryEntry:
|
|||||||
def add_all_to_history(uri: ClanURI) -> list[HistoryEntry]:
|
def add_all_to_history(uri: ClanURI) -> list[HistoryEntry]:
|
||||||
history = list_history()
|
history = list_history()
|
||||||
new_entries: list[HistoryEntry] = []
|
new_entries: list[HistoryEntry] = []
|
||||||
for machine in list_machines(uri.get_url()):
|
for machine in list_nixos_machines(uri.get_url()):
|
||||||
new_entry = _add_maschine_to_history_list(uri.get_url(), machine, history)
|
new_entry = _add_maschine_to_history_list(uri.get_url(), machine, history)
|
||||||
new_entries.append(new_entry)
|
new_entries.append(new_entry)
|
||||||
write_history_file(history)
|
write_history_file(history)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user