Compare commits
40 Commits
improve-co
...
pinned-cla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd9141d2d | ||
|
|
f788313e97 | ||
|
|
89b70ffa6f | ||
|
|
ed1692574f | ||
|
|
1106c50924 | ||
|
|
e99e47da10 | ||
|
|
67def050fd | ||
|
|
c0d2787dee | ||
|
|
ecc327277c | ||
|
|
0064a8bfbc | ||
|
|
1e8b9def2a | ||
|
|
f0983ede5e | ||
|
|
10bc9e3e44 | ||
|
|
556fd8845e | ||
|
|
fab079af71 | ||
|
|
0370c1cf02 | ||
|
|
aa557f3a96 | ||
|
|
e8699e68b5 | ||
|
|
f8f31d430d | ||
|
|
3d345e0bca | ||
|
|
80711fcf72 | ||
|
|
35684090e3 | ||
|
|
8069b137f3 | ||
|
|
2fba6b15e8 | ||
|
|
cddee0ca86 | ||
|
|
0f3ab641d9 | ||
|
|
d5f90b2730 | ||
|
|
54335221d8 | ||
|
|
76b13476a5 | ||
|
|
b933dcf2e2 | ||
|
|
8a755fff8c | ||
|
|
5726dd1010 | ||
|
|
b306c748b8 | ||
|
|
2682581c09 | ||
|
|
a0a5827157 | ||
|
|
8638861a87 | ||
|
|
c5a28e2655 | ||
|
|
0af36d0a4d | ||
|
|
9867b6a894 | ||
|
|
7459566c2b |
29
.gitea/workflows/update-clan-core-for-checks.yml
Normal file
29
.gitea/workflows/update-clan-core-for-checks.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
name: "Update pinned clan-core for checks"
|
||||
on:
|
||||
repository_dispatch:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "51 2 * * *"
|
||||
jobs:
|
||||
update-pinned-clan-core:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Update clan-core for checks
|
||||
run: nix run .#update-clan-core-for-checks
|
||||
- name: Create pull request
|
||||
run: |
|
||||
git commit -am ""
|
||||
git push origin HEAD:update-clan-core-for-checks
|
||||
curl -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"head": "update-clan-core-branch",
|
||||
"base": "main",
|
||||
"title": "Automated Update: Clan Core",
|
||||
"body": "This PR updates the pinned clan-core for checks."
|
||||
}' \
|
||||
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/pulls"
|
||||
@@ -147,25 +147,7 @@
|
||||
perSystem =
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
clanCore = self.filter {
|
||||
include = [
|
||||
"checks/backups"
|
||||
"checks/flake-module.nix"
|
||||
"clanModules/borgbackup"
|
||||
"clanModules/flake-module.nix"
|
||||
"clanModules/localbackup"
|
||||
"clanModules/packages"
|
||||
"clanModules/single-disk"
|
||||
"clanModules/zerotier"
|
||||
"flake.lock"
|
||||
"flakeModules"
|
||||
"inventory.json"
|
||||
"nixosModules"
|
||||
# Just include everything in 'lib'
|
||||
# If anything changes in /lib that may affect everything
|
||||
"lib"
|
||||
];
|
||||
};
|
||||
clanCore = self.checks.x86_64-linux.clan-core-for-checks;
|
||||
in
|
||||
{
|
||||
checks = pkgs.lib.mkIf pkgs.stdenv.isLinux {
|
||||
@@ -182,11 +164,6 @@
|
||||
# import the inventory generated nixosModules
|
||||
self.clanInternals.inventoryClass.machines.test-backup.machineImports;
|
||||
clan.core.settings.directory = ./.;
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeShellScriptBin "foo" ''
|
||||
echo ${clanCore}
|
||||
'')
|
||||
];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
|
||||
6
checks/clan-core-for-checks.nix
Normal file
6
checks/clan-core-for-checks.nix
Normal file
@@ -0,0 +1,6 @@
|
||||
{ fetchgit }:
|
||||
fetchgit {
|
||||
url = "https://git.clan.lol/clan/clan-core.git";
|
||||
rev = "1e8b9def2a021877342491ca1f4c45533a580759";
|
||||
sha256 = "0f12vwr1abwa1iwjbb5z5xx8jlh80d9njwdm6iaw1z1h2m76xgzc";
|
||||
}
|
||||
@@ -34,33 +34,33 @@ clanLib.test.makeTestClan {
|
||||
|
||||
modules = {
|
||||
legacy-module = ./legacy-module;
|
||||
new-service = {
|
||||
_class = "clan.service";
|
||||
manifest.name = "new-service";
|
||||
roles.peer = { };
|
||||
perMachine = {
|
||||
nixosModule = {
|
||||
# This should be generated by:
|
||||
# nix run .#generate-test-vars -- checks/dummy-inventory-test dummy-inventory-test
|
||||
clan.core.vars.generators.new-service = {
|
||||
files.not-a-secret = {
|
||||
secret = false;
|
||||
deploy = true;
|
||||
};
|
||||
files.a-secret = {
|
||||
secret = true;
|
||||
deploy = true;
|
||||
owner = "nobody";
|
||||
group = "users";
|
||||
mode = "0644";
|
||||
};
|
||||
script = ''
|
||||
# This is a dummy script that does nothing
|
||||
echo -n "not-a-secret" > $out/not-a-secret
|
||||
echo -n "a-secret" > $out/a-secret
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
modules.new-service = {
|
||||
_class = "clan.service";
|
||||
manifest.name = "new-service";
|
||||
roles.peer = { };
|
||||
perMachine = {
|
||||
nixosModule = {
|
||||
# This should be generated by:
|
||||
# nix run .#generate-test-vars -- checks/dummy-inventory-test dummy-inventory-test
|
||||
clan.core.vars.generators.new-service = {
|
||||
files.not-a-secret = {
|
||||
secret = false;
|
||||
deploy = true;
|
||||
};
|
||||
files.a-secret = {
|
||||
secret = true;
|
||||
deploy = true;
|
||||
owner = "nobody";
|
||||
group = "users";
|
||||
mode = "0644";
|
||||
};
|
||||
script = ''
|
||||
# This is a dummy script that does nothing
|
||||
echo -n "not-a-secret" > $out/not-a-secret
|
||||
echo -n "a-secret" > $out/a-secret
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ in
|
||||
./installation/flake-module.nix
|
||||
./morph/flake-module.nix
|
||||
./nixos-documentation/flake-module.nix
|
||||
./sanity-checks/dont-depend-on-repo-root.nix
|
||||
./dont-depend-on-repo-root.nix
|
||||
];
|
||||
perSystem =
|
||||
{
|
||||
@@ -101,6 +101,12 @@ in
|
||||
mkdir -p $out
|
||||
cat $schemaFile > $out/allSchemas.json
|
||||
'';
|
||||
|
||||
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
|
||||
cp -r ${pkgs.callPackage ./clan-core-for-checks.nix { }} $out
|
||||
chmod +w $out/flake.lock
|
||||
cp ${../flake.lock} $out/flake.lock
|
||||
'';
|
||||
};
|
||||
legacyPackages = {
|
||||
nixosTests =
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
let
|
||||
dependencies = [
|
||||
pkgs.disko
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles
|
||||
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp
|
||||
|
||||
@@ -80,7 +81,7 @@
|
||||
|
||||
# Some distros like to automount disks with spaces
|
||||
machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdb && mount /dev/vdb "/mnt/with spaces"')
|
||||
machine.succeed("clan flash write --debug --flake ${../..} --yes --disk main /dev/vdb test-flash-machine-${pkgs.hostPlatform.system}")
|
||||
machine.succeed("clan flash write --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdb test-flash-machine-${pkgs.hostPlatform.system}")
|
||||
'';
|
||||
} { inherit pkgs self; };
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ let
|
||||
pkgs.bash.drvPath
|
||||
pkgs.nixos-anywhere
|
||||
pkgs.bubblewrap
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
in
|
||||
@@ -197,7 +198,7 @@ in
|
||||
installer.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../assets/ssh/privkey} /root/.ssh/id_ed25519")
|
||||
|
||||
installer.wait_until_succeeds("timeout 2 ssh -o StrictHostKeyChecking=accept-new -v nonrootuser@localhost hostname")
|
||||
installer.succeed("cp -r ${../..} test-flake && chmod -R +w test-flake")
|
||||
installer.succeed("cp -r ${self.checks.x86_64-linux.clan-core-for-checks} test-flake && chmod -R +w test-flake")
|
||||
|
||||
installer.succeed("clan machines install --no-reboot --debug --flake test-flake --yes test-install-machine-without-system --target-host nonrootuser@localhost --update-hardware-config nixos-facter >&2")
|
||||
installer.shutdown()
|
||||
@@ -217,7 +218,7 @@ in
|
||||
installer.start()
|
||||
installer.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../assets/ssh/privkey} /root/.ssh/id_ed25519")
|
||||
installer.wait_until_succeeds("timeout 2 ssh -o StrictHostKeyChecking=accept-new -v nonrootuser@localhost hostname")
|
||||
installer.succeed("cp -r ${../..} test-flake && chmod -R +w test-flake")
|
||||
installer.succeed("cp -r ${self.checks.x86_64-linux.clan-core-for-checks} test-flake && chmod -R +w test-flake")
|
||||
installer.fail("test -f test-flake/machines/test-install-machine/hardware-configuration.nix")
|
||||
installer.fail("test -f test-flake/machines/test-install-machine/facter.json")
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
dependencies = [
|
||||
self
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.stdenvNoCC
|
||||
self.nixosConfigurations.test-morph-machine.config.system.build.toplevel
|
||||
@@ -55,7 +54,7 @@
|
||||
testScript = ''
|
||||
start_all()
|
||||
actual.fail("cat /etc/testfile")
|
||||
actual.succeed("env CLAN_DIR=${self} clan machines morph test-morph-template --i-will-be-fired-for-using-this --debug --name test-morph-machine")
|
||||
actual.succeed("env CLAN_DIR=${self.checks.x86_64-linux.clan-core-for-checks} clan machines morph test-morph-template --i-will-be-fired-for-using-this --debug --name test-morph-machine")
|
||||
assert actual.succeed("cat /etc/testfile") == "morphed"
|
||||
'';
|
||||
} { inherit pkgs self; };
|
||||
|
||||
@@ -105,10 +105,7 @@ in
|
||||
private_key = {
|
||||
inherit owner;
|
||||
};
|
||||
public_key = {
|
||||
inherit owner;
|
||||
secret = false;
|
||||
};
|
||||
public_key.secret = false;
|
||||
};
|
||||
|
||||
runtimeInputs = [
|
||||
@@ -134,10 +131,7 @@ in
|
||||
private_key = {
|
||||
inherit owner;
|
||||
};
|
||||
public_key = {
|
||||
inherit owner;
|
||||
secret = false;
|
||||
};
|
||||
public_key.secret = false;
|
||||
};
|
||||
|
||||
runtimeInputs = [
|
||||
|
||||
@@ -18,7 +18,7 @@ let
|
||||
};
|
||||
|
||||
# Register the module for the test
|
||||
inventory.modules.hello-world = module;
|
||||
modules.hello-world = module;
|
||||
|
||||
# Use the module in the test
|
||||
inventory.instances = {
|
||||
|
||||
@@ -14,6 +14,9 @@ clanLib.test.makeTestClan {
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
modules = {
|
||||
hello-service = module;
|
||||
};
|
||||
inventory = {
|
||||
machines.peer1 = { };
|
||||
|
||||
@@ -21,10 +24,6 @@ clanLib.test.makeTestClan {
|
||||
module.name = "hello-service";
|
||||
roles.peer.machines.peer1 = { };
|
||||
};
|
||||
|
||||
modules = {
|
||||
hello-service = module;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ nav:
|
||||
- Autoincludes: manual/adding-machines.md
|
||||
- Inventory:
|
||||
- Inventory: manual/inventory.md
|
||||
- Instances: manual/distributed-services.md
|
||||
- Services: manual/distributed-services.md
|
||||
- Secure Boot: manual/secure-boot.md
|
||||
- Flake-parts: manual/flake-parts.md
|
||||
- Authoring:
|
||||
|
||||
@@ -12,7 +12,7 @@ We discussed the initial architecture in [01-clan-service-modules](https://git.c
|
||||
|
||||
### A Minimal module
|
||||
|
||||
First of all we need to register our module into the `inventory.modules` attribute. Make sure to choose a unique name so the module doesn't have a name collision with any of the core modules.
|
||||
First of all we need to register our module into the `clan.modules` attribute. Make sure to choose a unique name so the module doesn't have a name collision with any of the core modules.
|
||||
|
||||
While not required we recommend to prefix your module attribute name.
|
||||
|
||||
@@ -22,20 +22,15 @@ i.e. `@hsjobeki/customNetworking`
|
||||
|
||||
```nix title="flake.nix"
|
||||
# ...
|
||||
|
||||
outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({
|
||||
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } ({
|
||||
imports = [ inputs.clan-core.flakeModules.default ];
|
||||
# ...
|
||||
clan = {
|
||||
inventory = {
|
||||
# We could also inline the complete module spec here
|
||||
# For example
|
||||
# {...}: { _class = "clan.service"; ... };
|
||||
modules."@hsjobeki/customNetworking" = import ./service-modules/networking.nix;
|
||||
};
|
||||
|
||||
# If needed: Exporting the module for other people
|
||||
modules."@hsjobeki/customNetworking" = import ./service-modules/networking.nix;
|
||||
# We could also inline the complete module spec here
|
||||
# For example
|
||||
# {...}: { _class = "clan.service"; ... };
|
||||
};
|
||||
})
|
||||
```
|
||||
@@ -221,9 +216,6 @@ outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({self, lib, ...}:
|
||||
# ...
|
||||
clan = {
|
||||
# Register the module
|
||||
inventory.modules."@hsjobeki/messaging" = lib.importApply ./service-modules/messaging.nix { inherit self; };
|
||||
|
||||
# Expose the module for downstream users, 'self' would always point to this flake.
|
||||
modules."@hsjobeki/messaging" = lib.importApply ./service-modules/messaging.nix { inherit self; };
|
||||
};
|
||||
})
|
||||
@@ -250,7 +242,7 @@ outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({self, lib, ...}:
|
||||
# ...
|
||||
clan = {
|
||||
# Register the module
|
||||
inventory.modules."@hsjobeki/messaging" = {
|
||||
modules."@hsjobeki/messaging" = {
|
||||
# Create an option 'myClan' and assign it to 'self'
|
||||
options.myClan = lib.mkOption {
|
||||
default = self;
|
||||
|
||||
@@ -32,7 +32,7 @@ VM tests should be avoided wherever it is possible to implement a cheaper unit t
|
||||
|
||||
Existing nixos vm tests in clan-core can be found by using ripgrep:
|
||||
```shellSession
|
||||
rg "import.*/lib/test-base.nix"
|
||||
rg self.clanLib.test.baseTest
|
||||
```
|
||||
|
||||
### Locating definitions of failing VM tests
|
||||
@@ -50,7 +50,7 @@ example: locating the vm test named `borgbackup`:
|
||||
```shellSession
|
||||
$ rg "borgbackup =" ./checks
|
||||
./checks/flake-module.nix
|
||||
41: borgbackup = import ./borgbackup nixosTestArgs;
|
||||
44- wayland-proxy-virtwl = self.clanLib.test.baseTest ./wayland-proxy-virtwl nixosTestArgs;
|
||||
```
|
||||
|
||||
-> the location of that test is `/checks/flake-module.nix` line `41`.
|
||||
@@ -99,15 +99,15 @@ Basically everything stated under the NixOS VM tests sections applies here, exce
|
||||
|
||||
Limitations:
|
||||
|
||||
- does not yet support networking
|
||||
- supports only one machine as of now
|
||||
|
||||
- Cannot run in interactive mode, however while the container test runs, it logs a nsenter command that can be used to log into each of the container.
|
||||
- setuid binaries don't work
|
||||
|
||||
### Where to find examples for NixOS container tests
|
||||
|
||||
Existing nixos container tests in clan-core can be found by using ripgrep:
|
||||
|
||||
```shellSession
|
||||
rg "import.*/lib/container-test.nix"
|
||||
rg self.clanLib.test.containerTest
|
||||
```
|
||||
|
||||
|
||||
|
||||
20
flake.lock
generated
20
flake.lock
generated
@@ -16,11 +16,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746628829,
|
||||
"narHash": "sha256-q77HWbHlJTXFVRD2kRnYwqGfbGmCm9XYPG0ZgLLVh8M=",
|
||||
"rev": "9a3856421bd6733192b57037cdf26712d73c0871",
|
||||
"lastModified": 1747008053,
|
||||
"narHash": "sha256-rob/qftmEuk+/JVGCIrOpv+LWjdmayFtebEKqRZXVAI=",
|
||||
"rev": "2666bb11f4287cfbdf3b7c5f55231c6b5772a436",
|
||||
"type": "tarball",
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/9a3856421bd6733192b57037cdf26712d73c0871.tar.gz"
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/2666bb11f4287cfbdf3b7c5f55231c6b5772a436.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -34,11 +34,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746695594,
|
||||
"narHash": "sha256-pAAWYs3S+/tY65vemHZdVSXpeIz4JINEJZoPoBjr8JU=",
|
||||
"lastModified": 1746729224,
|
||||
"narHash": "sha256-9R4sOLAK1w3Bq54H3XOJogdc7a6C2bLLmatOQ+5pf5w=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "6bb82b77ce140137177e30df067759931ab60a73",
|
||||
"rev": "85555d27ded84604ad6657ecca255a03fd878607",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -184,11 +184,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746216483,
|
||||
"narHash": "sha256-4h3s1L/kKqt3gMDcVfN8/4v2jqHrgLIe4qok4ApH5x4=",
|
||||
"lastModified": 1746989248,
|
||||
"narHash": "sha256-uoQ21EWsAhyskNo8QxrTVZGjG/dV4x5NM1oSgrmNDJY=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "29ec5026372e0dec56f890e50dbe4f45930320fd",
|
||||
"rev": "708ec80ca82e2bbafa93402ccb66a35ff87900c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -37,7 +37,7 @@ let
|
||||
done
|
||||
if ! test -e ~/clan-core; then
|
||||
# git clone https://git.clan.lol/clan/clan-core.git ~/clan-core
|
||||
cp -rv ${self} clan-core
|
||||
cp -rv ${self.checks.x86_64-linux.clan-core-for-checks} clan-core
|
||||
fi
|
||||
cd clan-core
|
||||
clan machines morph demo-template --i-will-be-fired-for-using-this
|
||||
|
||||
@@ -45,7 +45,9 @@ let
|
||||
inherit inventory directory;
|
||||
flakeInputs = config.self.inputs;
|
||||
prefix = config._prefix ++ [ "inventoryClass" ];
|
||||
localModuleSet = config.modules;
|
||||
# TODO: remove inventory.modules, this is here for backwards compatibility
|
||||
localModuleSet =
|
||||
lib.filterAttrs (n: _: !inventory._legacyModules ? ${n}) inventory.modules // config.modules;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ let
|
||||
|
||||
distributedServices = clanLib.inventory.mapInstances {
|
||||
inherit (config) inventory;
|
||||
inherit localModuleSet;
|
||||
inherit flakeInputs;
|
||||
prefix = prefix ++ [ "distributedServices" ];
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ let
|
||||
)
|
||||
}
|
||||
|
||||
To import a local module from 'inventory.modules' remove the 'input' attribute from the module definition
|
||||
To import a local module from 'clan.modules' remove the 'input' attribute from the module definition
|
||||
Remove the following line from the module definition:
|
||||
|
||||
...
|
||||
@@ -81,6 +81,7 @@ in
|
||||
flakeInputs,
|
||||
# The clan inventory
|
||||
inventory,
|
||||
localModuleSet,
|
||||
prefix ? [ ],
|
||||
}:
|
||||
let
|
||||
@@ -92,7 +93,7 @@ in
|
||||
let
|
||||
resolvedModule = resolveModule {
|
||||
moduleSpec = instance.module;
|
||||
localModuleSet = inventory.modules;
|
||||
inherit localModuleSet;
|
||||
inherit flakeInputs;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,9 +41,13 @@ let
|
||||
|
||||
callInventoryAdapter =
|
||||
inventoryModule:
|
||||
let
|
||||
inventory = evalInventory inventoryModule;
|
||||
in
|
||||
clanLib.inventory.mapInstances {
|
||||
flakeInputs = flakeInputsFixture;
|
||||
inventory = evalInventory inventoryModule;
|
||||
inherit inventory;
|
||||
localModuleSet = inventory.modules;
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
@@ -58,7 +58,16 @@ in
|
||||
)
|
||||
)
|
||||
''
|
||||
The config.clan.core.vars.generators.${generator.name}.files.${file.name} is not secret, but has non-default owner/group/mode set.
|
||||
The config.clan.core.vars.generators.${generator.name}.files.${file.name} is not secret:
|
||||
${lib.optionalString (file.owner != "root") ''
|
||||
The owner is set to ${file.owner}, but should be root.
|
||||
''}
|
||||
${lib.optionalString (file.group != (if _class == "darwin" then "wheel" else "root")) ''
|
||||
The group is set to ${file.group}, but should be ${if _class == "darwin" then "wheel" else "root"}.
|
||||
''}
|
||||
${lib.optionalString (file.mode != "0400") ''
|
||||
The mode is set to ${file.mode}, but should be 0400.
|
||||
''}
|
||||
This doesn't work because the file will be added to the nix store
|
||||
''
|
||||
) [ ] (lib.attrValues generator.files)
|
||||
|
||||
@@ -18,7 +18,7 @@ let
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${machineName}/${secret.generator}/${secret.name}/secret";
|
||||
|
||||
vars = collectFiles config.clan.core.vars;
|
||||
vars = collectFiles config.clan.core.vars.generators;
|
||||
in
|
||||
{
|
||||
config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") {
|
||||
|
||||
@@ -13,7 +13,7 @@ in
|
||||
{
|
||||
|
||||
collectFiles =
|
||||
vars:
|
||||
generators:
|
||||
let
|
||||
relevantFiles =
|
||||
generator:
|
||||
@@ -30,7 +30,7 @@ in
|
||||
inherit (generator) share;
|
||||
inherit (file) owner group mode;
|
||||
}) (relevantFiles generator)
|
||||
) vars.generators
|
||||
) generators
|
||||
);
|
||||
in
|
||||
allFiles;
|
||||
|
||||
@@ -6,8 +6,9 @@ from urllib.parse import urlparse
|
||||
|
||||
from clan_lib.api import API
|
||||
|
||||
from clan_cli.cmd import run_no_stdout
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.errors import ClanCmdError, ClanError
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.inventory import Meta
|
||||
from clan_cli.nix import nix_eval
|
||||
|
||||
@@ -15,26 +16,26 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@API.register
|
||||
def show_clan_meta(uri: str) -> Meta:
|
||||
if uri.startswith("/") and not Path(uri).exists():
|
||||
msg = f"Path {uri} does not exist"
|
||||
def show_clan_meta(flake: Flake) -> Meta:
|
||||
if flake.is_local and not flake.path.exists():
|
||||
msg = f"Path {flake} does not exist"
|
||||
raise ClanError(msg, description="clan directory does not exist")
|
||||
cmd = nix_eval(
|
||||
[
|
||||
f"{uri}#clanInternals.inventory.meta",
|
||||
f"{flake}#clanInternals.inventory.meta",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
res = "{}"
|
||||
|
||||
try:
|
||||
proc = run_no_stdout(cmd)
|
||||
proc = run(cmd)
|
||||
res = proc.stdout.strip()
|
||||
except ClanCmdError as e:
|
||||
msg = "Evaluation failed on meta attribute"
|
||||
raise ClanError(
|
||||
msg,
|
||||
location=f"show_clan {uri}",
|
||||
location=f"show_clan {flake}",
|
||||
description=str(e.cmd),
|
||||
) from e
|
||||
|
||||
@@ -53,16 +54,16 @@ def show_clan_meta(uri: str) -> Meta:
|
||||
msg = "Invalid absolute path"
|
||||
raise ClanError(
|
||||
msg,
|
||||
location=f"show_clan {uri}",
|
||||
location=f"show_clan {flake}",
|
||||
description="Icon path must be a URL or a relative path",
|
||||
)
|
||||
|
||||
icon_path = str((Path(uri) / meta_icon).resolve())
|
||||
icon_path = str((flake.path / meta_icon).resolve())
|
||||
else:
|
||||
msg = "Invalid schema"
|
||||
raise ClanError(
|
||||
msg,
|
||||
location=f"show_clan {uri}",
|
||||
location=f"show_clan {flake}",
|
||||
description="Icon path must be a URL or a relative path",
|
||||
)
|
||||
|
||||
|
||||
@@ -403,23 +403,3 @@ def run(
|
||||
raise ClanCmdError(cmd_out)
|
||||
|
||||
return cmd_out
|
||||
|
||||
|
||||
def run_no_stdout(
|
||||
cmd: list[str],
|
||||
opts: RunOpts | None = None,
|
||||
) -> CmdOut:
|
||||
"""
|
||||
Like run, but automatically suppresses all output, if not in DEBUG log level.
|
||||
If in DEBUG log level the stdout of commands will be shown.
|
||||
"""
|
||||
if opts is None:
|
||||
opts = RunOpts()
|
||||
|
||||
if cmdlog.isEnabledFor(logging.DEBUG):
|
||||
opts.log = opts.log if opts.log.value > Log.STDERR.value else Log.STDERR
|
||||
|
||||
return run(
|
||||
cmd,
|
||||
opts,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from .errors import ClanError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.machines.machines import Machine
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -144,8 +145,8 @@ def machines_dir(flake: "Flake") -> Path:
|
||||
return Path(store_path) / "machines"
|
||||
|
||||
|
||||
def specific_machine_dir(flake: "Flake", machine: str) -> Path:
|
||||
return machines_dir(flake) / machine
|
||||
def specific_machine_dir(machine: "Machine") -> Path:
|
||||
return machines_dir(machine.flake) / machine.name
|
||||
|
||||
|
||||
def module_root() -> Path:
|
||||
|
||||
@@ -48,6 +48,7 @@ def bubblewrap_cmd(generator: str, facts_dir: Path, secrets_dir: Path) -> list[s
|
||||
"--unshare-all",
|
||||
"--tmpfs", "/",
|
||||
"--ro-bind", "/nix/store", "/nix/store",
|
||||
"--ro-bind", "/bin/sh", "/bin/sh",
|
||||
"--dev", "/dev",
|
||||
# not allowed to bind procfs in some sandboxes
|
||||
"--bind", str(facts_dir), str(facts_dir),
|
||||
|
||||
@@ -5,28 +5,27 @@ from tempfile import TemporaryDirectory
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.ssh.host import Host
|
||||
from clan_cli.ssh.upload import upload
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upload_secrets(machine: Machine, host: Host) -> None:
|
||||
if not machine.secret_facts_store.needs_upload(host):
|
||||
machine.info("Secrets already uploaded")
|
||||
return
|
||||
def upload_secrets(machine: Machine) -> None:
|
||||
with machine.target_host() as host:
|
||||
if not machine.secret_facts_store.needs_upload(host):
|
||||
machine.info("Secrets already uploaded")
|
||||
return
|
||||
|
||||
with TemporaryDirectory(prefix="facts-upload-") as _tempdir:
|
||||
local_secret_dir = Path(_tempdir).resolve()
|
||||
machine.secret_facts_store.upload(local_secret_dir)
|
||||
remote_secret_dir = Path(machine.secrets_upload_directory)
|
||||
upload(host, local_secret_dir, remote_secret_dir)
|
||||
with TemporaryDirectory(prefix="facts-upload-") as _tempdir:
|
||||
local_secret_dir = Path(_tempdir).resolve()
|
||||
machine.secret_facts_store.upload(local_secret_dir)
|
||||
remote_secret_dir = Path(machine.secrets_upload_directory)
|
||||
upload(host, local_secret_dir, remote_secret_dir)
|
||||
|
||||
|
||||
def upload_command(args: argparse.Namespace) -> None:
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
with machine.target_host() as host:
|
||||
upload_secrets(machine, host)
|
||||
upload_secrets(machine)
|
||||
|
||||
|
||||
def register_upload_parser(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
@@ -21,7 +21,7 @@ from typing import Any
|
||||
|
||||
from clan_lib.api import API, dataclass_to_dict, from_dict
|
||||
|
||||
from clan_cli.cmd import run_no_stdout
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.errors import ClanCmdError, ClanError
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.git import commit_file
|
||||
@@ -80,7 +80,7 @@ def load_inventory_eval(flake_dir: Flake) -> Inventory:
|
||||
]
|
||||
)
|
||||
|
||||
proc = run_no_stdout(cmd)
|
||||
proc = run(cmd)
|
||||
|
||||
try:
|
||||
res = proc.stdout.strip()
|
||||
@@ -380,7 +380,7 @@ def get_inventory_current_priority(flake: Flake) -> dict:
|
||||
]
|
||||
)
|
||||
|
||||
proc = run_no_stdout(cmd)
|
||||
proc = run(cmd)
|
||||
|
||||
try:
|
||||
res = proc.stdout.strip()
|
||||
|
||||
@@ -110,7 +110,7 @@ def create_machine(opts: CreateOptions, commit: bool = True) -> None:
|
||||
new_machine["deploy"] = {"targetHost": target_host}
|
||||
|
||||
patch_inventory_with(
|
||||
Flake(str(clan_dir)), f"machines.{machine_name}", dataclass_to_dict(new_machine)
|
||||
opts.clan_dir, f"machines.{machine_name}", dataclass_to_dict(new_machine)
|
||||
)
|
||||
|
||||
# Commit at the end in that order to avoid committing halve-baked machines
|
||||
|
||||
@@ -5,9 +5,10 @@ from pathlib import Path
|
||||
|
||||
from clan_lib.api import API
|
||||
|
||||
from clan_cli import Flake, inventory
|
||||
from clan_cli import inventory
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
from clan_cli.dirs import specific_machine_dir
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.secrets.folders import sops_secrets_folder
|
||||
from clan_cli.secrets.machines import has_machine as secrets_has_machine
|
||||
from clan_cli.secrets.machines import remove_machine as secrets_machine_remove
|
||||
@@ -15,49 +16,46 @@ from clan_cli.secrets.secrets import (
|
||||
list_secrets,
|
||||
)
|
||||
|
||||
from .machines import Machine
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@API.register
|
||||
def delete_machine(flake: Flake, name: str) -> None:
|
||||
def delete_machine(machine: Machine) -> None:
|
||||
try:
|
||||
inventory.delete(flake, {f"machines.{name}"})
|
||||
inventory.delete(machine.flake, {f"machines.{machine.name}"})
|
||||
except KeyError as exc:
|
||||
# louis@(2025-03-09): test infrastructure does not seem to set the
|
||||
# inventory properly, but more importantly only one machine in my
|
||||
# personal clan ended up in the inventory for some reason, so I think
|
||||
# it makes sense to eat the exception here.
|
||||
log.warning(
|
||||
f"{name} was missing or already deleted from the machines inventory: {exc}"
|
||||
f"{machine.name} was missing or already deleted from the machines inventory: {exc}"
|
||||
)
|
||||
|
||||
changed_paths: list[Path] = []
|
||||
|
||||
folder = specific_machine_dir(flake, name)
|
||||
folder = specific_machine_dir(machine)
|
||||
if folder.exists():
|
||||
changed_paths.append(folder)
|
||||
shutil.rmtree(folder)
|
||||
|
||||
# louis@(2025-02-04): clean-up legacy (pre-vars) secrets:
|
||||
sops_folder = sops_secrets_folder(flake.path)
|
||||
filter_fn = lambda secret_name: secret_name.startswith(f"{name}-")
|
||||
for secret_name in list_secrets(flake.path, filter_fn):
|
||||
sops_folder = sops_secrets_folder(machine.flake.path)
|
||||
filter_fn = lambda secret_name: secret_name.startswith(f"{machine.name}-")
|
||||
for secret_name in list_secrets(machine.flake.path, filter_fn):
|
||||
secret_path = sops_folder / secret_name
|
||||
changed_paths.append(secret_path)
|
||||
shutil.rmtree(secret_path)
|
||||
|
||||
machine = Machine(name, flake)
|
||||
changed_paths.extend(machine.public_vars_store.delete_store())
|
||||
changed_paths.extend(machine.secret_vars_store.delete_store())
|
||||
# Remove the machine's key, and update secrets & vars that referenced it:
|
||||
if secrets_has_machine(flake.path, name):
|
||||
secrets_machine_remove(flake.path, name)
|
||||
if secrets_has_machine(machine.flake.path, machine.name):
|
||||
secrets_machine_remove(machine.flake.path, machine.name)
|
||||
|
||||
|
||||
def delete_command(args: argparse.Namespace) -> None:
|
||||
delete_machine(args.flake, args.name)
|
||||
delete_machine(Machine(flake=args.flake, name=args.name))
|
||||
|
||||
|
||||
def register_delete_parser(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
@@ -7,11 +7,10 @@ from pathlib import Path
|
||||
|
||||
from clan_lib.api import API
|
||||
|
||||
from clan_cli.cmd import RunOpts, run_no_stdout
|
||||
from clan_cli.cmd import RunOpts, run
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
from clan_cli.dirs import specific_machine_dir
|
||||
from clan_cli.errors import ClanCmdError, ClanError
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.git import commit_file
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.nix import nix_config, nix_eval
|
||||
@@ -26,39 +25,35 @@ class HardwareConfig(Enum):
|
||||
NIXOS_GENERATE_CONFIG = "nixos-generate-config"
|
||||
NONE = "none"
|
||||
|
||||
def config_path(self, flake: Flake, machine_name: str) -> Path:
|
||||
machine_dir = specific_machine_dir(flake, machine_name)
|
||||
def config_path(self, machine: Machine) -> Path:
|
||||
machine_dir = specific_machine_dir(machine)
|
||||
if self == HardwareConfig.NIXOS_FACTER:
|
||||
return machine_dir / "facter.json"
|
||||
return machine_dir / "hardware-configuration.nix"
|
||||
|
||||
@classmethod
|
||||
def detect_type(
|
||||
cls: type["HardwareConfig"], flake: Flake, machine_name: str
|
||||
) -> "HardwareConfig":
|
||||
hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path(
|
||||
flake, machine_name
|
||||
)
|
||||
def detect_type(cls: type["HardwareConfig"], machine: Machine) -> "HardwareConfig":
|
||||
hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path(machine)
|
||||
|
||||
if hardware_config.exists() and "throw" not in hardware_config.read_text():
|
||||
return HardwareConfig.NIXOS_GENERATE_CONFIG
|
||||
|
||||
if HardwareConfig.NIXOS_FACTER.config_path(flake, machine_name).exists():
|
||||
if HardwareConfig.NIXOS_FACTER.config_path(machine).exists():
|
||||
return HardwareConfig.NIXOS_FACTER
|
||||
|
||||
return HardwareConfig.NONE
|
||||
|
||||
|
||||
@API.register
|
||||
def show_machine_hardware_config(flake: Flake, machine_name: str) -> HardwareConfig:
|
||||
def show_machine_hardware_config(machine: Machine) -> HardwareConfig:
|
||||
"""
|
||||
Show hardware information for a machine returns None if none exist.
|
||||
"""
|
||||
return HardwareConfig.detect_type(flake, machine_name)
|
||||
return HardwareConfig.detect_type(machine)
|
||||
|
||||
|
||||
@API.register
|
||||
def show_machine_hardware_platform(flake: Flake, machine_name: str) -> str | None:
|
||||
def show_machine_hardware_platform(machine: Machine) -> str | None:
|
||||
"""
|
||||
Show hardware information for a machine returns None if none exist.
|
||||
"""
|
||||
@@ -66,13 +61,13 @@ def show_machine_hardware_platform(flake: Flake, machine_name: str) -> str | Non
|
||||
system = config["system"]
|
||||
cmd = nix_eval(
|
||||
[
|
||||
f"{flake}#clanInternals.machines.{system}.{machine_name}",
|
||||
f"{machine.flake}#clanInternals.machines.{system}.{machine.name}",
|
||||
"--apply",
|
||||
"machine: { inherit (machine.pkgs) system; }",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
proc = run_no_stdout(cmd, RunOpts(prefix=machine_name))
|
||||
proc = run(cmd, RunOpts(prefix=machine.name))
|
||||
res = proc.stdout.strip()
|
||||
|
||||
host_platform = json.loads(res)
|
||||
@@ -81,11 +76,8 @@ def show_machine_hardware_platform(flake: Flake, machine_name: str) -> str | Non
|
||||
|
||||
@dataclass
|
||||
class HardwareGenerateOptions:
|
||||
flake: Flake
|
||||
machine: str
|
||||
machine: Machine
|
||||
backend: HardwareConfig
|
||||
target_host: str | None = None
|
||||
keyfile: str | None = None
|
||||
password: str | None = None
|
||||
|
||||
|
||||
@@ -96,14 +88,9 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
|
||||
and place the resulting *.nix file in the machine's directory.
|
||||
"""
|
||||
|
||||
machine = Machine(
|
||||
opts.machine,
|
||||
flake=opts.flake,
|
||||
private_key=Path(opts.keyfile) if opts.keyfile else None,
|
||||
override_target_host=opts.target_host,
|
||||
)
|
||||
machine = opts.machine
|
||||
|
||||
hw_file = opts.backend.config_path(opts.flake, opts.machine)
|
||||
hw_file = opts.backend.config_path(opts.machine)
|
||||
hw_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if opts.backend == HardwareConfig.NIXOS_FACTER:
|
||||
@@ -148,11 +135,11 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
|
||||
|
||||
commit_file(
|
||||
hw_file,
|
||||
opts.flake.path,
|
||||
opts.machine.flake.path,
|
||||
f"machines/{opts.machine}/{hw_file.name}: update hardware configuration",
|
||||
)
|
||||
try:
|
||||
show_machine_hardware_platform(opts.flake, opts.machine)
|
||||
show_machine_hardware_platform(opts.machine)
|
||||
if backup_file:
|
||||
backup_file.unlink(missing_ok=True)
|
||||
except ClanCmdError as e:
|
||||
@@ -173,10 +160,13 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
|
||||
|
||||
|
||||
def update_hardware_config_command(args: argparse.Namespace) -> None:
|
||||
opts = HardwareGenerateOptions(
|
||||
machine = Machine(
|
||||
flake=args.flake,
|
||||
machine=args.machine,
|
||||
target_host=args.target_host,
|
||||
name=args.machine,
|
||||
override_target_host=args.target_host,
|
||||
)
|
||||
opts = HardwareGenerateOptions(
|
||||
machine=machine,
|
||||
password=args.password,
|
||||
backend=HardwareConfig(args.backend),
|
||||
)
|
||||
|
||||
@@ -111,11 +111,7 @@ def install_machine(opts: InstallOptions) -> None:
|
||||
[
|
||||
"--generate-hardware-config",
|
||||
str(opts.update_hardware_config.value),
|
||||
str(
|
||||
opts.update_hardware_config.config_path(
|
||||
machine.flake, machine.name
|
||||
)
|
||||
),
|
||||
str(opts.update_hardware_config.config_path(machine)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ def get_machine_details(machine: Machine) -> MachineDetails:
|
||||
msg = f"Machine {machine.name} not found in inventory"
|
||||
raise ClanError(msg)
|
||||
|
||||
hw_config = HardwareConfig.detect_type(machine.flake, machine.name)
|
||||
hw_config = HardwareConfig.detect_type(machine)
|
||||
|
||||
machine_dir = specific_machine_dir(machine.flake, machine.name)
|
||||
machine_dir = specific_machine_dir(machine)
|
||||
disk_schema: MachineDiskMatter | None = None
|
||||
disk_path = machine_dir / "disko.nix"
|
||||
if disk_path.exists():
|
||||
|
||||
@@ -9,7 +9,7 @@ from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from clan_cli.cmd import Log, RunOpts, run_no_stdout
|
||||
from clan_cli.cmd import Log, RunOpts, run
|
||||
from clan_cli.errors import ClanCmdError, ClanError
|
||||
from clan_cli.facts import public_modules as facts_public_modules
|
||||
from clan_cli.facts import secret_modules as facts_secret_modules
|
||||
@@ -188,7 +188,7 @@ class Machine:
|
||||
# however there is a soon to be merged PR that requires deployment
|
||||
# as root to match NixOS: https://github.com/nix-darwin/nix-darwin/pull/1341
|
||||
return json.loads(
|
||||
run_no_stdout(
|
||||
run(
|
||||
nix_eval(
|
||||
[
|
||||
f"{self.flake}#darwinConfigurations.{self.name}.options.system",
|
||||
|
||||
@@ -10,7 +10,7 @@ from contextlib import ExitStack
|
||||
from clan_lib.api import API
|
||||
|
||||
from clan_cli.async_run import AsyncContext, AsyncOpts, AsyncRuntime, is_async_cancelled
|
||||
from clan_cli.cmd import MsgColor, RunOpts, run
|
||||
from clan_cli.cmd import Log, MsgColor, RunOpts, run
|
||||
from clan_cli.colors import AnsiColor
|
||||
from clan_cli.completions import (
|
||||
add_dynamic_completer,
|
||||
@@ -141,7 +141,7 @@ def deploy_machine(machine: Machine) -> None:
|
||||
generate_facts([machine], service=None, regenerate=False)
|
||||
generate_vars([machine], generator_name=None, regenerate=False)
|
||||
|
||||
upload_secrets(machine, target_host)
|
||||
upload_secrets(machine)
|
||||
upload_secret_vars(machine, target_host)
|
||||
|
||||
path = upload_sources(machine, host)
|
||||
@@ -182,16 +182,16 @@ def deploy_machine(machine: Machine) -> None:
|
||||
remote_env = host.nix_ssh_env(None, local_ssh=False)
|
||||
ret = host.run(
|
||||
switch_cmd,
|
||||
RunOpts(check=False, msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
|
||||
RunOpts(
|
||||
check=False,
|
||||
log=Log.BOTH,
|
||||
msg_color=MsgColor(stderr=AnsiColor.DEFAULT),
|
||||
needs_user_terminal=True,
|
||||
),
|
||||
extra_env=remote_env,
|
||||
become_root=become_root,
|
||||
)
|
||||
|
||||
# Last output line (config store path) is printed to stdout instead of stderr
|
||||
lines = ret.stdout.splitlines()
|
||||
if lines:
|
||||
print(lines[-1])
|
||||
|
||||
if is_async_cancelled():
|
||||
return
|
||||
|
||||
@@ -206,6 +206,7 @@ def deploy_machine(machine: Machine) -> None:
|
||||
ret = host.run(
|
||||
test_cmd if is_mobile else switch_cmd,
|
||||
RunOpts(
|
||||
log=Log.BOTH,
|
||||
msg_color=MsgColor(stderr=AnsiColor.DEFAULT),
|
||||
needs_user_terminal=True,
|
||||
),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.cmd import run, run_no_stdout
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.dirs import nixpkgs_flake, nixpkgs_source
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.locked_open import locked_open
|
||||
@@ -55,7 +56,7 @@ def nix_add_to_gcroots(nix_path: Path, dest: Path) -> None:
|
||||
@cache
|
||||
def nix_config() -> dict[str, Any]:
|
||||
cmd = nix_command(["config", "show", "--json"])
|
||||
proc = run_no_stdout(cmd)
|
||||
proc = run(cmd)
|
||||
data = json.loads(proc.stdout)
|
||||
config = {}
|
||||
for key, value in data.items():
|
||||
@@ -131,7 +132,16 @@ class Packages:
|
||||
cls.static_packages = set(
|
||||
os.environ.get("CLAN_PROVIDED_PACKAGES", "").split(":")
|
||||
)
|
||||
return program in cls.static_packages
|
||||
|
||||
if program in cls.static_packages:
|
||||
if shutil.which(program) is None:
|
||||
log.warning(
|
||||
"Program %s is not in the path even though it should be shipped with clan",
|
||||
program,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Features:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"age-plugin-sss",
|
||||
"age-plugin-tpm",
|
||||
"age-plugin-yubikey",
|
||||
"age-plugin-1p",
|
||||
"avahi",
|
||||
"bash",
|
||||
"bubblewrap",
|
||||
|
||||
@@ -63,7 +63,8 @@ def upload(
|
||||
for mdir in dirs:
|
||||
dir_path = Path(root) / mdir
|
||||
tarinfo = tar.gettarinfo(
|
||||
dir_path, arcname=str(dir_path.relative_to(str(local_src)))
|
||||
dir_path,
|
||||
arcname=str(dir_path.relative_to(str(local_src))),
|
||||
)
|
||||
tarinfo.mode = dir_mode
|
||||
tarinfo.uname = file_user
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.cmd import RunOpts, run_no_stdout
|
||||
from clan_cli.cmd import RunOpts, run
|
||||
from clan_cli.completions import (
|
||||
add_dynamic_completer,
|
||||
complete_machines,
|
||||
@@ -32,7 +32,7 @@ def list_state_folders(machine: Machine, service: None | str = None) -> None:
|
||||
res = "{}"
|
||||
|
||||
try:
|
||||
proc = run_no_stdout(cmd, opts=RunOpts(prefix=machine.name))
|
||||
proc = run(cmd, RunOpts(prefix=machine.name))
|
||||
res = proc.stdout.strip()
|
||||
except ClanCmdError as e:
|
||||
msg = "Clan might not have meta attributes"
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.cmd import run_no_stdout
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.nix import nix_eval
|
||||
|
||||
@@ -18,7 +18,7 @@ def list_tagged_machines(flake_url: str | Path) -> dict[str, Any]:
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
proc = run_no_stdout(cmd)
|
||||
proc = run(cmd)
|
||||
|
||||
try:
|
||||
res = proc.stdout.strip()
|
||||
|
||||
@@ -10,8 +10,15 @@ from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
from clan_cli.dirs import TemplateType, clan_templates, nixpkgs_source
|
||||
from clan_cli.dirs import (
|
||||
TemplateType,
|
||||
clan_templates,
|
||||
nixpkgs_source,
|
||||
specific_machine_dir,
|
||||
)
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.locked_open import locked_open
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.nix import nix_test_store
|
||||
from clan_cli.tests import age_keys
|
||||
from clan_cli.tests.fixture_error import FixtureError
|
||||
@@ -70,11 +77,10 @@ class FlakeForTest(NamedTuple):
|
||||
|
||||
|
||||
def set_machine_settings(
|
||||
flake: Path,
|
||||
machine_name: str,
|
||||
machine: Machine,
|
||||
machine_settings: dict,
|
||||
) -> None:
|
||||
config_path = flake / "machines" / machine_name / "configuration.json"
|
||||
config_path = specific_machine_dir(machine) / "configuration.json"
|
||||
config_path.write_text(json.dumps(machine_settings, indent=2))
|
||||
|
||||
|
||||
@@ -202,7 +208,8 @@ class ClanFlake:
|
||||
}}
|
||||
"""
|
||||
)
|
||||
set_machine_settings(self.path, machine_name, machine_config)
|
||||
machine = Machine(name=machine_name, flake=Flake(str(self.path)))
|
||||
set_machine_settings(machine, machine_config)
|
||||
sp.run(["git", "add", "."], cwd=self.path, check=True)
|
||||
sp.run(
|
||||
["git", "commit", "-a", "-m", "Update by flake generator"],
|
||||
|
||||
@@ -11,7 +11,7 @@ from clan_cli.inventory import (
|
||||
set_inventory,
|
||||
)
|
||||
from clan_cli.machines.create import CreateOptions, create_machine
|
||||
from clan_cli.nix import nix_eval, run_no_stdout
|
||||
from clan_cli.nix import nix_eval, run
|
||||
from clan_cli.tests.fixtures_flakes import FlakeForTest
|
||||
from clan_lib.api.modules import list_modules
|
||||
|
||||
@@ -120,7 +120,7 @@ def test_add_module_to_inventory(
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
proc = run_no_stdout(cmd)
|
||||
proc = run(cmd)
|
||||
res = json.loads(proc.stdout.strip())
|
||||
|
||||
assert res["machine1"]["authorizedKeys"] == [ssh_key.decode()]
|
||||
|
||||
@@ -103,6 +103,7 @@ def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
|
||||
"--unshare-all",
|
||||
"--tmpfs", "/",
|
||||
"--ro-bind", "/nix/store", "/nix/store",
|
||||
"--ro-bind", "/bin/sh", "/bin/sh",
|
||||
*(["--ro-bind", str(test_store), str(test_store)] if test_store else []),
|
||||
"--dev", "/dev",
|
||||
# not allowed to bind procfs in some sandboxes
|
||||
|
||||
@@ -4,9 +4,10 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from clan_cli.cmd import RunOpts
|
||||
from clan_cli.cmd import RunOpts, run
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.nix import nix_shell, run_no_stdout
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.nix import nix_shell
|
||||
|
||||
from . import API
|
||||
|
||||
@@ -52,8 +53,8 @@ class Directory:
|
||||
|
||||
|
||||
@API.register
|
||||
def get_directory(current_path: str) -> Directory:
|
||||
curr_dir = Path(current_path)
|
||||
def get_directory(flake: Flake) -> Directory:
|
||||
curr_dir = flake.path
|
||||
directory = Directory(path=str(curr_dir))
|
||||
|
||||
if not curr_dir.is_dir():
|
||||
@@ -135,7 +136,7 @@ def show_block_devices() -> Blockdevices:
|
||||
"PATH,NAME,RM,SIZE,RO,MOUNTPOINTS,TYPE,ID-LINK",
|
||||
],
|
||||
)
|
||||
proc = run_no_stdout(cmd, RunOpts(needs_user_terminal=True))
|
||||
proc = run(cmd, RunOpts(needs_user_terminal=True))
|
||||
res = proc.stdout.strip()
|
||||
|
||||
blk_info: dict[str, Any] = json.loads(res)
|
||||
|
||||
@@ -7,9 +7,9 @@ from uuid import uuid4
|
||||
|
||||
from clan_cli.dirs import TemplateType, clan_templates
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.git import commit_file
|
||||
from clan_cli.machines.hardware import HardwareConfig, show_machine_hardware_config
|
||||
from clan_cli.machines.machines import Machine
|
||||
|
||||
from clan_lib.api import API
|
||||
from clan_lib.api.modules import Frontmatter, extract_frontmatter
|
||||
@@ -74,9 +74,7 @@ templates: dict[str, dict[str, Callable[[dict[str, Any]], Placeholder]]] = {
|
||||
|
||||
|
||||
@API.register
|
||||
def get_disk_schemas(
|
||||
flake: Flake, machine_name: str | None = None
|
||||
) -> dict[str, DiskSchema]:
|
||||
def get_disk_schemas(machine: Machine) -> dict[str, DiskSchema]:
|
||||
"""
|
||||
Get the available disk schemas
|
||||
"""
|
||||
@@ -84,13 +82,12 @@ def get_disk_schemas(
|
||||
disk_schemas = {}
|
||||
hw_report = {}
|
||||
|
||||
if machine_name is not None:
|
||||
hw_report_path = HardwareConfig.NIXOS_FACTER.config_path(flake, machine_name)
|
||||
if not hw_report_path.exists():
|
||||
msg = "Hardware configuration missing"
|
||||
raise ClanError(msg)
|
||||
with hw_report_path.open("r") as hw_report_file:
|
||||
hw_report = json.load(hw_report_file)
|
||||
hw_report_path = HardwareConfig.NIXOS_FACTER.config_path(machine)
|
||||
if not hw_report_path.exists():
|
||||
msg = "Hardware configuration missing"
|
||||
raise ClanError(msg)
|
||||
with hw_report_path.open("r") as hw_report_file:
|
||||
hw_report = json.load(hw_report_file)
|
||||
|
||||
for disk_template in disk_templates.iterdir():
|
||||
if disk_template.is_dir():
|
||||
@@ -130,8 +127,7 @@ class MachineDiskMatter(TypedDict):
|
||||
|
||||
@API.register
|
||||
def set_machine_disk_schema(
|
||||
flake: Flake,
|
||||
machine_name: str,
|
||||
machine: Machine,
|
||||
schema_name: str,
|
||||
# Placeholders are used to fill in the disk schema
|
||||
# Use get disk schemas to get the placeholders and their options
|
||||
@@ -142,8 +138,8 @@ def set_machine_disk_schema(
|
||||
Set the disk placeholders of the template
|
||||
"""
|
||||
# Assert the hw-config must exist before setting the disk
|
||||
hw_config = show_machine_hardware_config(flake, machine_name)
|
||||
hw_config_path = hw_config.config_path(flake, machine_name)
|
||||
hw_config = show_machine_hardware_config(machine)
|
||||
hw_config_path = hw_config.config_path(machine)
|
||||
|
||||
if not hw_config_path.exists():
|
||||
msg = "Hardware configuration must exist before applying disk schema"
|
||||
@@ -160,7 +156,7 @@ def set_machine_disk_schema(
|
||||
raise ClanError(msg)
|
||||
|
||||
# Check that the placeholders are valid
|
||||
disk_schema = get_disk_schemas(flake, machine_name)[schema_name]
|
||||
disk_schema = get_disk_schemas(machine)[schema_name]
|
||||
# check that all required placeholders are present
|
||||
for placeholder_name, schema_placeholder in disk_schema.placeholders.items():
|
||||
if schema_placeholder.required and placeholder_name not in placeholders:
|
||||
@@ -221,6 +217,6 @@ def set_machine_disk_schema(
|
||||
|
||||
commit_file(
|
||||
disko_file_path,
|
||||
flake.path,
|
||||
commit_message=f"Set disk schema of machine: {machine_name} to {schema_name}",
|
||||
machine.flake.path,
|
||||
commit_message=f"Set disk schema of machine: {machine.name} to {schema_name}",
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import argparse
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from clan_cli.cmd import run_no_stdout
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.nix import nix_shell
|
||||
|
||||
from . import API
|
||||
@@ -100,7 +100,7 @@ def show_mdns() -> DNSInfo:
|
||||
"--terminate",
|
||||
],
|
||||
)
|
||||
proc = run_no_stdout(cmd)
|
||||
proc = run(cmd)
|
||||
data = parse_avahi_output(proc.stdout)
|
||||
|
||||
return data
|
||||
|
||||
@@ -240,7 +240,7 @@ def test_clan_create_api(
|
||||
facter_json = test_lib_root / "assets" / "facter.json"
|
||||
assert facter_json.exists(), f"Source facter file not found: {facter_json}"
|
||||
|
||||
dest_dir = specific_machine_dir(Flake(str(clan_dir_flake.path)), machine.name)
|
||||
dest_dir = specific_machine_dir(machine)
|
||||
# specific_machine_dir should create the directory, but ensure it exists just in case
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -253,10 +253,7 @@ def test_clan_create_api(
|
||||
)
|
||||
|
||||
# ===== Create Disko Config ======
|
||||
facter_path = (
|
||||
specific_machine_dir(Flake(str(clan_dir_flake.path)), machine.name)
|
||||
/ "facter.json"
|
||||
)
|
||||
facter_path = specific_machine_dir(machine) / "facter.json"
|
||||
with facter_path.open("r") as f:
|
||||
facter_report = json.load(f)
|
||||
|
||||
@@ -265,7 +262,7 @@ def test_clan_create_api(
|
||||
assert disk_devs is not None
|
||||
|
||||
placeholders = {"mainDisk": disk_devs[0]}
|
||||
set_machine_disk_schema(clan_dir_flake, machine.name, "single-disk", placeholders)
|
||||
set_machine_disk_schema(machine, "single-disk", placeholders)
|
||||
clan_dir_flake.invalidate_cache()
|
||||
|
||||
with pytest.raises(ClanError) as exc_info:
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
classgen = pkgs.callPackage ./classgen { };
|
||||
zerotierone = pkgs.callPackage ./zerotierone { };
|
||||
webview-lib = pkgs.callPackage ./webview-lib { };
|
||||
update-clan-core-for-checks = pkgs.callPackage ./update-clan-core-for-checks { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
35
pkgs/update-clan-core-for-checks/default.nix
Normal file
35
pkgs/update-clan-core-for-checks/default.nix
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
writeShellApplication,
|
||||
git,
|
||||
jq,
|
||||
nix-prefetch-git,
|
||||
}:
|
||||
writeShellApplication {
|
||||
name = "update-clan-core-for-checks";
|
||||
runtimeInputs = [
|
||||
git
|
||||
jq
|
||||
nix-prefetch-git
|
||||
];
|
||||
text = ''
|
||||
reporoot=$(git rev-parse --show-toplevel)
|
||||
if [ -z "$reporoot" ]; then
|
||||
echo "Not in a git repository. Please run this script from the root of the repository."
|
||||
exit 1
|
||||
fi
|
||||
cd "$reporoot"
|
||||
# get latest commit of clan-core
|
||||
json=$(nix-prefetch-git "$(pwd)")
|
||||
sha256=$(jq -r '.sha256' <<< "$json")
|
||||
rev=$(jq -r '.rev' <<< "$json")
|
||||
|
||||
cat > ./checks/clan-core-for-checks.nix <<EOF
|
||||
{ fetchgit }:
|
||||
fetchgit {
|
||||
url = "https://git.clan.lol/clan/clan-core.git";
|
||||
rev = "$rev";
|
||||
sha256 = "$sha256";
|
||||
}
|
||||
EOF
|
||||
'';
|
||||
}
|
||||
@@ -22,7 +22,9 @@ export { clanList, setClanList };
|
||||
(async function () {
|
||||
const curr = activeURI();
|
||||
if (curr) {
|
||||
const result = await callApi("show_clan_meta", { uri: curr });
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: curr },
|
||||
});
|
||||
console.log("refetched meta for ", curr);
|
||||
if (result.status === "error") {
|
||||
result.errors.forEach((error) => {
|
||||
|
||||
@@ -52,7 +52,9 @@ export const Sidebar = (props: RouteSectionProps) => {
|
||||
queryFn: async () => {
|
||||
const curr = activeURI();
|
||||
if (curr) {
|
||||
const result = await callApi("show_clan_meta", { uri: curr });
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: curr },
|
||||
});
|
||||
console.log("refetched meta for ", curr);
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
|
||||
|
||||
@@ -329,7 +329,9 @@ export const ClanDetails = () => {
|
||||
const clanQuery = createQuery(() => ({
|
||||
queryKey: [clan_dir, "inventory", "meta"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_clan_meta", { uri: clan_dir });
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: clan_dir },
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
|
||||
@@ -18,7 +18,9 @@ const ClanItem = (props: ClanItemProps) => {
|
||||
const details = createQuery(() => ({
|
||||
queryKey: [clan_dir, "meta"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_clan_meta", { uri: clan_dir });
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: clan_dir },
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
|
||||
@@ -117,8 +117,10 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
if (shouldRunDisk) {
|
||||
setProgressText("Setting up disk ... (1/5)");
|
||||
const disk_response = await callApi("set_machine_disk_schema", {
|
||||
flake: { identifier: curr_uri },
|
||||
machine_name: props.name,
|
||||
machine: {
|
||||
flake: { identifier: curr_uri },
|
||||
name: props.name,
|
||||
},
|
||||
placeholders: diskValues.placeholders,
|
||||
schema_name: diskValues.schema,
|
||||
force: true,
|
||||
@@ -559,7 +561,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
variant="light"
|
||||
class="w-full"
|
||||
size="s"
|
||||
onClick={() => handleUpdate()}
|
||||
onClick={() => handleUpdateButton()}
|
||||
endIcon={<Icon size={12} icon="Update" />}
|
||||
>
|
||||
Update
|
||||
|
||||
@@ -37,10 +37,12 @@ export const DiskStep = (props: StepProps<DiskValues>) => {
|
||||
queryKey: [props.dir, props.machine_id, "disk_schemas"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("get_disk_schemas", {
|
||||
flake: {
|
||||
identifier: props.dir,
|
||||
machine: {
|
||||
flake: {
|
||||
identifier: props.dir,
|
||||
},
|
||||
name: props.machine_id,
|
||||
},
|
||||
machine_name: props.machine_id,
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
|
||||
@@ -52,10 +52,12 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
||||
queryKey: [props.dir, props.machine_id, "hw_report"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_machine_hardware_config", {
|
||||
flake: {
|
||||
identifier: props.dir,
|
||||
machine: {
|
||||
flake: {
|
||||
identifier: props.dir,
|
||||
},
|
||||
name: props.machine_id,
|
||||
},
|
||||
machine_name: props.machine_id,
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
@@ -85,9 +87,13 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
||||
setIsGenerating(true);
|
||||
const r = await callApi("generate_machine_hardware_info", {
|
||||
opts: {
|
||||
flake: { identifier: curr_uri },
|
||||
machine: props.machine_id,
|
||||
target_host: target,
|
||||
machine: {
|
||||
name: props.machine_id,
|
||||
override_target_host: target,
|
||||
flake: {
|
||||
identifier: curr_uri,
|
||||
},
|
||||
},
|
||||
backend: "nixos-facter",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ export const MachineListView: Component = () => {
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [view, setView] = makePersisted(createSignal<"list" | "grid">("list"), {
|
||||
const [view, setView] = makePersisted(createSignal<"list" | "grid">("grid"), {
|
||||
name: "machines_view",
|
||||
storage: localStorage,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user