Merge branch 'main' into init/sunshine-accept
This commit is contained in:
commit
0695e2c0fc
@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- run: nix run --refresh github:Mic92/nix-fast-build -- --no-nom --eval-workers 20
|
- run: nix run --refresh github:Mic92/nix-fast-build -- --no-nom --eval-workers 10
|
||||||
check-links:
|
check-links:
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,7 +9,6 @@ example_clan
|
|||||||
result*
|
result*
|
||||||
/pkgs/clan-cli/clan_cli/nixpkgs
|
/pkgs/clan-cli/clan_cli/nixpkgs
|
||||||
/pkgs/clan-cli/clan_cli/webui/assets
|
/pkgs/clan-cli/clan_cli/webui/assets
|
||||||
/machines
|
|
||||||
nixos.qcow2
|
nixos.qcow2
|
||||||
**/*.glade~
|
**/*.glade~
|
||||||
|
|
||||||
|
@ -14,21 +14,27 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
flake.nixosConfigurations = { inherit (clan.nixosConfigurations) test_backup_client; };
|
flake.nixosConfigurations = {
|
||||||
|
inherit (clan.nixosConfigurations) test_backup_client;
|
||||||
|
};
|
||||||
flake.clanInternals = clan.clanInternals;
|
flake.clanInternals = clan.clanInternals;
|
||||||
flake.nixosModules = {
|
flake.nixosModules = {
|
||||||
test_backup_server = { ... }: {
|
test_backup_server =
|
||||||
imports = [
|
{ ... }:
|
||||||
self.clanModules.borgbackup
|
{
|
||||||
];
|
imports = [ self.clanModules.borgbackup ];
|
||||||
services.sshd.enable = true;
|
services.sshd.enable = true;
|
||||||
services.borgbackup.repos.testrepo = {
|
services.borgbackup.repos.testrepo = {
|
||||||
authorizedKeys = [
|
authorizedKeys = [ (builtins.readFile ../lib/ssh/pubkey) ];
|
||||||
(builtins.readFile ../lib/ssh/pubkey)
|
};
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
test_backup_client =
|
||||||
test_backup_client = { pkgs, lib, config, ... }:
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
dependencies = [
|
dependencies = [
|
||||||
self
|
self
|
||||||
@ -38,14 +44,10 @@ in
|
|||||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [ self.clanModules.borgbackup ];
|
||||||
self.clanModules.borgbackup
|
|
||||||
];
|
|
||||||
networking.hostName = "client";
|
networking.hostName = "client";
|
||||||
services.sshd.enable = true;
|
services.sshd.enable = true;
|
||||||
users.users.root.openssh.authorizedKeys.keyFiles = [
|
users.users.root.openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ];
|
||||||
../lib/ssh/pubkey
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.tmpfiles.settings."vmsecrets" = {
|
systemd.tmpfiles.settings."vmsecrets" = {
|
||||||
"/etc/secrets/borgbackup.ssh" = {
|
"/etc/secrets/borgbackup.ssh" = {
|
||||||
@ -75,71 +77,67 @@ in
|
|||||||
};
|
};
|
||||||
system.extraDependencies = dependencies;
|
system.extraDependencies = dependencies;
|
||||||
clanCore.state.test-backups.folders = [ "/var/test-backups" ];
|
clanCore.state.test-backups.folders = [ "/var/test-backups" ];
|
||||||
clan.borgbackup = {
|
clan.borgbackup.destinations.test_backup_server.repo = "borg@server:.";
|
||||||
enable = true;
|
|
||||||
destinations.test_backup_server.repo = "borg@server:.";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
perSystem = { nodes, pkgs, ... }: {
|
perSystem =
|
||||||
checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
|
{ nodes, pkgs, ... }:
|
||||||
test-backups =
|
{
|
||||||
(import ../lib/test-base.nix)
|
checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
|
||||||
{
|
test-backups = (import ../lib/test-base.nix) {
|
||||||
name = "test-backups";
|
name = "test-backups";
|
||||||
nodes.server = {
|
nodes.server = {
|
||||||
imports = [
|
imports = [
|
||||||
self.nixosModules.test_backup_server
|
self.nixosModules.test_backup_server
|
||||||
self.nixosModules.clanCore
|
self.nixosModules.clanCore
|
||||||
{
|
{
|
||||||
clanCore.machineName = "server";
|
clanCore.machineName = "server";
|
||||||
clanCore.clanDir = ../..;
|
clanCore.clanDir = ../..;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
nodes.client = {
|
nodes.client = {
|
||||||
imports = [
|
imports = [
|
||||||
self.nixosModules.test_backup_client
|
self.nixosModules.test_backup_client
|
||||||
self.nixosModules.clanCore
|
self.nixosModules.clanCore
|
||||||
{
|
{
|
||||||
clanCore.machineName = "client";
|
clanCore.machineName = "client";
|
||||||
clanCore.clanDir = ../..;
|
clanCore.clanDir = ../..;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = ''
|
testScript = ''
|
||||||
import json
|
import json
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
# setup
|
# setup
|
||||||
client.succeed("mkdir -m 700 /root/.ssh")
|
client.succeed("mkdir -m 700 /root/.ssh")
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"cat ${../lib/ssh/privkey} > /root/.ssh/id_ed25519"
|
"cat ${../lib/ssh/privkey} > /root/.ssh/id_ed25519"
|
||||||
)
|
)
|
||||||
client.succeed("chmod 600 /root/.ssh/id_ed25519")
|
client.succeed("chmod 600 /root/.ssh/id_ed25519")
|
||||||
client.wait_for_unit("sshd", timeout=30)
|
client.wait_for_unit("sshd", timeout=30)
|
||||||
client.succeed("ssh -o StrictHostKeyChecking=accept-new root@client hostname")
|
client.succeed("ssh -o StrictHostKeyChecking=accept-new root@client hostname")
|
||||||
|
|
||||||
# dummy data
|
# dummy data
|
||||||
client.succeed("mkdir /var/test-backups")
|
client.succeed("mkdir /var/test-backups")
|
||||||
client.succeed("echo testing > /var/test-backups/somefile")
|
client.succeed("echo testing > /var/test-backups/somefile")
|
||||||
|
|
||||||
# create
|
# create
|
||||||
client.succeed("clan --debug --flake ${../..} backups create test_backup_client")
|
client.succeed("clan --debug --flake ${../..} backups create test_backup_client")
|
||||||
client.wait_until_succeeds("! systemctl is-active borgbackup-job-test_backup_server")
|
client.wait_until_succeeds("! systemctl is-active borgbackup-job-test_backup_server")
|
||||||
|
|
||||||
# list
|
# list
|
||||||
backup_id = json.loads(client.succeed("borg-job-test_backup_server list --json"))["archives"][0]["archive"]
|
backup_id = json.loads(client.succeed("borg-job-test_backup_server list --json"))["archives"][0]["archive"]
|
||||||
assert(backup_id in client.succeed("clan --debug --flake ${../..} backups list test_backup_client"))
|
assert(backup_id in client.succeed("clan --debug --flake ${../..} backups list test_backup_client"))
|
||||||
|
|
||||||
# restore
|
# restore
|
||||||
client.succeed("rm -f /var/test-backups/somefile")
|
client.succeed("rm -f /var/test-backups/somefile")
|
||||||
client.succeed(f"clan --debug --flake ${../..} backups restore test_backup_client borgbackup {backup_id}")
|
client.succeed(f"clan --debug --flake ${../..} backups restore test_backup_client borgbackup {backup_id}")
|
||||||
assert(client.succeed("cat /var/test-backups/somefile").strip() == "testing")
|
assert(client.succeed("cat /var/test-backups/somefile").strip() == "testing")
|
||||||
'';
|
'';
|
||||||
}
|
} { inherit pkgs self; };
|
||||||
{ inherit pkgs self; };
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,51 @@
|
|||||||
(import ../lib/test-base.nix) ({ ... }: {
|
(import ../lib/test-base.nix) (
|
||||||
name = "borgbackup";
|
{ ... }:
|
||||||
|
{
|
||||||
|
name = "borgbackup";
|
||||||
|
|
||||||
nodes.machine = { self, pkgs, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ self, pkgs, ... }:
|
||||||
self.clanModules.borgbackup
|
|
||||||
self.nixosModules.clanCore
|
|
||||||
{
|
{
|
||||||
services.openssh.enable = true;
|
imports = [
|
||||||
services.borgbackup.repos.testrepo = {
|
self.clanModules.borgbackup
|
||||||
authorizedKeys = [
|
self.nixosModules.clanCore
|
||||||
(builtins.readFile ../lib/ssh/pubkey)
|
{
|
||||||
];
|
services.openssh.enable = true;
|
||||||
};
|
services.borgbackup.repos.testrepo = {
|
||||||
}
|
authorizedKeys = [ (builtins.readFile ../lib/ssh/pubkey) ];
|
||||||
{
|
|
||||||
clanCore.machineName = "machine";
|
|
||||||
clanCore.clanDir = ./.;
|
|
||||||
clanCore.state.testState.folders = [ "/etc/state" ];
|
|
||||||
environment.etc.state.text = "hello world";
|
|
||||||
systemd.tmpfiles.settings."vmsecrets" = {
|
|
||||||
"/etc/secrets/borgbackup.ssh" = {
|
|
||||||
C.argument = "${../lib/ssh/privkey}";
|
|
||||||
z = {
|
|
||||||
mode = "0400";
|
|
||||||
user = "root";
|
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
"/etc/secrets/borgbackup.repokey" = {
|
{
|
||||||
C.argument = builtins.toString (pkgs.writeText "repokey" "repokey12345");
|
clanCore.machineName = "machine";
|
||||||
z = {
|
clanCore.clanDir = ./.;
|
||||||
mode = "0400";
|
clanCore.state.testState.folders = [ "/etc/state" ];
|
||||||
user = "root";
|
environment.etc.state.text = "hello world";
|
||||||
|
systemd.tmpfiles.settings."vmsecrets" = {
|
||||||
|
"/etc/secrets/borgbackup.ssh" = {
|
||||||
|
C.argument = "${../lib/ssh/privkey}";
|
||||||
|
z = {
|
||||||
|
mode = "0400";
|
||||||
|
user = "root";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"/etc/secrets/borgbackup.repokey" = {
|
||||||
|
C.argument = builtins.toString (pkgs.writeText "repokey" "repokey12345");
|
||||||
|
z = {
|
||||||
|
mode = "0400";
|
||||||
|
user = "root";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
clanCore.secretStore = "vm";
|
||||||
};
|
|
||||||
clanCore.secretStore = "vm";
|
|
||||||
|
|
||||||
clan.borgbackup = {
|
clan.borgbackup.destinations.test.repo = "borg@localhost:.";
|
||||||
enable = true;
|
}
|
||||||
destinations.test.repo = "borg@localhost:.";
|
];
|
||||||
};
|
};
|
||||||
}
|
testScript = ''
|
||||||
];
|
start_all()
|
||||||
};
|
machine.systemctl("start --wait borgbackup-job-test.service")
|
||||||
testScript = ''
|
assert "machine-test" in machine.succeed("BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes /run/current-system/sw/bin/borg-job-test list")
|
||||||
start_all()
|
'';
|
||||||
machine.systemctl("start --wait borgbackup-job-test.service")
|
}
|
||||||
assert "machine-test" in machine.succeed("BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes /run/current-system/sw/bin/borg-job-test list")
|
)
|
||||||
'';
|
|
||||||
})
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
(import ../lib/container-test.nix) ({ ... }: {
|
(import ../lib/container-test.nix) (
|
||||||
name = "secrets";
|
{ ... }:
|
||||||
|
{
|
||||||
|
name = "secrets";
|
||||||
|
|
||||||
nodes.machine = { ... }: {
|
nodes.machine =
|
||||||
networking.hostName = "machine";
|
{ ... }:
|
||||||
services.openssh.enable = true;
|
{
|
||||||
services.openssh.startWhenNeeded = false;
|
networking.hostName = "machine";
|
||||||
};
|
services.openssh.enable = true;
|
||||||
testScript = ''
|
services.openssh.startWhenNeeded = false;
|
||||||
start_all()
|
};
|
||||||
machine.succeed("systemctl status sshd")
|
testScript = ''
|
||||||
machine.wait_for_unit("sshd")
|
start_all()
|
||||||
'';
|
machine.succeed("systemctl status sshd")
|
||||||
})
|
machine.wait_for_unit("sshd")
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -1,24 +1,29 @@
|
|||||||
(import ../lib/container-test.nix) ({ pkgs, ... }: {
|
(import ../lib/container-test.nix) (
|
||||||
name = "secrets";
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
name = "secrets";
|
||||||
|
|
||||||
nodes.machine = { self, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ self, ... }:
|
||||||
self.clanModules.deltachat
|
|
||||||
self.nixosModules.clanCore
|
|
||||||
{
|
{
|
||||||
clanCore.machineName = "machine";
|
imports = [
|
||||||
clanCore.clanDir = ./.;
|
self.clanModules.deltachat
|
||||||
}
|
self.nixosModules.clanCore
|
||||||
];
|
{
|
||||||
};
|
clanCore.machineName = "machine";
|
||||||
testScript = ''
|
clanCore.clanDir = ./.;
|
||||||
start_all()
|
}
|
||||||
machine.wait_for_unit("maddy")
|
];
|
||||||
# imap
|
};
|
||||||
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 143")
|
testScript = ''
|
||||||
# smtp submission
|
start_all()
|
||||||
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 587")
|
machine.wait_for_unit("maddy")
|
||||||
# smtp
|
# imap
|
||||||
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 25")
|
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 143")
|
||||||
'';
|
# smtp submission
|
||||||
})
|
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 587")
|
||||||
|
# smtp
|
||||||
|
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 25")
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -1,40 +1,20 @@
|
|||||||
{ self, ... }: {
|
{ self, ... }:
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./impure/flake-module.nix
|
./impure/flake-module.nix
|
||||||
./backups/flake-module.nix
|
./backups/flake-module.nix
|
||||||
./installation/flake-module.nix
|
./installation/flake-module.nix
|
||||||
|
./flash/flake-module.nix
|
||||||
];
|
];
|
||||||
perSystem = { pkgs, lib, self', ... }: {
|
perSystem =
|
||||||
checks =
|
{
|
||||||
let
|
pkgs,
|
||||||
nixosTestArgs = {
|
lib,
|
||||||
# reference to nixpkgs for the current system
|
self',
|
||||||
inherit pkgs;
|
...
|
||||||
# this gives us a reference to our flake but also all flake inputs
|
}:
|
||||||
inherit self;
|
{
|
||||||
};
|
checks =
|
||||||
nixosTests = lib.optionalAttrs (pkgs.stdenv.isLinux) {
|
|
||||||
# import our test
|
|
||||||
secrets = import ./secrets nixosTestArgs;
|
|
||||||
container = import ./container nixosTestArgs;
|
|
||||||
deltachat = import ./deltachat nixosTestArgs;
|
|
||||||
zt-tcp-relay = import ./zt-tcp-relay nixosTestArgs;
|
|
||||||
borgbackup = import ./borgbackup nixosTestArgs;
|
|
||||||
syncthing = import ./syncthing nixosTestArgs;
|
|
||||||
wayland-proxy-virtwl = import ./wayland-proxy-virtwl nixosTestArgs;
|
|
||||||
};
|
|
||||||
schemaTests = pkgs.callPackages ./schemas.nix {
|
|
||||||
inherit self;
|
|
||||||
};
|
|
||||||
|
|
||||||
flakeOutputs = lib.mapAttrs' (name: config: lib.nameValuePair "nixos-${name}" config.config.system.build.toplevel) self.nixosConfigurations
|
|
||||||
// lib.mapAttrs' (n: lib.nameValuePair "package-${n}") self'.packages
|
|
||||||
// lib.mapAttrs' (n: lib.nameValuePair "devShell-${n}") self'.devShells
|
|
||||||
// lib.mapAttrs' (name: config: lib.nameValuePair "home-manager-${name}" config.activation-script) (self'.legacyPackages.homeConfigurations or { });
|
|
||||||
in
|
|
||||||
nixosTests // schemaTests // flakeOutputs;
|
|
||||||
legacyPackages = {
|
|
||||||
nixosTests =
|
|
||||||
let
|
let
|
||||||
nixosTestArgs = {
|
nixosTestArgs = {
|
||||||
# reference to nixpkgs for the current system
|
# reference to nixpkgs for the current system
|
||||||
@ -42,12 +22,44 @@
|
|||||||
# this gives us a reference to our flake but also all flake inputs
|
# this gives us a reference to our flake but also all flake inputs
|
||||||
inherit self;
|
inherit self;
|
||||||
};
|
};
|
||||||
|
nixosTests = lib.optionalAttrs (pkgs.stdenv.isLinux) {
|
||||||
|
# import our test
|
||||||
|
secrets = import ./secrets nixosTestArgs;
|
||||||
|
container = import ./container nixosTestArgs;
|
||||||
|
deltachat = import ./deltachat nixosTestArgs;
|
||||||
|
zt-tcp-relay = import ./zt-tcp-relay nixosTestArgs;
|
||||||
|
borgbackup = import ./borgbackup nixosTestArgs;
|
||||||
|
syncthing = import ./syncthing nixosTestArgs;
|
||||||
|
wayland-proxy-virtwl = import ./wayland-proxy-virtwl nixosTestArgs;
|
||||||
|
};
|
||||||
|
schemaTests = pkgs.callPackages ./schemas.nix { inherit self; };
|
||||||
|
|
||||||
|
flakeOutputs =
|
||||||
|
lib.mapAttrs' (
|
||||||
|
name: config: lib.nameValuePair "nixos-${name}" config.config.system.build.toplevel
|
||||||
|
) self.nixosConfigurations
|
||||||
|
// lib.mapAttrs' (n: lib.nameValuePair "package-${n}") self'.packages
|
||||||
|
// lib.mapAttrs' (n: lib.nameValuePair "devShell-${n}") self'.devShells
|
||||||
|
// lib.mapAttrs' (name: config: lib.nameValuePair "home-manager-${name}" config.activation-script) (
|
||||||
|
self'.legacyPackages.homeConfigurations or { }
|
||||||
|
);
|
||||||
in
|
in
|
||||||
lib.optionalAttrs (pkgs.stdenv.isLinux) {
|
nixosTests // schemaTests // flakeOutputs;
|
||||||
# import our test
|
legacyPackages = {
|
||||||
secrets = import ./secrets nixosTestArgs;
|
nixosTests =
|
||||||
container = import ./container nixosTestArgs;
|
let
|
||||||
};
|
nixosTestArgs = {
|
||||||
|
# reference to nixpkgs for the current system
|
||||||
|
inherit pkgs;
|
||||||
|
# this gives us a reference to our flake but also all flake inputs
|
||||||
|
inherit self;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
lib.optionalAttrs (pkgs.stdenv.isLinux) {
|
||||||
|
# import our test
|
||||||
|
secrets = import ./secrets nixosTestArgs;
|
||||||
|
container = import ./container nixosTestArgs;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
49
checks/flash/flake-module.nix
Normal file
49
checks/flash/flake-module.nix
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{ self, ... }:
|
||||||
|
{
|
||||||
|
perSystem =
|
||||||
|
{
|
||||||
|
nodes,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
dependencies = [
|
||||||
|
self
|
||||||
|
pkgs.stdenv.drvPath
|
||||||
|
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.toplevel
|
||||||
|
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.diskoScript
|
||||||
|
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.clan.deployment.file
|
||||||
|
self.inputs.nixpkgs.legacyPackages.${pkgs.hostPlatform.system}.disko
|
||||||
|
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||||
|
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
|
||||||
|
flash = (import ../lib/test-base.nix) {
|
||||||
|
name = "flash";
|
||||||
|
nodes.target = {
|
||||||
|
virtualisation.emptyDiskImages = [ 4096 ];
|
||||||
|
virtualisation.memorySize = 3000;
|
||||||
|
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
||||||
|
environment.etc."install-closure".source = "${closureInfo}/store-paths";
|
||||||
|
|
||||||
|
nix.settings = {
|
||||||
|
substituters = lib.mkForce [ ];
|
||||||
|
hashed-mirrors = null;
|
||||||
|
connect-timeout = lib.mkForce 3;
|
||||||
|
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
||||||
|
experimental-features = [
|
||||||
|
"nix-command"
|
||||||
|
"flakes"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
machine.succeed("clan --flake ${../..} flash --debug --yes --disk main /dev/vdb test_install_machine")
|
||||||
|
'';
|
||||||
|
} { inherit pkgs self; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
perSystem = { pkgs, lib, ... }: {
|
perSystem =
|
||||||
# a script that executes all other checks
|
{ pkgs, lib, ... }:
|
||||||
packages.impure-checks = pkgs.writeShellScriptBin "impure-checks" ''
|
{
|
||||||
#!${pkgs.bash}/bin/bash
|
# a script that executes all other checks
|
||||||
set -euo pipefail
|
packages.impure-checks = pkgs.writeShellScriptBin "impure-checks" ''
|
||||||
|
#!${pkgs.bash}/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
export PATH="${lib.makeBinPath [
|
export PATH="${
|
||||||
pkgs.gitMinimal
|
lib.makeBinPath [
|
||||||
pkgs.nix
|
pkgs.gitMinimal
|
||||||
pkgs.rsync # needed to have rsync installed on the dummy ssh server
|
pkgs.nix
|
||||||
]}"
|
pkgs.rsync # needed to have rsync installed on the dummy ssh server
|
||||||
ROOT=$(git rev-parse --show-toplevel)
|
]
|
||||||
cd "$ROOT/pkgs/clan-cli"
|
}"
|
||||||
nix develop "$ROOT#clan-cli" -c bash -c "TMPDIR=/tmp python -m pytest -s -m impure ./tests $@"
|
ROOT=$(git rev-parse --show-toplevel)
|
||||||
'';
|
cd "$ROOT/pkgs/clan-cli"
|
||||||
};
|
nix develop "$ROOT#clan-cli" -c bash -c "TMPDIR=/tmp python -m pytest -s -m impure ./tests $@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,32 +12,34 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
flake.nixosConfigurations = { inherit (clan.nixosConfigurations) test_install_machine; };
|
flake.nixosConfigurations = {
|
||||||
|
inherit (clan.nixosConfigurations) test_install_machine;
|
||||||
|
};
|
||||||
flake.clanInternals = clan.clanInternals;
|
flake.clanInternals = clan.clanInternals;
|
||||||
flake.nixosModules = {
|
flake.nixosModules = {
|
||||||
test_install_machine = { lib, modulesPath, ... }: {
|
test_install_machine =
|
||||||
imports = [
|
{ lib, modulesPath, ... }:
|
||||||
self.clanModules.diskLayouts
|
{
|
||||||
(modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests
|
imports = [
|
||||||
(modulesPath + "/profiles/qemu-guest.nix")
|
self.clanModules.diskLayouts
|
||||||
];
|
(modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests
|
||||||
fileSystems."/nix/store" = lib.mkForce {
|
(modulesPath + "/profiles/qemu-guest.nix")
|
||||||
device = "nix-store";
|
];
|
||||||
fsType = "9p";
|
clan.diskLayouts.singleDiskExt4.device = "/dev/vdb";
|
||||||
neededForBoot = true;
|
|
||||||
options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
|
environment.etc."install-successful".text = "ok";
|
||||||
|
|
||||||
|
boot.consoleLogLevel = lib.mkForce 100;
|
||||||
|
boot.kernelParams = [ "boot.shell_on_fail" ];
|
||||||
};
|
};
|
||||||
clan.diskLayouts.singleDiskExt4.device = "/dev/vdb";
|
|
||||||
|
|
||||||
environment.etc."install-successful".text = "ok";
|
|
||||||
|
|
||||||
boot.consoleLogLevel = lib.mkForce 100;
|
|
||||||
boot.kernelParams = [
|
|
||||||
"boot.shell_on_fail"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
perSystem = { nodes, pkgs, lib, ... }:
|
perSystem =
|
||||||
|
{
|
||||||
|
nodes,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
dependencies = [
|
dependencies = [
|
||||||
self
|
self
|
||||||
@ -51,74 +53,69 @@ in
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
|
checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
|
||||||
test-installation =
|
test-installation = (import ../lib/test-base.nix) {
|
||||||
(import ../lib/test-base.nix)
|
name = "test-installation";
|
||||||
{
|
nodes.target = {
|
||||||
name = "test-installation";
|
services.openssh.enable = true;
|
||||||
nodes.target = {
|
users.users.root.openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ];
|
||||||
services.openssh.enable = true;
|
system.nixos.variant_id = "installer";
|
||||||
users.users.root.openssh.authorizedKeys.keyFiles = [
|
virtualisation.emptyDiskImages = [ 4096 ];
|
||||||
../lib/ssh/pubkey
|
nix.settings = {
|
||||||
];
|
substituters = lib.mkForce [ ];
|
||||||
system.nixos.variant_id = "installer";
|
hashed-mirrors = null;
|
||||||
virtualisation.emptyDiskImages = [ 4096 ];
|
connect-timeout = lib.mkForce 3;
|
||||||
nix.settings = {
|
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
||||||
substituters = lib.mkForce [ ];
|
experimental-features = [
|
||||||
hashed-mirrors = null;
|
"nix-command"
|
||||||
connect-timeout = lib.mkForce 3;
|
"flakes"
|
||||||
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
];
|
||||||
experimental-features = [
|
};
|
||||||
"nix-command"
|
};
|
||||||
"flakes"
|
nodes.client = {
|
||||||
];
|
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
||||||
};
|
environment.etc."install-closure".source = "${closureInfo}/store-paths";
|
||||||
};
|
virtualisation.memorySize = 2048;
|
||||||
nodes.client = {
|
nix.settings = {
|
||||||
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
substituters = lib.mkForce [ ];
|
||||||
environment.etc."install-closure".source = "${closureInfo}/store-paths";
|
hashed-mirrors = null;
|
||||||
virtualisation.memorySize = 2048;
|
connect-timeout = lib.mkForce 3;
|
||||||
nix.settings = {
|
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
||||||
substituters = lib.mkForce [ ];
|
experimental-features = [
|
||||||
hashed-mirrors = null;
|
"nix-command"
|
||||||
connect-timeout = lib.mkForce 3;
|
"flakes"
|
||||||
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
];
|
||||||
experimental-features = [
|
};
|
||||||
"nix-command"
|
system.extraDependencies = dependencies;
|
||||||
"flakes"
|
};
|
||||||
];
|
|
||||||
};
|
|
||||||
system.extraDependencies = dependencies;
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = ''
|
testScript = ''
|
||||||
def create_test_machine(oldmachine=None, args={}): # taken from <nixpkgs/nixos/tests/installer.nix>
|
def create_test_machine(oldmachine=None, args={}): # taken from <nixpkgs/nixos/tests/installer.nix>
|
||||||
machine = create_machine({
|
startCommand = "${pkgs.qemu_test}/bin/qemu-kvm"
|
||||||
"qemuFlags":
|
startCommand += " -cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store"
|
||||||
'-cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store,'
|
startCommand += f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report'
|
||||||
f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report'
|
startCommand += ' -device virtio-blk-pci,drive=drive1'
|
||||||
f' -device virtio-blk-pci,drive=drive1',
|
machine = create_machine({
|
||||||
} | args)
|
"startCommand": startCommand,
|
||||||
driver.machines.append(machine)
|
} | args)
|
||||||
return machine
|
driver.machines.append(machine)
|
||||||
|
return machine
|
||||||
|
|
||||||
|
start_all()
|
||||||
|
|
||||||
start_all()
|
client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519")
|
||||||
|
client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname")
|
||||||
|
|
||||||
client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519")
|
client.succeed("clan --debug --flake ${../..} machines install --yes test_install_machine root@target >&2")
|
||||||
client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname")
|
try:
|
||||||
|
target.shutdown()
|
||||||
|
except BrokenPipeError:
|
||||||
|
# qemu has already exited
|
||||||
|
pass
|
||||||
|
|
||||||
client.succeed("clan --debug --flake ${../..} machines install test_install_machine root@target >&2")
|
new_machine = create_test_machine(oldmachine=target, args={ "name": "new_machine" })
|
||||||
try:
|
assert(new_machine.succeed("cat /etc/install-successful").strip() == "ok")
|
||||||
target.shutdown()
|
'';
|
||||||
except BrokenPipeError:
|
} { inherit pkgs self; };
|
||||||
# qemu has already exited
|
|
||||||
pass
|
|
||||||
|
|
||||||
new_machine = create_test_machine(oldmachine=target, args={ "name": "new_machine" })
|
|
||||||
assert(new_machine.succeed("cat /etc/install-successful").strip() == "ok")
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{ inherit pkgs self; };
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
{ hostPkgs, lib, config, ... }:
|
{
|
||||||
|
hostPkgs,
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
testDriver = hostPkgs.python3.pkgs.callPackage ./package.nix {
|
testDriver = hostPkgs.python3.pkgs.callPackage ./package.nix {
|
||||||
inherit (config) extraPythonPackages;
|
inherit (config) extraPythonPackages;
|
||||||
inherit (hostPkgs.pkgs) util-linux systemd;
|
inherit (hostPkgs.pkgs) util-linux systemd;
|
||||||
};
|
};
|
||||||
containers = map (m: m.system.build.toplevel) (lib.attrValues config.nodes);
|
containers = map (m: m.system.build.toplevel) (lib.attrValues config.nodes);
|
||||||
pythonizeName = name:
|
pythonizeName =
|
||||||
|
name:
|
||||||
let
|
let
|
||||||
head = lib.substring 0 1 name;
|
head = lib.substring 0 1 name;
|
||||||
tail = lib.substring 1 (-1) name;
|
tail = lib.substring 1 (-1) name;
|
||||||
in
|
in
|
||||||
(if builtins.match "[A-z_]" head == null then "_" else head) +
|
(if builtins.match "[A-z_]" head == null then "_" else head)
|
||||||
lib.stringAsChars (c: if builtins.match "[A-z0-9_]" c == null then "_" else c) tail;
|
+ lib.stringAsChars (c: if builtins.match "[A-z0-9_]" c == null then "_" else c) tail;
|
||||||
nodeHostNames =
|
nodeHostNames =
|
||||||
let
|
let
|
||||||
nodesList = map (c: c.system.name) (lib.attrValues config.nodes);
|
nodesList = map (c: c.system.name) (lib.attrValues config.nodes);
|
||||||
@ -21,68 +27,72 @@ let
|
|||||||
pythonizedNames = map pythonizeName nodeHostNames;
|
pythonizedNames = map pythonizeName nodeHostNames;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
driver = lib.mkForce (hostPkgs.runCommand "nixos-test-driver-${config.name}"
|
driver = lib.mkForce (
|
||||||
{
|
hostPkgs.runCommand "nixos-test-driver-${config.name}"
|
||||||
nativeBuildInputs = [
|
{
|
||||||
hostPkgs.makeWrapper
|
nativeBuildInputs = [
|
||||||
] ++ lib.optionals (!config.skipTypeCheck) [ hostPkgs.mypy ];
|
hostPkgs.makeWrapper
|
||||||
buildInputs = [ testDriver ];
|
] ++ lib.optionals (!config.skipTypeCheck) [ hostPkgs.mypy ];
|
||||||
testScript = config.testScriptString;
|
buildInputs = [ testDriver ];
|
||||||
preferLocalBuild = true;
|
testScript = config.testScriptString;
|
||||||
passthru = config.passthru;
|
preferLocalBuild = true;
|
||||||
meta = config.meta // {
|
passthru = config.passthru;
|
||||||
mainProgram = "nixos-test-driver";
|
meta = config.meta // {
|
||||||
|
mainProgram = "nixos-test-driver";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
|
||||||
|
containers=(${toString containers})
|
||||||
|
|
||||||
|
${lib.optionalString (!config.skipTypeCheck) ''
|
||||||
|
# prepend type hints so the test script can be type checked with mypy
|
||||||
|
cat "${./test-script-prepend.py}" >> testScriptWithTypes
|
||||||
|
echo "${builtins.toString machineNames}" >> testScriptWithTypes
|
||||||
|
echo -n "$testScript" >> testScriptWithTypes
|
||||||
|
|
||||||
|
echo "Running type check (enable/disable: config.skipTypeCheck)"
|
||||||
|
echo "See https://nixos.org/manual/nixos/stable/#test-opt-skipTypeCheck"
|
||||||
|
|
||||||
|
mypy --no-implicit-optional \
|
||||||
|
--pretty \
|
||||||
|
--no-color-output \
|
||||||
|
testScriptWithTypes
|
||||||
|
''}
|
||||||
|
|
||||||
|
echo -n "$testScript" >> $out/test-script
|
||||||
|
|
||||||
|
ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver
|
||||||
|
|
||||||
|
wrapProgram $out/bin/nixos-test-driver \
|
||||||
|
${lib.concatStringsSep " " (map (name: "--add-flags '--container ${name}'") containers)} \
|
||||||
|
--add-flags "--test-script '$out/test-script'"
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
test = lib.mkForce (
|
||||||
|
lib.lazyDerivation {
|
||||||
|
# lazyDerivation improves performance when only passthru items and/or meta are used.
|
||||||
|
derivation = hostPkgs.stdenv.mkDerivation {
|
||||||
|
name = "vm-test-run-${config.name}";
|
||||||
|
|
||||||
|
requiredSystemFeatures = [ "uid-range" ];
|
||||||
|
|
||||||
|
buildCommand = ''
|
||||||
|
mkdir -p $out
|
||||||
|
|
||||||
|
# effectively mute the XMLLogger
|
||||||
|
export LOGFILE=/dev/null
|
||||||
|
|
||||||
|
${config.driver}/bin/nixos-test-driver -o $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
passthru = config.passthru;
|
||||||
|
|
||||||
|
meta = config.meta;
|
||||||
};
|
};
|
||||||
|
inherit (config) passthru meta;
|
||||||
}
|
}
|
||||||
''
|
);
|
||||||
mkdir -p $out/bin
|
|
||||||
|
|
||||||
containers=(${toString containers})
|
|
||||||
|
|
||||||
${lib.optionalString (!config.skipTypeCheck) ''
|
|
||||||
# prepend type hints so the test script can be type checked with mypy
|
|
||||||
cat "${./test-script-prepend.py}" >> testScriptWithTypes
|
|
||||||
echo "${builtins.toString machineNames}" >> testScriptWithTypes
|
|
||||||
echo -n "$testScript" >> testScriptWithTypes
|
|
||||||
|
|
||||||
echo "Running type check (enable/disable: config.skipTypeCheck)"
|
|
||||||
echo "See https://nixos.org/manual/nixos/stable/#test-opt-skipTypeCheck"
|
|
||||||
|
|
||||||
mypy --no-implicit-optional \
|
|
||||||
--pretty \
|
|
||||||
--no-color-output \
|
|
||||||
testScriptWithTypes
|
|
||||||
''}
|
|
||||||
|
|
||||||
echo -n "$testScript" >> $out/test-script
|
|
||||||
|
|
||||||
ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver
|
|
||||||
|
|
||||||
wrapProgram $out/bin/nixos-test-driver \
|
|
||||||
${lib.concatStringsSep " " (map (name: "--add-flags '--container ${name}'") containers)} \
|
|
||||||
--add-flags "--test-script '$out/test-script'"
|
|
||||||
'');
|
|
||||||
|
|
||||||
test = lib.mkForce (lib.lazyDerivation {
|
|
||||||
# lazyDerivation improves performance when only passthru items and/or meta are used.
|
|
||||||
derivation = hostPkgs.stdenv.mkDerivation {
|
|
||||||
name = "vm-test-run-${config.name}";
|
|
||||||
|
|
||||||
requiredSystemFeatures = [ "uid-range" ];
|
|
||||||
|
|
||||||
buildCommand = ''
|
|
||||||
mkdir -p $out
|
|
||||||
|
|
||||||
# effectively mute the XMLLogger
|
|
||||||
export LOGFILE=/dev/null
|
|
||||||
|
|
||||||
${config.driver}/bin/nixos-test-driver -o $out
|
|
||||||
'';
|
|
||||||
|
|
||||||
passthru = config.passthru;
|
|
||||||
|
|
||||||
meta = config.meta;
|
|
||||||
};
|
|
||||||
inherit (config) passthru meta;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
{ extraPythonPackages, python3Packages, buildPythonApplication, setuptools, util-linux, systemd }:
|
{
|
||||||
|
extraPythonPackages,
|
||||||
|
python3Packages,
|
||||||
|
buildPythonApplication,
|
||||||
|
setuptools,
|
||||||
|
util-linux,
|
||||||
|
systemd,
|
||||||
|
}:
|
||||||
buildPythonApplication {
|
buildPythonApplication {
|
||||||
pname = "test-driver";
|
pname = "test-driver";
|
||||||
version = "0.0.1";
|
version = "0.0.1";
|
||||||
propagatedBuildInputs = [ util-linux systemd ] ++ extraPythonPackages python3Packages;
|
propagatedBuildInputs = [
|
||||||
|
util-linux
|
||||||
|
systemd
|
||||||
|
] ++ extraPythonPackages python3Packages;
|
||||||
nativeBuildInputs = [ setuptools ];
|
nativeBuildInputs = [ setuptools ];
|
||||||
format = "pyproject";
|
format = "pyproject";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
@ -258,7 +258,7 @@ class Driver:
|
|||||||
|
|
||||||
self.machines = []
|
self.machines = []
|
||||||
for container in containers:
|
for container in containers:
|
||||||
name_match = re.match(r".*-nixos-system-(.+)-\d.+", container.name)
|
name_match = re.match(r".*-nixos-system-(.+)-(.+)", container.name)
|
||||||
if not name_match:
|
if not name_match:
|
||||||
raise ValueError(f"Unable to extract hostname from {container.name}")
|
raise ValueError(f"Unable to extract hostname from {container.name}")
|
||||||
name = name_match.group(1)
|
name = name_match.group(1)
|
||||||
|
@ -1,33 +1,33 @@
|
|||||||
test:
|
test:
|
||||||
{ pkgs
|
{ pkgs, self, ... }:
|
||||||
, self
|
|
||||||
, ...
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
inherit (pkgs) lib;
|
inherit (pkgs) lib;
|
||||||
nixos-lib = import (pkgs.path + "/nixos/lib") { };
|
nixos-lib = import (pkgs.path + "/nixos/lib") { };
|
||||||
in
|
in
|
||||||
(nixos-lib.runTest ({ hostPkgs, ... }: {
|
(nixos-lib.runTest (
|
||||||
hostPkgs = pkgs;
|
{ hostPkgs, ... }:
|
||||||
# speed-up evaluation
|
{
|
||||||
defaults = {
|
hostPkgs = pkgs;
|
||||||
documentation.enable = lib.mkDefault false;
|
# speed-up evaluation
|
||||||
boot.isContainer = true;
|
defaults = {
|
||||||
|
documentation.enable = lib.mkDefault false;
|
||||||
|
boot.isContainer = true;
|
||||||
|
|
||||||
# undo qemu stuff
|
# undo qemu stuff
|
||||||
system.build.initialRamdisk = "";
|
system.build.initialRamdisk = "";
|
||||||
virtualisation.sharedDirectories = lib.mkForce { };
|
virtualisation.sharedDirectories = lib.mkForce { };
|
||||||
networking.useDHCP = false;
|
networking.useDHCP = false;
|
||||||
|
|
||||||
# we have not private networking so far
|
# we have not private networking so far
|
||||||
networking.interfaces = lib.mkForce { };
|
networking.interfaces = lib.mkForce { };
|
||||||
#networking.primaryIPAddress = lib.mkForce null;
|
#networking.primaryIPAddress = lib.mkForce null;
|
||||||
systemd.services.backdoor.enable = false;
|
systemd.services.backdoor.enable = false;
|
||||||
};
|
};
|
||||||
# to accept external dependencies such as disko
|
# to accept external dependencies such as disko
|
||||||
node.specialArgs.self = self;
|
node.specialArgs.self = self;
|
||||||
imports = [
|
imports = [
|
||||||
test
|
test
|
||||||
./container-driver/module.nix
|
./container-driver/module.nix
|
||||||
];
|
];
|
||||||
})).config.result
|
}
|
||||||
|
)).config.result
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
test:
|
test:
|
||||||
{ pkgs
|
{ pkgs, self, ... }:
|
||||||
, self
|
|
||||||
, ...
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
inherit (pkgs) lib;
|
inherit (pkgs) lib;
|
||||||
nixos-lib = import (pkgs.path + "/nixos/lib") { };
|
nixos-lib = import (pkgs.path + "/nixos/lib") { };
|
||||||
|
@ -1,35 +1,48 @@
|
|||||||
{ self, runCommand, check-jsonschema, pkgs, lib, ... }:
|
{
|
||||||
|
self,
|
||||||
|
runCommand,
|
||||||
|
check-jsonschema,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
clanModules.clanCore = self.nixosModules.clanCore;
|
clanModules.clanCore = self.nixosModules.clanCore;
|
||||||
|
|
||||||
baseModule = {
|
baseModule = {
|
||||||
imports =
|
imports = (import (pkgs.path + "/nixos/modules/module-list.nix")) ++ [
|
||||||
(import (pkgs.path + "/nixos/modules/module-list.nix"))
|
{
|
||||||
++ [{
|
|
||||||
nixpkgs.hostPlatform = "x86_64-linux";
|
nixpkgs.hostPlatform = "x86_64-linux";
|
||||||
clanCore.clanName = "dummy";
|
clanCore.clanName = "dummy";
|
||||||
}];
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
optionsFromModule = module:
|
optionsFromModule =
|
||||||
|
module:
|
||||||
let
|
let
|
||||||
evaled = lib.evalModules {
|
evaled = lib.evalModules {
|
||||||
modules = [ module baseModule ];
|
modules = [
|
||||||
|
module
|
||||||
|
baseModule
|
||||||
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
evaled.options.clan;
|
evaled.options.clan;
|
||||||
|
|
||||||
clanModuleSchemas = lib.mapAttrs (_: module: self.lib.jsonschema.parseOptions (optionsFromModule module)) clanModules;
|
clanModuleSchemas = lib.mapAttrs (
|
||||||
|
_: module: self.lib.jsonschema.parseOptions (optionsFromModule module)
|
||||||
|
) clanModules;
|
||||||
|
|
||||||
mkTest = name: schema: runCommand "schema-${name}" { } ''
|
mkTest =
|
||||||
${check-jsonschema}/bin/check-jsonschema \
|
name: schema:
|
||||||
--check-metaschema ${builtins.toFile "schema-${name}" (builtins.toJSON schema)}
|
runCommand "schema-${name}" { } ''
|
||||||
touch $out
|
${check-jsonschema}/bin/check-jsonschema \
|
||||||
'';
|
--check-metaschema ${builtins.toFile "schema-${name}" (builtins.toJSON schema)}
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
lib.mapAttrs'
|
lib.mapAttrs' (name: schema: {
|
||||||
(name: schema: {
|
name = "schema-${name}";
|
||||||
name = "schema-${name}";
|
value = mkTest name schema;
|
||||||
value = mkTest name schema;
|
}) clanModuleSchemas
|
||||||
})
|
|
||||||
clanModuleSchemas
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
(import ../lib/test-base.nix) {
|
(import ../lib/test-base.nix) {
|
||||||
name = "secrets";
|
name = "secrets";
|
||||||
|
|
||||||
nodes.machine = { self, config, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ self, config, ... }:
|
||||||
(self.nixosModules.clanCore)
|
{
|
||||||
];
|
imports = [ (self.nixosModules.clanCore) ];
|
||||||
environment.etc."secret".source = config.sops.secrets.secret.path;
|
environment.etc."secret".source = config.sops.secrets.secret.path;
|
||||||
environment.etc."group-secret".source = config.sops.secrets.group-secret.path;
|
environment.etc."group-secret".source = config.sops.secrets.group-secret.path;
|
||||||
sops.age.keyFile = ./key.age;
|
sops.age.keyFile = ./key.age;
|
||||||
|
|
||||||
clanCore.clanDir = "${./.}";
|
clanCore.clanDir = "${./.}";
|
||||||
clanCore.machineName = "machine";
|
clanCore.machineName = "machine";
|
||||||
|
|
||||||
networking.hostName = "machine";
|
networking.hostName = "machine";
|
||||||
};
|
};
|
||||||
testScript = ''
|
testScript = ''
|
||||||
machine.succeed("cat /etc/secret >&2")
|
machine.succeed("cat /etc/secret >&2")
|
||||||
machine.succeed("cat /etc/group-secret >&2")
|
machine.succeed("cat /etc/group-secret >&2")
|
||||||
|
@ -1,25 +1,35 @@
|
|||||||
import ../lib/test-base.nix ({ config, pkgs, lib, ... }: {
|
import ../lib/test-base.nix (
|
||||||
name = "wayland-proxy-virtwl";
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
name = "wayland-proxy-virtwl";
|
||||||
|
|
||||||
nodes.machine = { self, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ self, ... }:
|
||||||
self.nixosModules.clanCore
|
|
||||||
{
|
{
|
||||||
clanCore.machineName = "machine";
|
imports = [
|
||||||
clanCore.clanDir = ./.;
|
self.nixosModules.clanCore
|
||||||
}
|
{
|
||||||
];
|
clanCore.machineName = "machine";
|
||||||
services.wayland-proxy-virtwl.enable = true;
|
clanCore.clanDir = ./.;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
services.wayland-proxy-virtwl.enable = true;
|
||||||
|
|
||||||
virtualisation.qemu.options = [
|
virtualisation.qemu.options = [
|
||||||
"-vga none -device virtio-gpu-rutabaga,cross-domain=on,hostmem=4G,wsi=headless"
|
"-vga none -device virtio-gpu-rutabaga,cross-domain=on,hostmem=4G,wsi=headless"
|
||||||
];
|
];
|
||||||
|
|
||||||
virtualisation.qemu.package = lib.mkForce pkgs.qemu_kvm;
|
virtualisation.qemu.package = lib.mkForce pkgs.qemu_kvm;
|
||||||
};
|
};
|
||||||
testScript = ''
|
testScript = ''
|
||||||
start_all()
|
start_all()
|
||||||
# use machinectl
|
# use machinectl
|
||||||
machine.succeed("machinectl shell .host ${config.nodes.machine.systemd.package}/bin/systemctl --user start wayland-proxy-virtwl >&2")
|
machine.succeed("machinectl shell .host ${config.nodes.machine.systemd.package}/bin/systemctl --user start wayland-proxy-virtwl >&2")
|
||||||
'';
|
'';
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
(import ../lib/container-test.nix) ({ pkgs, ... }: {
|
(import ../lib/container-test.nix) (
|
||||||
name = "zt-tcp-relay";
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
name = "zt-tcp-relay";
|
||||||
|
|
||||||
nodes.machine = { self, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ self, ... }:
|
||||||
self.nixosModules.clanCore
|
|
||||||
self.clanModules.zt-tcp-relay
|
|
||||||
{
|
{
|
||||||
clanCore.machineName = "machine";
|
imports = [
|
||||||
clanCore.clanDir = ./.;
|
self.nixosModules.clanCore
|
||||||
}
|
self.clanModules.zt-tcp-relay
|
||||||
];
|
{
|
||||||
};
|
clanCore.machineName = "machine";
|
||||||
testScript = ''
|
clanCore.clanDir = ./.;
|
||||||
start_all()
|
}
|
||||||
machine.wait_for_unit("zt-tcp-relay.service")
|
];
|
||||||
out = machine.succeed("${pkgs.netcat}/bin/nc -z -v localhost 4443")
|
};
|
||||||
print(out)
|
testScript = ''
|
||||||
'';
|
start_all()
|
||||||
})
|
machine.wait_for_unit("zt-tcp-relay.service")
|
||||||
|
out = machine.succeed("${pkgs.netcat}/bin/nc -z -v localhost 4443")
|
||||||
|
print(out)
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -1,68 +1,88 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.clan.borgbackup;
|
cfg = config.clan.borgbackup;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.clan.borgbackup = {
|
options.clan.borgbackup.destinations = lib.mkOption {
|
||||||
enable = lib.mkEnableOption "backups with borgbackup";
|
type = lib.types.attrsOf (
|
||||||
destinations = lib.mkOption {
|
lib.types.submodule (
|
||||||
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
|
{ name, ... }:
|
||||||
options = {
|
{
|
||||||
name = lib.mkOption {
|
options = {
|
||||||
type = lib.types.str;
|
name = lib.mkOption {
|
||||||
default = name;
|
type = lib.types.str;
|
||||||
description = "the name of the backup job";
|
default = name;
|
||||||
|
description = "the name of the backup job";
|
||||||
|
};
|
||||||
|
repo = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "the borgbackup repository to backup to";
|
||||||
|
};
|
||||||
|
rsh = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "ssh -i ${
|
||||||
|
config.clanCore.secrets.borgbackup.secrets."borgbackup.ssh".path
|
||||||
|
} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null";
|
||||||
|
description = "the rsh to use for the backup";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
repo = lib.mkOption {
|
}
|
||||||
type = lib.types.str;
|
)
|
||||||
description = "the borgbackup repository to backup to";
|
);
|
||||||
};
|
default = { };
|
||||||
rsh = lib.mkOption {
|
description = ''
|
||||||
type = lib.types.str;
|
destinations where the machine should be backuped to
|
||||||
default = "ssh -i ${config.clanCore.secrets.borgbackup.secrets."borgbackup.ssh".path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null";
|
'';
|
||||||
description = "the rsh to use for the backup";
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
description = ''
|
|
||||||
destinations where the machine should be backuped to
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
services.borgbackup.jobs = lib.mapAttrs
|
|
||||||
(_: dest: {
|
|
||||||
paths = lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state));
|
|
||||||
exclude = [ "*.pyc" ];
|
|
||||||
repo = dest.repo;
|
|
||||||
environment.BORG_RSH = dest.rsh;
|
|
||||||
compression = "auto,zstd";
|
|
||||||
startAt = "*-*-* 01:00:00";
|
|
||||||
persistentTimer = true;
|
|
||||||
preHook = ''
|
|
||||||
set -x
|
|
||||||
'';
|
|
||||||
|
|
||||||
encryption = {
|
imports = [
|
||||||
mode = "repokey";
|
(lib.mkRemovedOptionModule [
|
||||||
passCommand = "cat ${config.clanCore.secrets.borgbackup.secrets."borgbackup.repokey".path}";
|
"clan"
|
||||||
};
|
"borgbackup"
|
||||||
|
"enable"
|
||||||
|
] "Just define clan.borgbackup.destinations to enable it")
|
||||||
|
];
|
||||||
|
|
||||||
prune.keep = {
|
config = lib.mkIf (cfg.destinations != { }) {
|
||||||
within = "1d"; # Keep all archives from the last day
|
services.borgbackup.jobs = lib.mapAttrs (_: dest: {
|
||||||
daily = 7;
|
paths = lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state));
|
||||||
weekly = 4;
|
exclude = [ "*.pyc" ];
|
||||||
monthly = 0;
|
repo = dest.repo;
|
||||||
};
|
environment.BORG_RSH = dest.rsh;
|
||||||
})
|
compression = "auto,zstd";
|
||||||
cfg.destinations;
|
startAt = "*-*-* 01:00:00";
|
||||||
|
persistentTimer = true;
|
||||||
|
preHook = ''
|
||||||
|
set -x
|
||||||
|
'';
|
||||||
|
|
||||||
|
encryption = {
|
||||||
|
mode = "repokey";
|
||||||
|
passCommand = "cat ${config.clanCore.secrets.borgbackup.secrets."borgbackup.repokey".path}";
|
||||||
|
};
|
||||||
|
|
||||||
|
prune.keep = {
|
||||||
|
within = "1d"; # Keep all archives from the last day
|
||||||
|
daily = 7;
|
||||||
|
weekly = 4;
|
||||||
|
monthly = 0;
|
||||||
|
};
|
||||||
|
}) cfg.destinations;
|
||||||
|
|
||||||
clanCore.secrets.borgbackup = {
|
clanCore.secrets.borgbackup = {
|
||||||
facts."borgbackup.ssh.pub" = { };
|
facts."borgbackup.ssh.pub" = { };
|
||||||
secrets."borgbackup.ssh" = { };
|
secrets."borgbackup.ssh" = { };
|
||||||
secrets."borgbackup.repokey" = { };
|
secrets."borgbackup.repokey" = { };
|
||||||
generator.path = [ pkgs.openssh pkgs.coreutils pkgs.xkcdpass ];
|
generator.path = [
|
||||||
|
pkgs.openssh
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.xkcdpass
|
||||||
|
];
|
||||||
generator.script = ''
|
generator.script = ''
|
||||||
ssh-keygen -t ed25519 -N "" -f "$secrets"/borgbackup.ssh
|
ssh-keygen -t ed25519 -N "" -f "$secrets"/borgbackup.ssh
|
||||||
mv "$secrets"/borgbackup.ssh.pub "$facts"/borgbackup.ssh.pub
|
mv "$secrets"/borgbackup.ssh.pub "$facts"/borgbackup.ssh.pub
|
||||||
@ -74,8 +94,9 @@ in
|
|||||||
# TODO list needs to run locally or on the remote machine
|
# TODO list needs to run locally or on the remote machine
|
||||||
list = ''
|
list = ''
|
||||||
# we need yes here to skip the changed url verification
|
# we need yes here to skip the changed url verification
|
||||||
${lib.concatMapStringsSep "\n" (dest: ''yes y | borg-job-${dest.name} list --json | jq -r '. + {"job-name": "${dest.name}"}' '')
|
${lib.concatMapStringsSep "\n" (
|
||||||
(lib.attrValues cfg.destinations)}
|
dest: ''yes y | borg-job-${dest.name} list --json | jq -r '. + {"job-name": "${dest.name}"}' ''
|
||||||
|
) (lib.attrValues cfg.destinations)}
|
||||||
'';
|
'';
|
||||||
create = ''
|
create = ''
|
||||||
${lib.concatMapStringsSep "\n" (dest: ''
|
${lib.concatMapStringsSep "\n" (dest: ''
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{ config, pkgs, ... }: {
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
networking.firewall.interfaces."zt+".allowedTCPPorts = [ 25 ]; # smtp with other hosts
|
networking.firewall.interfaces."zt+".allowedTCPPorts = [ 25 ]; # smtp with other hosts
|
||||||
environment.systemPackages = [ pkgs.deltachat-desktop ];
|
environment.systemPackages = [ pkgs.deltachat-desktop ];
|
||||||
|
|
||||||
@ -134,9 +135,7 @@
|
|||||||
storage &local_mailboxes
|
storage &local_mailboxes
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
ensureAccounts = [
|
ensureAccounts = [ "user@${domain}" ];
|
||||||
"user@${domain}"
|
|
||||||
];
|
|
||||||
ensureCredentials = {
|
ensureCredentials = {
|
||||||
"user@${domain}".passwordFile = pkgs.writeText "dummy" "foobar";
|
"user@${domain}".passwordFile = pkgs.writeText "dummy" "foobar";
|
||||||
};
|
};
|
||||||
|
@ -41,4 +41,3 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,11 @@
|
|||||||
moonlight = ./moonlight.nix;
|
moonlight = ./moonlight.nix;
|
||||||
sunshine = ./sunshine.nix;
|
sunshine = ./sunshine.nix;
|
||||||
syncthing = ./syncthing.nix;
|
syncthing = ./syncthing.nix;
|
||||||
|
sshd = ./sshd.nix;
|
||||||
|
vm-user = ./vm-user.nix;
|
||||||
|
graphical = ./graphical.nix;
|
||||||
xfce = ./xfce.nix;
|
xfce = ./xfce.nix;
|
||||||
|
xfce-vm = ./xfce-vm.nix;
|
||||||
zt-tcp-relay = ./zt-tcp-relay.nix;
|
zt-tcp-relay = ./zt-tcp-relay.nix;
|
||||||
localsend = ./localsend.nix;
|
localsend = ./localsend.nix;
|
||||||
waypipe = ./waypipe.nix;
|
waypipe = ./waypipe.nix;
|
||||||
|
1
clanModules/graphical.nix
Normal file
1
clanModules/graphical.nix
Normal file
@ -0,0 +1 @@
|
|||||||
|
_: { fonts.enableDefaultPackages = true; }
|
@ -1,7 +1,8 @@
|
|||||||
{ config
|
{
|
||||||
, pkgs
|
config,
|
||||||
, lib
|
pkgs,
|
||||||
, ...
|
lib,
|
||||||
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
# Integration can be improved, if the following issues get implemented:
|
# Integration can be improved, if the following issues get implemented:
|
||||||
|
24
clanModules/sshd.nix
Normal file
24
clanModules/sshd.nix
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
services.openssh.enable = true;
|
||||||
|
|
||||||
|
services.openssh.hostKeys = [
|
||||||
|
{
|
||||||
|
path = config.clanCore.secrets.openssh.secrets."ssh.id_ed25519".path;
|
||||||
|
type = "ed25519";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
clanCore.secrets.openssh = {
|
||||||
|
secrets."ssh.id_ed25519" = { };
|
||||||
|
facts."ssh.id_ed25519.pub" = { };
|
||||||
|
generator.path = [
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.openssh
|
||||||
|
];
|
||||||
|
generator.script = ''
|
||||||
|
ssh-keygen -t ed25519 -N "" -f $secrets/ssh.id_ed25519
|
||||||
|
mv $secrets/ssh.id_ed25519.pub $facts/ssh.id_ed25519.pub
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
{ config
|
{
|
||||||
, pkgs
|
config,
|
||||||
, lib
|
pkgs,
|
||||||
, ...
|
lib,
|
||||||
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
options.clan.syncthing = {
|
options.clan.syncthing = {
|
||||||
@ -53,16 +54,16 @@
|
|||||||
|
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion =
|
assertion = lib.all (
|
||||||
lib.all (attr: builtins.hasAttr attr config.services.syncthing.settings.folders)
|
attr: builtins.hasAttr attr config.services.syncthing.settings.folders
|
||||||
config.clan.syncthing.autoShares;
|
) config.clan.syncthing.autoShares;
|
||||||
message = ''
|
message = ''
|
||||||
Syncthing: If you want to AutoShare a folder, you need to have it configured on the sharing device.
|
Syncthing: If you want to AutoShare a folder, you need to have it configured on the sharing device.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
# Activates inofify compatibilty on syncthing
|
# Activates inofify compatibility on syncthing
|
||||||
boot.kernel.sysctl."fs.inotify.max_user_watches" = lib.mkDefault 524288;
|
boot.kernel.sysctl."fs.inotify.max_user_watches" = lib.mkDefault 524288;
|
||||||
|
|
||||||
services.syncthing = {
|
services.syncthing = {
|
||||||
@ -80,12 +81,8 @@
|
|||||||
|
|
||||||
group = "syncthing";
|
group = "syncthing";
|
||||||
|
|
||||||
key =
|
key = lib.mkDefault config.clan.secrets.syncthing.secrets."syncthing.key".path or null;
|
||||||
lib.mkDefault
|
cert = lib.mkDefault config.clan.secrets.syncthing.secrets."syncthing.cert".path or null;
|
||||||
config.clan.secrets.syncthing.secrets."syncthing.key".path or null;
|
|
||||||
cert =
|
|
||||||
lib.mkDefault
|
|
||||||
config.clan.secrets.syncthing.secrets."syncthing.cert".path or null;
|
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
options = {
|
options = {
|
||||||
@ -127,47 +124,33 @@
|
|||||||
set -x
|
set -x
|
||||||
# query pending deviceID's
|
# query pending deviceID's
|
||||||
APIKEY=$(cat ${apiKey})
|
APIKEY=$(cat ${apiKey})
|
||||||
PENDING=$(${
|
PENDING=$(${lib.getExe pkgs.curl} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${getPendingDevices})
|
||||||
lib.getExe pkgs.curl
|
|
||||||
} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${getPendingDevices})
|
|
||||||
PENDING=$(echo $PENDING | ${lib.getExe pkgs.jq} keys[])
|
PENDING=$(echo $PENDING | ${lib.getExe pkgs.jq} keys[])
|
||||||
|
|
||||||
# accept pending deviceID's
|
# accept pending deviceID's
|
||||||
for ID in $PENDING;do
|
for ID in $PENDING;do
|
||||||
${
|
${lib.getExe pkgs.curl} -X POST -d "{\"deviceId\": $ID}" -H "Content-Type: application/json" -H "X-API-Key: $APIKEY" ${baseAddress}${postNewDevice}
|
||||||
lib.getExe pkgs.curl
|
|
||||||
} -X POST -d "{\"deviceId\": $ID}" -H "Content-Type: application/json" -H "X-API-Key: $APIKEY" ${baseAddress}${postNewDevice}
|
|
||||||
|
|
||||||
# get all shared folders by their ID
|
# get all shared folders by their ID
|
||||||
for folder in ${builtins.toString config.clan.syncthing.autoShares}; do
|
for folder in ${builtins.toString config.clan.syncthing.autoShares}; do
|
||||||
SHARED_IDS=$(${
|
SHARED_IDS=$(${lib.getExe pkgs.curl} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" | ${lib.getExe pkgs.jq} ."devices")
|
||||||
lib.getExe pkgs.curl
|
PATCHED_IDS=$(echo $SHARED_IDS | ${lib.getExe pkgs.jq} ".+= [{\"deviceID\": $ID, \"introducedBy\": \"\", \"encryptionPassword\": \"\"}]")
|
||||||
} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" | ${
|
${lib.getExe pkgs.curl} -X PATCH -d "{\"devices\": $PATCHED_IDS}" -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder"
|
||||||
lib.getExe pkgs.jq
|
|
||||||
} ."devices")
|
|
||||||
PATCHED_IDS=$(echo $SHARED_IDS | ${
|
|
||||||
lib.getExe pkgs.jq
|
|
||||||
} ".+= [{\"deviceID\": $ID, \"introducedBy\": \"\", \"encryptionPassword\": \"\"}]")
|
|
||||||
${
|
|
||||||
lib.getExe pkgs.curl
|
|
||||||
} -X PATCH -d "{\"devices\": $PATCHED_IDS}" -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder"
|
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.timers.syncthing-auto-accept =
|
systemd.timers.syncthing-auto-accept = lib.mkIf config.clan.syncthing.autoAcceptDevices {
|
||||||
lib.mkIf config.clan.syncthing.autoAcceptDevices
|
description = "Syncthing Auto Accept";
|
||||||
{
|
|
||||||
description = "Syncthing Auto Accept";
|
|
||||||
|
|
||||||
wantedBy = [ "syncthing-auto-accept.service" ];
|
wantedBy = [ "syncthing-auto-accept.service" ];
|
||||||
|
|
||||||
timerConfig = {
|
timerConfig = {
|
||||||
OnActiveSec = lib.mkDefault 60;
|
OnActiveSec = lib.mkDefault 60;
|
||||||
OnUnitActiveSec = lib.mkDefault 60;
|
OnUnitActiveSec = lib.mkDefault 60;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.syncthing-init-api-key =
|
systemd.services.syncthing-init-api-key =
|
||||||
let
|
let
|
||||||
@ -182,9 +165,7 @@
|
|||||||
set -efu pipefail
|
set -efu pipefail
|
||||||
|
|
||||||
APIKEY=$(cat ${apiKey})
|
APIKEY=$(cat ${apiKey})
|
||||||
${
|
${lib.getExe pkgs.gnused} -i "s/<apikey>.*<\/apikey>/<apikey>$APIKEY<\/apikey>/" /var/lib/syncthing/config.xml
|
||||||
lib.getExe pkgs.gnused
|
|
||||||
} -i "s/<apikey>.*<\/apikey>/<apikey>$APIKEY<\/apikey>/" /var/lib/syncthing/config.xml
|
|
||||||
# sudo systemctl restart syncthing.service
|
# sudo systemctl restart syncthing.service
|
||||||
systemctl restart syncthing.service
|
systemctl restart syncthing.service
|
||||||
'';
|
'';
|
||||||
|
20
clanModules/vm-user.nix
Normal file
20
clanModules/vm-user.nix
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
security = {
|
||||||
|
sudo.wheelNeedsPassword = false;
|
||||||
|
polkit.enable = true;
|
||||||
|
rtkit.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.user = {
|
||||||
|
isNormalUser = true;
|
||||||
|
createHome = true;
|
||||||
|
uid = 1000;
|
||||||
|
initialHashedPassword = "";
|
||||||
|
extraGroups = [
|
||||||
|
"wheel"
|
||||||
|
"video"
|
||||||
|
"render"
|
||||||
|
];
|
||||||
|
shell = "/run/current-system/sw/bin/bash";
|
||||||
|
};
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
{ pkgs
|
{
|
||||||
, lib
|
pkgs,
|
||||||
, config
|
lib,
|
||||||
, ...
|
config,
|
||||||
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
options.clan.services.waypipe = {
|
options.clan.services.waypipe = {
|
||||||
@ -49,7 +50,10 @@
|
|||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
uid = 1000;
|
uid = 1000;
|
||||||
password = "";
|
password = "";
|
||||||
extraGroups = [ "wheel" "video" ];
|
extraGroups = [
|
||||||
|
"wheel"
|
||||||
|
"video"
|
||||||
|
];
|
||||||
shell = "/run/current-system/sw/bin/bash";
|
shell = "/run/current-system/sw/bin/bash";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
15
clanModules/xfce-vm.nix
Normal file
15
clanModules/xfce-vm.nix
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./vm-user.nix
|
||||||
|
./graphical.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
services.xserver = {
|
||||||
|
enable = true;
|
||||||
|
displayManager.autoLogin.enable = true;
|
||||||
|
displayManager.autoLogin.user = "user";
|
||||||
|
desktopManager.xfce.enable = true;
|
||||||
|
desktopManager.xfce.enableScreensaver = false;
|
||||||
|
xkb.layout = "us";
|
||||||
|
};
|
||||||
|
}
|
@ -1,4 +1,10 @@
|
|||||||
{ pkgs, lib, config, ... }: {
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
options.clan.zt-tcp-relay = {
|
options.clan.zt-tcp-relay = {
|
||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
@ -13,7 +19,9 @@
|
|||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.callPackage ../pkgs/zt-tcp-relay {}}/bin/zt-tcp-relay --listen [::]:${builtins.toString config.clan.zt-tcp-relay.port}";
|
ExecStart = "${
|
||||||
|
pkgs.callPackage ../pkgs/zt-tcp-relay { }
|
||||||
|
}/bin/zt-tcp-relay --listen [::]:${builtins.toString config.clan.zt-tcp-relay.port}";
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
RestartSec = "5";
|
RestartSec = "5";
|
||||||
dynamicUsers = true;
|
dynamicUsers = true;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
perSystem =
|
perSystem =
|
||||||
{ pkgs
|
{
|
||||||
, self'
|
pkgs,
|
||||||
, lib
|
self',
|
||||||
, ...
|
lib,
|
||||||
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
python3 = pkgs.python3;
|
python3 = pkgs.python3;
|
||||||
@ -14,15 +15,11 @@
|
|||||||
ps:
|
ps:
|
||||||
clan-cli.propagatedBuildInputs
|
clan-cli.propagatedBuildInputs
|
||||||
++ clan-cli.devDependencies
|
++ clan-cli.devDependencies
|
||||||
++ [
|
++ [ ps.pip ]
|
||||||
ps.pip
|
++ [ clan-vm-manager.externalPythonDeps ]
|
||||||
# clan-vm-manager deps
|
# clan-vm-manager deps
|
||||||
ps.pygobject3
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
linuxOnlyPackages = lib.optionals pkgs.stdenv.isLinux [
|
linuxOnlyPackages = lib.optionals pkgs.stdenv.isLinux [ pkgs.xdg-utils ];
|
||||||
pkgs.xdg-utils
|
|
||||||
];
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells.python = pkgs.mkShell {
|
devShells.python = pkgs.mkShell {
|
||||||
@ -49,9 +46,9 @@
|
|||||||
|
|
||||||
## PYTHON
|
## PYTHON
|
||||||
|
|
||||||
tmp_path=$(realpath ./.direnv)
|
tmp_path="$(realpath ./.direnv/python)"
|
||||||
repo_root=$(realpath .)
|
repo_root=$(realpath .)
|
||||||
mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}"
|
mkdir -p "$tmp_path/${pythonWithDeps.sitePackages}"
|
||||||
|
|
||||||
# local dependencies
|
# local dependencies
|
||||||
localPackages=(
|
localPackages=(
|
||||||
@ -59,28 +56,41 @@
|
|||||||
$repo_root/pkgs/clan-vm-manager
|
$repo_root/pkgs/clan-vm-manager
|
||||||
)
|
)
|
||||||
|
|
||||||
# Install the package in editable mode
|
# Install executable wrappers for local python packages scripts
|
||||||
# This allows executing `clan` from within the dev-shell using the current
|
# This is done by utilizing `pip install --editable`
|
||||||
# version of the code and its dependencies.
|
# As a result, executables like `clan` can be executed from within the dev-shell
|
||||||
# TODO: this is slow. get rid of pip or add better caching
|
# while using the current version of the code and its dependencies.
|
||||||
echo "==== Installing local python packages in editable mode ===="
|
|
||||||
for package in "''${localPackages[@]}"; do
|
for package in "''${localPackages[@]}"; do
|
||||||
${pythonWithDeps}/bin/pip install \
|
pname=$(basename "$package")
|
||||||
--quiet \
|
if
|
||||||
--disable-pip-version-check \
|
[ ! -e "$tmp_path/meta/$pname/pyproject.toml" ] \
|
||||||
--no-index \
|
|| [ ! -e "$package/pyproject.toml" ] \
|
||||||
--no-build-isolation \
|
|| ! cmp -s "$tmp_path/meta/$pname/pyproject.toml" "$package/pyproject.toml"
|
||||||
--prefix "$tmp_path/python" \
|
then
|
||||||
--editable "$package"
|
echo "==== Installing local python package $pname in editable mode ===="
|
||||||
|
mkdir -p "$tmp_path/meta/$pname"
|
||||||
|
cp $package/pyproject.toml $tmp_path/meta/$pname/pyproject.toml
|
||||||
|
${python3.pkgs.pip}/bin/pip install \
|
||||||
|
--quiet \
|
||||||
|
--disable-pip-version-check \
|
||||||
|
--no-index \
|
||||||
|
--no-build-isolation \
|
||||||
|
--prefix "$tmp_path" \
|
||||||
|
--editable "$package"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
export PATH="$tmp_path/python/bin:$PATH"
|
export PATH="$tmp_path/bin:$PATH"
|
||||||
export PYTHONPATH="''${PYTHONPATH:+$PYTHONPATH:}$tmp_path/python/${pythonWithDeps.sitePackages}"
|
export PYTHONPATH="''${PYTHONPATH:+$PYTHONPATH:}$tmp_path/${pythonWithDeps.sitePackages}"
|
||||||
|
|
||||||
for package in "''${localPackages[@]}"; do
|
for package in "''${localPackages[@]}"; do
|
||||||
export PYTHONPATH="$package:$PYTHONPATH"
|
export PYTHONPATH="$package:$PYTHONPATH"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## GUI
|
||||||
|
|
||||||
if ! command -v xdg-mime &> /dev/null; then
|
if ! command -v xdg-mime &> /dev/null; then
|
||||||
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
|
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
|
||||||
fi
|
fi
|
||||||
@ -93,7 +103,6 @@
|
|||||||
UI_BIN="clan-vm-manager"
|
UI_BIN="clan-vm-manager"
|
||||||
|
|
||||||
cp -f $DESKTOP_SRC $DESKTOP_DST
|
cp -f $DESKTOP_SRC $DESKTOP_DST
|
||||||
sleep 2
|
|
||||||
sed -i "s|Exec=.*clan-vm-manager|Exec=$UI_BIN|" $DESKTOP_DST
|
sed -i "s|Exec=.*clan-vm-manager|Exec=$UI_BIN|" $DESKTOP_DST
|
||||||
xdg-mime default $DESKTOP_FILE_NAME x-scheme-handler/clan
|
xdg-mime default $DESKTOP_FILE_NAME x-scheme-handler/clan
|
||||||
echo "==== Validating desktop file installation ===="
|
echo "==== Validating desktop file installation ===="
|
||||||
|
17
devShell.nix
17
devShell.nix
@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
perSystem =
|
perSystem =
|
||||||
{ pkgs
|
{
|
||||||
, self'
|
pkgs,
|
||||||
, config
|
self',
|
||||||
, ...
|
config,
|
||||||
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
writers = pkgs.callPackage ./pkgs/builders/script-writers.nix { };
|
writers = pkgs.callPackage ./pkgs/builders/script-writers.nix { };
|
||||||
@ -16,16 +17,16 @@
|
|||||||
# A python program to switch between dev-shells
|
# A python program to switch between dev-shells
|
||||||
# usage: select-shell shell-name
|
# usage: select-shell shell-name
|
||||||
# the currently enabled dev-shell gets stored in ./.direnv/selected-shell
|
# the currently enabled dev-shell gets stored in ./.direnv/selected-shell
|
||||||
select-shell = writers.writePython3Bin "select-shell"
|
select-shell = writers.writePython3Bin "select-shell" {
|
||||||
{
|
flakeIgnore = [ "E501" ];
|
||||||
flakeIgnore = [ "E501" ];
|
} ./pkgs/scripts/select-shell.py;
|
||||||
} ./pkgs/scripts/select-shell.py;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
select-shell
|
select-shell
|
||||||
pkgs.tea
|
pkgs.tea
|
||||||
|
pkgs.nix
|
||||||
self'.packages.tea-create-pr
|
self'.packages.tea-create-pr
|
||||||
self'.packages.merge-after-ci
|
self'.packages.merge-after-ci
|
||||||
self'.packages.pending-reviews
|
self'.packages.pending-reviews
|
||||||
|
@ -160,7 +160,7 @@ examples.
|
|||||||
`clan secrets` stores each secrets in a single file, whereas [sops](https://github.com/Mic92/sops-nix)
|
`clan secrets` stores each secrets in a single file, whereas [sops](https://github.com/Mic92/sops-nix)
|
||||||
commonly allows to put all secrets in a yaml or json documents.
|
commonly allows to put all secrets in a yaml or json documents.
|
||||||
|
|
||||||
If you already happend to use sops-nix, you can migrate by using the `clan secrets import-sops` command by importing these documents:
|
If you already happened to use sops-nix, you can migrate by using the `clan secrets import-sops` command by importing these documents:
|
||||||
|
|
||||||
```shellSession
|
```shellSession
|
||||||
% clan secrets import-sops --prefix matchbox- --group admins --machine matchbox nixos/matchbox/secrets/secrets.yaml
|
% clan secrets import-sops --prefix matchbox- --group admins --machine matchbox nixos/matchbox/secrets/secrets.yaml
|
||||||
|
42
flake.lock
42
flake.lock
@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709632354,
|
"lastModified": 1710427903,
|
||||||
"narHash": "sha256-jxRHwqrtNze51WKFKvxlQ8Inf62UNRl5cFqEQ2V96vE=",
|
"narHash": "sha256-sV0Q5ndvfjK9JfCg/QM/HX/fcittohvtq8dD62isxdM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "0d11aa8d6431326e10b8656420f91085c3bd0b12",
|
"rev": "21d89b333ca300bef82c928c856d48b94a9f997c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -27,11 +27,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706830856,
|
"lastModified": 1709336216,
|
||||||
"narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=",
|
"narHash": "sha256-Dt/wOWeW6Sqm11Yh+2+t0dfEWxoMxGBvv3JpIocFl9E=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f",
|
"rev": "f7b3c975cf067e56e7cda6cb098ebe3fb4d74ca2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -42,11 +42,11 @@
|
|||||||
},
|
},
|
||||||
"nixlib": {
|
"nixlib": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1708217146,
|
"lastModified": 1710031547,
|
||||||
"narHash": "sha256-nGfEv7k78slqIR5E0zzWSx214d/4/ZPKDkObLJqVLVw=",
|
"narHash": "sha256-pkUg3hOKuGWMGF9WEMPPN/G4pqqdbNGJQ54yhyQYDVY=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixpkgs.lib",
|
"repo": "nixpkgs.lib",
|
||||||
"rev": "e623008d8a46517470e6365505f1a3ce171fa46a",
|
"rev": "630ebdc047ca96d8126e16bb664c7730dc52f6e6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -63,11 +63,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1708563055,
|
"lastModified": 1710398463,
|
||||||
"narHash": "sha256-FaojUZNu+YPFi3eCI7mL4kxPKQ51DoySa7mqmllUOuc=",
|
"narHash": "sha256-fQlYanU84E8uwBpcoTCcLCwU8cqn0eQ7nwTcrWfSngc=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixos-generators",
|
"repo": "nixos-generators",
|
||||||
"rev": "f4631dee1a0fd56c0db89860e83e3588a28c7631",
|
"rev": "efd4e38532b5abfaa5c9fc95c5a913157dc20ccb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -78,11 +78,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1708847675,
|
"lastModified": 1710672219,
|
||||||
"narHash": "sha256-RUZ7KEs/a4EzRELYDGnRB6i7M1Izii3JD/LyzH0c6Tg=",
|
"narHash": "sha256-Bp3Jsq1Jn8q4EesBlcOVNwnEipNpzYs73kvR3+3EUC4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "2a34566b67bef34c551f204063faeecc444ae9da",
|
"rev": "f471be9644f3ab2f3cb868de1787ab70a537b0e7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -110,11 +110,11 @@
|
|||||||
"nixpkgs-stable": []
|
"nixpkgs-stable": []
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1708830076,
|
"lastModified": 1710644594,
|
||||||
"narHash": "sha256-Cjh2xdjxC6S6nW6Whr2dxSeh8vjodzhTmQdI4zPJ4RA=",
|
"narHash": "sha256-RquCuzxfy4Nr8DPbdp3D/AsbYep21JgQzG8aMH9jJ4A=",
|
||||||
"owner": "Mic92",
|
"owner": "Mic92",
|
||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"rev": "2874fbbe4a65bd2484b0ad757d27a16107f6bc17",
|
"rev": "83b68a0e8c94b72cdd0a6e547a14ca7eb1c03616",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -130,11 +130,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1708897213,
|
"lastModified": 1710278050,
|
||||||
"narHash": "sha256-QECZB+Hgz/2F/8lWvHNk05N6NU/rD9bWzuNn6Cv8oUk=",
|
"narHash": "sha256-Oc6BP7soXqb8itlHI8UKkdf3V9GeJpa1S39SR5+HJys=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "e497a9ddecff769c2a7cbab51e1ed7a8501e7a3a",
|
"rev": "35791f76524086ab4b785a33e4abbedfda64bd22",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
70
flake.nix
70
flake.nix
@ -2,7 +2,9 @@
|
|||||||
description = "clan.lol base operating system";
|
description = "clan.lol base operating system";
|
||||||
|
|
||||||
nixConfig.extra-substituters = [ "https://cache.clan.lol" ];
|
nixConfig.extra-substituters = [ "https://cache.clan.lol" ];
|
||||||
nixConfig.extra-trusted-public-keys = [ "cache.clan.lol-1:3KztgSAB5R1M+Dz7vzkBGzXdodizbgLXGXKXlcQLA28=" ];
|
nixConfig.extra-trusted-public-keys = [
|
||||||
|
"cache.clan.lol-1:3KztgSAB5R1M+Dz7vzkBGzXdodizbgLXGXKXlcQLA28="
|
||||||
|
];
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";
|
||||||
@ -20,44 +22,42 @@
|
|||||||
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs @ { flake-parts, ... }:
|
outputs =
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } ({ lib, ... }: {
|
inputs@{ flake-parts, ... }:
|
||||||
systems = [
|
flake-parts.lib.mkFlake { inherit inputs; } (
|
||||||
"x86_64-linux"
|
{ lib, ... }:
|
||||||
"aarch64-linux"
|
{
|
||||||
"aarch64-darwin"
|
systems = [
|
||||||
];
|
"x86_64-linux"
|
||||||
imports = [
|
"aarch64-linux"
|
||||||
./checks/flake-module.nix
|
"aarch64-darwin"
|
||||||
./devShell.nix
|
];
|
||||||
./devShell-python.nix
|
imports = [
|
||||||
./formatter.nix
|
./checks/flake-module.nix
|
||||||
./templates/flake-module.nix
|
./devShell.nix
|
||||||
./clanModules/flake-module.nix
|
./devShell-python.nix
|
||||||
|
./formatter.nix
|
||||||
|
./templates/flake-module.nix
|
||||||
|
./clanModules/flake-module.nix
|
||||||
|
|
||||||
./pkgs/flake-module.nix
|
./pkgs/flake-module.nix
|
||||||
|
|
||||||
./lib/flake-module.nix
|
./lib/flake-module.nix
|
||||||
./nixosModules/flake-module.nix
|
./nixosModules/flake-module.nix
|
||||||
{
|
{
|
||||||
options.flake = flake-parts.lib.mkSubmoduleOptions {
|
options.flake = flake-parts.lib.mkSubmoduleOptions {
|
||||||
clanInternals = lib.mkOption {
|
clanInternals = lib.mkOption {
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
all-machines-json = lib.mkOption {
|
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.str; };
|
||||||
type = lib.types.attrsOf lib.types.str;
|
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); };
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
}
|
];
|
||||||
];
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,48 @@
|
|||||||
{ lib
|
{ lib, inputs, ... }:
|
||||||
, inputs
|
{
|
||||||
, ...
|
imports = [ inputs.treefmt-nix.flakeModule ];
|
||||||
}: {
|
perSystem =
|
||||||
imports = [
|
{ self', pkgs, ... }:
|
||||||
inputs.treefmt-nix.flakeModule
|
{
|
||||||
];
|
treefmt.projectRootFile = "flake.nix";
|
||||||
perSystem = { self', pkgs, ... }: {
|
treefmt.programs.shellcheck.enable = true;
|
||||||
treefmt.projectRootFile = "flake.nix";
|
|
||||||
treefmt.flakeCheck = true;
|
|
||||||
treefmt.flakeFormatter = true;
|
|
||||||
treefmt.programs.shellcheck.enable = true;
|
|
||||||
|
|
||||||
treefmt.programs.mypy.enable = true;
|
treefmt.programs.mypy.enable = true;
|
||||||
treefmt.programs.mypy.directories = {
|
treefmt.programs.mypy.directories = {
|
||||||
"pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies;
|
"pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies;
|
||||||
"pkgs/clan-vm-manager".extraPythonPackages = self'.packages.clan-vm-manager.propagatedBuildInputs;
|
"pkgs/clan-vm-manager".extraPythonPackages =
|
||||||
};
|
self'.packages.clan-vm-manager.externalPythonDeps ++ self'.packages.clan-cli.pytestDependencies;
|
||||||
|
};
|
||||||
|
|
||||||
treefmt.settings.formatter.nix = {
|
treefmt.settings.formatter.nix = {
|
||||||
command = "sh";
|
command = "sh";
|
||||||
options = [
|
options = [
|
||||||
"-eucx"
|
"-eucx"
|
||||||
''
|
''
|
||||||
# First deadnix
|
# First deadnix
|
||||||
${lib.getExe pkgs.deadnix} --edit "$@"
|
${lib.getExe pkgs.deadnix} --edit "$@"
|
||||||
# Then nixpkgs-fmt
|
# Then nixpkgs-fmt
|
||||||
${lib.getExe pkgs.nixpkgs-fmt} "$@"
|
${lib.getExe pkgs.nixfmt-rfc-style} "$@"
|
||||||
''
|
''
|
||||||
"--" # this argument is ignored by bash
|
"--" # this argument is ignored by bash
|
||||||
];
|
];
|
||||||
includes = [ "*.nix" ];
|
includes = [ "*.nix" ];
|
||||||
excludes = [
|
excludes = [
|
||||||
# Was copied from nixpkgs. Keep diff minimal to simplify upstreaming.
|
# Was copied from nixpkgs. Keep diff minimal to simplify upstreaming.
|
||||||
"pkgs/builders/script-writers.nix"
|
"pkgs/builders/script-writers.nix"
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
treefmt.settings.formatter.python = {
|
||||||
|
command = "sh";
|
||||||
|
options = [
|
||||||
|
"-eucx"
|
||||||
|
''
|
||||||
|
${lib.getExe pkgs.ruff} --fix "$@"
|
||||||
|
${lib.getExe pkgs.ruff} format "$@"
|
||||||
|
''
|
||||||
|
"--" # this argument is ignored by bash
|
||||||
|
];
|
||||||
|
includes = [ "*.py" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
treefmt.settings.formatter.python = {
|
|
||||||
command = "sh";
|
|
||||||
options = [
|
|
||||||
"-eucx"
|
|
||||||
''
|
|
||||||
${lib.getExe pkgs.ruff} --fix "$@"
|
|
||||||
${lib.getExe pkgs.ruff} format "$@"
|
|
||||||
''
|
|
||||||
"--" # this argument is ignored by bash
|
|
||||||
];
|
|
||||||
includes = [ "*.py" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,66 +1,80 @@
|
|||||||
{ clan-core, nixpkgs, lib }:
|
{
|
||||||
{ directory # The directory containing the machines subdirectory
|
clan-core,
|
||||||
, specialArgs ? { } # Extra arguments to pass to nixosSystem i.e. useful to make self available
|
nixpkgs,
|
||||||
, machines ? { } # allows to include machine-specific modules i.e. machines.${name} = { ... }
|
lib,
|
||||||
, clanName # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
|
}:
|
||||||
, clanIcon ? null # A path to an icon to be used for the clan, should be the same for all machines
|
{
|
||||||
, pkgsForSystem ? (_system: null) # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
|
directory, # The directory containing the machines subdirectory
|
||||||
# This improves performance, but all nipxkgs.* options will be ignored.
|
specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available
|
||||||
|
machines ? { }, # allows to include machine-specific modules i.e. machines.${name} = { ... }
|
||||||
|
clanName, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
|
||||||
|
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
|
||||||
|
pkgsForSystem ? (_system: null), # 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.
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (builtins.readDir (directory + /machines));
|
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
|
||||||
|
builtins.readDir (directory + /machines)
|
||||||
|
);
|
||||||
|
|
||||||
machineSettings = machineName:
|
machineSettings =
|
||||||
|
machineName:
|
||||||
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
|
# 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
|
# 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
|
# Using CLAN_MACHINE_SETTINGS_FILE requires passing --impure to nix eval
|
||||||
if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != ""
|
if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then
|
||||||
then builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE"))
|
builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE"))
|
||||||
else
|
else
|
||||||
lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json")
|
lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json") (
|
||||||
(builtins.fromJSON
|
builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json))
|
||||||
(builtins.readFile (directory + /machines/${machineName}/settings.json)));
|
);
|
||||||
|
|
||||||
# Read additional imports specified via a config option in settings.json
|
# Read additional imports specified via a config option in settings.json
|
||||||
# This is not an infinite recursion, because the imports are discovered here
|
# This is not an infinite recursion, because the imports are discovered here
|
||||||
# before calling evalModules.
|
# before calling evalModules.
|
||||||
# It is still useful to have the imports as an option, as this allows for type
|
# It is still useful to have the imports as an option, as this allows for type
|
||||||
# checking and easy integration with the config frontend(s)
|
# checking and easy integration with the config frontend(s)
|
||||||
machineImports = machineSettings:
|
machineImports =
|
||||||
map
|
machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]);
|
||||||
(module: clan-core.clanModules.${module})
|
|
||||||
(machineSettings.clanImports or [ ]);
|
|
||||||
|
|
||||||
# TODO: remove default system once we have a hardware-config mechanism
|
# TODO: remove default system once we have a hardware-config mechanism
|
||||||
nixosConfiguration = { system ? "x86_64-linux", name, pkgs ? null, extraConfig ? { } }: nixpkgs.lib.nixosSystem {
|
nixosConfiguration =
|
||||||
modules =
|
{
|
||||||
let
|
system ? "x86_64-linux",
|
||||||
settings = machineSettings name;
|
name,
|
||||||
in
|
pkgs ? null,
|
||||||
(machineImports settings)
|
extraConfig ? { },
|
||||||
++ [
|
}:
|
||||||
settings
|
nixpkgs.lib.nixosSystem {
|
||||||
clan-core.nixosModules.clanCore
|
modules =
|
||||||
extraConfig
|
let
|
||||||
(machines.${name} or { })
|
settings = machineSettings name;
|
||||||
({
|
in
|
||||||
clanCore.clanName = clanName;
|
(machineImports settings)
|
||||||
clanCore.clanIcon = clanIcon;
|
++ [
|
||||||
clanCore.clanDir = directory;
|
settings
|
||||||
clanCore.machineName = name;
|
clan-core.nixosModules.clanCore
|
||||||
nixpkgs.hostPlatform = lib.mkDefault system;
|
extraConfig
|
||||||
|
(machines.${name} or { })
|
||||||
|
(
|
||||||
|
{
|
||||||
|
clanCore.clanName = clanName;
|
||||||
|
clanCore.clanIcon = clanIcon;
|
||||||
|
clanCore.clanDir = directory;
|
||||||
|
clanCore.machineName = name;
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault system;
|
||||||
|
|
||||||
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
|
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
|
||||||
nix.registry.nixpkgs.to = {
|
nix.registry.nixpkgs.to = {
|
||||||
type = "path";
|
type = "path";
|
||||||
path = lib.mkDefault nixpkgs;
|
path = lib.mkDefault nixpkgs;
|
||||||
};
|
};
|
||||||
} // lib.optionalAttrs (pkgs != null) {
|
}
|
||||||
nixpkgs.pkgs = lib.mkForce pkgs;
|
// lib.optionalAttrs (pkgs != null) { nixpkgs.pkgs = lib.mkForce pkgs; }
|
||||||
})
|
)
|
||||||
];
|
];
|
||||||
inherit specialArgs;
|
inherit specialArgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
allMachines = machinesDirs // machines;
|
allMachines = machinesDirs // machines;
|
||||||
|
|
||||||
@ -77,27 +91,38 @@ let
|
|||||||
# This instantiates nixos for each system that we support:
|
# This instantiates nixos for each system that we support:
|
||||||
# configPerSystem = <system>.<machine>.nixosConfiguration
|
# configPerSystem = <system>.<machine>.nixosConfiguration
|
||||||
# We need this to build nixos secret generators for each system
|
# We need this to build nixos secret generators for each system
|
||||||
configsPerSystem = builtins.listToAttrs
|
configsPerSystem = builtins.listToAttrs (
|
||||||
(builtins.map
|
builtins.map (
|
||||||
(system: lib.nameValuePair system
|
system:
|
||||||
(lib.mapAttrs
|
lib.nameValuePair system (
|
||||||
(name: _: nixosConfiguration {
|
lib.mapAttrs (
|
||||||
|
name: _:
|
||||||
|
nixosConfiguration {
|
||||||
inherit name system;
|
inherit name system;
|
||||||
pkgs = pkgsForSystem system;
|
pkgs = pkgsForSystem system;
|
||||||
})
|
}
|
||||||
allMachines))
|
) allMachines
|
||||||
supportedSystems);
|
)
|
||||||
|
) supportedSystems
|
||||||
|
);
|
||||||
|
|
||||||
configsFuncPerSystem = builtins.listToAttrs
|
configsFuncPerSystem = builtins.listToAttrs (
|
||||||
(builtins.map
|
builtins.map (
|
||||||
(system: lib.nameValuePair system
|
system:
|
||||||
(lib.mapAttrs
|
lib.nameValuePair system (
|
||||||
(name: _: args: nixosConfiguration (args // {
|
lib.mapAttrs (
|
||||||
inherit name system;
|
name: _: args:
|
||||||
pkgs = pkgsForSystem system;
|
nixosConfiguration (
|
||||||
}))
|
args
|
||||||
allMachines))
|
// {
|
||||||
supportedSystems);
|
inherit name system;
|
||||||
|
pkgs = pkgsForSystem system;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) allMachines
|
||||||
|
)
|
||||||
|
) supportedSystems
|
||||||
|
);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit nixosConfigurations;
|
inherit nixosConfigurations;
|
||||||
@ -105,8 +130,11 @@ in
|
|||||||
clanInternals = {
|
clanInternals = {
|
||||||
machines = configsPerSystem;
|
machines = configsPerSystem;
|
||||||
machinesFunc = configsFuncPerSystem;
|
machinesFunc = configsFuncPerSystem;
|
||||||
all-machines-json = lib.mapAttrs
|
all-machines-json = lib.mapAttrs (
|
||||||
(system: configs: nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs))
|
system: configs:
|
||||||
configsPerSystem;
|
nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (
|
||||||
|
lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs
|
||||||
|
)
|
||||||
|
) configsPerSystem;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
{ lib, clan-core, nixpkgs, ... }:
|
{
|
||||||
|
lib,
|
||||||
|
clan-core,
|
||||||
|
nixpkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
{
|
{
|
||||||
jsonschema = import ./jsonschema { inherit lib; };
|
jsonschema = import ./jsonschema { inherit lib; };
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{ lib
|
{
|
||||||
, inputs
|
lib,
|
||||||
, self
|
inputs,
|
||||||
, ...
|
self,
|
||||||
}: {
|
...
|
||||||
imports = [
|
}:
|
||||||
./jsonschema/flake-module.nix
|
{
|
||||||
];
|
imports = [ ./jsonschema/flake-module.nix ];
|
||||||
flake.lib = import ./default.nix {
|
flake.lib = import ./default.nix {
|
||||||
inherit lib;
|
inherit lib;
|
||||||
inherit (inputs) nixpkgs;
|
inherit (inputs) nixpkgs;
|
||||||
|
@ -1,243 +1,290 @@
|
|||||||
{ lib ? import <nixpkgs/lib>
|
{
|
||||||
, excludedTypes ? [
|
lib ? import <nixpkgs/lib>,
|
||||||
|
excludedTypes ? [
|
||||||
"functionTo"
|
"functionTo"
|
||||||
"package"
|
"package"
|
||||||
]
|
],
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# remove _module attribute from options
|
# remove _module attribute from options
|
||||||
clean = opts: builtins.removeAttrs opts [ "_module" ];
|
clean = opts: builtins.removeAttrs opts [ "_module" ];
|
||||||
|
|
||||||
# throw error if option type is not supported
|
# throw error if option type is not supported
|
||||||
notSupported = option: lib.trace option throw ''
|
notSupported =
|
||||||
option type '${option.type.name}' ('${option.type.description}') not supported by jsonschema converter
|
option:
|
||||||
location: ${lib.concatStringsSep "." option.loc}
|
lib.trace option throw ''
|
||||||
'';
|
option type '${option.type.name}' ('${option.type.description}') not supported by jsonschema converter
|
||||||
|
location: ${lib.concatStringsSep "." option.loc}
|
||||||
|
'';
|
||||||
|
|
||||||
isExcludedOption = option: (lib.elem (option.type.name or null) excludedTypes);
|
isExcludedOption = option: (lib.elem (option.type.name or null) excludedTypes);
|
||||||
|
|
||||||
filterExcluded = lib.filter (opt: ! isExcludedOption opt);
|
filterExcluded = lib.filter (opt: !isExcludedOption opt);
|
||||||
|
|
||||||
filterExcludedAttrs = lib.filterAttrs (_name: opt: ! isExcludedOption opt);
|
filterExcludedAttrs = lib.filterAttrs (_name: opt: !isExcludedOption opt);
|
||||||
|
|
||||||
allBasicTypes =
|
|
||||||
[ "boolean" "integer" "number" "string" "array" "object" "null" ];
|
|
||||||
|
|
||||||
|
allBasicTypes = [
|
||||||
|
"boolean"
|
||||||
|
"integer"
|
||||||
|
"number"
|
||||||
|
"string"
|
||||||
|
"array"
|
||||||
|
"object"
|
||||||
|
"null"
|
||||||
|
];
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
|
|
||||||
# parses a nixos module to a jsonschema
|
# parses a nixos module to a jsonschema
|
||||||
parseModule = module:
|
parseModule =
|
||||||
|
module:
|
||||||
let
|
let
|
||||||
evaled = lib.evalModules {
|
evaled = lib.evalModules { modules = [ module ]; };
|
||||||
modules = [ module ];
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
parseOptions evaled.options;
|
parseOptions evaled.options;
|
||||||
|
|
||||||
# parses a set of evaluated nixos options to a jsonschema
|
# parses a set of evaluated nixos options to a jsonschema
|
||||||
parseOptions = options':
|
parseOptions =
|
||||||
|
options':
|
||||||
let
|
let
|
||||||
options = filterExcludedAttrs (clean options');
|
options = filterExcludedAttrs (clean options');
|
||||||
# parse options to jsonschema properties
|
# parse options to jsonschema properties
|
||||||
properties = lib.mapAttrs (_name: option: parseOption option) options;
|
properties = lib.mapAttrs (_name: option: parseOption option) options;
|
||||||
# TODO: figure out how to handle if prop.anyOf is used
|
# TODO: figure out how to handle if prop.anyOf is used
|
||||||
isRequired = prop: ! (prop ? default || prop.type or null == "object");
|
isRequired = prop: !(prop ? default || prop.type or null == "object");
|
||||||
requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties;
|
requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties;
|
||||||
required = lib.optionalAttrs (requiredProps != { }) {
|
required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; };
|
||||||
required = lib.attrNames requiredProps;
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
# return jsonschema
|
# return jsonschema
|
||||||
required // {
|
required
|
||||||
|
// {
|
||||||
type = "object";
|
type = "object";
|
||||||
inherit properties;
|
inherit properties;
|
||||||
};
|
};
|
||||||
|
|
||||||
# parses and evaluated nixos option to a jsonschema property definition
|
# parses and evaluated nixos option to a jsonschema property definition
|
||||||
parseOption = option:
|
parseOption =
|
||||||
|
option:
|
||||||
let
|
let
|
||||||
default = lib.optionalAttrs (option ? default) {
|
default = lib.optionalAttrs (option ? default) { inherit (option) default; };
|
||||||
inherit (option) default;
|
|
||||||
};
|
|
||||||
description = lib.optionalAttrs (option ? description) {
|
description = lib.optionalAttrs (option ? description) {
|
||||||
description = option.description.text or option.description;
|
description = option.description.text or option.description;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
||||||
# either type
|
# either type
|
||||||
# TODO: if all nested optiosn are excluded, the parent sould be excluded too
|
# TODO: if all nested optiosn are excluded, the parent sould be excluded too
|
||||||
if option.type.name or null == "either"
|
if
|
||||||
|
option.type.name or null == "either"
|
||||||
# return jsonschema property definition for either
|
# return jsonschema property definition for either
|
||||||
then
|
then
|
||||||
let
|
let
|
||||||
optionsList' = [
|
optionsList' = [
|
||||||
{ type = option.type.nestedTypes.left; _type = "option"; loc = option.loc; }
|
{
|
||||||
{ type = option.type.nestedTypes.right; _type = "option"; loc = option.loc; }
|
type = option.type.nestedTypes.left;
|
||||||
|
_type = "option";
|
||||||
|
loc = option.loc;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = option.type.nestedTypes.right;
|
||||||
|
_type = "option";
|
||||||
|
loc = option.loc;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
optionsList = filterExcluded optionsList';
|
optionsList = filterExcluded optionsList';
|
||||||
in
|
in
|
||||||
default // description // {
|
default // description // { anyOf = map parseOption optionsList; }
|
||||||
anyOf = map parseOption optionsList;
|
|
||||||
}
|
|
||||||
|
|
||||||
# handle nested options (not a submodule)
|
# handle nested options (not a submodule)
|
||||||
else if ! option ? _type
|
else if !option ? _type then
|
||||||
then parseOptions option
|
parseOptions option
|
||||||
|
|
||||||
# throw if not an option
|
# throw if not an option
|
||||||
else if option._type != "option" && option._type != "option-type"
|
else if option._type != "option" && option._type != "option-type" then
|
||||||
then throw "parseOption: not an option"
|
throw "parseOption: not an option"
|
||||||
|
|
||||||
# parse nullOr
|
# parse nullOr
|
||||||
else if option.type.name == "nullOr"
|
else if
|
||||||
|
option.type.name == "nullOr"
|
||||||
# return jsonschema property definition for nullOr
|
# return jsonschema property definition for nullOr
|
||||||
then
|
then
|
||||||
let
|
let
|
||||||
nestedOption =
|
nestedOption = {
|
||||||
{ type = option.type.nestedTypes.elemType; _type = "option"; loc = option.loc; };
|
type = option.type.nestedTypes.elemType;
|
||||||
|
_type = "option";
|
||||||
|
loc = option.loc;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
default // description // {
|
default
|
||||||
anyOf =
|
// description
|
||||||
[{ type = "null"; }]
|
// {
|
||||||
++ (
|
anyOf = [
|
||||||
lib.optional (! isExcludedOption nestedOption)
|
{ type = "null"; }
|
||||||
(parseOption nestedOption)
|
] ++ (lib.optional (!isExcludedOption nestedOption) (parseOption nestedOption));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# parse bool
|
# parse bool
|
||||||
else if option.type.name == "bool"
|
else if
|
||||||
|
option.type.name == "bool"
|
||||||
# return jsonschema property definition for bool
|
# return jsonschema property definition for bool
|
||||||
then default // description // {
|
then
|
||||||
type = "boolean";
|
default // description // { type = "boolean"; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse float
|
# parse float
|
||||||
else if option.type.name == "float"
|
else if
|
||||||
|
option.type.name == "float"
|
||||||
# return jsonschema property definition for float
|
# return jsonschema property definition for float
|
||||||
then default // description // {
|
then
|
||||||
type = "number";
|
default // description // { type = "number"; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse int
|
# parse int
|
||||||
else if (option.type.name == "int" || option.type.name == "positiveInt")
|
else if
|
||||||
|
(option.type.name == "int" || option.type.name == "positiveInt")
|
||||||
# return jsonschema property definition for int
|
# return jsonschema property definition for int
|
||||||
then default // description // {
|
then
|
||||||
type = "integer";
|
default // description // { type = "integer"; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse string
|
# parse string
|
||||||
else if option.type.name == "str"
|
else if
|
||||||
|
option.type.name == "str"
|
||||||
# return jsonschema property definition for string
|
# return jsonschema property definition for string
|
||||||
then default // description // {
|
then
|
||||||
type = "string";
|
default // description // { type = "string"; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse string
|
# parse string
|
||||||
else if option.type.name == "path"
|
else if
|
||||||
|
option.type.name == "path"
|
||||||
# return jsonschema property definition for path
|
# return jsonschema property definition for path
|
||||||
then default // description // {
|
then
|
||||||
type = "string";
|
default // description // { type = "string"; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse anything
|
# parse anything
|
||||||
else if option.type.name == "anything"
|
else if
|
||||||
|
option.type.name == "anything"
|
||||||
# return jsonschema property definition for anything
|
# return jsonschema property definition for anything
|
||||||
then default // description // {
|
then
|
||||||
type = allBasicTypes;
|
default // description // { type = allBasicTypes; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse unspecified
|
# parse unspecified
|
||||||
else if option.type.name == "unspecified"
|
else if
|
||||||
|
option.type.name == "unspecified"
|
||||||
# return jsonschema property definition for unspecified
|
# return jsonschema property definition for unspecified
|
||||||
then default // description // {
|
then
|
||||||
type = allBasicTypes;
|
default // description // { type = allBasicTypes; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse raw
|
# parse raw
|
||||||
else if option.type.name == "raw"
|
else if
|
||||||
|
option.type.name == "raw"
|
||||||
# return jsonschema property definition for raw
|
# return jsonschema property definition for raw
|
||||||
then default // description // {
|
then
|
||||||
type = allBasicTypes;
|
default // description // { type = allBasicTypes; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse enum
|
# parse enum
|
||||||
else if option.type.name == "enum"
|
else if
|
||||||
|
option.type.name == "enum"
|
||||||
# return jsonschema property definition for enum
|
# return jsonschema property definition for enum
|
||||||
then default // description // {
|
then
|
||||||
enum = option.type.functor.payload;
|
default // description // { enum = option.type.functor.payload; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse listOf submodule
|
# parse listOf submodule
|
||||||
else if option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule"
|
else if
|
||||||
|
option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule"
|
||||||
# return jsonschema property definition for listOf submodule
|
# return jsonschema property definition for listOf submodule
|
||||||
then default // description // {
|
then
|
||||||
type = "array";
|
default
|
||||||
items = parseOptions (option.type.functor.wrapped.getSubOptions option.loc);
|
// description
|
||||||
}
|
// {
|
||||||
|
type = "array";
|
||||||
|
items = parseOptions (option.type.functor.wrapped.getSubOptions option.loc);
|
||||||
|
}
|
||||||
|
|
||||||
# parse list
|
# parse list
|
||||||
else if (option.type.name == "listOf")
|
else if
|
||||||
|
(option.type.name == "listOf")
|
||||||
# return jsonschema property definition for list
|
# return jsonschema property definition for list
|
||||||
then
|
then
|
||||||
let
|
let
|
||||||
nestedOption = { type = option.type.functor.wrapped; _type = "option"; loc = option.loc; };
|
nestedOption = {
|
||||||
|
type = option.type.functor.wrapped;
|
||||||
|
_type = "option";
|
||||||
|
loc = option.loc;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
default // description // {
|
default
|
||||||
|
// description
|
||||||
|
// {
|
||||||
type = "array";
|
type = "array";
|
||||||
}
|
}
|
||||||
// (lib.optionalAttrs (! isExcludedOption nestedOption) {
|
// (lib.optionalAttrs (!isExcludedOption nestedOption) { items = parseOption nestedOption; })
|
||||||
items = parseOption nestedOption;
|
|
||||||
})
|
|
||||||
|
|
||||||
# parse list of unspecified
|
# parse list of unspecified
|
||||||
else if
|
else if
|
||||||
(option.type.name == "listOf")
|
(option.type.name == "listOf") && (option.type.functor.wrapped.name == "unspecified")
|
||||||
&& (option.type.functor.wrapped.name == "unspecified")
|
|
||||||
# return jsonschema property definition for list
|
# return jsonschema property definition for list
|
||||||
then default // description // {
|
then
|
||||||
type = "array";
|
default // description // { type = "array"; }
|
||||||
}
|
|
||||||
|
|
||||||
# parse attrsOf submodule
|
# parse attrsOf submodule
|
||||||
else if option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule"
|
else if
|
||||||
|
option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule"
|
||||||
# return jsonschema property definition for attrsOf submodule
|
# return jsonschema property definition for attrsOf submodule
|
||||||
then default // description // {
|
then
|
||||||
type = "object";
|
default
|
||||||
additionalProperties = parseOptions (option.type.nestedTypes.elemType.getSubOptions option.loc);
|
// description
|
||||||
}
|
// {
|
||||||
|
type = "object";
|
||||||
|
additionalProperties = parseOptions (option.type.nestedTypes.elemType.getSubOptions option.loc);
|
||||||
|
}
|
||||||
|
|
||||||
# parse attrs
|
# parse attrs
|
||||||
else if option.type.name == "attrs"
|
else if
|
||||||
|
option.type.name == "attrs"
|
||||||
# return jsonschema property definition for attrs
|
# return jsonschema property definition for attrs
|
||||||
then default // description // {
|
then
|
||||||
type = "object";
|
default
|
||||||
additionalProperties = true;
|
// description
|
||||||
}
|
// {
|
||||||
|
type = "object";
|
||||||
|
additionalProperties = true;
|
||||||
|
}
|
||||||
|
|
||||||
# parse attrsOf
|
# parse attrsOf
|
||||||
# TODO: if nested option is excluded, the parent sould be excluded too
|
# TODO: if nested option is excluded, the parent sould be excluded too
|
||||||
else if option.type.name == "attrsOf" || option.type.name == "lazyAttrsOf"
|
else if
|
||||||
|
option.type.name == "attrsOf" || option.type.name == "lazyAttrsOf"
|
||||||
# return jsonschema property definition for attrs
|
# return jsonschema property definition for attrs
|
||||||
then
|
then
|
||||||
let
|
let
|
||||||
nestedOption = { type = option.type.nestedTypes.elemType; _type = "option"; loc = option.loc; };
|
nestedOption = {
|
||||||
|
type = option.type.nestedTypes.elemType;
|
||||||
|
_type = "option";
|
||||||
|
loc = option.loc;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
default // description // {
|
default
|
||||||
|
// description
|
||||||
|
// {
|
||||||
type = "object";
|
type = "object";
|
||||||
additionalProperties =
|
additionalProperties =
|
||||||
if ! isExcludedOption nestedOption
|
if !isExcludedOption nestedOption then
|
||||||
then parseOption { type = option.type.nestedTypes.elemType; _type = "option"; loc = option.loc; }
|
parseOption {
|
||||||
else false;
|
type = option.type.nestedTypes.elemType;
|
||||||
|
_type = "option";
|
||||||
|
loc = option.loc;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
# parse submodule
|
# parse submodule
|
||||||
else if option.type.name == "submodule"
|
else if
|
||||||
|
option.type.name == "submodule"
|
||||||
# return jsonschema property definition for submodule
|
# return jsonschema property definition for submodule
|
||||||
# then (lib.attrNames (option.type.getSubOptions option.loc).opt)
|
# then (lib.attrNames (option.type.getSubOptions option.loc).opt)
|
||||||
then parseOptions (option.type.getSubOptions option.loc)
|
then
|
||||||
|
parseOptions (option.type.getSubOptions option.loc)
|
||||||
|
|
||||||
# throw error if option type is not supported
|
# throw error if option type is not supported
|
||||||
else notSupported option;
|
else
|
||||||
|
notSupported option;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
/*
|
# An example nixos module declaring an interface.
|
||||||
An example nixos module declaring an interface.
|
{ lib, ... }:
|
||||||
*/
|
{
|
||||||
{ lib, ... }: {
|
|
||||||
options = {
|
options = {
|
||||||
# str
|
# str
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
@ -44,7 +43,11 @@
|
|||||||
# list of str
|
# list of str
|
||||||
kernelModules = lib.mkOption {
|
kernelModules = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ "nvme" "xhci_pci" "ahci" ];
|
default = [
|
||||||
|
"nvme"
|
||||||
|
"xhci_pci"
|
||||||
|
"ahci"
|
||||||
|
];
|
||||||
description = "A list of enabled kernel modules";
|
description = "A list of enabled kernel modules";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,29 +1,31 @@
|
|||||||
{
|
{
|
||||||
perSystem = { pkgs, ... }: {
|
perSystem =
|
||||||
checks = {
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
checks = {
|
||||||
|
|
||||||
# check if the `clan config` example jsonschema and data is valid
|
# check if the `clan config` example jsonschema and data is valid
|
||||||
lib-jsonschema-example-valid = pkgs.runCommand "lib-jsonschema-example-valid" { } ''
|
lib-jsonschema-example-valid = pkgs.runCommand "lib-jsonschema-example-valid" { } ''
|
||||||
echo "Checking that example-schema.json is valid"
|
echo "Checking that example-schema.json is valid"
|
||||||
${pkgs.check-jsonschema}/bin/check-jsonschema \
|
${pkgs.check-jsonschema}/bin/check-jsonschema \
|
||||||
--check-metaschema ${./.}/example-schema.json
|
--check-metaschema ${./.}/example-schema.json
|
||||||
|
|
||||||
echo "Checking that example-data.json is valid according to example-schema.json"
|
echo "Checking that example-data.json is valid according to example-schema.json"
|
||||||
${pkgs.check-jsonschema}/bin/check-jsonschema \
|
${pkgs.check-jsonschema}/bin/check-jsonschema \
|
||||||
--schemafile ${./.}/example-schema.json \
|
--schemafile ${./.}/example-schema.json \
|
||||||
${./.}/example-data.json
|
${./.}/example-data.json
|
||||||
|
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# check if the `clan config` nix jsonschema converter unit tests succeed
|
# check if the `clan config` nix jsonschema converter unit tests succeed
|
||||||
lib-jsonschema-nix-unit-tests = pkgs.runCommand "lib-jsonschema-nix-unit-tests" { } ''
|
lib-jsonschema-nix-unit-tests = pkgs.runCommand "lib-jsonschema-nix-unit-tests" { } ''
|
||||||
export NIX_PATH=nixpkgs=${pkgs.path}
|
export NIX_PATH=nixpkgs=${pkgs.path}
|
||||||
${pkgs.nix-unit}/bin/nix-unit \
|
${pkgs.nix-unit}/bin/nix-unit \
|
||||||
${./.}/test.nix \
|
${./.}/test.nix \
|
||||||
--eval-store $(realpath .)
|
--eval-store $(realpath .)
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# run these tests via `nix-unit ./test.nix`
|
# run these tests via `nix-unit ./test.nix`
|
||||||
{ lib ? (import <nixpkgs> { }).lib
|
{
|
||||||
, slib ? import ./. { inherit lib; }
|
lib ? (import <nixpkgs> { }).lib,
|
||||||
|
slib ? import ./. { inherit lib; },
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
parseOption = import ./test_parseOption.nix { inherit lib slib; };
|
parseOption = import ./test_parseOption.nix { inherit lib slib; };
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
# tests for the nixos options to jsonschema converter
|
# tests for the nixos options to jsonschema converter
|
||||||
# run these tests via `nix-unit ./test.nix`
|
# run these tests via `nix-unit ./test.nix`
|
||||||
{ lib ? (import <nixpkgs> { }).lib
|
{
|
||||||
, slib ? import ./. { inherit lib; }
|
lib ? (import <nixpkgs> { }).lib,
|
||||||
|
slib ? import ./. { inherit lib; },
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
description = "Test Description";
|
description = "Test Description";
|
||||||
|
|
||||||
evalType = type: default:
|
evalType =
|
||||||
|
type: default:
|
||||||
let
|
let
|
||||||
evaledConfig = lib.evalModules {
|
evaledConfig = lib.evalModules {
|
||||||
modules = [{
|
modules = [
|
||||||
options.opt = lib.mkOption {
|
{
|
||||||
inherit type;
|
options.opt = lib.mkOption {
|
||||||
inherit default;
|
inherit type;
|
||||||
inherit description;
|
inherit default;
|
||||||
};
|
inherit description;
|
||||||
}];
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
evaledConfig.options.opt;
|
evaledConfig.options.opt;
|
||||||
@ -25,11 +29,7 @@ in
|
|||||||
testNoDefaultNoDescription =
|
testNoDefaultNoDescription =
|
||||||
let
|
let
|
||||||
evaledConfig = lib.evalModules {
|
evaledConfig = lib.evalModules {
|
||||||
modules = [{
|
modules = [ { options.opt = lib.mkOption { type = lib.types.bool; }; } ];
|
||||||
options.opt = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
}];
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@ -42,15 +42,17 @@ in
|
|||||||
testDescriptionIsAttrs =
|
testDescriptionIsAttrs =
|
||||||
let
|
let
|
||||||
evaledConfig = lib.evalModules {
|
evaledConfig = lib.evalModules {
|
||||||
modules = [{
|
modules = [
|
||||||
options.opt = lib.mkOption {
|
{
|
||||||
type = lib.types.bool;
|
options.opt = lib.mkOption {
|
||||||
description = {
|
type = lib.types.bool;
|
||||||
_type = "mdDoc";
|
description = {
|
||||||
text = description;
|
_type = "mdDoc";
|
||||||
|
text = description;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
}];
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@ -112,7 +114,11 @@ in
|
|||||||
testEnum =
|
testEnum =
|
||||||
let
|
let
|
||||||
default = "foo";
|
default = "foo";
|
||||||
values = [ "foo" "bar" "baz" ];
|
values = [
|
||||||
|
"foo"
|
||||||
|
"bar"
|
||||||
|
"baz"
|
||||||
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
expr = slib.parseOption (evalType (lib.types.enum values) default);
|
expr = slib.parseOption (evalType (lib.types.enum values) default);
|
||||||
@ -124,7 +130,11 @@ in
|
|||||||
|
|
||||||
testListOfInt =
|
testListOfInt =
|
||||||
let
|
let
|
||||||
default = [ 1 2 3 ];
|
default = [
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
expr = slib.parseOption (evalType (lib.types.listOf lib.types.int) default);
|
expr = slib.parseOption (evalType (lib.types.listOf lib.types.int) default);
|
||||||
@ -139,14 +149,26 @@ in
|
|||||||
|
|
||||||
testListOfUnspecified =
|
testListOfUnspecified =
|
||||||
let
|
let
|
||||||
default = [ 1 2 3 ];
|
default = [
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
expr = slib.parseOption (evalType (lib.types.listOf lib.types.unspecified) default);
|
expr = slib.parseOption (evalType (lib.types.listOf lib.types.unspecified) default);
|
||||||
expected = {
|
expected = {
|
||||||
type = "array";
|
type = "array";
|
||||||
items = {
|
items = {
|
||||||
type = [ "boolean" "integer" "number" "string" "array" "object" "null" ];
|
type = [
|
||||||
|
"boolean"
|
||||||
|
"integer"
|
||||||
|
"number"
|
||||||
|
"string"
|
||||||
|
"array"
|
||||||
|
"object"
|
||||||
|
"null"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
inherit default description;
|
inherit default description;
|
||||||
};
|
};
|
||||||
@ -154,7 +176,11 @@ in
|
|||||||
|
|
||||||
testAttrs =
|
testAttrs =
|
||||||
let
|
let
|
||||||
default = { foo = 1; bar = 2; baz = 3; };
|
default = {
|
||||||
|
foo = 1;
|
||||||
|
bar = 2;
|
||||||
|
baz = 3;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
expr = slib.parseOption (evalType (lib.types.attrs) default);
|
expr = slib.parseOption (evalType (lib.types.attrs) default);
|
||||||
@ -167,7 +193,11 @@ in
|
|||||||
|
|
||||||
testAttrsOfInt =
|
testAttrsOfInt =
|
||||||
let
|
let
|
||||||
default = { foo = 1; bar = 2; baz = 3; };
|
default = {
|
||||||
|
foo = 1;
|
||||||
|
bar = 2;
|
||||||
|
baz = 3;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
expr = slib.parseOption (evalType (lib.types.attrsOf lib.types.int) default);
|
expr = slib.parseOption (evalType (lib.types.attrsOf lib.types.int) default);
|
||||||
@ -182,7 +212,11 @@ in
|
|||||||
|
|
||||||
testLazyAttrsOfInt =
|
testLazyAttrsOfInt =
|
||||||
let
|
let
|
||||||
default = { foo = 1; bar = 2; baz = 3; };
|
default = {
|
||||||
|
foo = 1;
|
||||||
|
bar = 2;
|
||||||
|
baz = 3;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
expr = slib.parseOption (evalType (lib.types.lazyAttrsOf lib.types.int) default);
|
expr = slib.parseOption (evalType (lib.types.lazyAttrsOf lib.types.int) default);
|
||||||
@ -286,7 +320,10 @@ in
|
|||||||
inherit description;
|
inherit description;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
default = { foo.opt = false; bar.opt = true; };
|
default = {
|
||||||
|
foo.opt = false;
|
||||||
|
bar.opt = true;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
expr = slib.parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default);
|
expr = slib.parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default);
|
||||||
@ -315,7 +352,10 @@ in
|
|||||||
inherit description;
|
inherit description;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
default = [{ opt = false; } { opt = true; }];
|
default = [
|
||||||
|
{ opt = false; }
|
||||||
|
{ opt = true; }
|
||||||
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
expr = slib.parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default);
|
expr = slib.parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default);
|
||||||
@ -358,7 +398,15 @@ in
|
|||||||
expr = slib.parseOption (evalType lib.types.anything default);
|
expr = slib.parseOption (evalType lib.types.anything default);
|
||||||
expected = {
|
expected = {
|
||||||
inherit default description;
|
inherit default description;
|
||||||
type = [ "boolean" "integer" "number" "string" "array" "object" "null" ];
|
type = [
|
||||||
|
"boolean"
|
||||||
|
"integer"
|
||||||
|
"number"
|
||||||
|
"string"
|
||||||
|
"array"
|
||||||
|
"object"
|
||||||
|
"null"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -370,7 +418,15 @@ in
|
|||||||
expr = slib.parseOption (evalType lib.types.unspecified default);
|
expr = slib.parseOption (evalType lib.types.unspecified default);
|
||||||
expected = {
|
expected = {
|
||||||
inherit default description;
|
inherit default description;
|
||||||
type = [ "boolean" "integer" "number" "string" "array" "object" "null" ];
|
type = [
|
||||||
|
"boolean"
|
||||||
|
"integer"
|
||||||
|
"number"
|
||||||
|
"string"
|
||||||
|
"array"
|
||||||
|
"object"
|
||||||
|
"null"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -382,7 +438,15 @@ in
|
|||||||
expr = slib.parseOption (evalType lib.types.raw default);
|
expr = slib.parseOption (evalType lib.types.raw default);
|
||||||
expected = {
|
expected = {
|
||||||
inherit default description;
|
inherit default description;
|
||||||
type = [ "boolean" "integer" "number" "string" "array" "object" "null" ];
|
type = [
|
||||||
|
"boolean"
|
||||||
|
"integer"
|
||||||
|
"number"
|
||||||
|
"string"
|
||||||
|
"array"
|
||||||
|
"object"
|
||||||
|
"null"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
# tests for the nixos options to jsonschema converter
|
# tests for the nixos options to jsonschema converter
|
||||||
# run these tests via `nix-unit ./test.nix`
|
# run these tests via `nix-unit ./test.nix`
|
||||||
{ lib ? (import <nixpkgs> { }).lib
|
{
|
||||||
, slib ? import ./. { inherit lib; }
|
lib ? (import <nixpkgs> { }).lib,
|
||||||
|
slib ? import ./. { inherit lib; },
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
evaledOptions =
|
evaledOptions =
|
||||||
let
|
let
|
||||||
evaledConfig = lib.evalModules {
|
evaledConfig = lib.evalModules { modules = [ ./example-interface.nix ]; };
|
||||||
modules = [ ./example-interface.nix ];
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
evaledConfig.options;
|
evaledConfig.options;
|
||||||
in
|
in
|
||||||
@ -21,11 +20,7 @@ in
|
|||||||
testParseNestedOptions =
|
testParseNestedOptions =
|
||||||
let
|
let
|
||||||
evaled = lib.evalModules {
|
evaled = lib.evalModules {
|
||||||
modules = [{
|
modules = [ { options.foo.bar = lib.mkOption { type = lib.types.bool; }; } ];
|
||||||
options.foo.bar = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
}];
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@ -34,7 +29,9 @@ in
|
|||||||
properties = {
|
properties = {
|
||||||
foo = {
|
foo = {
|
||||||
properties = {
|
properties = {
|
||||||
bar = { type = "boolean"; };
|
bar = {
|
||||||
|
type = "boolean";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
required = [ "bar" ];
|
required = [ "bar" ];
|
||||||
type = "object";
|
type = "object";
|
||||||
|
1
machines/test_backup_client/facts/borgbackup.ssh.pub
Normal file
1
machines/test_backup_client/facts/borgbackup.ssh.pub
Normal file
@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBIbwIVnLy+uoDZ6uK/OCc1QK46SIGeC3mVc85dqLYQw lass@ignavia
|
@ -1,45 +1,48 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [ ./state.nix ];
|
||||||
./state.nix
|
|
||||||
];
|
|
||||||
options.clanCore.backups = {
|
options.clanCore.backups = {
|
||||||
providers = lib.mkOption {
|
providers = lib.mkOption {
|
||||||
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
|
type = lib.types.attrsOf (
|
||||||
options = {
|
lib.types.submodule (
|
||||||
name = lib.mkOption {
|
{ name, ... }:
|
||||||
type = lib.types.str;
|
{
|
||||||
default = name;
|
options = {
|
||||||
description = ''
|
name = lib.mkOption {
|
||||||
Name of the backup provider
|
type = lib.types.str;
|
||||||
'';
|
default = name;
|
||||||
};
|
description = ''
|
||||||
list = lib.mkOption {
|
Name of the backup provider
|
||||||
type = lib.types.str;
|
'';
|
||||||
description = ''
|
};
|
||||||
script to list backups
|
list = lib.mkOption {
|
||||||
'';
|
type = lib.types.str;
|
||||||
};
|
description = ''
|
||||||
restore = lib.mkOption {
|
script to list backups
|
||||||
type = lib.types.str;
|
'';
|
||||||
description = ''
|
};
|
||||||
script to restore a backup
|
restore = lib.mkOption {
|
||||||
should take an optional service name as argument
|
type = lib.types.str;
|
||||||
gets ARCHIVE_ID, LOCATION, JOB and FOLDERS as environment variables
|
description = ''
|
||||||
ARCHIVE_ID is the id of the backup
|
script to restore a backup
|
||||||
LOCATION is the remote identifier of the backup
|
should take an optional service name as argument
|
||||||
JOB is the job name of the backup
|
gets ARCHIVE_ID, LOCATION, JOB and FOLDERS as environment variables
|
||||||
FOLDERS is a colon separated list of folders to restore
|
ARCHIVE_ID is the id of the backup
|
||||||
'';
|
LOCATION is the remote identifier of the backup
|
||||||
};
|
JOB is the job name of the backup
|
||||||
create = lib.mkOption {
|
FOLDERS is a colon separated list of folders to restore
|
||||||
type = lib.types.str;
|
'';
|
||||||
description = ''
|
};
|
||||||
script to start a backup
|
create = lib.mkOption {
|
||||||
'';
|
type = lib.types.str;
|
||||||
};
|
description = ''
|
||||||
};
|
script to start a backup
|
||||||
}));
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
default = { };
|
default = { };
|
||||||
description = ''
|
description = ''
|
||||||
Configured backup providers which are used by this machine
|
Configured backup providers which are used by this machine
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{ lib
|
{ lib, ... }:
|
||||||
, ...
|
{
|
||||||
}: {
|
|
||||||
/*
|
/*
|
||||||
Declaring imports inside the module system does not trigger an infinite
|
Declaring imports inside the module system does not trigger an infinite
|
||||||
recursion in this case because buildClan generates the imports from the
|
recursion in this case because buildClan generates the imports from the
|
||||||
|
@ -1 +1,4 @@
|
|||||||
{ pkgs, ... }: { documentation.nixos.enable = pkgs.lib.mkDefault false; }
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
documentation.nixos.enable = pkgs.lib.mkDefault false;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{ lib, pkgs, ... }: {
|
{ lib, pkgs, ... }:
|
||||||
|
{
|
||||||
options.clanCore = {
|
options.clanCore = {
|
||||||
clanName = lib.mkOption {
|
clanName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
|
@ -49,7 +49,18 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
(lib.mkRenamedOptionModule [ "clan" "networking" "deploymentAddress" ] [ "clan" "networking" "targetHost" ])
|
(lib.mkRenamedOptionModule
|
||||||
|
[
|
||||||
|
"clan"
|
||||||
|
"networking"
|
||||||
|
"deploymentAddress"
|
||||||
|
]
|
||||||
|
[
|
||||||
|
"clan"
|
||||||
|
"networking"
|
||||||
|
"targetHost"
|
||||||
|
]
|
||||||
|
)
|
||||||
];
|
];
|
||||||
config = {
|
config = {
|
||||||
# conflicts with systemd-resolved
|
# conflicts with systemd-resolved
|
||||||
@ -64,16 +75,18 @@
|
|||||||
systemd.network.wait-online.enable = false;
|
systemd.network.wait-online.enable = false;
|
||||||
|
|
||||||
# Provide a default network configuration but don't compete with network-manager or dhcpcd
|
# Provide a default network configuration but don't compete with network-manager or dhcpcd
|
||||||
systemd.network.networks."50-uplink" = lib.mkIf (!(config.networking.networkmanager.enable || config.networking.dhcpcd.enable)) {
|
systemd.network.networks."50-uplink" =
|
||||||
matchConfig.Type = "ether";
|
lib.mkIf (!(config.networking.networkmanager.enable || config.networking.dhcpcd.enable))
|
||||||
networkConfig = {
|
{
|
||||||
DHCP = "yes";
|
matchConfig.Type = "ether";
|
||||||
LLDP = "yes";
|
networkConfig = {
|
||||||
LLMNR = "yes";
|
DHCP = "yes";
|
||||||
MulticastDNS = "yes";
|
LLDP = "yes";
|
||||||
IPv6AcceptRA = "yes";
|
LLMNR = "yes";
|
||||||
};
|
MulticastDNS = "yes";
|
||||||
};
|
IPv6AcceptRA = "yes";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Use networkd instead of the pile of shell scripts
|
# Use networkd instead of the pile of shell scripts
|
||||||
networking.useNetworkd = lib.mkDefault true;
|
networking.useNetworkd = lib.mkDefault true;
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
{ pkgs, options, lib, ... }: {
|
{
|
||||||
|
pkgs,
|
||||||
|
options,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
options.clanCore.optionsNix = lib.mkOption {
|
options.clanCore.optionsNix = lib.mkOption {
|
||||||
type = lib.types.raw;
|
type = lib.types.raw;
|
||||||
internal = true;
|
internal = true;
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
{ config, lib, pkgs, ... }: {
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
# TODO: factor these out into a separate interface.nix.
|
# TODO: factor these out into a separate interface.nix.
|
||||||
# Also think about moving these options out of `system.clan`.
|
# Also think about moving these options out of `system.clan`.
|
||||||
# Maybe we should not re-use the already polluted confg.system namespace
|
# Maybe we should not re-use the already polluted confg.system namespace
|
||||||
@ -90,6 +96,8 @@
|
|||||||
inherit (config.clan.deployment) requireExplicitUpdate;
|
inherit (config.clan.deployment) requireExplicitUpdate;
|
||||||
inherit (config.clanCore) secretsUploadDirectory;
|
inherit (config.clanCore) secretsUploadDirectory;
|
||||||
};
|
};
|
||||||
system.clan.deployment.file = pkgs.writeText "deployment.json" (builtins.toJSON config.system.clan.deployment.data);
|
system.clan.deployment.file = pkgs.writeText "deployment.json" (
|
||||||
|
builtins.toJSON config.system.clan.deployment.data
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{ pkgs, ... }: {
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
# essential debugging tools for networked services
|
# essential debugging tools for networked services
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
pkgs.dnsutils
|
pkgs.dnsutils
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
{
|
{
|
||||||
options.clanCore.secretStore = lib.mkOption {
|
options.clanCore.secretStore = lib.mkOption {
|
||||||
type = lib.types.enum [ "sops" "password-store" "vm" "custom" ];
|
type = lib.types.enum [
|
||||||
|
"sops"
|
||||||
|
"password-store"
|
||||||
|
"vm"
|
||||||
|
"custom"
|
||||||
|
];
|
||||||
default = "sops";
|
default = "sops";
|
||||||
description = ''
|
description = ''
|
||||||
method to store secrets
|
method to store secrets
|
||||||
@ -34,8 +44,8 @@
|
|||||||
|
|
||||||
options.clanCore.secrets = lib.mkOption {
|
options.clanCore.secrets = lib.mkOption {
|
||||||
default = { };
|
default = { };
|
||||||
type = lib.types.attrsOf
|
type = lib.types.attrsOf (
|
||||||
(lib.types.submodule (service: {
|
lib.types.submodule (service: {
|
||||||
options = {
|
options = {
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
@ -45,123 +55,138 @@
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
generator = lib.mkOption {
|
generator = lib.mkOption {
|
||||||
type = lib.types.submodule ({ config, ... }: {
|
type = lib.types.submodule (
|
||||||
options = {
|
{ config, ... }:
|
||||||
path = lib.mkOption {
|
{
|
||||||
type = lib.types.listOf (lib.types.either lib.types.path lib.types.package);
|
options = {
|
||||||
default = [ ];
|
path = lib.mkOption {
|
||||||
description = ''
|
type = lib.types.listOf (lib.types.either lib.types.path lib.types.package);
|
||||||
Extra paths to add to the PATH environment variable when running the generator.
|
default = [ ];
|
||||||
'';
|
description = ''
|
||||||
};
|
Extra paths to add to the PATH environment variable when running the generator.
|
||||||
prompt = lib.mkOption {
|
'';
|
||||||
type = lib.types.nullOr lib.types.str;
|
};
|
||||||
default = null;
|
prompt = lib.mkOption {
|
||||||
description = ''
|
type = lib.types.nullOr lib.types.str;
|
||||||
prompt text to ask for a value.
|
default = null;
|
||||||
This value will be passed to the script as the environment variable $prompt_value.
|
description = ''
|
||||||
'';
|
prompt text to ask for a value.
|
||||||
};
|
This value will be passed to the script as the environment variable $prompt_value.
|
||||||
script = lib.mkOption {
|
'';
|
||||||
type = lib.types.str;
|
};
|
||||||
description = ''
|
script = lib.mkOption {
|
||||||
Script to generate the secret.
|
type = lib.types.str;
|
||||||
The script will be called with the following variables:
|
description = ''
|
||||||
- facts: path to a directory where facts can be stored
|
Script to generate the secret.
|
||||||
- secrets: path to a directory where secrets can be stored
|
The script will be called with the following variables:
|
||||||
The script is expected to generate all secrets and facts defined in the module.
|
- facts: path to a directory where facts can be stored
|
||||||
'';
|
- secrets: path to a directory where secrets can be stored
|
||||||
};
|
The script is expected to generate all secrets and facts defined in the module.
|
||||||
finalScript = lib.mkOption {
|
'';
|
||||||
type = lib.types.str;
|
};
|
||||||
readOnly = true;
|
finalScript = lib.mkOption {
|
||||||
internal = true;
|
type = lib.types.str;
|
||||||
default = ''
|
readOnly = true;
|
||||||
set -eu -o pipefail
|
internal = true;
|
||||||
|
default = ''
|
||||||
|
set -eu -o pipefail
|
||||||
|
|
||||||
export PATH="${lib.makeBinPath config.path}:${pkgs.coreutils}/bin"
|
export PATH="${lib.makeBinPath config.path}:${pkgs.coreutils}/bin"
|
||||||
|
|
||||||
# prepare sandbox user
|
# prepare sandbox user
|
||||||
mkdir -p /etc
|
mkdir -p /etc
|
||||||
cp ${pkgs.runCommand "fake-etc" {} ''
|
cp ${
|
||||||
export PATH="${pkgs.coreutils}/bin"
|
pkgs.runCommand "fake-etc" { } ''
|
||||||
mkdir -p $out
|
export PATH="${pkgs.coreutils}/bin"
|
||||||
cp /etc/* $out/
|
mkdir -p $out
|
||||||
''}/* /etc/
|
cp /etc/* $out/
|
||||||
|
''
|
||||||
|
}/* /etc/
|
||||||
|
|
||||||
${config.script}
|
${config.script}
|
||||||
'';
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
secrets =
|
secrets =
|
||||||
let
|
let
|
||||||
config' = config;
|
config' = config;
|
||||||
in
|
in
|
||||||
lib.mkOption {
|
lib.mkOption {
|
||||||
type = lib.types.attrsOf (lib.types.submodule ({ config, name, ... }: {
|
default = { };
|
||||||
options = {
|
type = lib.types.attrsOf (
|
||||||
name = lib.mkOption {
|
lib.types.submodule (
|
||||||
type = lib.types.str;
|
{ config, name, ... }:
|
||||||
description = ''
|
{
|
||||||
name of the secret
|
options =
|
||||||
'';
|
{
|
||||||
default = name;
|
name = lib.mkOption {
|
||||||
};
|
type = lib.types.str;
|
||||||
path = lib.mkOption {
|
description = ''
|
||||||
type = lib.types.str;
|
name of the secret
|
||||||
description = ''
|
'';
|
||||||
path to a secret which is generated by the generator
|
default = name;
|
||||||
'';
|
};
|
||||||
default = "${config'.clanCore.secretsDirectory}/${config'.clanCore.secretsPrefix}${config.name}";
|
path = lib.mkOption {
|
||||||
};
|
type = lib.types.str;
|
||||||
} // lib.optionalAttrs (config'.clanCore.secretStore == "sops") {
|
description = ''
|
||||||
groups = lib.mkOption {
|
path to a secret which is generated by the generator
|
||||||
type = lib.types.listOf lib.types.str;
|
'';
|
||||||
default = config'.clanCore.sops.defaultGroups;
|
default = "${config'.clanCore.secretsDirectory}/${config'.clanCore.secretsPrefix}${config.name}";
|
||||||
description = ''
|
};
|
||||||
Groups to decrypt the secret for. By default we always use the user's key.
|
}
|
||||||
'';
|
// lib.optionalAttrs (config'.clanCore.secretStore == "sops") {
|
||||||
};
|
groups = lib.mkOption {
|
||||||
};
|
type = lib.types.listOf lib.types.str;
|
||||||
}));
|
default = config'.clanCore.sops.defaultGroups;
|
||||||
|
description = ''
|
||||||
|
Groups to decrypt the secret for. By default we always use the user's key.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
description = ''
|
description = ''
|
||||||
path where the secret is located in the filesystem
|
path where the secret is located in the filesystem
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
facts = lib.mkOption {
|
facts = lib.mkOption {
|
||||||
default = { };
|
default = { };
|
||||||
type = lib.types.attrsOf (lib.types.submodule (fact: {
|
type = lib.types.attrsOf (
|
||||||
options = {
|
lib.types.submodule (fact: {
|
||||||
name = lib.mkOption {
|
options = {
|
||||||
type = lib.types.str;
|
name = lib.mkOption {
|
||||||
description = ''
|
type = lib.types.str;
|
||||||
name of the fact
|
description = ''
|
||||||
'';
|
name of the fact
|
||||||
default = fact.config._module.args.name;
|
'';
|
||||||
|
default = fact.config._module.args.name;
|
||||||
|
};
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = ''
|
||||||
|
path to a fact which is generated by the generator
|
||||||
|
'';
|
||||||
|
default =
|
||||||
|
config.clanCore.clanDir
|
||||||
|
+ "/machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}";
|
||||||
|
};
|
||||||
|
value = lib.mkOption {
|
||||||
|
defaultText = lib.literalExpression "\${config.clanCore.clanDir}/\${fact.config.path}";
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default =
|
||||||
|
if builtins.pathExists fact.config.path then lib.strings.fileContents fact.config.path else null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
path = lib.mkOption {
|
})
|
||||||
type = lib.types.str;
|
);
|
||||||
description = ''
|
|
||||||
path to a fact which is generated by the generator
|
|
||||||
'';
|
|
||||||
default = "machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}";
|
|
||||||
};
|
|
||||||
value = lib.mkOption {
|
|
||||||
defaultText = lib.literalExpression "\${config.clanCore.clanDir}/\${fact.config.path}";
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default =
|
|
||||||
if builtins.pathExists "${config.clanCore.clanDir}/${fact.config.path}" then
|
|
||||||
lib.strings.removeSuffix "\n" (builtins.readFile "${config.clanCore.clanDir}/${fact.config.path}")
|
|
||||||
else
|
|
||||||
null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
imports = [
|
imports = [
|
||||||
./sops.nix
|
./sops.nix
|
||||||
|
@ -13,4 +13,3 @@
|
|||||||
system.clan.secretsModule = "clan_cli.secrets.modules.password_store";
|
system.clan.secretsModule = "clan_cli.secrets.modules.password_store";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,33 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
secretsDir = config.clanCore.clanDir + "/sops/secrets";
|
secretsDir = config.clanCore.clanDir + "/sops/secrets";
|
||||||
groupsDir = config.clanCore.clanDir + "/sops/groups";
|
groupsDir = config.clanCore.clanDir + "/sops/groups";
|
||||||
|
|
||||||
|
|
||||||
# My symlink is in the nixos module detected as a directory also it works in the repl. Is this because of pure evaluation?
|
# My symlink is in the nixos module detected as a directory also it works in the repl. Is this because of pure evaluation?
|
||||||
containsSymlink = path:
|
containsSymlink =
|
||||||
builtins.pathExists path && (builtins.readFileType path == "directory" || builtins.readFileType path == "symlink");
|
path:
|
||||||
|
builtins.pathExists path
|
||||||
|
&& (builtins.readFileType path == "directory" || builtins.readFileType path == "symlink");
|
||||||
|
|
||||||
containsMachine = parent: name: type:
|
containsMachine =
|
||||||
|
parent: name: type:
|
||||||
type == "directory" && containsSymlink "${parent}/${name}/machines/${config.clanCore.machineName}";
|
type == "directory" && containsSymlink "${parent}/${name}/machines/${config.clanCore.machineName}";
|
||||||
|
|
||||||
containsMachineOrGroups = name: type:
|
containsMachineOrGroups =
|
||||||
(containsMachine secretsDir name type) || lib.any (group: type == "directory" && containsSymlink "${secretsDir}/${name}/groups/${group}") groups;
|
name: type:
|
||||||
|
(containsMachine secretsDir name type)
|
||||||
|
|| lib.any (
|
||||||
|
group: type == "directory" && containsSymlink "${secretsDir}/${name}/groups/${group}"
|
||||||
|
) groups;
|
||||||
|
|
||||||
filterDir = filter: dir:
|
filterDir =
|
||||||
lib.optionalAttrs (builtins.pathExists dir)
|
filter: dir:
|
||||||
(lib.filterAttrs filter (builtins.readDir dir));
|
lib.optionalAttrs (builtins.pathExists dir) (lib.filterAttrs filter (builtins.readDir dir));
|
||||||
|
|
||||||
groups = builtins.attrNames (filterDir (containsMachine groupsDir) groupsDir);
|
groups = builtins.attrNames (filterDir (containsMachine groupsDir) groupsDir);
|
||||||
secrets = filterDir containsMachineOrGroups secretsDir;
|
secrets = filterDir containsMachineOrGroups secretsDir;
|
||||||
@ -34,17 +45,18 @@ in
|
|||||||
clanCore.secretsDirectory = "/run/secrets";
|
clanCore.secretsDirectory = "/run/secrets";
|
||||||
clanCore.secretsPrefix = config.clanCore.machineName + "-";
|
clanCore.secretsPrefix = config.clanCore.machineName + "-";
|
||||||
system.clan.secretsModule = "clan_cli.secrets.modules.sops";
|
system.clan.secretsModule = "clan_cli.secrets.modules.sops";
|
||||||
sops.secrets = builtins.mapAttrs
|
sops.secrets = builtins.mapAttrs (name: _: {
|
||||||
(name: _: {
|
sopsFile = config.clanCore.clanDir + "/sops/secrets/${name}/secret";
|
||||||
sopsFile = config.clanCore.clanDir + "/sops/secrets/${name}/secret";
|
format = "binary";
|
||||||
format = "binary";
|
}) secrets;
|
||||||
})
|
|
||||||
secrets;
|
|
||||||
# To get proper error messages about missing secrets we need a dummy secret file that is always present
|
# To get proper error messages about missing secrets we need a dummy secret file that is always present
|
||||||
sops.defaultSopsFile = lib.mkIf config.sops.validateSopsFiles (lib.mkDefault (builtins.toString (pkgs.writeText "dummy.yaml" "")));
|
sops.defaultSopsFile = lib.mkIf config.sops.validateSopsFiles (
|
||||||
|
lib.mkDefault (builtins.toString (pkgs.writeText "dummy.yaml" ""))
|
||||||
|
);
|
||||||
|
|
||||||
sops.age.keyFile = lib.mkIf (builtins.pathExists (config.clanCore.clanDir + "/sops/secrets/${config.clanCore.machineName}-age.key/secret"))
|
sops.age.keyFile = lib.mkIf (builtins.pathExists (
|
||||||
(lib.mkDefault "/var/lib/sops-nix/key.txt");
|
config.clanCore.clanDir + "/sops/secrets/${config.clanCore.machineName}-age.key/secret"
|
||||||
|
)) (lib.mkDefault "/var/lib/sops-nix/key.txt");
|
||||||
clanCore.secretsUploadDirectory = lib.mkDefault "/var/lib/sops-nix";
|
clanCore.secretsUploadDirectory = lib.mkDefault "/var/lib/sops-nix";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,3 @@
|
|||||||
system.clan.factsModule = "clan_cli.facts.modules.vm";
|
system.clan.factsModule = "clan_cli.facts.modules.vm";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,41 +1,43 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
# defaults
|
# defaults
|
||||||
config.clanCore.state.HOME.folders = [
|
config.clanCore.state.HOME.folders = [ "/home" ];
|
||||||
"/home"
|
|
||||||
];
|
|
||||||
|
|
||||||
# interface
|
# interface
|
||||||
options.clanCore.state = lib.mkOption {
|
options.clanCore.state = lib.mkOption {
|
||||||
default = { };
|
default = { };
|
||||||
type = lib.types.attrsOf
|
type = lib.types.attrsOf (
|
||||||
(lib.types.submodule ({ ... }: {
|
lib.types.submodule (
|
||||||
options = {
|
{ ... }:
|
||||||
folders = lib.mkOption {
|
{
|
||||||
type = lib.types.listOf lib.types.str;
|
options = {
|
||||||
description = ''
|
folders = lib.mkOption {
|
||||||
Folder where state resides in
|
type = lib.types.listOf lib.types.str;
|
||||||
'';
|
description = ''
|
||||||
};
|
Folder where state resides in
|
||||||
preRestoreScript = lib.mkOption {
|
'';
|
||||||
type = lib.types.str;
|
};
|
||||||
default = ":";
|
preRestoreScript = lib.mkOption {
|
||||||
description = ''
|
type = lib.types.str;
|
||||||
script to run before restoring the state dir from a backup
|
default = ":";
|
||||||
|
description = ''
|
||||||
|
script to run before restoring the state dir from a backup
|
||||||
|
|
||||||
Utilize this to stop services which currently access these folders
|
Utilize this to stop services which currently access these folders
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
postRestoreScript = lib.mkOption {
|
postRestoreScript = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = ":";
|
default = ":";
|
||||||
description = ''
|
description = ''
|
||||||
script to restore the service after the state dir was restored from a backup
|
script to restore the service after the state dir was restored from a backup
|
||||||
|
|
||||||
Utilize this to start services which were previously stopped
|
Utilize this to start services which were previously stopped
|
||||||
'';
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
}));
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
{ lib, config, pkgs, options, extendModules, modulesPath, ... }:
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
options,
|
||||||
|
extendModules,
|
||||||
|
modulesPath,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
# Flatten the list of state folders into a single list
|
# Flatten the list of state folders into a single list
|
||||||
stateFolders = lib.flatten (
|
stateFolders = lib.flatten (lib.mapAttrsToList (_item: attrs: attrs.folders) config.clanCore.state);
|
||||||
lib.mapAttrsToList
|
|
||||||
(_item: attrs: attrs.folders)
|
|
||||||
config.clanCore.state
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
vmModule = {
|
vmModule = {
|
||||||
imports = [
|
imports = [
|
||||||
@ -24,12 +27,18 @@ let
|
|||||||
services.acpid.handlers.power.event = "button/power.*";
|
services.acpid.handlers.power.event = "button/power.*";
|
||||||
services.acpid.handlers.power.action = "poweroff";
|
services.acpid.handlers.power.action = "poweroff";
|
||||||
|
|
||||||
|
# only works on x11
|
||||||
|
services.spice-vdagentd.enable = config.services.xserver.enable;
|
||||||
|
|
||||||
boot.initrd.systemd.enable = true;
|
boot.initrd.systemd.enable = true;
|
||||||
|
|
||||||
# currently needed for system.etc.overlay.enable
|
# currently needed for system.etc.overlay.enable
|
||||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||||
|
|
||||||
boot.initrd.systemd.storePaths = [ pkgs.util-linux pkgs.e2fsprogs ];
|
boot.initrd.systemd.storePaths = [
|
||||||
|
pkgs.util-linux
|
||||||
|
pkgs.e2fsprogs
|
||||||
|
];
|
||||||
boot.initrd.systemd.emergencyAccess = true;
|
boot.initrd.systemd.emergencyAccess = true;
|
||||||
|
|
||||||
# sysusers is faster than nixos's perl scripts
|
# sysusers is faster than nixos's perl scripts
|
||||||
@ -40,50 +49,72 @@ let
|
|||||||
|
|
||||||
boot.initrd.kernelModules = [ "virtiofs" ];
|
boot.initrd.kernelModules = [ "virtiofs" ];
|
||||||
virtualisation.writableStore = false;
|
virtualisation.writableStore = false;
|
||||||
virtualisation.fileSystems = lib.mkForce ({
|
virtualisation.fileSystems = lib.mkForce (
|
||||||
"/nix/store" = {
|
{
|
||||||
device = "nix-store";
|
"/nix/store" = {
|
||||||
options = [ "x-systemd.requires=systemd-modules-load.service" "ro" ];
|
device = "nix-store";
|
||||||
fsType = "virtiofs";
|
options = [
|
||||||
};
|
"x-systemd.requires=systemd-modules-load.service"
|
||||||
|
"ro"
|
||||||
|
];
|
||||||
|
fsType = "virtiofs";
|
||||||
|
};
|
||||||
|
|
||||||
"/" = {
|
"/" = {
|
||||||
device = "/dev/vda";
|
device = "/dev/vda";
|
||||||
fsType = "ext4";
|
fsType = "ext4";
|
||||||
options = [ "defaults" "x-systemd.makefs" "nobarrier" "noatime" "nodiratime" "data=writeback" "discard" ];
|
options = [
|
||||||
};
|
"defaults"
|
||||||
|
"x-systemd.makefs"
|
||||||
|
"nobarrier"
|
||||||
|
"noatime"
|
||||||
|
"nodiratime"
|
||||||
|
"data=writeback"
|
||||||
|
"discard"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
"/vmstate" = {
|
"/vmstate" = {
|
||||||
device = "/dev/vdb";
|
device = "/dev/vdb";
|
||||||
options = [ "x-systemd.makefs" "noatime" "nodiratime" "discard" ];
|
options = [
|
||||||
noCheck = true;
|
"x-systemd.makefs"
|
||||||
fsType = "ext4";
|
"noatime"
|
||||||
};
|
"nodiratime"
|
||||||
|
"discard"
|
||||||
|
];
|
||||||
|
noCheck = true;
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
|
||||||
${config.clanCore.secretsUploadDirectory} = {
|
${config.clanCore.secretsUploadDirectory} = {
|
||||||
device = "secrets";
|
device = "secrets";
|
||||||
fsType = "9p";
|
fsType = "9p";
|
||||||
neededForBoot = true;
|
neededForBoot = true;
|
||||||
options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
|
options = [
|
||||||
};
|
"trans=virtio"
|
||||||
|
"version=9p2000.L"
|
||||||
} // lib.listToAttrs (map
|
"cache=loose"
|
||||||
(folder:
|
];
|
||||||
lib.nameValuePair folder {
|
};
|
||||||
device = "/vmstate${folder}";
|
}
|
||||||
fsType = "none";
|
// lib.listToAttrs (
|
||||||
options = [ "bind" ];
|
map (
|
||||||
})
|
folder:
|
||||||
stateFolders));
|
lib.nameValuePair folder {
|
||||||
|
device = "/vmstate${folder}";
|
||||||
|
fsType = "none";
|
||||||
|
options = [ "bind" ];
|
||||||
|
}
|
||||||
|
) stateFolders
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
# We cannot simply merge the VM config into the current system config, because
|
# We cannot simply merge the VM config into the current system config, because
|
||||||
# it is not necessarily a VM.
|
# it is not necessarily a VM.
|
||||||
# Instead we use extendModules to create a second instance of the current
|
# Instead we use extendModules to create a second instance of the current
|
||||||
# system configuration, and then merge the VM config into that.
|
# system configuration, and then merge the VM config into that.
|
||||||
vmConfig = extendModules {
|
vmConfig = extendModules { modules = [ vmModule ]; };
|
||||||
modules = [ vmModule ];
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
@ -207,12 +238,14 @@ in
|
|||||||
};
|
};
|
||||||
# for clan vm create
|
# for clan vm create
|
||||||
system.clan.vm = {
|
system.clan.vm = {
|
||||||
create = pkgs.writeText "vm.json" (builtins.toJSON {
|
create = pkgs.writeText "vm.json" (
|
||||||
initrd = "${vmConfig.config.system.build.initialRamdisk}/${vmConfig.config.system.boot.loader.initrdFile}";
|
builtins.toJSON {
|
||||||
toplevel = vmConfig.config.system.build.toplevel;
|
initrd = "${vmConfig.config.system.build.initialRamdisk}/${vmConfig.config.system.boot.loader.initrdFile}";
|
||||||
regInfo = (pkgs.closureInfo { rootPaths = vmConfig.config.virtualisation.additionalPaths; });
|
toplevel = vmConfig.config.system.build.toplevel;
|
||||||
inherit (config.clan.virtualisation) memorySize cores graphics;
|
regInfo = (pkgs.closureInfo { rootPaths = vmConfig.config.virtualisation.additionalPaths; });
|
||||||
});
|
inherit (config.clan.virtualisation) memorySize cores graphics;
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
virtualisation = lib.optionalAttrs (options.virtualisation ? cores) {
|
virtualisation = lib.optionalAttrs (options.virtualisation ? cores) {
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
{ pkgs, config, lib, ... }:
|
{
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
# maybe upstream this?
|
# maybe upstream this?
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.clan.networking.zerotier;
|
cfg = config.clan.networking.zerotier;
|
||||||
facts = config.clanCore.secrets.zerotier.facts or { };
|
facts = config.clanCore.secrets.zerotier.facts or { };
|
||||||
@ -76,16 +81,18 @@ in
|
|||||||
};
|
};
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
description = lib.mdDoc "override the network config in /var/lib/zerotier/bla/$network.json";
|
description = lib.mdDoc "override the network config in /var/lib/zerotier/bla/$network.json";
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule { freeformType = (pkgs.formats.json { }).type; };
|
||||||
freeformType = (pkgs.formats.json { }).type;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
({
|
({
|
||||||
# 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 (pkgs.zerotierone.overrideAttrs (_old: { meta = { }; }));
|
services.zerotierone.package = lib.mkDefault (
|
||||||
|
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;
|
||||||
@ -104,29 +111,33 @@ in
|
|||||||
|
|
||||||
systemd.services.zerotierone.serviceConfig.ExecStartPre = [
|
systemd.services.zerotierone.serviceConfig.ExecStartPre = [
|
||||||
"+${pkgs.writeShellScript "init-zerotier" ''
|
"+${pkgs.writeShellScript "init-zerotier" ''
|
||||||
cp ${config.clanCore.secrets.zerotier.secrets.zerotier-identity-secret.path} /var/lib/zerotier-one/identity.secret
|
cp ${config.clanCore.secrets.zerotier.secrets.zerotier-identity-secret.path} /var/lib/zerotier-one/identity.secret
|
||||||
zerotier-idtool getpublic /var/lib/zerotier-one/identity.secret > /var/lib/zerotier-one/identity.public
|
zerotier-idtool getpublic /var/lib/zerotier-one/identity.secret > /var/lib/zerotier-one/identity.public
|
||||||
|
|
||||||
${lib.optionalString (cfg.controller.enable) ''
|
${lib.optionalString (cfg.controller.enable) ''
|
||||||
mkdir -p /var/lib/zerotier-one/controller.d/network
|
mkdir -p /var/lib/zerotier-one/controller.d/network
|
||||||
ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON cfg.settings)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json
|
ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON cfg.settings)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json
|
||||||
''}
|
''}
|
||||||
${lib.optionalString (cfg.moon.stableEndpoints != []) ''
|
${lib.optionalString (cfg.moon.stableEndpoints != [ ]) ''
|
||||||
if [[ ! -f /var/lib/zerotier-one/moon.json ]]; then
|
if [[ ! -f /var/lib/zerotier-one/moon.json ]]; then
|
||||||
zerotier-idtool initmoon /var/lib/zerotier-one/identity.public > /var/lib/zerotier-one/moon.json
|
zerotier-idtool initmoon /var/lib/zerotier-one/identity.public > /var/lib/zerotier-one/moon.json
|
||||||
fi
|
fi
|
||||||
${genMoonScript}/bin/genmoon /var/lib/zerotier-one/moon.json ${builtins.toFile "moon.json" (builtins.toJSON cfg.moon.stableEndpoints)} /var/lib/zerotier-one/moons.d
|
${genMoonScript}/bin/genmoon /var/lib/zerotier-one/moon.json ${builtins.toFile "moon.json" (builtins.toJSON cfg.moon.stableEndpoints)} /var/lib/zerotier-one/moons.d
|
||||||
''}
|
''}
|
||||||
|
|
||||||
# cleanup old networks
|
# cleanup old networks
|
||||||
if [[ -d /var/lib/zerotier-one/networks.d ]]; then
|
if [[ -d /var/lib/zerotier-one/networks.d ]]; then
|
||||||
find /var/lib/zerotier-one/networks.d \
|
find /var/lib/zerotier-one/networks.d \
|
||||||
-type f \
|
-type f \
|
||||||
-name "*.conf" \
|
-name "*.conf" \
|
||||||
-not \( ${lib.concatMapStringsSep " -o " (netId: ''-name "${netId}.conf"'') config.services.zerotierone.joinNetworks} \) \
|
-not \( ${
|
||||||
-delete
|
lib.concatMapStringsSep " -o " (
|
||||||
fi
|
netId: ''-name "${netId}.conf"''
|
||||||
''}"
|
) config.services.zerotierone.joinNetworks
|
||||||
|
} \) \
|
||||||
|
-delete
|
||||||
|
fi
|
||||||
|
''}"
|
||||||
];
|
];
|
||||||
systemd.services.zerotierone.serviceConfig.ExecStartPost = [
|
systemd.services.zerotierone.serviceConfig.ExecStartPost = [
|
||||||
"+${pkgs.writeShellScript "configure-interface" ''
|
"+${pkgs.writeShellScript "configure-interface" ''
|
||||||
@ -145,7 +156,7 @@ in
|
|||||||
${lib.concatMapStringsSep "\n" (moon: ''
|
${lib.concatMapStringsSep "\n" (moon: ''
|
||||||
zerotier-cli orbit ${moon} ${moon}
|
zerotier-cli orbit ${moon} ${moon}
|
||||||
'') cfg.moon.orbitMoons}
|
'') cfg.moon.orbitMoons}
|
||||||
''}"
|
''}"
|
||||||
];
|
];
|
||||||
|
|
||||||
networking.firewall.interfaces."zt+".allowedTCPPorts = [ 5353 ]; # mdns
|
networking.firewall.interfaces."zt+".allowedTCPPorts = [ 5353 ]; # mdns
|
||||||
@ -172,7 +183,11 @@ in
|
|||||||
facts.zerotier-ip = { };
|
facts.zerotier-ip = { };
|
||||||
facts.zerotier-network-id = { };
|
facts.zerotier-network-id = { };
|
||||||
secrets.zerotier-identity-secret = { };
|
secrets.zerotier-identity-secret = { };
|
||||||
generator.path = [ config.services.zerotierone.package pkgs.fakeroot pkgs.python3 ];
|
generator.path = [
|
||||||
|
config.services.zerotierone.package
|
||||||
|
pkgs.fakeroot
|
||||||
|
pkgs.python3
|
||||||
|
];
|
||||||
generator.script = ''
|
generator.script = ''
|
||||||
python3 ${./generate.py} --mode network \
|
python3 ${./generate.py} --mode network \
|
||||||
--ip "$facts/zerotier-ip" \
|
--ip "$facts/zerotier-ip" \
|
||||||
@ -188,7 +203,10 @@ in
|
|||||||
clanCore.secrets.zerotier = {
|
clanCore.secrets.zerotier = {
|
||||||
facts.zerotier-ip = { };
|
facts.zerotier-ip = { };
|
||||||
secrets.zerotier-identity-secret = { };
|
secrets.zerotier-identity-secret = { };
|
||||||
generator.path = [ config.services.zerotierone.package pkgs.python3 ];
|
generator.path = [
|
||||||
|
config.services.zerotierone.package
|
||||||
|
pkgs.python3
|
||||||
|
];
|
||||||
generator.script = ''
|
generator.script = ''
|
||||||
python3 ${./generate.py} --mode identity \
|
python3 ${./generate.py} --mode identity \
|
||||||
--ip "$facts/zerotier-ip" \
|
--ip "$facts/zerotier-ip" \
|
||||||
@ -200,9 +218,7 @@ in
|
|||||||
(lib.mkIf (cfg.controller.enable && (facts.zerotier-network-id.value or null) != null) {
|
(lib.mkIf (cfg.controller.enable && (facts.zerotier-network-id.value or null) != null) {
|
||||||
clan.networking.zerotier.networkId = facts.zerotier-network-id.value;
|
clan.networking.zerotier.networkId = facts.zerotier-network-id.value;
|
||||||
clan.networking.zerotier.settings = {
|
clan.networking.zerotier.settings = {
|
||||||
authTokens = [
|
authTokens = [ null ];
|
||||||
null
|
|
||||||
];
|
|
||||||
authorizationEndpoint = "";
|
authorizationEndpoint = "";
|
||||||
capabilities = [ ];
|
capabilities = [ ];
|
||||||
clientId = "";
|
clientId = "";
|
||||||
@ -242,7 +258,9 @@ in
|
|||||||
environment.etc."zerotier/network-id".text = facts.zerotier-network-id.value;
|
environment.etc."zerotier/network-id".text = facts.zerotier-network-id.value;
|
||||||
systemd.services.zerotierone.serviceConfig.ExecStartPost = [
|
systemd.services.zerotierone.serviceConfig.ExecStartPost = [
|
||||||
"+${pkgs.writeShellScript "whitelist-controller" ''
|
"+${pkgs.writeShellScript "whitelist-controller" ''
|
||||||
${config.clanCore.clanPkgs.zerotier-members}/bin/zerotier-members allow ${builtins.substring 0 10 cfg.networkId}
|
${config.clanCore.clanPkgs.zerotier-members}/bin/zerotier-members allow ${
|
||||||
|
builtins.substring 0 10 cfg.networkId
|
||||||
|
}
|
||||||
''}"
|
''}"
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
{ inputs, self, ... }: {
|
{ inputs, self, ... }:
|
||||||
|
{
|
||||||
flake.nixosModules = {
|
flake.nixosModules = {
|
||||||
hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ];
|
hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ];
|
||||||
installer.imports = [ ./installer ];
|
installer.imports = [
|
||||||
|
./installer
|
||||||
|
self.nixosModules.hidden-ssh-announce
|
||||||
|
inputs.disko.nixosModules.disko
|
||||||
|
];
|
||||||
clanCore.imports = [
|
clanCore.imports = [
|
||||||
inputs.sops-nix.nixosModules.sops
|
inputs.sops-nix.nixosModules.sops
|
||||||
./clanCore
|
./clanCore
|
||||||
./iso
|
./iso
|
||||||
({ pkgs, lib, ... }: {
|
(
|
||||||
clanCore.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system};
|
{ pkgs, lib, ... }:
|
||||||
})
|
{
|
||||||
|
clanCore.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system};
|
||||||
|
}
|
||||||
|
)
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
{ config
|
{
|
||||||
, lib
|
config,
|
||||||
, pkgs
|
lib,
|
||||||
, ...
|
pkgs,
|
||||||
}: {
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
options.hidden-ssh-announce = {
|
options.hidden-ssh-announce = {
|
||||||
enable = lib.mkEnableOption "hidden-ssh-announce";
|
enable = lib.mkEnableOption "hidden-ssh-announce";
|
||||||
script = lib.mkOption {
|
script = lib.mkOption {
|
||||||
@ -32,8 +34,14 @@
|
|||||||
};
|
};
|
||||||
systemd.services.hidden-ssh-announce = {
|
systemd.services.hidden-ssh-announce = {
|
||||||
description = "announce hidden ssh";
|
description = "announce hidden ssh";
|
||||||
after = [ "tor.service" "network-online.target" ];
|
after = [
|
||||||
wants = [ "tor.service" "network-online.target" ];
|
"tor.service"
|
||||||
|
"network-online.target"
|
||||||
|
];
|
||||||
|
wants = [
|
||||||
|
"tor.service"
|
||||||
|
"network-online.target"
|
||||||
|
];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
# ${pkgs.tor}/bin/torify
|
# ${pkgs.tor}/bin/torify
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{ lib
|
{
|
||||||
, pkgs
|
lib,
|
||||||
, modulesPath
|
pkgs,
|
||||||
, ...
|
modulesPath,
|
||||||
}: {
|
...
|
||||||
systemd.tmpfiles.rules = [
|
}:
|
||||||
"d /var/shared 0777 root root - -"
|
{
|
||||||
];
|
systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ];
|
||||||
imports = [
|
imports = [
|
||||||
(modulesPath + "/profiles/installation-device.nix")
|
(modulesPath + "/profiles/installation-device.nix")
|
||||||
(modulesPath + "/profiles/all-hardware.nix")
|
(modulesPath + "/profiles/all-hardware.nix")
|
||||||
@ -21,7 +21,17 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
script = pkgs.writeShellScript "write-hostname" ''
|
script = pkgs.writeShellScript "write-hostname" ''
|
||||||
set -efu
|
set -efu
|
||||||
export PATH=${lib.makeBinPath (with pkgs; [ iproute2 coreutils jq qrencode ])}
|
export PATH=${
|
||||||
|
lib.makeBinPath (
|
||||||
|
with pkgs;
|
||||||
|
[
|
||||||
|
iproute2
|
||||||
|
coreutils
|
||||||
|
jq
|
||||||
|
qrencode
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
mkdir -p /var/shared
|
mkdir -p /var/shared
|
||||||
echo "$1" > /var/shared/onion-hostname
|
echo "$1" > /var/shared/onion-hostname
|
||||||
@ -37,7 +47,7 @@
|
|||||||
};
|
};
|
||||||
services.getty.autologinUser = lib.mkForce "root";
|
services.getty.autologinUser = lib.mkForce "root";
|
||||||
programs.bash.interactiveShellInit = ''
|
programs.bash.interactiveShellInit = ''
|
||||||
if [ "$(tty)" = "/dev/tty1" ]; then
|
if [[ "$(tty)" =~ /dev/(tty1|hvc0|ttyS0)$ ]]; then
|
||||||
echo -n 'waiting for tor to generate the hidden service'
|
echo -n 'waiting for tor to generate the hidden service'
|
||||||
until test -e /var/shared/qrcode.utf8; do echo -n .; sleep 1; done
|
until test -e /var/shared/qrcode.utf8; do echo -n .; sleep 1; done
|
||||||
echo
|
echo
|
||||||
@ -48,8 +58,13 @@
|
|||||||
cat /var/shared/qrcode.utf8
|
cat /var/shared/qrcode.utf8
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
boot.loader.grub.efiInstallAsRemovable = true;
|
|
||||||
boot.loader.grub.efiSupport = true;
|
boot.loader.systemd-boot.enable = true;
|
||||||
|
|
||||||
|
# Grub doesn't find devices for both BIOS and UEFI?
|
||||||
|
|
||||||
|
#boot.loader.grub.efiInstallAsRemovable = true;
|
||||||
|
#boot.loader.grub.efiSupport = true;
|
||||||
disko.devices = {
|
disko.devices = {
|
||||||
disk = {
|
disk = {
|
||||||
stick = {
|
stick = {
|
||||||
@ -59,10 +74,10 @@
|
|||||||
content = {
|
content = {
|
||||||
type = "gpt";
|
type = "gpt";
|
||||||
partitions = {
|
partitions = {
|
||||||
boot = {
|
#boot = {
|
||||||
size = "1M";
|
# size = "1M";
|
||||||
type = "EF02"; # for grub MBR
|
# type = "EF02"; # for grub MBR
|
||||||
};
|
#};
|
||||||
ESP = {
|
ESP = {
|
||||||
size = "100M";
|
size = "100M";
|
||||||
type = "EF00";
|
type = "EF00";
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
{ config, extendModules, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
extendModules,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
# Generates a fileSystems entry for bind mounting a given state folder path
|
# Generates a fileSystems entry for bind mounting a given state folder path
|
||||||
# It binds directories from /var/clanstate/{some-path} to /{some-path}.
|
# It binds directories from /var/clanstate/{some-path} to /{some-path}.
|
||||||
@ -13,54 +19,47 @@ let
|
|||||||
};
|
};
|
||||||
|
|
||||||
# Flatten the list of state folders into a single list
|
# Flatten the list of state folders into a single list
|
||||||
stateFolders = lib.flatten (
|
stateFolders = lib.flatten (lib.mapAttrsToList (_item: attrs: attrs.folders) config.clanCore.state);
|
||||||
lib.mapAttrsToList
|
|
||||||
(_item: attrs: attrs.folders)
|
|
||||||
config.clanCore.state
|
|
||||||
);
|
|
||||||
|
|
||||||
# A module setting up bind mounts for all state folders
|
# A module setting up bind mounts for all state folders
|
||||||
stateMounts = {
|
stateMounts = {
|
||||||
fileSystems =
|
fileSystems = lib.listToAttrs (map mkBindMount stateFolders);
|
||||||
lib.listToAttrs
|
|
||||||
(map mkBindMount stateFolders);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
isoModule = { config, ... }: {
|
isoModule =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
stateMounts
|
{
|
||||||
];
|
imports = [ stateMounts ];
|
||||||
options.clan.iso.disko = lib.mkOption {
|
options.clan.iso.disko = lib.mkOption {
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule { freeformType = (pkgs.formats.json { }).type; };
|
||||||
freeformType = (pkgs.formats.json { }).type;
|
default = {
|
||||||
};
|
disk = {
|
||||||
default = {
|
iso = {
|
||||||
disk = {
|
type = "disk";
|
||||||
iso = {
|
imageSize = "10G"; # TODO add auto image size in disko
|
||||||
type = "disk";
|
content = {
|
||||||
imageSize = "10G"; # TODO add auto image size in disko
|
type = "gpt";
|
||||||
content = {
|
partitions = {
|
||||||
type = "gpt";
|
boot = {
|
||||||
partitions = {
|
size = "1M";
|
||||||
boot = {
|
type = "EF02"; # for grub MBR
|
||||||
size = "1M";
|
|
||||||
type = "EF02"; # for grub MBR
|
|
||||||
};
|
|
||||||
ESP = {
|
|
||||||
size = "100M";
|
|
||||||
type = "EF00";
|
|
||||||
content = {
|
|
||||||
type = "filesystem";
|
|
||||||
format = "vfat";
|
|
||||||
mountpoint = "/boot";
|
|
||||||
};
|
};
|
||||||
};
|
ESP = {
|
||||||
root = {
|
size = "100M";
|
||||||
size = "100%";
|
type = "EF00";
|
||||||
content = {
|
content = {
|
||||||
type = "filesystem";
|
type = "filesystem";
|
||||||
format = "ext4";
|
format = "vfat";
|
||||||
mountpoint = "/";
|
mountpoint = "/boot";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -68,19 +67,16 @@ let
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
config = {
|
||||||
|
disko.devices = lib.mkOverride 51 config.clan.iso.disko;
|
||||||
|
boot.loader.grub.enable = true;
|
||||||
|
boot.loader.grub.efiSupport = true;
|
||||||
|
boot.loader.grub.device = lib.mkForce "/dev/vda";
|
||||||
|
boot.loader.grub.efiInstallAsRemovable = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
config = {
|
|
||||||
disko.devices = lib.mkOverride 51 config.clan.iso.disko;
|
|
||||||
boot.loader.grub.enable = true;
|
|
||||||
boot.loader.grub.efiSupport = true;
|
|
||||||
boot.loader.grub.device = lib.mkForce "/dev/vda";
|
|
||||||
boot.loader.grub.efiInstallAsRemovable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
isoConfig = extendModules {
|
isoConfig = extendModules { modules = [ isoModule ]; };
|
||||||
modules = [ isoModule ];
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = {
|
config = {
|
||||||
|
@ -3,167 +3,171 @@ import dataclasses
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum, member
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Self
|
from typing import Any
|
||||||
|
|
||||||
from .errors import ClanError
|
from .errors import ClanError
|
||||||
|
|
||||||
|
|
||||||
# Define an enum with different members that have different values
|
@dataclass
|
||||||
class ClanScheme(Enum):
|
class FlakeId:
|
||||||
# Use the dataclass decorator to add fields and methods to the members
|
_value: str | Path
|
||||||
@member
|
|
||||||
@dataclass
|
|
||||||
class REMOTE:
|
|
||||||
url: str # The url field holds the HTTP URL
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"REMOTE({self.url})" # The __str__ method returns a custom string representation
|
return f"{self._value}" # The __str__ method returns a custom string representation
|
||||||
|
|
||||||
@member
|
@property
|
||||||
@dataclass
|
def path(self) -> Path:
|
||||||
class LOCAL:
|
assert isinstance(self._value, Path)
|
||||||
path: Path # The path field holds the local path
|
return self._value
|
||||||
|
|
||||||
def __str__(self) -> str:
|
@property
|
||||||
return f"LOCAL({self.path})" # The __str__ method returns a custom string representation
|
def url(self) -> str:
|
||||||
|
assert isinstance(self._value, str)
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"ClanUrl({self._value})"
|
||||||
|
|
||||||
|
def is_local(self) -> bool:
|
||||||
|
return isinstance(self._value, Path)
|
||||||
|
|
||||||
|
def is_remote(self) -> bool:
|
||||||
|
return isinstance(self._value, str)
|
||||||
|
|
||||||
|
|
||||||
# Parameters defined here will be DELETED from the nested uri
|
# Parameters defined here will be DELETED from the nested uri
|
||||||
# so make sure there are no conflicts with other webservices
|
# so make sure there are no conflicts with other webservices
|
||||||
@dataclass
|
@dataclass
|
||||||
class ClanParameters:
|
class MachineParams:
|
||||||
flake_attr: str = "defaultVM"
|
dummy_opt: str = "dummy"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MachineData:
|
||||||
|
flake_id: FlakeId
|
||||||
|
name: str = "defaultVM"
|
||||||
|
params: MachineParams = dataclasses.field(default_factory=MachineParams)
|
||||||
|
|
||||||
|
def get_id(self) -> str:
|
||||||
|
return f"{self.flake_id}#{self.name}"
|
||||||
|
|
||||||
|
|
||||||
# Define the ClanURI class
|
# Define the ClanURI class
|
||||||
class ClanURI:
|
class ClanURI:
|
||||||
|
_orig_uri: str
|
||||||
|
_components: urllib.parse.ParseResult
|
||||||
|
flake_id: FlakeId
|
||||||
|
_machines: list[MachineData]
|
||||||
|
|
||||||
# Initialize the class with a clan:// URI
|
# Initialize the class with a clan:// URI
|
||||||
def __init__(self, uri: str) -> None:
|
def __init__(self, uri: str) -> None:
|
||||||
|
self._machines = []
|
||||||
|
|
||||||
# users might copy whitespace along with the uri
|
# users might copy whitespace along with the uri
|
||||||
uri = uri.strip()
|
uri = uri.strip()
|
||||||
self._full_uri = uri
|
self._orig_uri = uri
|
||||||
|
|
||||||
# Check if the URI starts with clan://
|
# Check if the URI starts with clan://
|
||||||
# If it does, remove the clan:// prefix
|
# If it does, remove the clan:// prefix
|
||||||
if uri.startswith("clan://"):
|
if uri.startswith("clan://"):
|
||||||
self._nested_uri = uri[7:]
|
nested_uri = uri[7:]
|
||||||
else:
|
else:
|
||||||
raise ClanError(f"Invalid scheme: expected clan://, got {uri}")
|
raise ClanError(f"Invalid uri: expected clan://, got {uri}")
|
||||||
|
|
||||||
# Parse the URI into components
|
# Parse the URI into components
|
||||||
# scheme://netloc/path;parameters?query#fragment
|
# url://netloc/path;parameters?query#fragment
|
||||||
self._components = urllib.parse.urlparse(self._nested_uri)
|
self._components = urllib.parse.urlparse(nested_uri)
|
||||||
|
|
||||||
# Parse the query string into a dictionary
|
# Replace the query string in the components with the new query string
|
||||||
query = urllib.parse.parse_qs(self._components.query)
|
clean_comps = self._components._replace(
|
||||||
|
query=self._components.query, fragment=""
|
||||||
|
)
|
||||||
|
|
||||||
# Create a new dictionary with only the parameters we want
|
# Parse the URL into a ClanUrl object
|
||||||
# example: https://example.com?flake_attr=myVM&password=1234
|
self.flake_id = self._parse_url(clean_comps)
|
||||||
# becomes: https://example.com?password=1234
|
|
||||||
# clan_params = {"flake_attr": "myVM"}
|
# Parse the fragment into a list of machine queries
|
||||||
# query = {"password": ["1234"]}
|
# Then parse every machine query into a MachineParameters object
|
||||||
clan_params: dict[str, str] = {}
|
machine_frags = list(
|
||||||
for field in dataclasses.fields(ClanParameters):
|
filter(lambda x: len(x) > 0, self._components.fragment.split("#"))
|
||||||
if field.name in query:
|
)
|
||||||
values = query[field.name]
|
for machine_frag in machine_frags:
|
||||||
|
machine = self._parse_machine_query(machine_frag)
|
||||||
|
self._machines.append(machine)
|
||||||
|
|
||||||
|
# If there are no machine fragments, add a default machine
|
||||||
|
if len(machine_frags) == 0:
|
||||||
|
default_machine = MachineData(flake_id=self.flake_id)
|
||||||
|
self._machines.append(default_machine)
|
||||||
|
|
||||||
|
def _parse_url(self, comps: urllib.parse.ParseResult) -> FlakeId:
|
||||||
|
comb = (
|
||||||
|
comps.scheme,
|
||||||
|
comps.netloc,
|
||||||
|
comps.path,
|
||||||
|
comps.params,
|
||||||
|
comps.query,
|
||||||
|
comps.fragment,
|
||||||
|
)
|
||||||
|
match comb:
|
||||||
|
case ("file", "", path, "", "", _) | ("", "", path, "", "", _): # type: ignore
|
||||||
|
flake_id = FlakeId(Path(path).expanduser().resolve())
|
||||||
|
case _:
|
||||||
|
flake_id = FlakeId(comps.geturl())
|
||||||
|
|
||||||
|
return flake_id
|
||||||
|
|
||||||
|
def _parse_machine_query(self, machine_frag: str) -> MachineData:
|
||||||
|
comp = urllib.parse.urlparse(machine_frag)
|
||||||
|
query = urllib.parse.parse_qs(comp.query)
|
||||||
|
machine_name = comp.path
|
||||||
|
|
||||||
|
machine_params: dict[str, Any] = {}
|
||||||
|
for dfield in dataclasses.fields(MachineParams):
|
||||||
|
if dfield.name in query:
|
||||||
|
values = query[dfield.name]
|
||||||
if len(values) > 1:
|
if len(values) > 1:
|
||||||
raise ClanError(f"Multiple values for parameter: {field.name}")
|
raise ClanError(f"Multiple values for parameter: {dfield.name}")
|
||||||
clan_params[field.name] = values[0]
|
machine_params[dfield.name] = values[0]
|
||||||
|
|
||||||
# Remove the field from the query dictionary
|
# Remove the field from the query dictionary
|
||||||
# clan uri and nested uri share one namespace for query parameters
|
# clan uri and nested uri share one namespace for query parameters
|
||||||
# we need to make sure there are no conflicts
|
# we need to make sure there are no conflicts
|
||||||
del query[field.name]
|
del query[dfield.name]
|
||||||
# Reencode the query dictionary into a query string
|
params = MachineParams(**machine_params)
|
||||||
real_query = urllib.parse.urlencode(query, doseq=True)
|
machine = MachineData(flake_id=self.flake_id, name=machine_name, params=params)
|
||||||
|
return machine
|
||||||
|
|
||||||
# If the fragment contains a #, use the part after the # as the flake_attr
|
@property
|
||||||
# on multiple #, use the first one
|
def machine(self) -> MachineData:
|
||||||
if self._components.fragment != "":
|
return self._machines[0]
|
||||||
clan_params["flake_attr"] = self._components.fragment.split("#")[0]
|
|
||||||
|
|
||||||
# Replace the query string in the components with the new query string
|
def get_orig_uri(self) -> str:
|
||||||
self._components = self._components._replace(query=real_query, fragment="")
|
return self._orig_uri
|
||||||
|
|
||||||
# Create a ClanParameters object from the clan_params dictionary
|
def get_url(self) -> str:
|
||||||
self.params = ClanParameters(**clan_params)
|
return str(self.flake_id)
|
||||||
|
|
||||||
comb = (
|
|
||||||
self._components.scheme,
|
|
||||||
self._components.netloc,
|
|
||||||
self._components.path,
|
|
||||||
self._components.params,
|
|
||||||
self._components.query,
|
|
||||||
self._components.fragment,
|
|
||||||
)
|
|
||||||
match comb:
|
|
||||||
case ("file", "", path, "", "", "") | ("", "", path, "", "", _): # type: ignore
|
|
||||||
self.scheme = ClanScheme.LOCAL.value(Path(path).expanduser().resolve()) # type: ignore
|
|
||||||
case _:
|
|
||||||
self.scheme = ClanScheme.REMOTE.value(self._components.geturl()) # type: ignore
|
|
||||||
|
|
||||||
def get_internal(self) -> str:
|
|
||||||
match self.scheme:
|
|
||||||
case ClanScheme.LOCAL.value(path):
|
|
||||||
return str(path)
|
|
||||||
case ClanScheme.REMOTE.value(url):
|
|
||||||
return url
|
|
||||||
case _:
|
|
||||||
raise ClanError(f"Unsupported uri components: {self.scheme}")
|
|
||||||
|
|
||||||
def get_full_uri(self) -> str:
|
|
||||||
return self._full_uri
|
|
||||||
|
|
||||||
def get_id(self) -> str:
|
|
||||||
return f"{self.get_internal()}#{self.params.flake_attr}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_path(
|
|
||||||
cls, # noqa
|
|
||||||
path: Path,
|
|
||||||
flake_attr: str | None = None,
|
|
||||||
params: dict[str, Any] | ClanParameters | None = None,
|
|
||||||
) -> Self:
|
|
||||||
return cls.from_str(str(path), flake_attr=flake_attr, params=params)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_str(
|
def from_str(
|
||||||
cls, # noqa
|
cls, # noqa
|
||||||
url: str,
|
url: str,
|
||||||
flake_attr: str | None = None,
|
machine_name: str | None = None,
|
||||||
params: dict[str, Any] | ClanParameters | None = None,
|
) -> "ClanURI":
|
||||||
) -> Self:
|
clan_uri = ""
|
||||||
if flake_attr is not None and params is not None:
|
if not url.startswith("clan://"):
|
||||||
raise ClanError("flake_attr and params are mutually exclusive")
|
clan_uri += "clan://"
|
||||||
|
|
||||||
prefix = "clan://"
|
clan_uri += url
|
||||||
if url.startswith(prefix):
|
|
||||||
url = url[len(prefix) :]
|
|
||||||
|
|
||||||
if params is None and flake_attr is None:
|
if machine_name:
|
||||||
return cls(f"clan://{url}")
|
clan_uri += f"#{machine_name}"
|
||||||
|
|
||||||
comp = urllib.parse.urlparse(url)
|
return cls(clan_uri)
|
||||||
query = urllib.parse.parse_qs(comp.query)
|
|
||||||
|
|
||||||
if isinstance(params, dict):
|
|
||||||
query.update(params)
|
|
||||||
elif isinstance(params, ClanParameters):
|
|
||||||
query.update(params.__dict__)
|
|
||||||
elif flake_attr is not None:
|
|
||||||
query["flake_attr"] = [flake_attr]
|
|
||||||
else:
|
|
||||||
raise ClanError(f"Unsupported params type: {type(params)}")
|
|
||||||
|
|
||||||
new_query = urllib.parse.urlencode(query, doseq=True)
|
|
||||||
comp = comp._replace(query=new_query)
|
|
||||||
new_url = urllib.parse.urlunparse(comp)
|
|
||||||
return cls(f"clan://{new_url}")
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.get_full_uri()
|
return self.get_orig_uri()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"ClanURI({self.get_full_uri()})"
|
return f"ClanURI({self})"
|
||||||
|
@ -4,7 +4,8 @@ import select
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
import weakref
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO, Any
|
from typing import IO, Any
|
||||||
@ -58,6 +59,45 @@ def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]:
|
|||||||
return stdout_buf.decode("utf-8"), stderr_buf.decode("utf-8")
|
return stdout_buf.decode("utf-8"), stderr_buf.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class TimeTable:
|
||||||
|
"""
|
||||||
|
This class is used to store the time taken by each command
|
||||||
|
and print it at the end of the program if env PERF=1 is set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.table: dict[str, timedelta] = {}
|
||||||
|
weakref.finalize(self, self.table_print)
|
||||||
|
|
||||||
|
def table_print(self) -> None:
|
||||||
|
if os.getenv("PERF") != "1":
|
||||||
|
return
|
||||||
|
print("======== CMD TIMETABLE ========")
|
||||||
|
|
||||||
|
# Sort the table by time in descending order
|
||||||
|
sorted_table = sorted(
|
||||||
|
self.table.items(), key=lambda item: item[1], reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for k, v in sorted_table:
|
||||||
|
# Check if timedelta is greater than 1 second
|
||||||
|
if v.total_seconds() > 1:
|
||||||
|
# Print in red
|
||||||
|
print(f"\033[91mTook {v}s\033[0m for command: '{k}'")
|
||||||
|
else:
|
||||||
|
# Print in default color
|
||||||
|
print(f"Took {v} for command: '{k}'")
|
||||||
|
|
||||||
|
def add(self, cmd: str, time: timedelta) -> None:
|
||||||
|
if cmd in self.table:
|
||||||
|
self.table[cmd] += time
|
||||||
|
else:
|
||||||
|
self.table[cmd] = time
|
||||||
|
|
||||||
|
|
||||||
|
TIME_TABLE = TimeTable()
|
||||||
|
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
*,
|
*,
|
||||||
@ -83,7 +123,8 @@ def run(
|
|||||||
rc = process.wait()
|
rc = process.wait()
|
||||||
tend = datetime.now()
|
tend = datetime.now()
|
||||||
|
|
||||||
glog.debug(f"Command took {tend - tstart}s to run")
|
global TIME_TABLE
|
||||||
|
TIME_TABLE.add(shlex.join(cmd), tend - tstart)
|
||||||
|
|
||||||
# Wait for the subprocess to finish
|
# Wait for the subprocess to finish
|
||||||
cmd_out = CmdOut(
|
cmd_out = CmdOut(
|
||||||
|
@ -86,7 +86,6 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
|
|||||||
|
|
||||||
# Get the flake metadata
|
# Get the flake metadata
|
||||||
meta = nix_metadata(flake_url)
|
meta = nix_metadata(flake_url)
|
||||||
|
|
||||||
return FlakeConfig(
|
return FlakeConfig(
|
||||||
vm=vm,
|
vm=vm,
|
||||||
flake_url=flake_url,
|
flake_url=flake_url,
|
||||||
|
@ -1,51 +1,110 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .cmd import Log, run
|
||||||
|
from .errors import ClanError
|
||||||
from .machines.machines import Machine
|
from .machines.machines import Machine
|
||||||
from .secrets.generate import generate_secrets
|
from .nix import nix_shell
|
||||||
|
from .secrets.modules import SecretStoreBase
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def flash_machine(machine: Machine, device: str | None = None) -> None:
|
def flash_machine(
|
||||||
|
machine: Machine, disks: dict[str, str], dry_run: bool, debug: bool
|
||||||
|
) -> None:
|
||||||
secrets_module = importlib.import_module(machine.secrets_module)
|
secrets_module = importlib.import_module(machine.secrets_module)
|
||||||
secret_store = secrets_module.SecretStore(machine=machine)
|
secret_store: SecretStoreBase = secrets_module.SecretStore(machine=machine)
|
||||||
|
|
||||||
generate_secrets(machine)
|
|
||||||
|
|
||||||
with TemporaryDirectory() as tmpdir_:
|
with TemporaryDirectory() as tmpdir_:
|
||||||
tmpdir = Path(tmpdir_)
|
tmpdir = Path(tmpdir_)
|
||||||
upload_dir_ = machine.secrets_upload_directory
|
upload_dir = machine.secrets_upload_directory
|
||||||
|
|
||||||
if upload_dir_.startswith("/"):
|
if upload_dir.startswith("/"):
|
||||||
upload_dir_ = upload_dir_[1:]
|
local_dir = tmpdir / upload_dir[1:]
|
||||||
upload_dir = tmpdir / upload_dir_
|
else:
|
||||||
upload_dir.mkdir(parents=True)
|
local_dir = tmpdir / upload_dir
|
||||||
secret_store.upload(upload_dir)
|
|
||||||
|
|
||||||
fs_image = machine.build_nix("config.system.clan.iso")
|
local_dir.mkdir(parents=True)
|
||||||
print(fs_image)
|
secret_store.upload(local_dir)
|
||||||
|
disko_install = []
|
||||||
|
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
if shutil.which("sudo") is None:
|
||||||
|
raise ClanError(
|
||||||
|
"sudo is required to run disko-install as a non-root user"
|
||||||
|
)
|
||||||
|
disko_install.append("sudo")
|
||||||
|
|
||||||
|
disko_install.append("disko-install")
|
||||||
|
if dry_run:
|
||||||
|
disko_install.append("--dry-run")
|
||||||
|
if debug:
|
||||||
|
disko_install.append("--debug")
|
||||||
|
for name, device in disks.items():
|
||||||
|
disko_install.extend(["--disk", name, device])
|
||||||
|
|
||||||
|
disko_install.extend(["--extra-files", str(local_dir), upload_dir])
|
||||||
|
disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name])
|
||||||
|
|
||||||
|
cmd = nix_shell(
|
||||||
|
["nixpkgs#disko"],
|
||||||
|
disko_install,
|
||||||
|
)
|
||||||
|
print("$", " ".join(map(shlex.quote, cmd)))
|
||||||
|
run(cmd, log=Log.BOTH, error_msg=f"Failed to flash {machine}")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FlashOptions:
|
class FlashOptions:
|
||||||
flake: Path
|
flake: Path
|
||||||
machine: str
|
machine: str
|
||||||
device: str | None
|
disks: dict[str, str]
|
||||||
|
dry_run: bool
|
||||||
|
confirm: bool
|
||||||
|
debug: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AppendDiskAction(argparse.Action):
|
||||||
|
def __init__(self, option_strings: str, dest: str, **kwargs: Any) -> None:
|
||||||
|
super().__init__(option_strings, dest, **kwargs)
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
parser: argparse.ArgumentParser,
|
||||||
|
namespace: argparse.Namespace,
|
||||||
|
values: str | Sequence[str] | None,
|
||||||
|
option_string: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
disks = getattr(namespace, self.dest)
|
||||||
|
assert isinstance(values, list), "values must be a list"
|
||||||
|
disks[values[0]] = values[1]
|
||||||
|
|
||||||
|
|
||||||
def flash_command(args: argparse.Namespace) -> None:
|
def flash_command(args: argparse.Namespace) -> None:
|
||||||
opts = FlashOptions(
|
opts = FlashOptions(
|
||||||
flake=args.flake,
|
flake=args.flake,
|
||||||
machine=args.machine,
|
machine=args.machine,
|
||||||
device=args.device,
|
disks=args.disk,
|
||||||
|
dry_run=args.dry_run,
|
||||||
|
confirm=not args.yes,
|
||||||
|
debug=args.debug,
|
||||||
)
|
)
|
||||||
machine = Machine(opts.machine, flake=opts.flake)
|
machine = Machine(opts.machine, flake=opts.flake)
|
||||||
flash_machine(machine, device=opts.device)
|
if opts.confirm and not opts.dry_run:
|
||||||
|
disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items())
|
||||||
|
ask = input(f"Install {machine.name} to {disk_str}? [y/N] ")
|
||||||
|
if ask != "y":
|
||||||
|
return
|
||||||
|
flash_machine(machine, disks=opts.disks, dry_run=opts.dry_run, debug=opts.debug)
|
||||||
|
|
||||||
|
|
||||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
@ -55,8 +114,30 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
help="machine to install",
|
help="machine to install",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--device",
|
"--disk",
|
||||||
type=str,
|
type=str,
|
||||||
help="device to flash the system to",
|
nargs=2,
|
||||||
|
metavar=("name", "value"),
|
||||||
|
action=AppendDiskAction,
|
||||||
|
help="device to flash to",
|
||||||
|
default={},
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--yes",
|
||||||
|
action="store_true",
|
||||||
|
help="do not ask for confirmation",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
help="Only build the system, don't flash it",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--debug",
|
||||||
|
help="Print debug information",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
)
|
)
|
||||||
parser.set_defaults(func=flash_command)
|
parser.set_defaults(func=flash_command)
|
||||||
|
@ -17,13 +17,6 @@ from ..locked_open import read_history_file, write_history_file
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class EnhancedJSONEncoder(json.JSONEncoder):
|
|
||||||
def default(self, o: Any) -> Any:
|
|
||||||
if dataclasses.is_dataclass(o):
|
|
||||||
return dataclasses.asdict(o)
|
|
||||||
return super().default(o)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class HistoryEntry:
|
class HistoryEntry:
|
||||||
last_used: str
|
last_used: str
|
||||||
@ -79,8 +72,8 @@ 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_internal()):
|
for machine in list_machines(uri.get_url()):
|
||||||
new_entry = _add_maschine_to_history_list(uri.get_internal(), 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)
|
||||||
return new_entries
|
return new_entries
|
||||||
@ -89,9 +82,7 @@ def add_all_to_history(uri: ClanURI) -> list[HistoryEntry]:
|
|||||||
def add_history(uri: ClanURI) -> HistoryEntry:
|
def add_history(uri: ClanURI) -> HistoryEntry:
|
||||||
user_history_file().parent.mkdir(parents=True, exist_ok=True)
|
user_history_file().parent.mkdir(parents=True, exist_ok=True)
|
||||||
history = list_history()
|
history = list_history()
|
||||||
new_entry = _add_maschine_to_history_list(
|
new_entry = _add_maschine_to_history_list(uri.get_url(), uri.machine.name, history)
|
||||||
uri.get_internal(), uri.params.flake_attr, history
|
|
||||||
)
|
|
||||||
write_history_file(history)
|
write_history_file(history)
|
||||||
return new_entry
|
return new_entry
|
||||||
|
|
||||||
@ -121,9 +112,7 @@ def add_history_command(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
# takes a (sub)parser and configures it
|
# takes a (sub)parser and configures it
|
||||||
def register_add_parser(parser: argparse.ArgumentParser) -> None:
|
def register_add_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
parser.add_argument(
|
parser.add_argument("uri", type=ClanURI, help="Path to the flake", default=".")
|
||||||
"uri", type=ClanURI.from_str, help="Path to the flake", default="."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--all", help="Add all machines", default=False, action="store_true"
|
"--all", help="Add all machines", default=False, action="store_true"
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,7 @@ import datetime
|
|||||||
|
|
||||||
from clan_cli.flakes.inspect import inspect_flake
|
from clan_cli.flakes.inspect import inspect_flake
|
||||||
|
|
||||||
from ..clan_uri import ClanParameters, ClanURI
|
from ..clan_uri import ClanURI
|
||||||
from ..errors import ClanCmdError
|
from ..errors import ClanCmdError
|
||||||
from ..locked_open import write_history_file
|
from ..locked_open import write_history_file
|
||||||
from ..nix import nix_metadata
|
from ..nix import nix_metadata
|
||||||
@ -28,9 +28,9 @@ def update_history() -> list[HistoryEntry]:
|
|||||||
)
|
)
|
||||||
uri = ClanURI.from_str(
|
uri = ClanURI.from_str(
|
||||||
url=str(entry.flake.flake_url),
|
url=str(entry.flake.flake_url),
|
||||||
params=ClanParameters(entry.flake.flake_attr),
|
machine_name=entry.flake.flake_attr,
|
||||||
)
|
)
|
||||||
flake = inspect_flake(uri.get_internal(), uri.params.flake_attr)
|
flake = inspect_flake(uri.get_url(), uri.machine.name)
|
||||||
flake.flake_url = str(flake.flake_url)
|
flake.flake_url = str(flake.flake_url)
|
||||||
entry = HistoryEntry(
|
entry = HistoryEntry(
|
||||||
flake=flake, last_used=datetime.datetime.now().isoformat()
|
flake=flake, last_used=datetime.datetime.now().isoformat()
|
||||||
|
15
pkgs/clan-cli/clan_cli/jsonrpc.py
Normal file
15
pkgs/clan-cli/clan_cli/jsonrpc.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class ClanJSONEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o: Any) -> Any:
|
||||||
|
# Check if the object has a to_json method
|
||||||
|
if hasattr(o, "to_json") and callable(o.to_json):
|
||||||
|
return o.to_json()
|
||||||
|
# Check if the object is a dataclass
|
||||||
|
elif dataclasses.is_dataclass(o):
|
||||||
|
return dataclasses.asdict(o)
|
||||||
|
# Otherwise, use the default serialization
|
||||||
|
return super().default(o)
|
@ -1,4 +1,3 @@
|
|||||||
import dataclasses
|
|
||||||
import fcntl
|
import fcntl
|
||||||
import json
|
import json
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
@ -6,16 +5,11 @@ from contextlib import contextmanager
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from clan_cli.jsonrpc import ClanJSONEncoder
|
||||||
|
|
||||||
from .dirs import user_history_file
|
from .dirs import user_history_file
|
||||||
|
|
||||||
|
|
||||||
class EnhancedJSONEncoder(json.JSONEncoder):
|
|
||||||
def default(self, o: Any) -> Any:
|
|
||||||
if dataclasses.is_dataclass(o):
|
|
||||||
return dataclasses.asdict(o)
|
|
||||||
return super().default(o)
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _locked_open(filename: str | Path, mode: str = "r") -> Generator:
|
def _locked_open(filename: str | Path, mode: str = "r") -> Generator:
|
||||||
"""
|
"""
|
||||||
@ -29,7 +23,7 @@ def _locked_open(filename: str | Path, mode: str = "r") -> Generator:
|
|||||||
|
|
||||||
def write_history_file(data: Any) -> None:
|
def write_history_file(data: Any) -> None:
|
||||||
with _locked_open(user_history_file(), "w+") as f:
|
with _locked_open(user_history_file(), "w+") as f:
|
||||||
f.write(json.dumps(data, cls=EnhancedJSONEncoder, indent=4))
|
f.write(json.dumps(data, cls=ClanJSONEncoder, indent=4))
|
||||||
|
|
||||||
|
|
||||||
def read_history_file() -> list[dict]:
|
def read_history_file() -> list[dict]:
|
||||||
|
@ -13,10 +13,11 @@ from ..secrets.generate import generate_secrets
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def install_nixos(machine: Machine, kexec: str | None = None) -> None:
|
def install_nixos(
|
||||||
|
machine: Machine, kexec: str | None = None, debug: bool = False
|
||||||
|
) -> None:
|
||||||
secrets_module = importlib.import_module(machine.secrets_module)
|
secrets_module = importlib.import_module(machine.secrets_module)
|
||||||
log.info(f"installing {machine.name}")
|
log.info(f"installing {machine.name}")
|
||||||
log.info(f"using secret store: {secrets_module.SecretStore}")
|
|
||||||
secret_store = secrets_module.SecretStore(machine=machine)
|
secret_store = secrets_module.SecretStore(machine=machine)
|
||||||
|
|
||||||
h = machine.target_host
|
h = machine.target_host
|
||||||
@ -44,8 +45,12 @@ def install_nixos(machine: Machine, kexec: str | None = None) -> None:
|
|||||||
"--extra-files",
|
"--extra-files",
|
||||||
str(tmpdir),
|
str(tmpdir),
|
||||||
]
|
]
|
||||||
|
if machine.target_host.port:
|
||||||
|
cmd += ["--ssh-port", str(machine.target_host.port)]
|
||||||
if kexec:
|
if kexec:
|
||||||
cmd += ["--kexec", kexec]
|
cmd += ["--kexec", kexec]
|
||||||
|
if debug:
|
||||||
|
cmd.append("--debug")
|
||||||
cmd.append(target_host)
|
cmd.append(target_host)
|
||||||
|
|
||||||
run(
|
run(
|
||||||
@ -63,6 +68,8 @@ class InstallOptions:
|
|||||||
machine: str
|
machine: str
|
||||||
target_host: str
|
target_host: str
|
||||||
kexec: str | None
|
kexec: str | None
|
||||||
|
confirm: bool
|
||||||
|
debug: bool
|
||||||
|
|
||||||
|
|
||||||
def install_command(args: argparse.Namespace) -> None:
|
def install_command(args: argparse.Namespace) -> None:
|
||||||
@ -71,11 +78,18 @@ def install_command(args: argparse.Namespace) -> None:
|
|||||||
machine=args.machine,
|
machine=args.machine,
|
||||||
target_host=args.target_host,
|
target_host=args.target_host,
|
||||||
kexec=args.kexec,
|
kexec=args.kexec,
|
||||||
|
confirm=not args.yes,
|
||||||
|
debug=args.debug,
|
||||||
)
|
)
|
||||||
machine = Machine(opts.machine, flake=opts.flake)
|
machine = Machine(opts.machine, flake=opts.flake)
|
||||||
machine.target_host_address = opts.target_host
|
machine.target_host_address = opts.target_host
|
||||||
|
|
||||||
install_nixos(machine, kexec=opts.kexec)
|
if opts.confirm:
|
||||||
|
ask = input(f"Install {machine.name} to {opts.target_host}? [y/N] ")
|
||||||
|
if ask != "y":
|
||||||
|
return
|
||||||
|
|
||||||
|
install_nixos(machine, kexec=opts.kexec, debug=opts.debug)
|
||||||
|
|
||||||
|
|
||||||
def register_install_parser(parser: argparse.ArgumentParser) -> None:
|
def register_install_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
@ -84,6 +98,18 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
type=str,
|
type=str,
|
||||||
help="use another kexec tarball to bootstrap NixOS",
|
help="use another kexec tarball to bootstrap NixOS",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--yes",
|
||||||
|
action="store_true",
|
||||||
|
help="do not ask for confirmation",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--debug",
|
||||||
|
action="store_true",
|
||||||
|
help="print debug information",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"machine",
|
"machine",
|
||||||
type=str,
|
type=str,
|
||||||
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from clan_cli.clan_uri import ClanURI, MachineData
|
||||||
from clan_cli.dirs import vm_state_dir
|
from clan_cli.dirs import vm_state_dir
|
||||||
from qemu.qmp import QEMUMonitorProtocol
|
from qemu.qmp import QEMUMonitorProtocol
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ from ..ssh import Host, parse_deployment_address
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VMAttr:
|
class QMPWrapper:
|
||||||
def __init__(self, state_dir: Path) -> None:
|
def __init__(self, state_dir: Path) -> None:
|
||||||
# These sockets here are just symlinks to the real sockets which
|
# These sockets here are just symlinks to the real sockets which
|
||||||
# are created by the run.py file. The reason being that we run into
|
# are created by the run.py file. The reason being that we run into
|
||||||
@ -40,11 +41,21 @@ class VMAttr:
|
|||||||
|
|
||||||
|
|
||||||
class Machine:
|
class Machine:
|
||||||
|
flake: str | Path
|
||||||
|
name: str
|
||||||
|
data: MachineData
|
||||||
|
eval_cache: dict[str, str]
|
||||||
|
build_cache: dict[str, Path]
|
||||||
|
_flake_path: Path | None
|
||||||
|
_deployment_info: None | dict[str, str]
|
||||||
|
vm: QMPWrapper
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
flake: Path | str,
|
flake: Path | str,
|
||||||
deployment_info: dict | None = None,
|
deployment_info: dict | None = None,
|
||||||
|
machine: MachineData | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Creates a Machine
|
Creates a Machine
|
||||||
@ -52,20 +63,26 @@ class Machine:
|
|||||||
@clan_dir: the directory of the clan, optional, if not set it will be determined from the current working directory
|
@clan_dir: the directory of the clan, optional, if not set it will be determined from the current working directory
|
||||||
@machine_json: can be optionally used to skip evaluation of the machine, location of the json file with machine data
|
@machine_json: can be optionally used to skip evaluation of the machine, location of the json file with machine data
|
||||||
"""
|
"""
|
||||||
self.name: str = name
|
if machine is None:
|
||||||
self.flake: str | Path = flake
|
uri = ClanURI.from_str(str(flake), name)
|
||||||
|
machine = uri.machine
|
||||||
|
self.flake: str | Path = machine.flake_id._value
|
||||||
|
self.name: str = machine.name
|
||||||
|
self.data: MachineData = machine
|
||||||
|
else:
|
||||||
|
self.data: MachineData = machine
|
||||||
|
|
||||||
self.eval_cache: dict[str, str] = {}
|
self.eval_cache: dict[str, str] = {}
|
||||||
self.build_cache: dict[str, Path] = {}
|
self.build_cache: dict[str, Path] = {}
|
||||||
|
self._flake_path: Path | None = None
|
||||||
self._deployment_info: None | dict[str, str] = deployment_info
|
self._deployment_info: None | dict[str, str] = deployment_info
|
||||||
|
|
||||||
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.name)
|
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name)
|
||||||
|
|
||||||
self.vm: VMAttr = VMAttr(state_dir)
|
self.vm: QMPWrapper = QMPWrapper(state_dir)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Machine(name={self.name}, flake={self.flake})"
|
return f"Machine(name={self.data.name}, flake={self.data.flake_id})"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return str(self)
|
return str(self)
|
||||||
@ -86,7 +103,7 @@ class Machine:
|
|||||||
"deploymentAddress"
|
"deploymentAddress"
|
||||||
)
|
)
|
||||||
if val is None:
|
if val is None:
|
||||||
msg = f"the 'clan.networking.targetHost' nixos option is not set for machine '{self.name}'"
|
msg = f"the 'clan.networking.targetHost' nixos option is not set for machine '{self.data.name}'"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@ -109,7 +126,7 @@ class Machine:
|
|||||||
return json.loads(Path(self.deployment_info["secretsData"]).read_text())
|
return json.loads(Path(self.deployment_info["secretsData"]).read_text())
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
raise ClanError(
|
raise ClanError(
|
||||||
f"Failed to parse secretsData for machine {self.name} as json"
|
f"Failed to parse secretsData for machine {self.data.name} as json"
|
||||||
) from e
|
) from e
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@ -119,19 +136,23 @@ class Machine:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def flake_dir(self) -> Path:
|
def flake_dir(self) -> Path:
|
||||||
if isinstance(self.flake, Path):
|
if self._flake_path:
|
||||||
return self.flake
|
return self._flake_path
|
||||||
|
|
||||||
if hasattr(self, "flake_path"):
|
if self.data.flake_id.is_local():
|
||||||
return Path(self.flake_path)
|
self._flake_path = self.data.flake_id.path
|
||||||
|
elif self.data.flake_id.is_remote():
|
||||||
|
self._flake_path = Path(nix_metadata(self.data.flake_id.url)["path"])
|
||||||
|
else:
|
||||||
|
raise ClanError(f"Unsupported flake url: {self.data.flake_id}")
|
||||||
|
|
||||||
self.flake_path: str = nix_metadata(self.flake)["path"]
|
assert self._flake_path is not None
|
||||||
return Path(self.flake_path)
|
return self._flake_path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_host(self) -> Host:
|
def target_host(self) -> Host:
|
||||||
return parse_deployment_address(
|
return parse_deployment_address(
|
||||||
self.name, self.target_host_address, meta={"machine": self}
|
self.data.name, self.target_host_address, meta={"machine": self}
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -145,7 +166,7 @@ class Machine:
|
|||||||
return self.target_host
|
return self.target_host
|
||||||
# enable ssh agent forwarding to allow the build host to access the target host
|
# enable ssh agent forwarding to allow the build host to access the target host
|
||||||
return parse_deployment_address(
|
return parse_deployment_address(
|
||||||
self.name,
|
self.data.name,
|
||||||
build_host,
|
build_host,
|
||||||
forward_agent=True,
|
forward_agent=True,
|
||||||
meta={"machine": self, "target_host": self.target_host},
|
meta={"machine": self, "target_host": self.target_host},
|
||||||
@ -204,7 +225,7 @@ class Machine:
|
|||||||
args += [
|
args += [
|
||||||
"--expr",
|
"--expr",
|
||||||
f"""
|
f"""
|
||||||
((builtins.getFlake "{url}").clanInternals.machinesFunc."{system}"."{self.name}" {{
|
((builtins.getFlake "{url}").clanInternals.machinesFunc."{system}"."{self.data.name}" {{
|
||||||
extraConfig = builtins.fromJSON (builtins.readFile (builtins.fetchTree {{
|
extraConfig = builtins.fromJSON (builtins.readFile (builtins.fetchTree {{
|
||||||
type = "file";
|
type = "file";
|
||||||
url = if (builtins.compareVersions builtins.nixVersion "2.19") == -1 then "{file_info["path"]}" else "file:{file_info["path"]}";
|
url = if (builtins.compareVersions builtins.nixVersion "2.19") == -1 then "{file_info["path"]}" else "file:{file_info["path"]}";
|
||||||
@ -214,15 +235,13 @@ class Machine:
|
|||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
if isinstance(self.flake, Path):
|
if (self.flake_dir / ".git").exists():
|
||||||
if (self.flake / ".git").exists():
|
flake = f"git+file://{self.flake_dir}"
|
||||||
flake = f"git+file://{self.flake}"
|
|
||||||
else:
|
|
||||||
flake = f"path:{self.flake}"
|
|
||||||
else:
|
else:
|
||||||
flake = self.flake
|
flake = f"path:{self.flake_dir}"
|
||||||
|
|
||||||
args += [
|
args += [
|
||||||
f'{flake}#clanInternals.machines."{system}".{self.name}.{attr}',
|
f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}',
|
||||||
*nix_options,
|
*nix_options,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
0
pkgs/clan-cli/clan_cli/py.typed
Normal file
0
pkgs/clan-cli/clan_cli/py.typed
Normal file
@ -2,6 +2,7 @@ import argparse
|
|||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
@ -19,6 +20,15 @@ from .modules import SecretStoreBase
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def read_multiline_input(prompt: str = "Finish with Ctrl-D") -> str:
|
||||||
|
"""
|
||||||
|
Read multi-line input from stdin.
|
||||||
|
"""
|
||||||
|
print(prompt, flush=True)
|
||||||
|
proc = subprocess.run(["cat"], stdout=subprocess.PIPE, text=True)
|
||||||
|
return proc.stdout
|
||||||
|
|
||||||
|
|
||||||
def generate_service_secrets(
|
def generate_service_secrets(
|
||||||
machine: Machine,
|
machine: Machine,
|
||||||
service: str,
|
service: str,
|
||||||
@ -128,7 +138,12 @@ def generate_secrets(
|
|||||||
fact_store = facts_module.FactStore(machine=machine)
|
fact_store = facts_module.FactStore(machine=machine)
|
||||||
|
|
||||||
if prompt is None:
|
if prompt is None:
|
||||||
prompt = lambda text: input(f"{text}: ")
|
|
||||||
|
def prompt_func(text: str) -> str:
|
||||||
|
print(f"{text}: ")
|
||||||
|
return read_multiline_input()
|
||||||
|
|
||||||
|
prompt = prompt_func
|
||||||
|
|
||||||
with TemporaryDirectory() as tmp:
|
with TemporaryDirectory() as tmp:
|
||||||
tmpdir = Path(tmp)
|
tmpdir = Path(tmp)
|
||||||
|
@ -22,7 +22,7 @@ class VmConfig:
|
|||||||
|
|
||||||
def inspect_vm(machine: Machine) -> VmConfig:
|
def inspect_vm(machine: Machine) -> VmConfig:
|
||||||
data = json.loads(machine.eval_nix("config.clanCore.vm.inspect"))
|
data = json.loads(machine.eval_nix("config.clanCore.vm.inspect"))
|
||||||
return VmConfig(flake_url=machine.flake, **data)
|
return VmConfig(flake_url=str(machine.flake), **data)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -174,12 +174,13 @@ def run_vm(
|
|||||||
if vm.graphics and not vm.waypipe:
|
if vm.graphics and not vm.waypipe:
|
||||||
packages.append("nixpkgs#virt-viewer")
|
packages.append("nixpkgs#virt-viewer")
|
||||||
remote_viewer_mimetypes = module_root() / "vms" / "mimetypes"
|
remote_viewer_mimetypes = module_root() / "vms" / "mimetypes"
|
||||||
env[
|
env["XDG_DATA_DIRS"] = (
|
||||||
"XDG_DATA_DIRS"
|
f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}"
|
||||||
] = f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}"
|
)
|
||||||
|
|
||||||
with start_waypipe(qemu_cmd.vsock_cid, f"[{vm.machine_name}] "), start_virtiofsd(
|
with (
|
||||||
virtiofsd_socket
|
start_waypipe(qemu_cmd.vsock_cid, f"[{vm.machine_name}] "),
|
||||||
|
start_virtiofsd(virtiofsd_socket),
|
||||||
):
|
):
|
||||||
run(
|
run(
|
||||||
nix_shell(packages, qemu_cmd.args),
|
nix_shell(packages, qemu_cmd.args),
|
||||||
|
@ -1,36 +1,37 @@
|
|||||||
{ age
|
{
|
||||||
, lib
|
age,
|
||||||
, argcomplete
|
lib,
|
||||||
, installShellFiles
|
argcomplete,
|
||||||
, nix
|
installShellFiles,
|
||||||
, openssh
|
nix,
|
||||||
, pytest
|
openssh,
|
||||||
, pytest-cov
|
pytest,
|
||||||
, pytest-xdist
|
pytest-cov,
|
||||||
, pytest-subprocess
|
pytest-xdist,
|
||||||
, pytest-timeout
|
pytest-subprocess,
|
||||||
, remote-pdb
|
pytest-timeout,
|
||||||
, ipdb
|
remote-pdb,
|
||||||
, python3
|
ipdb,
|
||||||
, runCommand
|
python3,
|
||||||
, setuptools
|
runCommand,
|
||||||
, sops
|
setuptools,
|
||||||
, stdenv
|
sops,
|
||||||
, wheel
|
stdenv,
|
||||||
, fakeroot
|
wheel,
|
||||||
, rsync
|
fakeroot,
|
||||||
, bash
|
rsync,
|
||||||
, sshpass
|
bash,
|
||||||
, zbar
|
sshpass,
|
||||||
, tor
|
zbar,
|
||||||
, git
|
tor,
|
||||||
, nixpkgs
|
git,
|
||||||
, qemu
|
nixpkgs,
|
||||||
, gnupg
|
qemu,
|
||||||
, e2fsprogs
|
gnupg,
|
||||||
, mypy
|
e2fsprogs,
|
||||||
, rope
|
mypy,
|
||||||
, clan-core-path
|
rope,
|
||||||
|
clan-core-path,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
|
||||||
@ -38,19 +39,22 @@ let
|
|||||||
argcomplete # optional dependency: if not enabled, shell completion will not work
|
argcomplete # optional dependency: if not enabled, shell completion will not work
|
||||||
];
|
];
|
||||||
|
|
||||||
pytestDependencies = runtimeDependencies ++ dependencies ++ [
|
pytestDependencies =
|
||||||
pytest
|
runtimeDependencies
|
||||||
pytest-cov
|
++ dependencies
|
||||||
pytest-subprocess
|
++ [
|
||||||
pytest-xdist
|
pytest
|
||||||
pytest-timeout
|
pytest-cov
|
||||||
remote-pdb
|
pytest-subprocess
|
||||||
ipdb
|
pytest-xdist
|
||||||
openssh
|
pytest-timeout
|
||||||
git
|
remote-pdb
|
||||||
gnupg
|
ipdb
|
||||||
stdenv.cc
|
openssh
|
||||||
];
|
git
|
||||||
|
gnupg
|
||||||
|
stdenv.cc
|
||||||
|
];
|
||||||
|
|
||||||
# Optional dependencies for clan cli, we re-expose them here to make sure they all build.
|
# Optional dependencies for clan cli, we re-expose them here to make sure they all build.
|
||||||
runtimeDependencies = [
|
runtimeDependencies = [
|
||||||
@ -70,7 +74,9 @@ let
|
|||||||
e2fsprogs
|
e2fsprogs
|
||||||
];
|
];
|
||||||
|
|
||||||
runtimeDependenciesAsSet = builtins.listToAttrs (builtins.map (p: lib.nameValuePair (lib.getName p.name) p) runtimeDependencies);
|
runtimeDependenciesAsSet = builtins.listToAttrs (
|
||||||
|
builtins.map (p: lib.nameValuePair (lib.getName p.name) p) runtimeDependencies
|
||||||
|
);
|
||||||
|
|
||||||
checkPython = python3.withPackages (_ps: pytestDependencies);
|
checkPython = python3.withPackages (_ps: pytestDependencies);
|
||||||
|
|
||||||
@ -121,42 +127,48 @@ python3.pkgs.buildPythonApplication {
|
|||||||
propagatedBuildInputs = dependencies;
|
propagatedBuildInputs = dependencies;
|
||||||
|
|
||||||
# also re-expose dependencies so we test them in CI
|
# also re-expose dependencies so we test them in CI
|
||||||
passthru.tests = (lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet) // rec {
|
passthru.tests =
|
||||||
clan-pytest-without-core = runCommand "clan-pytest-without-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; } ''
|
(lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet)
|
||||||
cp -r ${source} ./src
|
// rec {
|
||||||
chmod +w -R ./src
|
clan-pytest-without-core =
|
||||||
cd ./src
|
runCommand "clan-pytest-without-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; }
|
||||||
|
''
|
||||||
|
cp -r ${source} ./src
|
||||||
|
chmod +w -R ./src
|
||||||
|
cd ./src
|
||||||
|
|
||||||
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
|
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
|
||||||
${checkPython}/bin/python -m pytest -m "not impure and not with_core" ./tests
|
${checkPython}/bin/python -m pytest -m "not impure and not with_core" ./tests
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
# separate the tests that can never be cached
|
# separate the tests that can never be cached
|
||||||
clan-pytest-with-core = runCommand "clan-pytest-with-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; } ''
|
clan-pytest-with-core =
|
||||||
cp -r ${source} ./src
|
runCommand "clan-pytest-with-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; }
|
||||||
chmod +w -R ./src
|
''
|
||||||
cd ./src
|
cp -r ${source} ./src
|
||||||
|
chmod +w -R ./src
|
||||||
|
cd ./src
|
||||||
|
|
||||||
export CLAN_CORE=${clan-core-path}
|
export CLAN_CORE=${clan-core-path}
|
||||||
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
|
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
|
||||||
${checkPython}/bin/python -m pytest -m "not impure and with_core" ./tests
|
${checkPython}/bin/python -m pytest -m "not impure and with_core" ./tests
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
clan-pytest = runCommand "clan-pytest" { } ''
|
clan-pytest = runCommand "clan-pytest" { } ''
|
||||||
echo ${clan-pytest-without-core}
|
echo ${clan-pytest-without-core}
|
||||||
echo ${clan-pytest-with-core}
|
echo ${clan-pytest-with-core}
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
check-for-breakpoints = runCommand "breakpoints" { } ''
|
check-for-breakpoints = runCommand "breakpoints" { } ''
|
||||||
if grep --include \*.py -Rq "breakpoint()" ${source}; then
|
if grep --include \*.py -Rq "breakpoint()" ${source}; then
|
||||||
echo "breakpoint() found in ${source}:"
|
echo "breakpoint() found in ${source}:"
|
||||||
grep --include \*.py -Rn "breakpoint()" ${source}
|
grep --include \*.py -Rn "breakpoint()" ${source}
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
passthru.nixpkgs = nixpkgs';
|
passthru.nixpkgs = nixpkgs';
|
||||||
passthru.checkPython = checkPython;
|
passthru.checkPython = checkPython;
|
||||||
@ -178,7 +190,7 @@ python3.pkgs.buildPythonApplication {
|
|||||||
<(${argcomplete}/bin/register-python-argcomplete --shell fish clan)
|
<(${argcomplete}/bin/register-python-argcomplete --shell fish clan)
|
||||||
'';
|
'';
|
||||||
# Don't leak python packages into a devshell.
|
# Don't leak python packages into a devshell.
|
||||||
# It can be very confusing if you `nix run` than load the cli from the devshell instead.
|
# It can be very confusing if you `nix run` then load the cli from the devshell instead.
|
||||||
postFixup = ''
|
postFixup = ''
|
||||||
rm $out/nix-support/propagated-build-inputs
|
rm $out/nix-support/propagated-build-inputs
|
||||||
'';
|
'';
|
||||||
|
@ -1,37 +1,44 @@
|
|||||||
{ inputs, self, lib, ... }:
|
|
||||||
{
|
{
|
||||||
perSystem = { self', pkgs, ... }:
|
inputs,
|
||||||
|
self,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
perSystem =
|
||||||
|
{ self', pkgs, ... }:
|
||||||
let
|
let
|
||||||
flakeLock = lib.importJSON (self + /flake.lock);
|
flakeLock = lib.importJSON (self + /flake.lock);
|
||||||
flakeInputs = (builtins.removeAttrs inputs [ "self" ]);
|
flakeInputs = (builtins.removeAttrs inputs [ "self" ]);
|
||||||
flakeLockVendoredDeps = flakeLock // {
|
flakeLockVendoredDeps = flakeLock // {
|
||||||
nodes = flakeLock.nodes // (
|
nodes =
|
||||||
lib.flip lib.mapAttrs flakeInputs (name: _: flakeLock.nodes.${name} // {
|
flakeLock.nodes
|
||||||
locked = {
|
// (lib.flip lib.mapAttrs flakeInputs (
|
||||||
inherit (flakeLock.nodes.${name}.locked) narHash;
|
name: _:
|
||||||
lastModified =
|
flakeLock.nodes.${name}
|
||||||
# lol, nixpkgs has a different timestamp on the fs???
|
// {
|
||||||
if name == "nixpkgs"
|
locked = {
|
||||||
then 0
|
inherit (flakeLock.nodes.${name}.locked) narHash;
|
||||||
else 1;
|
lastModified =
|
||||||
path = "${inputs.${name}}";
|
# lol, nixpkgs has a different timestamp on the fs???
|
||||||
type = "path";
|
if name == "nixpkgs" then 0 else 1;
|
||||||
};
|
path = "${inputs.${name}}";
|
||||||
})
|
type = "path";
|
||||||
);
|
};
|
||||||
|
}
|
||||||
|
));
|
||||||
};
|
};
|
||||||
flakeLockFile = builtins.toFile "clan-core-flake.lock"
|
flakeLockFile = builtins.toFile "clan-core-flake.lock" (builtins.toJSON flakeLockVendoredDeps);
|
||||||
(builtins.toJSON flakeLockVendoredDeps);
|
clanCoreWithVendoredDeps =
|
||||||
clanCoreWithVendoredDeps = lib.trace flakeLockFile pkgs.runCommand "clan-core-with-vendored-deps" { } ''
|
lib.trace flakeLockFile pkgs.runCommand "clan-core-with-vendored-deps" { }
|
||||||
cp -r ${self} $out
|
''
|
||||||
chmod +w -R $out
|
cp -r ${self} $out
|
||||||
cp ${flakeLockFile} $out/flake.lock
|
chmod +w -R $out
|
||||||
'';
|
cp ${flakeLockFile} $out/flake.lock
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells.clan-cli = pkgs.callPackage ./shell.nix {
|
devShells.clan-cli = pkgs.callPackage ./shell.nix { inherit (self'.packages) clan-cli; };
|
||||||
inherit (self'.packages) clan-cli;
|
|
||||||
};
|
|
||||||
packages = {
|
packages = {
|
||||||
clan-cli = pkgs.python3.pkgs.callPackage ./default.nix {
|
clan-cli = pkgs.python3.pkgs.callPackage ./default.nix {
|
||||||
inherit (inputs) nixpkgs;
|
inherit (inputs) nixpkgs;
|
||||||
@ -42,5 +49,4 @@
|
|||||||
|
|
||||||
checks = self'.packages.clan-cli.tests;
|
checks = self'.packages.clan-cli.tests;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ scripts = { clan = "clan_cli:main" }
|
|||||||
exclude = ["clan_cli.nixpkgs*", "result"]
|
exclude = ["clan_cli.nixpkgs*", "result"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
clan_cli = ["config/jsonschema/*", "webui/assets/**/*", "vms/mimetypes/**/*"]
|
clan_cli = ["py.typed", "config/jsonschema/*", "webui/assets/**/*", "vms/mimetypes/**/*"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = "tests"
|
testpaths = "tests"
|
||||||
@ -29,7 +29,6 @@ warn_redundant_casts = true
|
|||||||
disallow_untyped_calls = true
|
disallow_untyped_calls = true
|
||||||
disallow_untyped_defs = true
|
disallow_untyped_defs = true
|
||||||
no_implicit_optional = true
|
no_implicit_optional = true
|
||||||
disable_error_code = ["has-type"]
|
|
||||||
exclude = "clan_cli.nixpkgs"
|
exclude = "clan_cli.nixpkgs"
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# mypy: ignore-errors
|
# mypy: ignore-errors
|
||||||
|
|
||||||
""" QEMU Monitor Protocol Python class """
|
"""QEMU Monitor Protocol Python class"""
|
||||||
# Copyright (C) 2009, 2010 Red Hat Inc.
|
# Copyright (C) 2009, 2010 Red Hat Inc.
|
||||||
#
|
#
|
||||||
# Authors:
|
# Authors:
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
{ nix-unit, clan-cli, system, mkShell, writeScriptBin, openssh, ruff, python3 }:
|
{
|
||||||
|
nix-unit,
|
||||||
|
clan-cli,
|
||||||
|
system,
|
||||||
|
mkShell,
|
||||||
|
writeScriptBin,
|
||||||
|
openssh,
|
||||||
|
ruff,
|
||||||
|
python3,
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
checkScript = writeScriptBin "check" ''
|
checkScript = writeScriptBin "check" ''
|
||||||
nix build .#checks.${system}.{treefmt,clan-pytest} -L "$@"
|
nix build .#checks.${system}.{treefmt,clan-pytest} -L "$@"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
pythonWithDeps = python3.withPackages (
|
pythonWithDeps = python3.withPackages (
|
||||||
ps:
|
ps: clan-cli.propagatedBuildInputs ++ clan-cli.devDependencies ++ [ ps.pip ]
|
||||||
clan-cli.propagatedBuildInputs
|
|
||||||
++ clan-cli.devDependencies
|
|
||||||
++ [
|
|
||||||
ps.pip
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
in
|
in
|
||||||
mkShell {
|
mkShell {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{ lib, ... }: {
|
{ lib, ... }:
|
||||||
|
{
|
||||||
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
||||||
system.stateVersion = lib.version;
|
system.stateVersion = lib.version;
|
||||||
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{ lib, ... }: {
|
{ lib, ... }:
|
||||||
|
{
|
||||||
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
||||||
system.stateVersion = lib.version;
|
system.stateVersion = lib.version;
|
||||||
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{ lib, ... }: {
|
{ lib, ... }:
|
||||||
|
{
|
||||||
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
||||||
system.stateVersion = lib.version;
|
system.stateVersion = lib.version;
|
||||||
clan.virtualisation.graphics = false;
|
clan.virtualisation.graphics = false;
|
||||||
|
@ -1,145 +1,97 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from clan_cli.clan_uri import ClanParameters, ClanScheme, ClanURI
|
from clan_cli.clan_uri import ClanURI
|
||||||
|
|
||||||
|
|
||||||
def test_get_internal() -> None:
|
def test_get_url() -> None:
|
||||||
# Create a ClanURI object from a remote URI with parameters
|
# Create a ClanURI object from a remote URI with parameters
|
||||||
uri = ClanURI("clan://https://example.com?flake_attr=myVM&password=1234")
|
uri = ClanURI("clan://https://example.com?password=1234#myVM")
|
||||||
assert uri.get_internal() == "https://example.com?password=1234"
|
assert uri.get_url() == "https://example.com?password=1234"
|
||||||
|
|
||||||
uri = ClanURI("clan://~/Downloads")
|
uri = ClanURI("clan://~/Downloads")
|
||||||
assert uri.get_internal().endswith("/Downloads")
|
assert uri.get_url().endswith("/Downloads")
|
||||||
|
|
||||||
uri = ClanURI("clan:///home/user/Downloads")
|
uri = ClanURI("clan:///home/user/Downloads")
|
||||||
assert uri.get_internal() == "/home/user/Downloads"
|
assert uri.get_url() == "/home/user/Downloads"
|
||||||
|
|
||||||
uri = ClanURI("clan://file:///home/user/Downloads")
|
uri = ClanURI("clan://file:///home/user/Downloads")
|
||||||
assert uri.get_internal() == "/home/user/Downloads"
|
assert uri.get_url() == "/home/user/Downloads"
|
||||||
|
|
||||||
|
|
||||||
def test_local_uri() -> None:
|
def test_local_uri() -> None:
|
||||||
# Create a ClanURI object from a local URI
|
# Create a ClanURI object from a local URI
|
||||||
uri = ClanURI("clan://file:///home/user/Downloads")
|
uri = ClanURI("clan://file:///home/user/Downloads")
|
||||||
match uri.scheme:
|
assert uri.flake_id.path == Path("/home/user/Downloads")
|
||||||
case ClanScheme.LOCAL.value(path):
|
|
||||||
assert path == Path("/home/user/Downloads") # type: ignore
|
|
||||||
case _:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_remote() -> None:
|
def test_is_remote() -> None:
|
||||||
# Create a ClanURI object from a remote URI
|
# Create a ClanURI object from a remote URI
|
||||||
uri = ClanURI("clan://https://example.com")
|
uri = ClanURI("clan://https://example.com")
|
||||||
|
assert uri.flake_id.url == "https://example.com"
|
||||||
match uri.scheme:
|
|
||||||
case ClanScheme.REMOTE.value(url):
|
|
||||||
assert url == "https://example.com" # type: ignore
|
|
||||||
case _:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
def test_direct_local_path() -> None:
|
def test_direct_local_path() -> None:
|
||||||
# Create a ClanURI object from a remote URI
|
# Create a ClanURI object from a remote URI
|
||||||
uri = ClanURI("clan://~/Downloads")
|
uri = ClanURI("clan://~/Downloads")
|
||||||
assert uri.get_internal().endswith("/Downloads")
|
assert uri.get_url().endswith("/Downloads")
|
||||||
|
|
||||||
|
|
||||||
def test_direct_local_path2() -> None:
|
def test_direct_local_path2() -> None:
|
||||||
# Create a ClanURI object from a remote URI
|
# Create a ClanURI object from a remote URI
|
||||||
uri = ClanURI("clan:///home/user/Downloads")
|
uri = ClanURI("clan:///home/user/Downloads")
|
||||||
assert uri.get_internal() == "/home/user/Downloads"
|
assert uri.get_url() == "/home/user/Downloads"
|
||||||
|
|
||||||
|
|
||||||
def test_remote_with_clanparams() -> None:
|
def test_remote_with_clanparams() -> None:
|
||||||
# Create a ClanURI object from a remote URI with parameters
|
# Create a ClanURI object from a remote URI with parameters
|
||||||
uri = ClanURI("clan://https://example.com")
|
uri = ClanURI("clan://https://example.com")
|
||||||
|
|
||||||
assert uri.params.flake_attr == "defaultVM"
|
assert uri.machine.name == "defaultVM"
|
||||||
|
assert uri.flake_id.url == "https://example.com"
|
||||||
match uri.scheme:
|
|
||||||
case ClanScheme.REMOTE.value(url):
|
|
||||||
assert url == "https://example.com" # type: ignore
|
|
||||||
case _:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
def test_from_path_with_custom() -> None:
|
|
||||||
# Create a ClanURI object from a remote URI with parameters
|
|
||||||
uri_str = Path("/home/user/Downloads")
|
|
||||||
params = ClanParameters(flake_attr="myVM")
|
|
||||||
uri = ClanURI.from_path(uri_str, params=params)
|
|
||||||
assert uri.params.flake_attr == "myVM"
|
|
||||||
|
|
||||||
match uri.scheme:
|
|
||||||
case ClanScheme.LOCAL.value(path):
|
|
||||||
assert path == Path("/home/user/Downloads") # type: ignore
|
|
||||||
case _:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
def test_from_path_with_default() -> None:
|
|
||||||
# Create a ClanURI object from a remote URI with parameters
|
|
||||||
uri_str = Path("/home/user/Downloads")
|
|
||||||
params = ClanParameters()
|
|
||||||
uri = ClanURI.from_path(uri_str, params=params)
|
|
||||||
assert uri.params.flake_attr == "defaultVM"
|
|
||||||
|
|
||||||
match uri.scheme:
|
|
||||||
case ClanScheme.LOCAL.value(path):
|
|
||||||
assert path == Path("/home/user/Downloads") # type: ignore
|
|
||||||
case _:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
def test_from_str() -> None:
|
|
||||||
# Create a ClanURI object from a remote URI with parameters
|
|
||||||
uri_str = "https://example.com?password=asdasd&test=1234"
|
|
||||||
params = ClanParameters(flake_attr="myVM")
|
|
||||||
uri = ClanURI.from_str(url=uri_str, params=params)
|
|
||||||
assert uri.params.flake_attr == "myVM"
|
|
||||||
|
|
||||||
match uri.scheme:
|
|
||||||
case ClanScheme.REMOTE.value(url):
|
|
||||||
assert url == "https://example.com?password=asdasd&test=1234" # type: ignore
|
|
||||||
case _:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
uri = ClanURI.from_str(url=uri_str, params={"flake_attr": "myVM"})
|
|
||||||
assert uri.params.flake_attr == "myVM"
|
|
||||||
|
|
||||||
uri = ClanURI.from_str(uri_str, "myVM")
|
|
||||||
assert uri.params.flake_attr == "myVM"
|
|
||||||
|
|
||||||
uri_str = "~/Downloads/democlan"
|
|
||||||
params = ClanParameters(flake_attr="myVM")
|
|
||||||
uri = ClanURI.from_str(url=uri_str, params=params)
|
|
||||||
assert uri.params.flake_attr == "myVM"
|
|
||||||
assert uri.get_internal().endswith("/Downloads/democlan")
|
|
||||||
|
|
||||||
uri_str = "~/Downloads/democlan"
|
|
||||||
uri = ClanURI.from_str(url=uri_str)
|
|
||||||
assert uri.params.flake_attr == "defaultVM"
|
|
||||||
assert uri.get_internal().endswith("/Downloads/democlan")
|
|
||||||
|
|
||||||
uri_str = "clan://~/Downloads/democlan"
|
|
||||||
uri = ClanURI.from_str(url=uri_str)
|
|
||||||
assert uri.params.flake_attr == "defaultVM"
|
|
||||||
assert uri.get_internal().endswith("/Downloads/democlan")
|
|
||||||
|
|
||||||
|
|
||||||
def test_remote_with_all_params() -> None:
|
def test_remote_with_all_params() -> None:
|
||||||
# Create a ClanURI object from a remote URI with parameters
|
uri = ClanURI("clan://https://example.com?password=12345#myVM#secondVM?dummy_opt=1")
|
||||||
uri = ClanURI("clan://https://example.com?flake_attr=myVM&password=1234")
|
assert uri.machine.name == "myVM"
|
||||||
assert uri.params.flake_attr == "myVM"
|
assert uri._machines[1].name == "secondVM"
|
||||||
|
assert uri._machines[1].params.dummy_opt == "1"
|
||||||
match uri.scheme:
|
assert uri.flake_id.url == "https://example.com?password=12345"
|
||||||
case ClanScheme.REMOTE.value(url):
|
|
||||||
assert url == "https://example.com?password=1234" # type: ignore
|
|
||||||
case _:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
def test_with_hashtag() -> None:
|
def test_from_str_remote() -> None:
|
||||||
uri = ClanURI("clan://https://example.com?flake_attr=thirdVM#myVM#secondVM")
|
uri = ClanURI.from_str(url="https://example.com", machine_name="myVM")
|
||||||
assert uri.params.flake_attr == "myVM"
|
assert uri.get_url() == "https://example.com"
|
||||||
|
assert uri.get_orig_uri() == "clan://https://example.com#myVM"
|
||||||
|
assert uri.machine.name == "myVM"
|
||||||
|
assert len(uri._machines) == 1
|
||||||
|
assert uri.flake_id.url == "https://example.com"
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_str_local() -> None:
|
||||||
|
uri = ClanURI.from_str(url="~/Projects/democlan", machine_name="myVM")
|
||||||
|
assert uri.get_url().endswith("/Projects/democlan")
|
||||||
|
assert uri.get_orig_uri() == "clan://~/Projects/democlan#myVM"
|
||||||
|
assert uri.machine.name == "myVM"
|
||||||
|
assert len(uri._machines) == 1
|
||||||
|
assert uri.flake_id.is_local()
|
||||||
|
assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_str_local_no_machine() -> None:
|
||||||
|
uri = ClanURI.from_str("~/Projects/democlan")
|
||||||
|
assert uri.get_url().endswith("/Projects/democlan")
|
||||||
|
assert uri.get_orig_uri() == "clan://~/Projects/democlan"
|
||||||
|
assert uri.machine.name == "defaultVM"
|
||||||
|
assert len(uri._machines) == 1
|
||||||
|
assert uri.flake_id.is_local()
|
||||||
|
assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_str_local_no_machine2() -> None:
|
||||||
|
uri = ClanURI.from_str("~/Projects/democlan#syncthing-peer1")
|
||||||
|
assert uri.get_url().endswith("/Projects/democlan")
|
||||||
|
assert uri.get_orig_uri() == "clan://~/Projects/democlan#syncthing-peer1"
|
||||||
|
assert uri.machine.name == "syncthing-peer1"
|
||||||
|
assert len(uri._machines) == 1
|
||||||
|
assert uri.flake_id.is_local()
|
||||||
|
assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{ lib
|
{ lib, ... }:
|
||||||
, ...
|
{
|
||||||
}: {
|
|
||||||
options.clan.fake-module.fake-flag = lib.mkOption {
|
options.clan.fake-module.fake-flag = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
|
@ -2,32 +2,41 @@
|
|||||||
# this placeholder is replaced by the path to nixpkgs
|
# this placeholder is replaced by the path to nixpkgs
|
||||||
inputs.nixpkgs.url = "__NIXPKGS__";
|
inputs.nixpkgs.url = "__NIXPKGS__";
|
||||||
|
|
||||||
outputs = inputs':
|
outputs =
|
||||||
|
inputs':
|
||||||
let
|
let
|
||||||
# fake clan-core input
|
# fake clan-core input
|
||||||
fake-clan-core = {
|
fake-clan-core = {
|
||||||
clanModules.fake-module = ./fake-module.nix;
|
clanModules.fake-module = ./fake-module.nix;
|
||||||
};
|
};
|
||||||
inputs = inputs' // { clan-core = fake-clan-core; };
|
inputs = inputs' // {
|
||||||
|
clan-core = fake-clan-core;
|
||||||
|
};
|
||||||
machineSettings = (
|
machineSettings = (
|
||||||
if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != ""
|
if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then
|
||||||
then builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE"))
|
builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE"))
|
||||||
else if builtins.pathExists ./machines/machine1/settings.json
|
else if builtins.pathExists ./machines/machine1/settings.json then
|
||||||
then builtins.fromJSON (builtins.readFile ./machines/machine1/settings.json)
|
builtins.fromJSON (builtins.readFile ./machines/machine1/settings.json)
|
||||||
else { }
|
else
|
||||||
|
{ }
|
||||||
|
);
|
||||||
|
machineImports = map (module: fake-clan-core.clanModules.${module}) (
|
||||||
|
machineSettings.clanImports or [ ]
|
||||||
);
|
);
|
||||||
machineImports =
|
|
||||||
map
|
|
||||||
(module: fake-clan-core.clanModules.${module})
|
|
||||||
(machineSettings.clanImports or [ ]);
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem {
|
nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem {
|
||||||
modules =
|
modules = machineImports ++ [
|
||||||
machineImports ++ [
|
./nixosModules/machine1.nix
|
||||||
./nixosModules/machine1.nix
|
machineSettings
|
||||||
machineSettings
|
(
|
||||||
({ lib, options, pkgs, ... }: {
|
{
|
||||||
|
lib,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
config = {
|
config = {
|
||||||
nixpkgs.hostPlatform = "x86_64-linux";
|
nixpkgs.hostPlatform = "x86_64-linux";
|
||||||
# speed up by not instantiating nixpkgs twice and disable documentation
|
# speed up by not instantiating nixpkgs twice and disable documentation
|
||||||
@ -51,8 +60,9 @@
|
|||||||
The buildClan function will automatically import these modules for the current machine.
|
The buildClan function will automatically import these modules for the current machine.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
];
|
)
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{ lib, ... }: {
|
{ lib, ... }:
|
||||||
|
{
|
||||||
options.clan.jitsi.enable = lib.mkOption {
|
options.clan.jitsi.enable = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
|
@ -5,40 +5,45 @@
|
|||||||
# this placeholder is replaced by the path to nixpkgs
|
# this placeholder is replaced by the path to nixpkgs
|
||||||
inputs.clan-core.url = "__CLAN_CORE__";
|
inputs.clan-core.url = "__CLAN_CORE__";
|
||||||
|
|
||||||
outputs = { self, clan-core }:
|
outputs =
|
||||||
|
{ self, clan-core }:
|
||||||
let
|
let
|
||||||
clan = clan-core.lib.buildClan {
|
clan = clan-core.lib.buildClan {
|
||||||
directory = self;
|
directory = self;
|
||||||
clanName = "test_flake_with_core";
|
clanName = "test_flake_with_core";
|
||||||
machines = {
|
machines = {
|
||||||
vm1 = { lib, ... }: {
|
vm1 =
|
||||||
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
{ lib, ... }:
|
||||||
system.stateVersion = lib.version;
|
{
|
||||||
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
||||||
clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__";
|
system.stateVersion = lib.version;
|
||||||
clanCore.sops.defaultGroups = [ "admins" ];
|
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
||||||
clan.virtualisation.graphics = false;
|
clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__";
|
||||||
|
clanCore.sops.defaultGroups = [ "admins" ];
|
||||||
|
clan.virtualisation.graphics = false;
|
||||||
|
|
||||||
clan.networking.zerotier.controller.enable = true;
|
clan.networking.zerotier.controller.enable = true;
|
||||||
networking.useDHCP = false;
|
networking.useDHCP = false;
|
||||||
|
|
||||||
systemd.services.shutdown-after-boot = {
|
systemd.services.shutdown-after-boot = {
|
||||||
enable = true;
|
enable = true;
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "multi-user.target" ];
|
after = [ "multi-user.target" ];
|
||||||
script = ''
|
script = ''
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
shutdown -h now
|
shutdown -h now
|
||||||
'';
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
vm2 =
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
||||||
|
system.stateVersion = lib.version;
|
||||||
|
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
||||||
|
clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__";
|
||||||
|
clan.networking.zerotier.networkId = "82b44b162ec6c013";
|
||||||
};
|
};
|
||||||
};
|
|
||||||
vm2 = { lib, ... }: {
|
|
||||||
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
|
||||||
system.stateVersion = lib.version;
|
|
||||||
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
|
||||||
clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__";
|
|
||||||
clan.networking.zerotier.networkId = "82b44b162ec6c013";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
@ -5,30 +5,33 @@
|
|||||||
# this placeholder is replaced by the path to clan-core
|
# this placeholder is replaced by the path to clan-core
|
||||||
inputs.clan-core.url = "__CLAN_CORE__";
|
inputs.clan-core.url = "__CLAN_CORE__";
|
||||||
|
|
||||||
outputs = { self, clan-core }:
|
outputs =
|
||||||
|
{ self, clan-core }:
|
||||||
let
|
let
|
||||||
clan = clan-core.lib.buildClan {
|
clan = clan-core.lib.buildClan {
|
||||||
directory = self;
|
directory = self;
|
||||||
clanName = "test_flake_with_core_and_pass";
|
clanName = "test_flake_with_core_and_pass";
|
||||||
machines = {
|
machines = {
|
||||||
vm1 = { lib, ... }: {
|
vm1 =
|
||||||
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
{ lib, ... }:
|
||||||
system.stateVersion = lib.version;
|
{
|
||||||
clanCore.secretStore = "password-store";
|
clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__";
|
||||||
clanCore.secretsUploadDirectory = lib.mkForce "__CLAN_SOPS_KEY_DIR__/secrets";
|
system.stateVersion = lib.version;
|
||||||
|
clanCore.secretStore = "password-store";
|
||||||
|
clanCore.secretsUploadDirectory = lib.mkForce "__CLAN_SOPS_KEY_DIR__/secrets";
|
||||||
|
|
||||||
clan.networking.zerotier.controller.enable = true;
|
clan.networking.zerotier.controller.enable = true;
|
||||||
|
|
||||||
systemd.services.shutdown-after-boot = {
|
systemd.services.shutdown-after-boot = {
|
||||||
enable = true;
|
enable = true;
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "multi-user.target" ];
|
after = [ "multi-user.target" ];
|
||||||
script = ''
|
script = ''
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
shutdown -h now
|
shutdown -h now
|
||||||
'';
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
# this placeholder is replaced by the path to nixpkgs
|
# this placeholder is replaced by the path to nixpkgs
|
||||||
inputs.clan-core.url = "__CLAN_CORE__";
|
inputs.clan-core.url = "__CLAN_CORE__";
|
||||||
|
|
||||||
outputs = { self, clan-core }:
|
outputs =
|
||||||
|
{ self, clan-core }:
|
||||||
let
|
let
|
||||||
clan = clan-core.lib.buildClan {
|
clan = clan-core.lib.buildClan {
|
||||||
directory = self;
|
directory = self;
|
||||||
@ -14,9 +15,7 @@
|
|||||||
let
|
let
|
||||||
machineModules = builtins.readDir (self + "/machines");
|
machineModules = builtins.readDir (self + "/machines");
|
||||||
in
|
in
|
||||||
builtins.mapAttrs
|
builtins.mapAttrs (name: _type: import (self + "/machines/${name}")) machineModules;
|
||||||
(name: _type: import (self + "/machines/${name}"))
|
|
||||||
machineModules;
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
@ -6,7 +6,7 @@ from cli import Cli
|
|||||||
from fixtures_flakes import FlakeForTest
|
from fixtures_flakes import FlakeForTest
|
||||||
from pytest import CaptureFixture
|
from pytest import CaptureFixture
|
||||||
|
|
||||||
from clan_cli.clan_uri import ClanParameters, ClanURI
|
from clan_cli.clan_uri import ClanURI
|
||||||
from clan_cli.dirs import user_history_file
|
from clan_cli.dirs import user_history_file
|
||||||
from clan_cli.history.add import HistoryEntry
|
from clan_cli.history.add import HistoryEntry
|
||||||
|
|
||||||
@ -19,8 +19,7 @@ def test_history_add(
|
|||||||
test_flake_with_core: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
) -> None:
|
) -> None:
|
||||||
cli = Cli()
|
cli = Cli()
|
||||||
params = ClanParameters(flake_attr="vm1")
|
uri = ClanURI.from_str(str(test_flake_with_core.path), "vm1")
|
||||||
uri = ClanURI.from_path(test_flake_with_core.path, params=params)
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"history",
|
"history",
|
||||||
"add",
|
"add",
|
||||||
@ -40,8 +39,7 @@ def test_history_list(
|
|||||||
test_flake_with_core: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
) -> None:
|
) -> None:
|
||||||
cli = Cli()
|
cli = Cli()
|
||||||
params = ClanParameters(flake_attr="vm1")
|
uri = ClanURI.from_str(str(test_flake_with_core.path), "vm1")
|
||||||
uri = ClanURI.from_path(test_flake_with_core.path, params=params)
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"history",
|
"history",
|
||||||
"list",
|
"list",
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
## Developing GTK4 Applications
|
# Clan VM Manager
|
||||||
|
|
||||||
|
Provides users with the simple functionality to manage their locally registered clans.
|
||||||
|
|
||||||
|
![app-preview](screenshots/image.png)
|
||||||
|
|
||||||
|
## Available commands
|
||||||
|
|
||||||
|
Run this application
|
||||||
|
|
||||||
## Demos
|
|
||||||
Adw has a demo application showing all widgets. You can run it by executing:
|
|
||||||
```bash
|
```bash
|
||||||
adwaita-1-demo
|
./bin/clan-vm-manager
|
||||||
```
|
|
||||||
GTK4 has a demo application showing all widgets. You can run it by executing:
|
|
||||||
```bash
|
|
||||||
gtk4-widget-factory
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To find available icons execute:
|
Join the default machine of a clan
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gtk4-icon-browser
|
./bin/clan-vm-manager [clan-uri]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Join a specific machine of a clan
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/clan-vm-manager [clan-uri]#[machine]
|
||||||
|
```
|
||||||
|
|
||||||
## Links
|
For more available commands see the developer section below.
|
||||||
- [Adw PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Adw-1)
|
|
||||||
- [GTK4 PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Gtk-4.0)
|
|
||||||
- [Adw Widget Gallery](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/widget-gallery.html)
|
|
||||||
- [Python + GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/textview.html)
|
|
||||||
|
|
||||||
|
## Developing this Application
|
||||||
|
|
||||||
|
### Debugging Style and Layout
|
||||||
## Debugging Style and Layout
|
|
||||||
|
|
||||||
You can append `--debug` flag to enable debug logging printed into the console.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enable the GTK debugger
|
# Enable the GTK debugger
|
||||||
@ -38,8 +38,57 @@ gsettings set org.gtk.Settings.Debug enable-inspector-keybinding true
|
|||||||
GTK_DEBUG=interactive ./bin/clan-vm-manager --debug
|
GTK_DEBUG=interactive ./bin/clan-vm-manager --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
## Profiling
|
Appending `--debug` flag enables debug logging printed into the console.
|
||||||
To activate profiling execute:
|
|
||||||
```
|
### Profiling
|
||||||
|
|
||||||
|
To activate profiling you can run
|
||||||
|
|
||||||
|
```bash
|
||||||
PERF=1 ./bin/clan-vm-manager
|
PERF=1 ./bin/clan-vm-manager
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Library Components
|
||||||
|
|
||||||
|
> Note:
|
||||||
|
>
|
||||||
|
> we recognized bugs when starting some cli-commands through the integrated vs-code terminal.
|
||||||
|
> If encountering issues make sure to run commands in a regular os-shell.
|
||||||
|
|
||||||
|
lib-Adw has a demo application showing all widgets. You can run it by executing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adwaita-1-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
GTK4 has a demo application showing all widgets. You can run it by executing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gtk4-widget-factory
|
||||||
|
```
|
||||||
|
|
||||||
|
To find available icons execute
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gtk4-icon-browser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Links
|
||||||
|
|
||||||
|
Here are some important documentation links related to the Clan VM Manager:
|
||||||
|
|
||||||
|
- [Adw PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Adw-1): This link provides the PyGObject reference documentation for the Adw library, which is used in the Clan VM Manager. It contains detailed information about the Adw widgets and their usage.
|
||||||
|
|
||||||
|
- [GTK4 PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Gtk-4.0): This link provides the PyGObject reference documentation for GTK4, the toolkit used for building the user interface of the Clan VM Manager. It includes information about GTK4 widgets, signals, and other features.
|
||||||
|
|
||||||
|
- [Adw Widget Gallery](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/widget-gallery.html): This link showcases a widget gallery for Adw, allowing you to see the available widgets and their visual appearance. It can be helpful for designing the user interface of the Clan VM Manager.
|
||||||
|
|
||||||
|
- [Python + GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/textview.html): Although the Clan VM Manager uses GTK4, this tutorial for GTK3 can still be useful as it covers the basics of building GTK-based applications with Python. It includes examples and explanations for various GTK widgets, including text views.
|
||||||
|
|
||||||
|
- [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/): This link provides the GNOME Human Interface Guidelines, which offer design and usability recommendations for creating GNOME applications. It covers topics such as layout, navigation, and interaction patterns.
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
> Error dialogs should be avoided where possible, since they are disruptive.
|
||||||
|
>
|
||||||
|
> For simple non-critical errors, toasts can be a good alternative.
|
@ -33,10 +33,8 @@ class MainApplication(Adw.Application):
|
|||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
*args,
|
|
||||||
application_id="org.clan.vm-manager",
|
application_id="org.clan.vm-manager",
|
||||||
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
|
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
|
||||||
**kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.add_main_option(
|
self.add_main_option(
|
||||||
@ -48,7 +46,7 @@ class MainApplication(Adw.Application):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.window: Adw.ApplicationWindow | None = None
|
self.window: "MainWindow" | None = None
|
||||||
self.connect("activate", self.on_activate)
|
self.connect("activate", self.on_activate)
|
||||||
self.connect("shutdown", self.on_shutdown)
|
self.connect("shutdown", self.on_shutdown)
|
||||||
|
|
||||||
@ -113,8 +111,10 @@ class MainApplication(Adw.Application):
|
|||||||
log.debug(f"Style css path: {resource_path}")
|
log.debug(f"Style css path: {resource_path}")
|
||||||
css_provider = Gtk.CssProvider()
|
css_provider = Gtk.CssProvider()
|
||||||
css_provider.load_from_path(str(resource_path))
|
css_provider.load_from_path(str(resource_path))
|
||||||
|
display = Gdk.Display.get_default()
|
||||||
|
assert display is not None
|
||||||
Gtk.StyleContext.add_provider_for_display(
|
Gtk.StyleContext.add_provider_for_display(
|
||||||
Gdk.Display.get_default(),
|
display,
|
||||||
css_provider,
|
css_provider,
|
||||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
)
|
)
|
||||||
|
@ -17,6 +17,7 @@ avatar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.join-list {
|
.join-list {
|
||||||
|
margin-top: 1px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
|
|
||||||
@ -56,3 +57,10 @@ avatar {
|
|||||||
searchbar {
|
searchbar {
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.log-view {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user