Compare commits

..

2 Commits

Author SHA1 Message Date
Jörg Thalheim
160f7d2cf5 Revert "Merge pull request 'Fix deploying with sudo + password' (#3470) from target-host into main"
This reverts commit 8a849eb90f, reversing
changes made to 3b5c22ebcf.
2025-05-04 13:37:09 +02:00
Jörg Thalheim
4c9aaa09d5 fix ssh control master check 2025-05-04 13:36:55 +02:00
151 changed files with 4082 additions and 8589 deletions

View File

@@ -1,29 +0,0 @@
name: "Update pinned clan-core for checks"
on:
repository_dispatch:
workflow_dispatch:
schedule:
- cron: "51 2 * * *"
jobs:
update-pinned-clan-core:
runs-on: nix
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Update clan-core for checks
run: nix run .#update-clan-core-for-checks
- name: Create pull request
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"

1
.gitignore vendored
View File

@@ -14,7 +14,6 @@ example_clan
nixos.qcow2 nixos.qcow2
**/*.glade~ **/*.glade~
/docs/out /docs/out
/pkgs/clan-cli/clan_cli/select
**/.local.env **/.local.env
# MacOS stuff # MacOS stuff

View File

@@ -1,2 +0,0 @@
nixosModules/clanCore/vars/.* @lopter
pkgs/clan-cli/clan_cli/(secrets|vars)/.* @lopter

View File

@@ -147,7 +147,25 @@
perSystem = perSystem =
{ pkgs, ... }: { pkgs, ... }:
let let
clanCore = self.checks.x86_64-linux.clan-core-for-checks; 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"
];
};
in in
{ {
checks = pkgs.lib.mkIf pkgs.stdenv.isLinux { checks = pkgs.lib.mkIf pkgs.stdenv.isLinux {
@@ -164,6 +182,11 @@
# import the inventory generated nixosModules # import the inventory generated nixosModules
self.clanInternals.inventoryClass.machines.test-backup.machineImports; self.clanInternals.inventoryClass.machines.test-backup.machineImports;
clan.core.settings.directory = ./.; clan.core.settings.directory = ./.;
environment.systemPackages = [
(pkgs.writeShellScriptBin "foo" ''
echo ${clanCore}
'')
];
}; };
testScript = '' testScript = ''

View File

@@ -1,6 +0,0 @@
{ fetchgit }:
fetchgit {
url = "https://git.clan.lol/clan/clan-core.git";
rev = "1e8b9def2a021877342491ca1f4c45533a580759";
sha256 = "0f12vwr1abwa1iwjbb5z5xx8jlh80d9njwdm6iaw1z1h2m76xgzc";
}

View File

@@ -26,7 +26,6 @@ clanLib.test.makeTestClan {
roles.admin.machines = [ "admin1" ]; roles.admin.machines = [ "admin1" ];
}; };
}; };
instances."test" = { instances."test" = {
module.name = "new-service"; module.name = "new-service";
roles.peer.machines.peer1 = { }; roles.peer.machines.peer1 = { };
@@ -34,33 +33,25 @@ clanLib.test.makeTestClan {
modules = { modules = {
legacy-module = ./legacy-module; legacy-module = ./legacy-module;
}; new-service = {
}; _class = "clan.service";
modules.new-service = { manifest.name = "new-service";
_class = "clan.service"; roles.peer = { };
manifest.name = "new-service"; perMachine = {
roles.peer = { }; nixosModule = {
perMachine = { # This should be generated by:
nixosModule = { # ./pkgs/scripts/update-vars.py
# This should be generated by: clan.core.vars.generators.new-service = {
# nix run .#generate-test-vars -- checks/dummy-inventory-test dummy-inventory-test files.hello = {
clan.core.vars.generators.new-service = { secret = false;
files.not-a-secret = { deploy = true;
secret = false; };
deploy = true; script = ''
# This is a dummy script that does nothing
echo "This is a dummy script" > $out/hello
'';
};
}; };
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
'';
}; };
}; };
}; };
@@ -78,15 +69,7 @@ clanLib.test.makeTestClan {
print(peer1.succeed("systemctl status dummy-service")) print(peer1.succeed("systemctl status dummy-service"))
# peer1 should have the 'hello' file # peer1 should have the 'hello' file
peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.not-a-secret.path}") peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.hello.path}")
ls_out = peer1.succeed("ls -la ${nodes.peer1.clan.core.vars.generators.new-service.files.a-secret.path}")
# Check that the file is owned by 'nobody'
assert "nobody" in ls_out, f"File is not owned by 'nobody': {ls_out}"
# Check that the file is in the 'users' group
assert "users" in ls_out, f"File is not in the 'users' group: {ls_out}"
# Check that the file is in the '0644' mode
assert "-rw-r--r--" in ls_out, f"File is not in the '0644' mode: {ls_out}"
''; '';
} }
); );

View File

@@ -1,6 +1,6 @@
[ [
{ {
"publickey": "age12yt078p9ewxy2sh0a36nxdpgglv8wqqftmj4dkj9rgy5fuyn4p0q5nje9m", "publickey": "age1hd2exjq88h7538y6mvjvexx3u5gp6a03yfn5nj32h2667yyksyaqcuk5qs",
"type": "age" "type": "age"
} }
] ]

View File

@@ -1,6 +1,6 @@
[ [
{ {
"publickey": "age12w2ld4vxfyf3hdq2d8la4cu0tye4pq97egvv3me4wary7xkdnq2snh0zx2", "publickey": "age19urkt89q45a2wk6a4yaramzufjtnw6nq2snls0v7hmf7tqf73axsfx50tk",
"type": "age" "type": "age"
} }
] ]

View File

@@ -1,15 +1,15 @@
{ {
"data": "ENC[AES256_GCM,data:GPpsUhSzWPtTP8EUNKsobFXjYqDldhkkIH6hBk11RsDLAGWdhVrwcISGbhsWpYhvAdPKA84DB6Zqyh9lL2bLM9//ybC1kzY20BQ=,iv:NrxMLdedT2FCkUAD00SwsAHchIsxWvqe7BQekWuJcxw=,tag:pMDXcMyHnLF2t3Qhb1KolA==,type:str]", "data": "ENC[AES256_GCM,data:hhuFgZcPqht0h3tKxGtheS4GlrVDo4TxH0a9lxgPYj2i12QUmE04rB07A+hu4Z8WNWLYvdM5069mEOZYm3lSeTzBHQPxYZRuVj0=,iv:sA1srRFQqsMlJTAjFcb09tI/Jg2WjOVJL5NZkPwiLoU=,tag:6xXo9FZpmAJw6hCBsWzf8Q==,type:str]",
"sops": { "sops": {
"age": [ "age": [
{ {
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzb2tWb1ExKzdmUTRzaGVj\nK3cyYTBHZTJwVjM1SzUvbHFiMnVhY05iKzFZCnJTSE1VSVdpcUFLSEJuaE1CZzJD\nWjZxYzN2cUltdThNMVRKU3FIb20vUXMKLS0tIFlHQXRIdnMybDZFUVEzWlQrc1dw\nbUxhZURXblhHd0pka0JIK1FTZEVqdUEKI/rfxQRBc+xGRelhswkJQ9GcZs6lzfgy\nuCxS5JI9npdPLQ/131F3b21+sP5YWqks41uZG+vslM1zQ+BlENNhDw==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGaGVHeTgrN3dJQ2VITFBM\neWVzbDhjb0pwNUhBUjdUc0p5OTVta1dvSno4ClJxeUc4Z0hiaFRkVlJ1YTA4Lyta\neWdwV005WGYvMUNRVG1qOVdicTk0NUkKLS0tIFQvaDNFS1JMSFlHRXlhc3lsZm03\nYVhDaHNsam5wN1VqdzA3WTZwM1JwV2sKZk/SiZJgjllADdfHLSWuQcU4+LttDpt/\nqqDUATEuqYaALljC/y3COT+grTM2bwGjj6fsfsfiO/EL9iwzD3+7oA==\n-----END AGE ENCRYPTED FILE-----\n"
} }
], ],
"lastmodified": "2025-05-04T12:44:13Z", "lastmodified": "2025-04-09T15:10:16Z",
"mac": "ENC[AES256_GCM,data:fWxLHXBWolHVxv6Q7utcy6OVLV13ziswrIYyNKiwy1vsU8i7xvvuGO1HlnE+q43D2WuHR53liKq1UHuf1JMrWzTwZ0PYe+CVugtoEtbR2qu3rK/jAkOyMyhmmHzmf6Rp4ZMCzKgZeC/X2bDKY/z0firHAvjWydEyogutHpvtznM=,iv:OQI3FfkLneqbdztAXVQB3UkHwDPK+0hWu5hZ9m8Oczg=,tag:em6GfS2QHsXs391QKPxfmA==,type:str]", "mac": "ENC[AES256_GCM,data:xuXj4833G6nhvcRo2ekDxz8G5phltmU8h1GgGofH9WndzrqLKeRSqm/n03IHRW0f4F68XxnyAkfvokVh6vW3LRQAFkqIlXz5U4+zFNcaVaPobS5gHTgxsCoTUoalWPvHWtXd50hUVXeAt8rPfTfeveVGja8bOERk8mvwUPxb6h4=,iv:yP1usA9m8tKl6Z/UK9PaVMJlZlF5qpY4EiM4+ByVlik=,tag:8DgoIhLstp3MRki90VfEvw==,type:str]",
"unencrypted_suffix": "_unencrypted", "unencrypted_suffix": "_unencrypted",
"version": "3.10.2" "version": "3.10.1"
} }
} }

View File

@@ -1,15 +1,15 @@
{ {
"data": "ENC[AES256_GCM,data:W3cOkUYL5/YulW2pEISyTlMaA/t7/WBE7BoCdFlqrqgaCL7tG4IV2HgjiPWzIVMs0zvDSaghdEvAIoB4wOf470d1nSWs0/E8SDk=,iv:wXXaZIw3sPY8L/wxsu7+C5v+d3RQRuwxZRP4YLkS8K4=,tag:HeK4okj7O7XDA9JDz2KULw==,type:str]", "data": "ENC[AES256_GCM,data:rwPhbayGf6mE1E9NCN+LuL7VfWWOfhoJW6H2tNSoyebtyTpM3GO2jWca1+N7hI0juhNkUk+rIsYQYbCa/5DZQiV0/2Jgu4US1XY=,iv:B5mcaQsDjb6BacxGB4Kk88/qLCpVOjQNRvGN+fgUiEo=,tag:Uz0A8kAF5NzFetbv9yHIjQ==,type:str]",
"sops": { "sops": {
"age": [ "age": [
{ {
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxRC83b3dtSVpXcGovNnVs\nTzFka2J2MEFhYkF1ajVrdjMrNUtPWGRObjM4Cm5zSUR5OGw0T0FaL3BaWmR6L29W\nU2syMFIyMUhFRUZpWFpCT28vWko2ZU0KLS0tIFpHK3BjU1V1L0FrMGtwTGFuU3Mz\nRkV5VjI2Vndod202bUR3RWQwNXpmVzQKNk8/y7M62wTIIKqY4r3ZRk5aUCRUfine\n1LUSHMKa2bRe+hR7nS7AF4BGXp03h2UPY0FP5+U5q8XuIj1jfMX8kg==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWY0hKQ1dnV0tMYytDMCtj\nTDV4Zk5NeVN0bCtqaWRQV3d4M0VlcGVZMkhZCm02dHZyOGVlYzJ5Z3FlUWNXMVQ0\nb2ZrTXZQRzRNdzFDeWZCVGhlTS9rMm8KLS0tIEJkY1QwOENRYWw3cjIwd3I0bzdz\nOEtQNm1saE5wNWt2UUVnYlN4NWtGdFkKmWHU5ttZoQ3NZu/zkX5VxfC2sMpSOyod\neb7LRhFqPfo5N1XphJcCqr5QUoZOfnH0xFhZ2lxWUS3ItiRpU4VDwg==\n-----END AGE ENCRYPTED FILE-----\n"
} }
], ],
"lastmodified": "2025-05-04T12:44:16Z", "lastmodified": "2025-04-09T15:10:41Z",
"mac": "ENC[AES256_GCM,data:yTkQeFvKrN1+5FP+yInsaRWSAG+ZGG0uWF3+gVRvzJTFxab8kT2XkAMc+4D7SKgcjsmwBBb77GNoAKaKByhZ92UaCfZ2X66i7ZmYUwLM1NVVmm+xiwwjsh7PJXlZO/70anTzd1evtlZse0jEmRnV5Y0F0M6YqXmuwU+qGUJU2F8=,iv:sy6ozhXonWVruaQfa7pdEoV5GkNZR/UbbINKAPbgWeg=,tag:VMruQ1KExmlMR7TsGNgMlg==,type:str]", "mac": "ENC[AES256_GCM,data:pab0G2GPjgs59sbiZ8XIV5SdRtq5NPU0yq18FcqiMV8noAL94fyVAY7fb+9HILQWQsEjcykgk9mA2MQ0KpK/XG8+tDQKcBH+F+2aQnw5GJevXmfi7KLTU0P224SNo7EnKlfFruB/+NZ0WBtkbbg1OzekrbplchpSI6BxWz/jASE=,iv:TCj9FCxgfMF2+PJejr67zgGnF+CFS+YeJiejnHbf7j0=,tag:s7r9SqxeqpAkncohYvIQ2Q==,type:str]",
"unencrypted_suffix": "_unencrypted", "unencrypted_suffix": "_unencrypted",
"version": "3.10.2" "version": "3.10.1"
} }
} }

View File

@@ -1,19 +1,19 @@
{ {
"data": "ENC[AES256_GCM,data:T8edCvw=,iv:7/G5xt5fv38I9uFzk7WMIr9xQdz/6lFxqOC+18HBg8Q=,tag:F39Cxbgmzml+lZLsZ59Kmg==,type:str]", "data": "ENC[AES256_GCM,data:bxM9aYMK,iv:SMNYtk9FSyZ1PIfEzayTKKdCnZWdhcyUEiTwFUNb988=,tag:qJYW4+VQyhF1tGPQPTKlOQ==,type:str]",
"sops": { "sops": {
"age": [ "age": [
{ {
"recipient": "age12yt078p9ewxy2sh0a36nxdpgglv8wqqftmj4dkj9rgy5fuyn4p0q5nje9m", "recipient": "age1hd2exjq88h7538y6mvjvexx3u5gp6a03yfn5nj32h2667yyksyaqcuk5qs",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPNUhiYkZWK3dPMHNiRTVM\nRHNvaHFsOFp1c0UxQitwVG0zY01MNDZRV1E4CjEybENoTVIzN29vQ3FtUTRSYmFU\nNXIzQllVSllXRGN2M1B6WXJLdHZSajgKLS0tIDllZ0ZmZUcxMHhDQUpUOEdWbmkv\neUQweHArYTdFSmNteVpuQ3BKdnh0Y0UKs8Hm3D+rXRRfpUVSZM3zYjs6b9z8g10D\nGTkvreUMim4CS22pjdQ3eNA9TGeDXfWXE7XzwXLCb+wVcf7KwbDmvg==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvZDZYYXdpcXVqRFRnQ2Jx\nTFhFWEJTR290cHZhTXZadFFvcHM4MHVIN3lFCmJhOEZrL3g4TFBZVllxdDFZakJn\nR3NxdXo0eE8vTDh3QlhWOFpVZ0lNUHcKLS0tIEE4dkpCalNzaXJ0Qks3VHJSUzZF\nb2N3NGdjNHJnSUN6bW8welZ1VDdJakEKGKZ7nn1p11IyJB6DMxu2HJMvZ+0+5WpE\nPLWh2NlGJO3XrrL4Fw7xetwbqE+QUZPNl/JbEbu4KLIUGLjqk9JDhQ==\n-----END AGE ENCRYPTED FILE-----\n"
}, },
{ {
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKSDhpT3cvck9PenZYVEZH\ndFQreVRBdG93L1dBUGlvYjFWcDlHWUJsZUVBCm9DMTJ4UytiYzlEVHNWdUcwS1ds\nT0dhbzAzNDdmbDBCU0dvL2xNeHpXcGsKLS0tIFArbmpsbzU3WnpJdUt1MGN0L1d0\nV1JkTDJYWUxsbmhTQVNOeVRaSUhTODQKk9Vph2eldS5nwuvVX0SCsxEm4B+sO76Z\ndIjJ3OQxzoZmXMaOOuKHC5U0Y75Qn7eXC43w5KHsl2CMIUYsBGJOZw==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHckJCQVFyb21aT1R0d2Rr\nMWxNMHVqcGxabHBmS0RibW9sN0gyZDI1b1dFCnRWUk5LSWdxV3c4RWVZdUtEN1Fv\nRk4xVmwwT2xrdWVERkJXUVVlVXJjTVUKLS0tIC9ERG9KMGxTNEsrbzFHUGRiVUlm\nRi9qakxoc1FOVVV1TkUrckwxRUVnajQKE8ms/np2NMswden3xkjdC8cXccASLOoN\nu+EaEk69UvBvnOg9VBjyPAraIKgNrTc4WWwz+DOBj1pCwVbu9XxUlA==\n-----END AGE ENCRYPTED FILE-----\n"
} }
], ],
"lastmodified": "2025-05-04T12:44:14Z", "lastmodified": "2025-04-09T15:10:30Z",
"mac": "ENC[AES256_GCM,data:6fKrS1eLLUWlHkQpxLFXBRk6f2wa5ADLMViVvYXXGU24ayl9UuNSKrCRHp9cbzhqhti3HdwyNt6TM+2X6qhiiAQanKEB2PF7JRYX74NfNKil9BEDjt5AqqtpSgVv5l7Ku/uSHaPkd2sDmzHsy5Q4bSGxJQokStk1kidrwle+mbc=,iv:I/Aad82L/TCxStM8d8IZICUrwdjRbGx2fuGWqexr21o=,tag:BfgRbGUxhPZzK2fLik1kxA==,type:str]", "mac": "ENC[AES256_GCM,data:cIwWctUbAFI8TRMxYWy5xqlKDVLMqBIxVv4LInnLqi3AauL0rJ3Z7AxK/wb2dCQM07E1N7YaORNqgUpFC1xo0hObAA8mrPaToPotKDkjua0zuyTUNS1COoraYjZpI/LKwmik/qtk399LMhiC7aHs+IliT9Dd41B8LSMBXwdMldY=,iv:sZ+//BrYH5Ay2JJAGs7K+WfO2ASK82syDlilQjGmgFs=,tag:nY+Af9eQRLwkiHZe85dQ9A==,type:str]",
"unencrypted_suffix": "_unencrypted", "unencrypted_suffix": "_unencrypted",
"version": "3.10.2" "version": "3.10.1"
} }
} }

View File

@@ -1,19 +1,19 @@
{ {
"data": "ENC[AES256_GCM,data:vp0yW0Gt,iv:FO2cy+UpEl5aRay/LUGu//c82QiVxuKuGSaVh0rGJvc=,tag:vf2RAOPpcRW0HwxHoGy17A==,type:str]", "data": "ENC[AES256_GCM,data:ImlGIKxE,iv:UUWxjLNRKJCD2WHNpw8lfvCc8rnXPCqc2pni1ODckjE=,tag:HFCqiv31E9bShIIaAEjF0A==,type:str]",
"sops": { "sops": {
"age": [ "age": [
{ {
"recipient": "age12w2ld4vxfyf3hdq2d8la4cu0tye4pq97egvv3me4wary7xkdnq2snh0zx2", "recipient": "age19urkt89q45a2wk6a4yaramzufjtnw6nq2snls0v7hmf7tqf73axsfx50tk",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjaFVNMEd2YUxpSm5XVVRi\nY2ZUc3NTOStJUFNMWWVPQTgxZ2tCK1QrMW1ZCjYwMlA4dkIzSlc0TGtvZjcyK3Bi\nM3pob2JOOFUyeVJ6M2JpaTRCZlc1R0kKLS0tIDJMb1dFcVRWckhwYWNCQng0RlFO\nTkw3OGt4dkFIZVY5aVEzZE5mMzJSM0EKUv8bUqg48L2FfYVUVlpXvyZvPye699of\nG6PcjLh1ZMbNCfnsCzr+P8Vdk/F4J/ifxL66lRGfu2xOLxwciwQ+5Q==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpTEROZjh6NjBhSlJSc1Av\nSHhjdkhwVUd3VzBZemhQb3dhMlJXalBmZlFjCkZPYkhZZGVOVTNjUWdFU0s4cWFn\nL2NXbkRCdUlMdElnK2lGbG5iV0w1cHMKLS0tIFREcmxDdHlUNVBFVGRVZSt0c0E5\nbnpHaW1Vb3R3ZFFnZVMxY3djSjJmOU0KIwqCSQf5S9oA59BXu7yC/V6yqvCh88pa\nYgmNyBjulytPh1aAfOuNWIGdIxBpcEf+gFjz3EiJY9Kft3fTmhp2bw==\n-----END AGE ENCRYPTED FILE-----\n"
}, },
{ {
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg", "recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnZ2dDbVhoQngxM3lTSmZF\nUTAwS1lCTGhEMU1GVXpFUzlIUFdqZy9LajF3Ck9mdVpBRjlyVUNhZXZIUFZjUzF1\nNlhFN28vNmwzcUVkNmlzUnpkWjJuZE0KLS0tIHpXVHVlNk9vU1ZPTGRrYStWbmRO\nbDM4U2o1SlEwYWtqOXBqd3BFUTAvMHcKkI8UVd0v+x+ELZ5CoGq9DzlA6DnVNU2r\nrV9wLfbFd7RHxS0/TYZh5tmU42nO3iMYA9FqERQXCtZgXS9KvfqHwQ==\n-----END AGE ENCRYPTED FILE-----\n" "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArN3R4TThibjdYbE9TMDE1\naUhuNDlscExjaktIR2VmTk1OMWtVM0NpTUJZClJUNEcwVDlibExWQk84TTNEWFhp\nMjYyZStHc1N0ZTh1S3VTVk45WGxlWWMKLS0tIHFab25LY1R1d1l6NE5XbHJvQ3lj\nNGsxUldFVHQ5RVJERDlGbi9NY29hNWsKENBTcAS/R/dTGRYdaWv5Mc/YG4bkah5w\nb421ZMQF+r4CYnzUqnwivTG8TMRMqJLavfkutE6ZUfJbbLufrTk5Lw==\n-----END AGE ENCRYPTED FILE-----\n"
} }
], ],
"lastmodified": "2025-05-04T12:44:18Z", "lastmodified": "2025-04-09T15:11:04Z",
"mac": "ENC[AES256_GCM,data:1ZZ+ZI1JsHmxTov1bRijfol3kTkyheg2o3ivLsMHRhCmScsUry97hQJchF78+y2Izt7avaQEHYn6pVbYt/0rLrSYD7Ru7ITVxXoYHOiN5Qb98masUzpibZjrdyg5nO+LW5/Hmmwsc3yn/+o3IH1AUYpsxlJRdnHHCmoSOFaiFFM=,iv:OQlgmpOTw4ljujNzqwQ5/0Mz8pQpCSUtqRvj3FJAxDs=,tag:foZvdeW7gK9ZVKkWqnlxGA==,type:str]", "mac": "ENC[AES256_GCM,data:JdJzocQZWVprOmZ4Ni04k1tpD1TpFcK5neKy3+0/c3+uPBwjwaMayISKRaa/ILUXlalg60oTqxB4fUFoYVm8KGQVhDwPhO/T1hyYVQqidonrcYfJfCYg00mVSREV/AWqXb7RTnaEBfrdnRJvaAQF9g2qDXGVgzp3eACdlItclv4=,iv:nOw1jQjIWHWwU3SiKpuQgMKXyu8MZYI+zI9UYYd9fCI=,tag:ewUkemIPm/5PkmuUD0EcAQ==,type:str]",
"unencrypted_suffix": "_unencrypted", "unencrypted_suffix": "_unencrypted",
"version": "3.10.2" "version": "3.10.1"
} }
} }

View File

@@ -1 +0,0 @@
../../../../../../sops/machines/peer1

View File

@@ -1,19 +0,0 @@
{
"data": "ENC[AES256_GCM,data:prFl0EJy8bM=,iv:zITWxf+6Ebk0iB5vhhd7SBQa1HFrIJXm8xpSM+D9I0M=,tag:NZCRMCs1SzNKLBu/KUDKMQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age12w2ld4vxfyf3hdq2d8la4cu0tye4pq97egvv3me4wary7xkdnq2snh0zx2",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0S0RZRWxaZVZvTUhjdWVL\naU9WZmtEcm1qa2JsRmdvdmZmNENMaWFEVUFRCmdoVnRXSGlpRlFjNmVVbDJ5VnFT\nMnVJUlVnM3lxNmZCRTdoRVJ4NW1oYWcKLS0tIFFNbXBFUk1RWnlUTW1SeG1vYzlM\nVVpEclFVOE9PWWQxVkZ0eEgwWndoRWcKDAOHe+FIxqGsc6LhxMy164qjwG6t2Ei2\nP0FSs+bcKMDpudxeuxCjnDm/VoLxOWeuqkB+9K2vSm2W/c/fHTSbrA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2VU5jOEpwYUtDVEVFcVpU\nQkExTVZ3ejZHcGo5TG8zdUQwNktoV09WdUZvCmQ0dE1TOWRFbTlxdVd4WWRxd3VF\nQUNTTkNNT3NKYjQ5dEJDY0xVZ3pZVUUKLS0tIDFjajRZNFJZUTdNeS8yN05FMFZU\ncEtjRjhRbGE0MnRLdk10NkFLMkxqencKGzJ66dHluIghH04RV/FccfEQP07yqnfb\n25Hi0XIVJfXBwje4UEyszrWTxPPwVXdQDQmoNKf76Qy2jYqJ56uksw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-05-04T12:44:20Z",
"mac": "ENC[AES256_GCM,data:FIkilsni5kOdNlVwDuLsQ/zExypHRWdqIBQDNWMLTwe8OrsNPkX+KYutUvt9GaSoGv4iDULaMRoizO/OZUNfc2d8XYSdj0cxOG1Joov4GPUcC/UGyNuQneAejZBKolvlnidKZArofnuK9g+lOTANEUtEXUTnx8L+VahqPZayQas=,iv:NAo6sT3L8OOB3wv1pjr3RY2FwXgVmZ4N0F4BEX4YPUY=,tag:zHwmXygyvkdpASZCodQT9Q==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
This is a dummy script

View File

@@ -14,14 +14,13 @@ in
./installation/flake-module.nix ./installation/flake-module.nix
./morph/flake-module.nix ./morph/flake-module.nix
./nixos-documentation/flake-module.nix ./nixos-documentation/flake-module.nix
./dont-depend-on-repo-root.nix ./sanity-checks/dont-depend-on-repo-root.nix
]; ];
perSystem = perSystem =
{ {
pkgs, pkgs,
lib, lib,
self', self',
system,
... ...
}: }:
{ {
@@ -84,10 +83,7 @@ in
schema = schema =
(self.clanLib.inventory.evalClanService { (self.clanLib.inventory.evalClanService {
modules = [ m ]; modules = [ m ];
prefix = [ key = "checks";
"checks"
system
];
}).config.result.api.schema; }).config.result.api.schema;
in in
schema schema
@@ -101,12 +97,6 @@ in
mkdir -p $out mkdir -p $out
cat $schemaFile > $out/allSchemas.json 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 = { legacyPackages = {
nixosTests = nixosTests =

View File

@@ -43,7 +43,6 @@
let let
dependencies = [ dependencies = [
pkgs.disko 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.ConfigIniFiles
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp
@@ -81,7 +80,7 @@
# Some distros like to automount disks with spaces # 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('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdb && mount /dev/vdb "/mnt/with spaces"')
machine.succeed("clan flash write --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdb test-flash-machine-${pkgs.hostPlatform.system}") machine.succeed("clan flash write --debug --flake ${../..} --yes --disk main /dev/vdb test-flash-machine-${pkgs.hostPlatform.system}")
''; '';
} { inherit pkgs self; }; } { inherit pkgs self; };
}; };

View File

@@ -15,7 +15,6 @@ let
pkgs.bash.drvPath pkgs.bash.drvPath
pkgs.nixos-anywhere pkgs.nixos-anywhere
pkgs.bubblewrap pkgs.bubblewrap
pkgs.buildPackages.xorg.lndir
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs); ] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; }; closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in in
@@ -198,7 +197,7 @@ in
installer.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../assets/ssh/privkey} /root/.ssh/id_ed25519") 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.wait_until_succeeds("timeout 2 ssh -o StrictHostKeyChecking=accept-new -v nonrootuser@localhost hostname")
installer.succeed("cp -r ${self.checks.x86_64-linux.clan-core-for-checks} test-flake && chmod -R +w test-flake") installer.succeed("cp -r ${../..} 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.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() installer.shutdown()
@@ -218,7 +217,7 @@ in
installer.start() installer.start()
installer.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../assets/ssh/privkey} /root/.ssh/id_ed25519") 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.wait_until_succeeds("timeout 2 ssh -o StrictHostKeyChecking=accept-new -v nonrootuser@localhost hostname")
installer.succeed("cp -r ${self.checks.x86_64-linux.clan-core-for-checks} test-flake && chmod -R +w test-flake") installer.succeed("cp -r ${../..} 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/hardware-configuration.nix")
installer.fail("test -f test-flake/machines/test-install-machine/facter.json") installer.fail("test -f test-flake/machines/test-install-machine/facter.json")

View File

@@ -32,6 +32,7 @@
{ pkgs, ... }: { pkgs, ... }:
let let
dependencies = [ dependencies = [
self
pkgs.stdenv.drvPath pkgs.stdenv.drvPath
pkgs.stdenvNoCC pkgs.stdenvNoCC
self.nixosConfigurations.test-morph-machine.config.system.build.toplevel self.nixosConfigurations.test-morph-machine.config.system.build.toplevel
@@ -54,7 +55,7 @@
testScript = '' testScript = ''
start_all() start_all()
actual.fail("cat /etc/testfile") actual.fail("cat /etc/testfile")
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") actual.succeed("env CLAN_DIR=${self} 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" assert actual.succeed("cat /etc/testfile") == "morphed"
''; '';
} { inherit pkgs self; }; } { inherit pkgs self; };

View File

@@ -105,7 +105,10 @@ in
private_key = { private_key = {
inherit owner; inherit owner;
}; };
public_key.secret = false; public_key = {
inherit owner;
secret = false;
};
}; };
runtimeInputs = [ runtimeInputs = [
@@ -131,7 +134,10 @@ in
private_key = { private_key = {
inherit owner; inherit owner;
}; };
public_key.secret = false; public_key = {
inherit owner;
secret = false;
};
}; };
runtimeInputs = [ runtimeInputs = [

View File

@@ -4,15 +4,7 @@
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/hello-word"; manifest.name = "clan-core/hello-word";
roles.peer = { roles.peer = { };
interface =
{ lib, ... }:
{
options.foo = lib.mkOption {
type = lib.types.str;
};
};
};
perMachine = perMachine =
{ machine, ... }: { machine, ... }:

View File

@@ -10,6 +10,9 @@ let
}; };
in in
{ {
clan.inventory.modules = {
hello-world = module;
};
clan.modules = { clan.modules = {
hello-world = module; hello-world = module;
}; };
@@ -47,7 +50,6 @@ in
hello-service = import ./tests/vm/default.nix { hello-service = import ./tests/vm/default.nix {
inherit module; inherit module;
inherit self inputs pkgs; inherit self inputs pkgs;
# clanLib is exposed from inputs.clan-core
clanLib = self.clanLib; clanLib = self.clanLib;
}; };
}; };

View File

@@ -18,7 +18,7 @@ let
}; };
# Register the module for the test # Register the module for the test
modules.hello-world = module; inventory.modules.hello-world = module;
# Use the module in the test # Use the module in the test
inventory.instances = { inventory.instances = {

View File

@@ -14,9 +14,6 @@ clanLib.test.makeTestClan {
clan = { clan = {
directory = ./.; directory = ./.;
modules = {
hello-service = module;
};
inventory = { inventory = {
machines.peer1 = { }; machines.peer1 = { };
@@ -24,6 +21,10 @@ clanLib.test.makeTestClan {
module.name = "hello-service"; module.name = "hello-service";
roles.peer.machines.peer1 = { }; roles.peer.machines.peer1 = { };
}; };
modules = {
hello-service = module;
};
}; };
}; };

View File

@@ -19,7 +19,7 @@ We might not be sure whether all of those will exist but the architecture should
## Decision ## Decision
This leads to the conclusion that we should do `library` centric development. This leads to the conclusion that we should do `library` centric development.
With the current `clan` python code being a library that can be imported to create various tools ontop of it. With the current `clan` python code beeing a library that can be imported to create various tools ontop of it.
All **CLI** or **UI** related parts should be moved out of the main library. All **CLI** or **UI** related parts should be moved out of the main library.
*Note: The next person who wants implement any new frontend should do this first. Currently it looks like the TUI is the next one.* *Note: The next person who wants implement any new frontend should do this first. Currently it looks like the TUI is the next one.*

View File

@@ -1,47 +0,0 @@
# ADR Numbering process
## Status
Proposed after some conversation between @lassulus, @Mic92, & @lopter.
## Context
It can be useful to refer to ADRs by their numbers, rather than their full title. To that end, short and sequential numbers are useful.
The issue is that an ADR number is effectively assigned when the ADR is merged, before being merged its number is provisional. Because multiple ADRs can be written at the same time, you end-up with multiple provisional ADRs with the same number, for example this is the third ADR-3:
1. ADR-3-clan-compat: see [#3212];
2. ADR-3-fetching-nix-from-python: see [#3452];
3. ADR-3-numbering-process: this ADR.
This situation makes it impossible to refer to an ADR by its number, and why I (@lopter) went with the arbitrary number 7 in [#3196].
We could solve this problem by using the PR number as the ADR number (@lassulus). The issue is that PR numbers are getting big in clan-core which does not make them easy to remember, or use in conversation and code (@lopter).
Another approach would be to move the ADRs in a different repository, this would reset the counter back to 1, and make it straightforward to keep ADR and PR numbers in sync (@lopter). The issue then is that ADR are not in context with their changes which makes them more difficult to review (@Mic92).
## Decision
A third approach would be to:
1. Commit ADRs before they are approved, so that the next ADR number gets assigned;
1. Open a PR for the proposed ADR;
1. Update the ADR file committed in step 1, so that its markdown contents point to the PR that tracks it.
## Consequences
### ADR have unique and memorable numbers trough their entire life cycle
This makes it easier to refer to them in conversation or in code.
### You need to have commit access to get an ADR number assigned
This makes it more difficult for someone external to the project to contribute an ADR.
### Creating a new ADR requires multiple commits
Maybe a script or CI flow could help with that if it becomes painful.
[#3212]: https://git.clan.lol/clan/clan-core/pulls/3212/
[#3452]: https://git.clan.lol/clan/clan-core/pulls/3452/
[#3196]: https://git.clan.lol/clan/clan-core/pulls/3196/

View File

@@ -58,7 +58,7 @@ nav:
- Autoincludes: manual/adding-machines.md - Autoincludes: manual/adding-machines.md
- Inventory: - Inventory:
- Inventory: manual/inventory.md - Inventory: manual/inventory.md
- Services: manual/distributed-services.md - Instances: manual/distributed-services.md
- Secure Boot: manual/secure-boot.md - Secure Boot: manual/secure-boot.md
- Flake-parts: manual/flake-parts.md - Flake-parts: manual/flake-parts.md
- Authoring: - Authoring:

View File

@@ -12,7 +12,7 @@ We discussed the initial architecture in [01-clan-service-modules](https://git.c
### A Minimal module ### A Minimal module
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. 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.
While not required we recommend to prefix your module attribute name. While not required we recommend to prefix your module attribute name.
@@ -22,15 +22,20 @@ i.e. `@hsjobeki/customNetworking`
```nix title="flake.nix" ```nix title="flake.nix"
# ... # ...
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } ({
outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({
imports = [ inputs.clan-core.flakeModules.default ]; imports = [ inputs.clan-core.flakeModules.default ];
# ... # ...
clan = { 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 # If needed: Exporting the module for other people
modules."@hsjobeki/customNetworking" = import ./service-modules/networking.nix; modules."@hsjobeki/customNetworking" = import ./service-modules/networking.nix;
# We could also inline the complete module spec here
# For example
# {...}: { _class = "clan.service"; ... };
}; };
}) })
``` ```
@@ -216,6 +221,9 @@ outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({self, lib, ...}:
# ... # ...
clan = { clan = {
# Register the module # 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; }; modules."@hsjobeki/messaging" = lib.importApply ./service-modules/messaging.nix { inherit self; };
}; };
}) })
@@ -242,7 +250,7 @@ outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({self, lib, ...}:
# ... # ...
clan = { clan = {
# Register the module # Register the module
modules."@hsjobeki/messaging" = { inventory.modules."@hsjobeki/messaging" = {
# Create an option 'myClan' and assign it to 'self' # Create an option 'myClan' and assign it to 'self'
options.myClan = lib.mkOption { options.myClan = lib.mkOption {
default = self; default = self;

View File

@@ -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: Existing nixos vm tests in clan-core can be found by using ripgrep:
```shellSession ```shellSession
rg self.clanLib.test.baseTest rg "import.*/lib/test-base.nix"
``` ```
### Locating definitions of failing VM tests ### Locating definitions of failing VM tests
@@ -50,7 +50,7 @@ example: locating the vm test named `borgbackup`:
```shellSession ```shellSession
$ rg "borgbackup =" ./checks $ rg "borgbackup =" ./checks
./checks/flake-module.nix ./checks/flake-module.nix
44- wayland-proxy-virtwl = self.clanLib.test.baseTest ./wayland-proxy-virtwl nixosTestArgs; 41: borgbackup = import ./borgbackup nixosTestArgs;
``` ```
-> the location of that test is `/checks/flake-module.nix` line `41`. -> 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: Limitations:
- 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. - does not yet support networking
- setuid binaries don't work - supports only one machine as of now
### Where to find examples for NixOS container tests ### Where to find examples for NixOS container tests
Existing nixos container tests in clan-core can be found by using ripgrep: Existing nixos container tests in clan-core can be found by using ripgrep:
```shellSession ```shellSession
rg self.clanLib.test.containerTest rg "import.*/lib/container-test.nix"
``` ```

38
flake.lock generated
View File

@@ -16,11 +16,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1747008053, "lastModified": 1746334246,
"narHash": "sha256-rob/qftmEuk+/JVGCIrOpv+LWjdmayFtebEKqRZXVAI=", "narHash": "sha256-YU4wtH9Y5yRjqbMwczOdDakOjSiTkOUP/JAYd1f3jBc=",
"rev": "2666bb11f4287cfbdf3b7c5f55231c6b5772a436", "rev": "607ce65fbfe20bb38170b76826a11006f526c05d",
"type": "tarball", "type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/2666bb11f4287cfbdf3b7c5f55231c6b5772a436.tar.gz" "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/607ce65fbfe20bb38170b76826a11006f526c05d.tar.gz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@@ -34,11 +34,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1746729224, "lastModified": 1745812220,
"narHash": "sha256-9R4sOLAK1w3Bq54H3XOJogdc7a6C2bLLmatOQ+5pf5w=", "narHash": "sha256-hotBG0EJ9VmAHJYF0yhWuTVZpENHvwcJ2SxvIPrXm+g=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "85555d27ded84604ad6657ecca255a03fd878607", "rev": "d0c543d740fad42fe2c035b43c9d41127e073c78",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -74,11 +74,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1746708654, "lastModified": 1746254942,
"narHash": "sha256-GeC99gu5H6+AjBXsn5dOhP4/ApuioGCBkufdmEIWPRs=", "narHash": "sha256-Y062AuRx6l+TJNX8wxZcT59SSLsqD9EedAY0mqgTtQE=",
"owner": "nix-darwin", "owner": "nix-darwin",
"repo": "nix-darwin", "repo": "nix-darwin",
"rev": "6cb36e8327421c61e5a3bbd08ed63491b616364a", "rev": "760a11c87009155afa0140d55c40e7c336d62d7a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -118,10 +118,10 @@
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 315532800, "lastModified": 315532800,
"narHash": "sha256-EbVl0wIdDYZWrxpQoxPlXfliaR4KHA9xP5dVjG1CZxI=", "narHash": "sha256-pxwYhAgOyComW58BCfboADZWr4b5oS8hP9E9fQ489HM=",
"rev": "ed30f8aba41605e3ab46421e3dcb4510ec560ff8", "rev": "f21e4546e3ede7ae34d12a84602a22246b31f7e0",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.05pre794180.ed30f8aba416/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.05pre793694.f21e4546e3ed/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@@ -149,11 +149,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1746485181, "lastModified": 1745310711,
"narHash": "sha256-PxrrSFLaC7YuItShxmYbMgSuFFuwxBB+qsl9BZUnRvg=", "narHash": "sha256-ePyTpKEJTgX0gvgNQWd7tQYQ3glIkbqcW778RpHlqgA=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "e93ee1d900ad264d65e9701a5c6f895683433386", "rev": "5e3e92b16d6fdf9923425a8d4df7496b2434f39c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -184,11 +184,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1746989248, "lastModified": 1746216483,
"narHash": "sha256-uoQ21EWsAhyskNo8QxrTVZGjG/dV4x5NM1oSgrmNDJY=", "narHash": "sha256-4h3s1L/kKqt3gMDcVfN8/4v2jqHrgLIe4qok4ApH5x4=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "708ec80ca82e2bbafa93402ccb66a35ff87900c5", "rev": "29ec5026372e0dec56f890e50dbe4f45930320fd",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -37,7 +37,7 @@ let
done done
if ! test -e ~/clan-core; then if ! test -e ~/clan-core; then
# git clone https://git.clan.lol/clan/clan-core.git ~/clan-core # git clone https://git.clan.lol/clan/clan-core.git ~/clan-core
cp -rv ${self.checks.x86_64-linux.clan-core-for-checks} clan-core cp -rv ${self} clan-core
fi fi
cd clan-core cd clan-core
clan machines morph demo-template --i-will-be-fired-for-using-this clan machines morph demo-template --i-will-be-fired-for-using-this

View File

@@ -10,11 +10,6 @@ let
in in
{ {
options = { options = {
_prefix = lib.mkOption {
type = types.listOf types.str;
internal = true;
default = [ ];
};
self = lib.mkOption { self = lib.mkOption {
type = types.raw; type = types.raw;
default = self; default = self;
@@ -173,7 +168,6 @@ in
inventoryFile = lib.mkOption { type = lib.types.raw; }; inventoryFile = lib.mkOption { type = lib.types.raw; };
# The machine 'imports' generated by the inventory per machine # The machine 'imports' generated by the inventory per machine
inventoryClass = lib.mkOption { type = lib.types.raw; }; inventoryClass = lib.mkOption { type = lib.types.raw; };
evalServiceSchema = lib.mkOption { };
# clan-core's modules # clan-core's modules
clanModules = lib.mkOption { type = lib.types.raw; }; clanModules = lib.mkOption { type = lib.types.raw; };
source = lib.mkOption { type = lib.types.raw; }; source = lib.mkOption { type = lib.types.raw; };

View File

@@ -44,10 +44,6 @@ let
buildInventory { buildInventory {
inherit inventory directory; inherit inventory directory;
flakeInputs = config.self.inputs; flakeInputs = config.self.inputs;
prefix = config._prefix ++ [ "inventoryClass" ];
# TODO: remove inventory.modules, this is here for backwards compatibility
localModuleSet =
lib.filterAttrs (n: _: !inventory._legacyModules ? ${n}) inventory.modules // config.modules;
} }
); );
@@ -181,7 +177,6 @@ in
# Merge the meta attributes from the buildClan function # Merge the meta attributes from the buildClan function
{ {
inventory.modules = clan-core.clanModules; inventory.modules = clan-core.clanModules;
inventory._legacyModules = clan-core.clanModules;
} }
# config.inventory.meta <- config.meta # config.inventory.meta <- config.meta
{ inventory.meta = config.meta; } { inventory.meta = config.meta; }
@@ -209,9 +204,6 @@ in
inherit inventoryClass; inherit inventoryClass;
# Endpoint that can be called to get a service schema
evalServiceSchema = clan-core.clanLib.evalServiceSchema config.self;
# TODO: unify this interface # TODO: unify this interface
# We should have only clan.modules. (consistent with clan.templates) # We should have only clan.modules. (consistent with clan.templates)
inherit (clan-core) clanModules clanLib; inherit (clan-core) clanModules clanLib;

View File

@@ -15,27 +15,10 @@ lib.fix (clanLib: {
*/ */
callLib = file: args: import file ({ inherit lib clanLib; } // args); callLib = file: args: import file ({ inherit lib clanLib; } // args);
# ------------------------------------
buildClan = clanLib.buildClanModule.buildClanWith { buildClan = clanLib.buildClanModule.buildClanWith {
clan-core = self; clan-core = self;
inherit nixpkgs nix-darwin; inherit nixpkgs nix-darwin;
}; };
evalServiceSchema =
self:
{
moduleSpec,
flakeInputs ? self.inputs,
localModuleSet ? self.clan.modules,
}:
let
resolvedModule = clanLib.inventory.resolveModule {
inherit moduleSpec flakeInputs localModuleSet;
};
in
(clanLib.inventory.evalClanService {
modules = [ resolvedModule ];
prefix = [ ];
}).config.result.api.schema;
# ------------------------------------ # ------------------------------------
# ClanLib functions # ClanLib functions
evalClan = clanLib.callLib ./inventory/eval-clan-modules { }; evalClan = clanLib.callLib ./inventory/eval-clan-modules { };

View File

@@ -12,35 +12,25 @@ let
inventory, inventory,
directory, directory,
flakeInputs, flakeInputs,
prefix ? [ ],
localModuleSet ? { },
}: }:
(lib.evalModules { (lib.evalModules {
# TODO: remove clanLib from specialArgs
specialArgs = { specialArgs = {
inherit clanLib; inherit clanLib;
}; };
modules = [ modules = [
./builder ./builder
(lib.modules.importApply ./service-list-from-inputs.nix {
inherit flakeInputs clanLib localModuleSet;
})
{ inherit directory inventory; } { inherit directory inventory; }
( (
# config.distributedServices.allMachines.${name} or [ ]; # config.distributedServices.allMachines.${name} or [ ];
{ config, ... }: { config, ... }:
{ {
distributedServices = clanLib.inventory.mapInstances { distributedServices = clanLib.inventory.mapInstances {
inherit (config) inventory; inherit (config) inventory;
inherit localModuleSet;
inherit flakeInputs; inherit flakeInputs;
prefix = prefix ++ [ "distributedServices" ];
}; };
machines = lib.mapAttrs (_machineName: v: { machines = lib.mapAttrs (_machineName: v: {
machineImports = v; machineImports = v;
}) config.distributedServices.allMachines; }) config.distributedServices.allMachines;
} }
) )
(lib.modules.importApply ./inventory-introspection.nix { inherit clanLib; }) (lib.modules.importApply ./inventory-introspection.nix { inherit clanLib; })

View File

@@ -96,12 +96,6 @@ in
./assertions.nix ./assertions.nix
]; ];
options = { options = {
_legacyModules = lib.mkOption {
internal = true;
visible = false;
default = { };
};
options = lib.mkOption { options = lib.mkOption {
internal = true; internal = true;
visible = false; visible = false;
@@ -144,28 +138,6 @@ in
}; };
``` ```
''; '';
apply =
moduleSet:
let
allowedNames = lib.attrNames config._legacyModules;
in
if builtins.all (moduleName: builtins.elem moduleName allowedNames) (lib.attrNames moduleSet) then
moduleSet
else
lib.warn ''
`inventory.modules` will be deprecated soon.
Please migrate the following modules into `clan.service` modules
and register them in `clan.modules`
${lib.concatStringsSep "\n" (
map (m: "'${m}'") (lib.attrNames (lib.filterAttrs (n: _v: !builtins.elem n allowedNames) moduleSet))
)}
See: https://docs.clan.lol/manual/distributed-services/
And: https://docs.clan.lol/authoring/clanServices/
'' moduleSet;
}; };
assertions = lib.mkOption { assertions = lib.mkOption {

View File

@@ -1,43 +0,0 @@
{
flakeInputs,
clanLib,
localModuleSet,
}:
{ lib, config, ... }:
let
inspectModule =
inputName: moduleName: module:
let
eval = clanLib.inventory.evalClanService {
modules = [ module ];
prefix = [
inputName
"clan"
"modules"
moduleName
];
};
in
{
manifest = eval.config.manifest;
roles = lib.mapAttrs (_n: _v: { }) eval.config.roles;
};
in
{
options.modulesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }}
default =
let
inputsWithModules = lib.filterAttrs (_inputName: v: v ? clan.modules) flakeInputs;
in
lib.mapAttrs (
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
) inputsWithModules;
};
options.localModules = lib.mkOption {
default = lib.mapAttrs (inspectModule "self") localModuleSet;
};
}

View File

@@ -3,7 +3,7 @@ let
services = clanLib.callLib ./distributed-service/inventory-adapter.nix { }; services = clanLib.callLib ./distributed-service/inventory-adapter.nix { };
in in
{ {
inherit (services) evalClanService mapInstances resolveModule; inherit (services) evalClanService mapInstances;
inherit (import ./build-inventory { inherit lib clanLib; }) buildInventory; inherit (import ./build-inventory { inherit lib clanLib; }) buildInventory;
interface = ./build-inventory/interface.nix; interface = ./build-inventory/interface.nix;
# Returns the list of machine names # Returns the list of machine names

View File

@@ -1,7 +1,7 @@
# This module enables itself if # This module enables itself if
# manifest.features.API = true # manifest.features.API = true
# It converts the roles.interface to a json-schema # It converts the roles.interface to a json-schema
{ clanLib, prefix }: { clanLib, attrName }:
let let
converter = clanLib.jsonschema { converter = clanLib.jsonschema {
includeDefaults = true; includeDefaults = true;
@@ -45,7 +45,7 @@ in
To see the evaluation problem run To see the evaluation problem run
nix eval .#${lib.concatStringsSep "." prefix}.config.result.api.schema.${roleName} nix eval .#clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.${attrName}.config.result.api.schema.${roleName}
''; '';
assertion = (builtins.tryEval (lib.deepSeq config.result.api.schema.${roleName} true)).success; assertion = (builtins.tryEval (lib.deepSeq config.result.api.schema.${roleName} true)).success;
}; };

View File

@@ -16,73 +16,27 @@
}: }:
let let
evalClanService = evalClanService =
{ modules, prefix }: { modules, key }:
(lib.evalModules { (lib.evalModules {
class = "clan.service"; class = "clan.service";
modules = [ modules = [
./service-module.nix ./service-module.nix
# feature modules # feature modules
(lib.modules.importApply ./api-feature.nix { (lib.modules.importApply ./api-feature.nix {
inherit clanLib prefix; inherit clanLib;
attrName = key;
}) })
] ++ modules; ] ++ modules;
}); });
resolveModule =
{
moduleSpec,
flakeInputs,
localModuleSet,
}:
let
# TODO:
resolvedModuleSet =
# If the module.name is self then take the modules defined in the flake
# Otherwise its an external input which provides the modules via 'clan.modules' attribute
if moduleSpec.input == null then
localModuleSet
else
let
input =
flakeInputs.${moduleSpec.input} or (throw ''
Flake doesn't provide input with name '${moduleSpec.input}'
Choose one of the following inputs:
- ${
builtins.concatStringsSep "\n- " (
lib.attrNames (lib.filterAttrs (_name: input: input ? clan) flakeInputs)
)
}
To import a local module from 'clan.modules' remove the 'input' attribute from the module definition
Remove the following line from the module definition:
...
- module.input = "${moduleSpec.input}"
'');
clanAttrs =
input.clan
or (throw "It seems the flake input ${moduleSpec.input} doesn't export any clan resources");
in
clanAttrs.modules;
resolvedModule =
resolvedModuleSet.${moduleSpec.name}
or (throw "flake doesn't provide clan-module with name ${moduleSpec.name}");
in
resolvedModule;
in in
{ {
inherit evalClanService resolveModule; inherit evalClanService;
mapInstances = mapInstances =
{ {
# This is used to resolve the module imports from 'flake.inputs' # This is used to resolve the module imports from 'flake.inputs'
flakeInputs, flakeInputs,
# The clan inventory # The clan inventory
inventory, inventory,
localModuleSet,
prefix ? [ ],
}: }:
let let
# machineHasTag = machineName: tagName: lib.elem tagName inventory.machines.${machineName}.tags; # machineHasTag = machineName: tagName: lib.elem tagName inventory.machines.${machineName}.tags;
@@ -91,11 +45,42 @@ in
importedModuleWithInstances = lib.mapAttrs ( importedModuleWithInstances = lib.mapAttrs (
instanceName: instance: instanceName: instance:
let let
resolvedModule = resolveModule { # TODO:
moduleSpec = instance.module; resolvedModuleSet =
inherit localModuleSet; # If the module.name is self then take the modules defined in the flake
inherit flakeInputs; # Otherwise its an external input which provides the modules via 'clan.modules' attribute
}; if instance.module.input == null then
inventory.modules
else
let
input =
flakeInputs.${instance.module.input} or (throw ''
Flake doesn't provide input with name '${instance.module.input}'
Choose one of the following inputs:
- ${
builtins.concatStringsSep "\n- " (
lib.attrNames (lib.filterAttrs (_name: input: input ? clan) flakeInputs)
)
}
To import a local module from 'inventory.modules' remove the 'input' attribute from the module definition
Remove the following line from the module definition:
...
- module.input = "${instance.module.input}"
'');
clanAttrs =
input.clan
or (throw "It seems the flake input ${instance.module.input} doesn't export any clan resources");
in
clanAttrs.modules;
resolvedModule =
resolvedModuleSet.${instance.module.name}
or (throw "flake doesn't provide clan-module with name ${instance.module.name}");
# Every instance includes machines via roles # Every instance includes machines via roles
# :: { client :: ... } # :: { client :: ... }
@@ -153,7 +138,7 @@ in
importedModulesEvaluated = lib.mapAttrs ( importedModulesEvaluated = lib.mapAttrs (
module_ident: instances: module_ident: instances:
evalClanService { evalClanService {
prefix = prefix ++ [ module_ident ]; key = module_ident;
modules = modules =
[ [
# Import the resolved module. # Import the resolved module.

View File

@@ -255,9 +255,7 @@ in
{ {
options.API = mkOption { options.API = mkOption {
type = types.bool; type = types.bool;
# This is read only, because we don't support turning it off yet default = false;
readOnly = true;
default = true;
description = '' description = ''
Enables automatic API schema conversion for the interface of this module. Enables automatic API schema conversion for the interface of this module.
''; '';

View File

@@ -41,13 +41,9 @@ let
callInventoryAdapter = callInventoryAdapter =
inventoryModule: inventoryModule:
let
inventory = evalInventory inventoryModule;
in
clanLib.inventory.mapInstances { clanLib.inventory.mapInstances {
flakeInputs = flakeInputsFixture; flakeInputs = flakeInputsFixture;
inherit inventory; inventory = evalInventory inventoryModule;
localModuleSet = inventory.modules;
}; };
in in
{ {

View File

@@ -92,7 +92,7 @@ in
lib.lazyDerivation { lib.lazyDerivation {
# lazyDerivation improves performance when only passthru items and/or meta are used. # lazyDerivation improves performance when only passthru items and/or meta are used.
derivation = hostPkgs.stdenv.mkDerivation { derivation = hostPkgs.stdenv.mkDerivation {
name = "container-test-run-${config.name}"; name = "vm-test-run-${config.name}";
requiredSystemFeatures = [ "uid-range" ]; requiredSystemFeatures = [ "uid-range" ];

View File

@@ -2,11 +2,9 @@ import argparse
import ctypes import ctypes
import os import os
import re import re
import shutil
import subprocess import subprocess
import time import time
import types import types
import uuid
from collections.abc import Callable from collections.abc import Callable
from contextlib import _GeneratorContextManager from contextlib import _GeneratorContextManager
from dataclasses import dataclass from dataclasses import dataclass
@@ -15,8 +13,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any from typing import Any
from colorama import Fore, Style
from .logger import AbstractLogger, CompositeLogger, TerminalLogger from .logger import AbstractLogger, CompositeLogger, TerminalLogger
# Load the C library # Load the C library
@@ -191,22 +187,6 @@ class Machine:
if line_pattern.match(line) if line_pattern.match(line)
) )
def nsenter_command(self, command: str) -> list[str]:
return [
"nsenter",
"--target",
str(self.container_pid),
"--mount",
"--uts",
"--ipc",
"--net",
"--pid",
"--cgroup",
"/bin/sh",
"-c",
command,
]
def execute( def execute(
self, self,
command: str, command: str,
@@ -248,10 +228,23 @@ class Machine:
""" """
# Always run command with shell opts # Always run command with shell opts
command = f"set -eo pipefail; source /etc/profile; set -xu; {command}" command = f"set -eo pipefail; source /etc/profile; set -u; {command}"
proc = subprocess.run( proc = subprocess.run(
self.nsenter_command(command), [
"nsenter",
"--target",
str(self.container_pid),
"--mount",
"--uts",
"--ipc",
"--net",
"--pid",
"--cgroup",
"/bin/sh",
"-c",
command,
],
timeout=timeout, timeout=timeout,
check=False, check=False,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -472,43 +465,6 @@ class Driver:
print(f"Starting {machine.name}") print(f"Starting {machine.name}")
machine.start() machine.start()
# Print copy-pastable nsenter command to debug container tests
for machine in self.machines:
nspawn_uuid = uuid.uuid4()
# We lauch a sleep here, so we can pgrep the process cmdline for
# the uuid
sleep = shutil.which("sleep")
assert sleep is not None, "sleep command not found"
machine.execute(
f"systemd-run /bin/sh -c '{sleep} 999999999 && echo {nspawn_uuid}'",
)
print(f"nsenter for {machine.name}:")
print(
" ".join(
[
Style.BRIGHT,
Fore.CYAN,
"sudo",
"nsenter",
"--user",
"--target",
f"$(\\pgrep -f '^/bin/sh.*{nspawn_uuid}')",
"--mount",
"--uts",
"--ipc",
"--net",
"--pid",
"--cgroup",
"/bin/sh",
"-c",
"bash",
Style.RESET_ALL,
]
)
)
def test_symbols(self) -> dict[str, Any]: def test_symbols(self) -> dict[str, Any]:
general_symbols = { general_symbols = {
"start_all": self.start_all, "start_all": self.start_all,

View File

@@ -22,9 +22,6 @@ in
pkgs, pkgs,
self, self,
useContainers ? true, useContainers ? true,
# Displayed for better error messages, otherwise the placeholder
system ? "<system>",
attrName ? "<check_name>",
... ...
}: }:
let let
@@ -38,7 +35,7 @@ in
{ {
imports = [ imports = [
nixosTest nixosTest
] ++ lib.optionals useContainers [ ./container-test-driver/driver-module.nix ]; ] ++ lib.optionals (useContainers) [ ./container-test-driver/driver-module.nix ];
options = { options = {
clanSettings = mkOption { clanSettings = mkOption {
default = { }; default = { };
@@ -63,15 +60,6 @@ in
}; };
modules = [ modules = [
clanLib.buildClanModule.flakePartsModule clanLib.buildClanModule.flakePartsModule
{
_prefix = [
"checks"
system
attrName
"config"
"clan"
];
}
]; ];
}; };
}; };

View File

@@ -1,21 +0,0 @@
{ lib, ... }:
{
_class = "clan.service";
manifest.name = "test";
roles.peer.interface =
{ ... }:
{
options.debug = lib.mkOption { default = 1; };
};
roles.peer.perInstance =
{ settings, ... }:
{
nixosModule = {
options.debug = lib.mkOption {
default = settings;
};
};
};
}

View File

@@ -39,44 +39,9 @@ in
type = submodule { imports = [ ./interface.nix ]; }; type = submodule { imports = [ ./interface.nix ]; };
}; };
config = { config.system.clan.deployment.data = {
# check all that all non-secret files have no owner/group/mode set vars = config.clan.core.vars._serialized;
warnings = lib.foldl' ( inherit (config.clan.core.networking) targetHost buildHost;
warnings: generator: inherit (config.clan.core.deployment) requireExplicitUpdate;
warnings
++ lib.foldl' (
warnings: file:
warnings
++
lib.optional
(
!file.secret
&& (
file.owner != "root"
|| file.group != (if _class == "darwin" then "wheel" else "root")
|| file.mode != "0400"
)
)
''
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)
) [ ] (lib.attrValues config.clan.core.vars.generators);
system.clan.deployment.data = {
vars = config.clan.core.vars._serialized;
inherit (config.clan.core.networking) targetHost buildHost;
inherit (config.clan.core.deployment) requireExplicitUpdate;
};
}; };
} }

View File

@@ -39,7 +39,7 @@ in
internal = true; internal = true;
description = '' description = ''
JSON serialization of the generators. JSON serialization of the generators.
This is read from the python client to generate the specified resources. This is read from the python client to generate the specified ressources.
''; '';
default = { default = {
# TODO: We don't support per-machine choice of backends # TODO: We don't support per-machine choice of backends
@@ -258,7 +258,7 @@ in
defaultText = '' defaultText = ''
builtins.path { builtins.path {
name = "$${generator.config._module.args.name}_$${file.config._module.args.name}"; name = "$${generator.config._module.args.name}_$${file.config._module.args.name}";
path = file.config.flakePath; path = file.config.inRepoPath;
} }
''; '';
default = builtins.path { default = builtins.path {

View File

@@ -18,7 +18,7 @@ let
config.clan.core.settings.directory config.clan.core.settings.directory
+ "/vars/per-machine/${machineName}/${secret.generator}/${secret.name}/secret"; + "/vars/per-machine/${machineName}/${secret.generator}/${secret.name}/secret";
vars = collectFiles config.clan.core.vars.generators; vars = collectFiles config.clan.core.vars;
in in
{ {
config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") { config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") {

View File

@@ -13,7 +13,7 @@ in
{ {
collectFiles = collectFiles =
generators: vars:
let let
relevantFiles = relevantFiles =
generator: generator:
@@ -30,7 +30,7 @@ in
inherit (generator) share; inherit (generator) share;
inherit (file) owner group mode; inherit (file) owner group mode;
}) (relevantFiles generator) }) (relevantFiles generator)
) generators ) vars.generators
); );
in in
allFiles; allFiles;

View File

@@ -29,8 +29,6 @@ mkShell {
export GIT_ROOT=$(git rev-parse --show-toplevel) export GIT_ROOT=$(git rev-parse --show-toplevel)
export PKG_ROOT=$GIT_ROOT/pkgs/clan-app export PKG_ROOT=$GIT_ROOT/pkgs/clan-app
export CLAN_CORE_PATH="$GIT_ROOT"
# Add current package to PYTHONPATH # Add current package to PYTHONPATH
export PYTHONPATH="$PKG_ROOT''${PYTHONPATH:+:$PYTHONPATH:}" export PYTHONPATH="$PKG_ROOT''${PYTHONPATH:+:$PYTHONPATH:}"

View File

@@ -6,13 +6,13 @@ from pathlib import Path
from types import ModuleType from types import ModuleType
# These imports are unused, but necessary for @API.register to run once. # These imports are unused, but necessary for @API.register to run once.
from clan_lib.api import directory, disk, iwd, mdns_discovery, modules from clan_lib.api import admin, directory, disk, iwd, mdns_discovery, modules
from .arg_actions import AppendOptionAction from .arg_actions import AppendOptionAction
from .clan import show, update from .clan import show, update
# API endpoints that are not used in the cli. # API endpoints that are not used in the cli.
__all__ = ["directory", "disk", "iwd", "mdns_discovery", "modules", "update"] __all__ = ["admin", "directory", "disk", "iwd", "mdns_discovery", "modules", "update"]
from . import ( from . import (
backups, backups,

View File

@@ -19,23 +19,21 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
if not backup_scripts["providers"]: if not backup_scripts["providers"]:
msg = "No providers specified" msg = "No providers specified"
raise ClanError(msg) raise ClanError(msg)
with machine.target_host() as host: for provider in backup_scripts["providers"]:
for provider in backup_scripts["providers"]: proc = machine.target_host.run(
proc = host.run( [backup_scripts["providers"][provider]["create"]],
[backup_scripts["providers"][provider]["create"]], )
) if proc.returncode != 0:
if proc.returncode != 0: msg = "failed to start backup"
msg = "failed to start backup" raise ClanError(msg)
raise ClanError(msg) print("successfully started backup")
print("successfully started backup")
else: else:
if provider not in backup_scripts["providers"]: if provider not in backup_scripts["providers"]:
msg = f"provider {provider} not found" msg = f"provider {provider} not found"
raise ClanError(msg) raise ClanError(msg)
with machine.target_host() as host: proc = machine.target_host.run(
proc = host.run( [backup_scripts["providers"][provider]["create"]],
[backup_scripts["providers"][provider]["create"]], )
)
if proc.returncode != 0: if proc.returncode != 0:
msg = "failed to start backup" msg = "failed to start backup"
raise ClanError(msg) raise ClanError(msg)

View File

@@ -10,7 +10,6 @@ from clan_cli.completions import (
) )
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
@dataclass @dataclass
@@ -19,11 +18,11 @@ class Backup:
job_name: str | None = None job_name: str | None = None
def list_provider(machine: Machine, host: Host, provider: str) -> list[Backup]: def list_provider(machine: Machine, provider: str) -> list[Backup]:
results = [] results = []
backup_metadata = machine.eval_nix("config.clan.core.backups") backup_metadata = machine.eval_nix("config.clan.core.backups")
list_command = backup_metadata["providers"][provider]["list"] list_command = backup_metadata["providers"][provider]["list"]
proc = host.run( proc = machine.target_host.run(
[list_command], [list_command],
RunOpts(log=Log.NONE, check=False), RunOpts(log=Log.NONE, check=False),
) )
@@ -49,13 +48,12 @@ def list_provider(machine: Machine, host: Host, provider: str) -> list[Backup]:
def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]: def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
backup_metadata = machine.eval_nix("config.clan.core.backups") backup_metadata = machine.eval_nix("config.clan.core.backups")
results = [] results = []
with machine.target_host() as host: if provider is None:
if provider is None: for _provider in backup_metadata["providers"]:
for _provider in backup_metadata["providers"]: results += list_provider(machine, _provider)
results += list_provider(machine, host, _provider)
else: else:
results += list_provider(machine, host, provider) results += list_provider(machine, provider)
return results return results

View File

@@ -8,12 +8,9 @@ from clan_cli.completions import (
) )
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
def restore_service( def restore_service(machine: Machine, name: str, provider: str, service: str) -> None:
machine: Machine, host: Host, name: str, provider: str, service: str
) -> None:
backup_metadata = machine.eval_nix("config.clan.core.backups") backup_metadata = machine.eval_nix("config.clan.core.backups")
backup_folders = machine.eval_nix("config.clan.core.state") backup_folders = machine.eval_nix("config.clan.core.state")
@@ -28,7 +25,7 @@ def restore_service(
env["FOLDERS"] = ":".join(set(folders)) env["FOLDERS"] = ":".join(set(folders))
if pre_restore := backup_folders[service]["preRestoreCommand"]: if pre_restore := backup_folders[service]["preRestoreCommand"]:
proc = host.run( proc = machine.target_host.run(
[pre_restore], [pre_restore],
RunOpts(log=Log.STDERR), RunOpts(log=Log.STDERR),
extra_env=env, extra_env=env,
@@ -37,7 +34,7 @@ def restore_service(
msg = f"failed to run preRestoreCommand: {pre_restore}, error was: {proc.stdout}" msg = f"failed to run preRestoreCommand: {pre_restore}, error was: {proc.stdout}"
raise ClanError(msg) raise ClanError(msg)
proc = host.run( proc = machine.target_host.run(
[backup_metadata["providers"][provider]["restore"]], [backup_metadata["providers"][provider]["restore"]],
RunOpts(log=Log.STDERR), RunOpts(log=Log.STDERR),
extra_env=env, extra_env=env,
@@ -47,7 +44,7 @@ def restore_service(
raise ClanError(msg) raise ClanError(msg)
if post_restore := backup_folders[service]["postRestoreCommand"]: if post_restore := backup_folders[service]["postRestoreCommand"]:
proc = host.run( proc = machine.target_host.run(
[post_restore], [post_restore],
RunOpts(log=Log.STDERR), RunOpts(log=Log.STDERR),
extra_env=env, extra_env=env,
@@ -64,19 +61,18 @@ def restore_backup(
service: str | None = None, service: str | None = None,
) -> None: ) -> None:
errors = [] errors = []
with machine.target_host() as host: if service is None:
if service is None: backup_folders = machine.eval_nix("config.clan.core.state")
backup_folders = machine.eval_nix("config.clan.core.state") for _service in backup_folders:
for _service in backup_folders:
try:
restore_service(machine, host, name, provider, _service)
except ClanError as e:
errors.append(f"{_service}: {e}")
else:
try: try:
restore_service(machine, host, name, provider, service) restore_service(machine, name, provider, _service)
except ClanError as e: except ClanError as e:
errors.append(f"{service}: {e}") errors.append(f"{_service}: {e}")
else:
try:
restore_service(machine, name, provider, service)
except ClanError as e:
errors.append(f"{service}: {e}")
if errors: if errors:
raise ClanError( raise ClanError(
"Restore failed for the following services:\n" + "\n".join(errors) "Restore failed for the following services:\n" + "\n".join(errors)

View File

@@ -1,8 +1,4 @@
import os from clan_cli.cmd import run
import shutil
from pathlib import Path
from clan_cli.cmd import Log, RunOpts, run
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
_works: bool | None = None _works: bool | None = None
@@ -16,11 +12,6 @@ def bubblewrap_works() -> bool:
def _bubblewrap_works() -> bool: def _bubblewrap_works() -> bool:
real_bash_path = Path("bash")
if os.environ.get("IN_NIX_SANDBOX"):
bash_executable_path = Path(str(shutil.which("bash")))
real_bash_path = bash_executable_path.resolve()
# fmt: off # fmt: off
cmd = nix_shell( cmd = nix_shell(
[ [
@@ -39,10 +30,13 @@ def _bubblewrap_works() -> bool:
"--gid", "1000", "--gid", "1000",
"--", "--",
# do nothing, just test if bash executes # do nothing, just test if bash executes
str(real_bash_path), "-c", ":" "bash", "-c", ":"
], ],
) )
# fmt: on # fmt: on
res = run(cmd, RunOpts(log=Log.BOTH, check=False)) try:
return res.returncode == 0 run(cmd)
except Exception:
return False
else:
return True

View File

@@ -107,7 +107,7 @@ def create_clan(opts: CreateOptions) -> CreateClanResponse:
response.flake_update = flake_update response.flake_update = flake_update
if opts.initial: if opts.initial:
init_inventory(Flake(str(opts.dest)), init=opts.initial) init_inventory(str(opts.dest), init=opts.initial)
return response return response

View File

@@ -6,9 +6,8 @@ from urllib.parse import urlparse
from clan_lib.api import API from clan_lib.api import API
from clan_cli.cmd import run from clan_cli.cmd import run_no_stdout
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.flake import Flake
from clan_cli.inventory import Meta from clan_cli.inventory import Meta
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
@@ -16,26 +15,26 @@ log = logging.getLogger(__name__)
@API.register @API.register
def show_clan_meta(flake: Flake) -> Meta: def show_clan_meta(uri: str) -> Meta:
if flake.is_local and not flake.path.exists(): if uri.startswith("/") and not Path(uri).exists():
msg = f"Path {flake} does not exist" msg = f"Path {uri} does not exist"
raise ClanError(msg, description="clan directory does not exist") raise ClanError(msg, description="clan directory does not exist")
cmd = nix_eval( cmd = nix_eval(
[ [
f"{flake}#clanInternals.inventory.meta", f"{uri}#clanInternals.inventory.meta",
"--json", "--json",
] ]
) )
res = "{}" res = "{}"
try: try:
proc = run(cmd) proc = run_no_stdout(cmd)
res = proc.stdout.strip() res = proc.stdout.strip()
except ClanCmdError as e: except ClanCmdError as e:
msg = "Evaluation failed on meta attribute" msg = "Evaluation failed on meta attribute"
raise ClanError( raise ClanError(
msg, msg,
location=f"show_clan {flake}", location=f"show_clan {uri}",
description=str(e.cmd), description=str(e.cmd),
) from e ) from e
@@ -54,16 +53,16 @@ def show_clan_meta(flake: Flake) -> Meta:
msg = "Invalid absolute path" msg = "Invalid absolute path"
raise ClanError( raise ClanError(
msg, msg,
location=f"show_clan {flake}", location=f"show_clan {uri}",
description="Icon path must be a URL or a relative path", description="Icon path must be a URL or a relative path",
) )
icon_path = str((flake.path / meta_icon).resolve()) icon_path = str((Path(uri) / meta_icon).resolve())
else: else:
msg = "Invalid schema" msg = "Invalid schema"
raise ClanError( raise ClanError(
msg, msg,
location=f"show_clan {flake}", location=f"show_clan {uri}",
description="Icon path must be a URL or a relative path", description="Icon path must be a URL or a relative path",
) )

View File

@@ -2,21 +2,20 @@ from dataclasses import dataclass
from clan_lib.api import API from clan_lib.api import API
from clan_cli.flake import Flake
from clan_cli.inventory import Inventory, Meta, load_inventory_json, set_inventory from clan_cli.inventory import Inventory, Meta, load_inventory_json, set_inventory
@dataclass @dataclass
class UpdateOptions: class UpdateOptions:
flake: Flake directory: str
meta: Meta meta: Meta
@API.register @API.register
def update_clan_meta(options: UpdateOptions) -> Inventory: def update_clan_meta(options: UpdateOptions) -> Inventory:
inventory = load_inventory_json(options.flake) inventory = load_inventory_json(options.directory)
inventory["meta"] = options.meta inventory["meta"] = options.meta
set_inventory(inventory, options.flake, "Update clan metadata") set_inventory(inventory, options.directory, "Update clan metadata")
return inventory return inventory

View File

@@ -403,3 +403,23 @@ def run(
raise ClanCmdError(cmd_out) raise ClanCmdError(cmd_out)
return 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,
)

View File

@@ -1,3 +1,9 @@
import pytest
from clan_cli.custom_logger import setup_logging
# collect_ignore = ["./nixpkgs"]
pytest_plugins = [ pytest_plugins = [
"clan_cli.tests.temporary_dir", "clan_cli.tests.temporary_dir",
"clan_cli.tests.root", "clan_cli.tests.root",
@@ -13,3 +19,13 @@ pytest_plugins = [
"clan_cli.tests.stdout", "clan_cli.tests.stdout",
"clan_cli.tests.nix_config", "clan_cli.tests.nix_config",
] ]
# Executed on pytest session start
def pytest_sessionstart(session: pytest.Session) -> None:
# This function will be called once at the beginning of the test session
print("Starting pytest session")
# You can access the session config, items, testsfailed, etc.
print(f"Session config: {session.config}")
setup_logging(level="INFO")

View File

@@ -4,14 +4,9 @@ import sys
import urllib import urllib
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from .errors import ClanError from .errors import ClanError
if TYPE_CHECKING:
from clan_cli.flake import Flake
from clan_cli.machines.machines import Machine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -136,17 +131,12 @@ def vm_state_dir(flake_url: str, vm_name: str) -> Path:
return user_data_dir() / "clan" / "vmstate" / clan_key / vm_name return user_data_dir() / "clan" / "vmstate" / clan_key / vm_name
def machines_dir(flake: "Flake") -> Path: def machines_dir(flake_dir: Path) -> Path:
if flake.is_local: return flake_dir / "machines"
return flake.path / "machines"
store_path = flake.store_path
assert store_path is not None, "Invalid flake object. Doesn't have a store path"
return Path(store_path) / "machines"
def specific_machine_dir(machine: "Machine") -> Path: def specific_machine_dir(flake_dir: Path, machine: str) -> Path:
return machines_dir(machine.flake) / machine.name return machines_dir(flake_dir) / machine
def module_root() -> Path: def module_root() -> Path:

View File

@@ -48,7 +48,6 @@ def bubblewrap_cmd(generator: str, facts_dir: Path, secrets_dir: Path) -> list[s
"--unshare-all", "--unshare-all",
"--tmpfs", "/", "--tmpfs", "/",
"--ro-bind", "/nix/store", "/nix/store", "--ro-bind", "/nix/store", "/nix/store",
"--ro-bind", "/bin/sh", "/bin/sh",
"--dev", "/dev", "--dev", "/dev",
# not allowed to bind procfs in some sandboxes # not allowed to bind procfs in some sandboxes
"--bind", str(facts_dir), str(facts_dir), "--bind", str(facts_dir), str(facts_dir),

View File

@@ -4,7 +4,6 @@ from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
import clan_cli.machines.machines as machines import clan_cli.machines.machines as machines
from clan_cli.ssh.host import Host
class SecretStoreBase(ABC): class SecretStoreBase(ABC):
@@ -26,7 +25,7 @@ class SecretStoreBase(ABC):
def exists(self, service: str, name: str) -> bool: def exists(self, service: str, name: str) -> bool:
pass pass
def needs_upload(self, host: Host) -> bool: def needs_upload(self) -> bool:
return True return True
@abstractmethod @abstractmethod

View File

@@ -6,7 +6,6 @@ from typing import override
from clan_cli.cmd import Log, RunOpts from clan_cli.cmd import Log, RunOpts
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
from clan_cli.ssh.host import Host
from . import SecretStoreBase from . import SecretStoreBase
@@ -94,9 +93,9 @@ class SecretStore(SecretStoreBase):
return b"\n".join(hashes) return b"\n".join(hashes)
@override @override
def needs_upload(self, host: Host) -> bool: def needs_upload(self) -> bool:
local_hash = self.generate_hash() local_hash = self.generate_hash()
remote_hash = host.run( remote_hash = self.machine.target_host.run(
# TODO get the path to the secrets from the machine # TODO get the path to the secrets from the machine
["cat", f"{self.machine.secrets_upload_directory}/.pass_info"], ["cat", f"{self.machine.secrets_upload_directory}/.pass_info"],
RunOpts(log=Log.STDERR, check=False), RunOpts(log=Log.STDERR, check=False),

View File

@@ -6,7 +6,6 @@ from clan_cli.secrets.folders import sops_secrets_folder
from clan_cli.secrets.machines import add_machine, has_machine from clan_cli.secrets.machines import add_machine, has_machine
from clan_cli.secrets.secrets import decrypt_secret, encrypt_secret, has_secret from clan_cli.secrets.secrets import decrypt_secret, encrypt_secret, has_secret
from clan_cli.secrets.sops import generate_private_key from clan_cli.secrets.sops import generate_private_key
from clan_cli.ssh.host import Host
from . import SecretStoreBase from . import SecretStoreBase
@@ -61,7 +60,7 @@ class SecretStore(SecretStoreBase):
) )
@override @override
def needs_upload(self, host: Host) -> bool: def needs_upload(self) -> bool:
return False return False
# We rely now on the vars backend to upload the age key # We rely now on the vars backend to upload the age key

View File

@@ -1,6 +1,5 @@
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import override
from clan_cli.dirs import vm_state_dir from clan_cli.dirs import vm_state_dir
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
@@ -29,7 +28,6 @@ class SecretStore(SecretStoreBase):
def exists(self, service: str, name: str) -> bool: def exists(self, service: str, name: str) -> bool:
return (self.dir / service / name).exists() return (self.dir / service / name).exists()
@override
def upload(self, output_dir: Path) -> None: def upload(self, output_dir: Path) -> None:
if output_dir.exists(): if output_dir.exists():
shutil.rmtree(output_dir) shutil.rmtree(output_dir)

View File

@@ -11,16 +11,16 @@ log = logging.getLogger(__name__)
def upload_secrets(machine: Machine) -> None: def upload_secrets(machine: Machine) -> None:
with machine.target_host() as host: if not machine.secret_facts_store.needs_upload():
if not machine.secret_facts_store.needs_upload(host): machine.info("Secrets already uploaded")
machine.info("Secrets already uploaded") return
return
with TemporaryDirectory(prefix="facts-upload-") as _tempdir: with TemporaryDirectory(prefix="facts-upload-") as _tempdir:
local_secret_dir = Path(_tempdir).resolve() local_secret_dir = Path(_tempdir).resolve()
machine.secret_facts_store.upload(local_secret_dir) machine.secret_facts_store.upload(local_secret_dir)
remote_secret_dir = Path(machine.secrets_upload_directory) remote_secret_dir = Path(machine.secrets_upload_directory)
upload(host, local_secret_dir, remote_secret_dir)
upload(machine.target_host, local_secret_dir, remote_secret_dir)
def upload_command(args: argparse.Namespace) -> None: def upload_command(args: argparse.Namespace) -> None:

View File

@@ -14,7 +14,6 @@ from clan_cli.nix import (
nix_build, nix_build,
nix_command, nix_command,
nix_config, nix_config,
nix_eval,
nix_metadata, nix_metadata,
nix_test_store, nix_test_store,
) )
@@ -575,12 +574,12 @@ class Flake:
identifier: str identifier: str
inputs_from: str | None = None inputs_from: str | None = None
hash: str | None = None hash: str | None = None
flake_cache_path: Path | None = None
store_path: str | None = None store_path: str | None = None
cache: FlakeCache | None = None
_flake_cache_path: Path | None = field(init=False, default=None) _cache: FlakeCache | None = None
_cache: FlakeCache | None = field(init=False, default=None) _path: Path | None = None
_path: Path | None = field(init=False, default=None) _is_local: bool | None = None
_is_local: bool | None = field(init=False, default=None)
@classmethod @classmethod
def from_json(cls: type["Flake"], data: dict[str, Any]) -> "Flake": def from_json(cls: type["Flake"], data: dict[str, Any]) -> "Flake":
@@ -620,9 +619,11 @@ class Flake:
except Exception as e: except Exception as e:
log.warning(f"Failed load eval cache: {e}. Continue without cache") log.warning(f"Failed load eval cache: {e}. Continue without cache")
def prefetch(self) -> None: def invalidate_cache(self) -> None:
""" """
Loads the flake into the store and populates self.store_path and self.hash such that the flake can evaluate locally and offline Invalidate the cache and reload it.
This method is used to refresh the cache by reloading it from the flake.
""" """
cmd = [ cmd = [
"flake", "flake",
@@ -641,15 +642,6 @@ class Flake:
flake_metadata = json.loads(flake_prefetch.stdout) flake_metadata = json.loads(flake_prefetch.stdout)
self.store_path = flake_metadata["storePath"] self.store_path = flake_metadata["storePath"]
self.hash = flake_metadata["hash"] self.hash = flake_metadata["hash"]
self.flake_metadata = flake_metadata
def invalidate_cache(self) -> None:
"""
Invalidate the cache and reload it.
This method is used to refresh the cache by reloading it from the flake.
"""
self.prefetch()
self._cache = FlakeCache() self._cache = FlakeCache()
assert self.hash is not None assert self.hash is not None
@@ -659,17 +651,17 @@ class Flake:
) )
self.load_cache() self.load_cache()
if "original" not in self.flake_metadata: if "original" not in flake_metadata:
self.flake_metadata = nix_metadata(self.identifier) flake_metadata = nix_metadata(self.identifier)
if self.flake_metadata["original"].get("url", "").startswith("file:"): if flake_metadata["original"].get("url", "").startswith("file:"):
self._is_local = True self._is_local = True
path = self.flake_metadata["original"]["url"].removeprefix("file://") path = flake_metadata["original"]["url"].removeprefix("file://")
path = path.removeprefix("file:") path = path.removeprefix("file:")
self._path = Path(path) self._path = Path(path)
elif self.flake_metadata["original"].get("path"): elif flake_metadata["original"].get("path"):
self._is_local = True self._is_local = True
self._path = Path(self.flake_metadata["original"]["path"]) self._path = Path(flake_metadata["original"]["path"])
else: else:
self._is_local = False self._is_local = False
assert self.store_path is not None assert self.store_path is not None
@@ -763,56 +755,6 @@ class Flake:
if self.flake_cache_path: if self.flake_cache_path:
self._cache.save_to_file(self.flake_cache_path) self._cache.save_to_file(self.flake_cache_path)
def uncached_nix_eval_with_args(
self,
attr_path: str,
f_args: dict[str, str],
nix_options: list[str] | None = None,
) -> str:
"""
Calls a nix function with the provided arguments 'f_args'
The argument must be an attribute set.
Args:
attr_path (str): The attribute path to the nix function
f_args (dict[str, nix_expr]): A python dictionary mapping from the name of the argument to a raw nix expression.
Example
flake.uncached_nix_eval_with_args(
"clanInternals.evalServiceSchema",
{ "moduleSpec": "{ name = \"hello-world\"; input = null; }" }
)
> '{ ...JSONSchema... }'
"""
# Always prefetch, so we don't get any stale information
self.prefetch()
if nix_options is None:
nix_options = []
arg_expr = "{"
for arg_name, arg_value in f_args.items():
arg_expr += f" {arg_name} = {arg_value}; "
arg_expr += "}"
nix_code = f"""
let
flake = builtins.getFlake "path:{self.store_path}?narHash={self.hash}";
in
flake.{attr_path} {arg_expr}
"""
if tmp_store := nix_test_store():
nix_options += ["--store", str(tmp_store)]
nix_options.append("--impure")
output = run(
nix_eval(["--expr", nix_code, *nix_options]), RunOpts(log=Log.NONE)
).stdout.strip()
return output
def precache( def precache(
self, self,
selectors: list[str], selectors: list[str],

View File

@@ -14,7 +14,7 @@ from clan_cli.facts.generate import generate_facts
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
from clan_cli.vars.generate import generate_vars from clan_cli.vars.generate import generate_vars
from clan_cli.vars.upload import populate_secret_vars from clan_cli.vars.upload import upload_secret_vars
from .automount import pause_automounting from .automount import pause_automounting
from .list import list_possible_keymaps, list_possible_languages from .list import list_possible_keymaps, list_possible_languages
@@ -35,7 +35,6 @@ class Disk:
device: str device: str
# TODO: unify this with machine install
@API.register @API.register
def flash_machine( def flash_machine(
machine: Machine, machine: Machine,
@@ -108,7 +107,7 @@ def flash_machine(
local_dir.mkdir(parents=True) local_dir.mkdir(parents=True)
machine.secret_facts_store.upload(local_dir) machine.secret_facts_store.upload(local_dir)
populate_secret_vars(machine, local_dir) upload_secret_vars(machine, local_dir)
disko_install = [] disko_install = []
if os.geteuid() != 0: if os.geteuid() != 0:

View File

@@ -21,9 +21,8 @@ from typing import Any
from clan_lib.api import API, dataclass_to_dict, from_dict from clan_lib.api import API, dataclass_to_dict, from_dict
from clan_cli.cmd import run from clan_cli.cmd import run_no_stdout
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.flake import Flake
from clan_cli.git import commit_file from clan_cli.git import commit_file
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
@@ -50,11 +49,11 @@ __all__ = [
] ]
def get_inventory_path(flake: Flake) -> Path: def get_inventory_path(flake_dir: str | Path) -> Path:
""" """
Get the path to the inventory file in the flake directory Get the path to the inventory file in the flake directory
""" """
inventory_file = (flake.path / "inventory.json").resolve() inventory_file = (Path(flake_dir) / "inventory.json").resolve()
return inventory_file return inventory_file
@@ -62,7 +61,8 @@ def get_inventory_path(flake: Flake) -> Path:
default_inventory: Inventory = {"meta": {"name": "New Clan"}} default_inventory: Inventory = {"meta": {"name": "New Clan"}}
def load_inventory_eval(flake_dir: Flake) -> Inventory: @API.register
def load_inventory_eval(flake_dir: str | Path) -> Inventory:
""" """
Loads the evaluated inventory. Loads the evaluated inventory.
After all merge operations with eventual nix code in buildClan. After all merge operations with eventual nix code in buildClan.
@@ -80,7 +80,7 @@ def load_inventory_eval(flake_dir: Flake) -> Inventory:
] ]
) )
proc = run(cmd) proc = run_no_stdout(cmd)
try: try:
res = proc.stdout.strip() res = proc.stdout.strip()
@@ -355,7 +355,7 @@ def determine_writeability(
return results return results
def get_inventory_current_priority(flake: Flake) -> dict: def get_inventory_current_priority(flake_dir: str | Path) -> dict:
""" """
Returns the current priority of the inventory values Returns the current priority of the inventory values
@@ -375,12 +375,12 @@ def get_inventory_current_priority(flake: Flake) -> dict:
""" """
cmd = nix_eval( cmd = nix_eval(
[ [
f"{flake}#clanInternals.inventoryClass.introspection", f"{flake_dir}#clanInternals.inventoryClass.introspection",
"--json", "--json",
] ]
) )
proc = run(cmd) proc = run_no_stdout(cmd)
try: try:
res = proc.stdout.strip() res = proc.stdout.strip()
@@ -393,7 +393,7 @@ def get_inventory_current_priority(flake: Flake) -> dict:
@API.register @API.register
def load_inventory_json(flake: Flake) -> Inventory: def load_inventory_json(flake_dir: str | Path) -> Inventory:
""" """
Load the inventory FILE from the flake directory Load the inventory FILE from the flake directory
If no file is found, returns an empty dictionary If no file is found, returns an empty dictionary
@@ -403,7 +403,7 @@ def load_inventory_json(flake: Flake) -> Inventory:
Use load_inventory_eval instead Use load_inventory_eval instead
""" """
inventory_file = get_inventory_path(flake) inventory_file = get_inventory_path(flake_dir)
if not inventory_file.exists(): if not inventory_file.exists():
return {} return {}
@@ -473,14 +473,14 @@ def patch(d: dict[str, Any], path: str, content: Any) -> None:
@API.register @API.register
def patch_inventory_with(flake: Flake, section: str, content: dict[str, Any]) -> None: def patch_inventory_with(base_dir: Path, section: str, content: dict[str, Any]) -> None:
""" """
Pass only the section to update and the content to update with. Pass only the section to update and the content to update with.
Make sure you pass only attributes that you would like to persist. Make sure you pass only attributes that you would like to persist.
ATTENTION: Don't pass nix eval values unintentionally. ATTENTION: Don't pass nix eval values unintentionally.
""" """
inventory_file = get_inventory_path(flake) inventory_file = get_inventory_path(base_dir)
curr_inventory = {} curr_inventory = {}
if inventory_file.exists(): if inventory_file.exists():
@@ -492,9 +492,7 @@ def patch_inventory_with(flake: Flake, section: str, content: dict[str, Any]) ->
with inventory_file.open("w") as f: with inventory_file.open("w") as f:
json.dump(curr_inventory, f, indent=2) json.dump(curr_inventory, f, indent=2)
commit_file( commit_file(inventory_file, base_dir, commit_message=f"inventory.{section}: Update")
inventory_file, flake.path, commit_message=f"inventory.{section}: Update"
)
@dataclass @dataclass
@@ -506,16 +504,16 @@ class WriteInfo:
@API.register @API.register
def get_inventory_with_writeable_keys( def get_inventory_with_writeable_keys(
flake: Flake, flake_dir: str | Path,
) -> WriteInfo: ) -> WriteInfo:
""" """
Load the inventory and determine the writeable keys Load the inventory and determine the writeable keys
Performs 2 nix evaluations to get the current priority and the inventory Performs 2 nix evaluations to get the current priority and the inventory
""" """
current_priority = get_inventory_current_priority(flake) current_priority = get_inventory_current_priority(flake_dir)
data_eval: Inventory = load_inventory_eval(flake) data_eval: Inventory = load_inventory_eval(flake_dir)
data_disk: Inventory = load_inventory_json(flake) data_disk: Inventory = load_inventory_json(flake_dir)
writeables = determine_writeability( writeables = determine_writeability(
current_priority, dict(data_eval), dict(data_disk) current_priority, dict(data_eval), dict(data_disk)
@@ -524,17 +522,16 @@ def get_inventory_with_writeable_keys(
return WriteInfo(writeables, data_eval, data_disk) return WriteInfo(writeables, data_eval, data_disk)
# TODO: remove this function in favor of a proper read/write API
@API.register @API.register
def set_inventory( def set_inventory(
inventory: Inventory, flake: Flake, message: str, commit: bool = True inventory: Inventory, flake_dir: str | Path, message: str, commit: bool = True
) -> None: ) -> None:
""" """
Write the inventory to the flake directory Write the inventory to the flake directory
and commit it to git with the given message and commit it to git with the given message
""" """
write_info = get_inventory_with_writeable_keys(flake) write_info = get_inventory_with_writeable_keys(flake_dir)
# Remove internals from the inventory # Remove internals from the inventory
inventory.pop("tags", None) # type: ignore inventory.pop("tags", None) # type: ignore
@@ -555,43 +552,43 @@ def set_inventory(
for delete_path in delete_set: for delete_path in delete_set:
delete_by_path(persisted, delete_path) delete_by_path(persisted, delete_path)
inventory_file = get_inventory_path(flake) inventory_file = get_inventory_path(flake_dir)
with inventory_file.open("w") as f: with inventory_file.open("w") as f:
json.dump(persisted, f, indent=2) json.dump(persisted, f, indent=2)
if commit: if commit:
commit_file(inventory_file, flake.path, commit_message=message) commit_file(inventory_file, Path(flake_dir), commit_message=message)
# TODO: wrap this in a proper persistence API @API.register
def delete(flake: Flake, delete_set: set[str]) -> None: def delete(directory: str | Path, delete_set: set[str]) -> None:
""" """
Delete keys from the inventory Delete keys from the inventory
""" """
write_info = get_inventory_with_writeable_keys(flake) write_info = get_inventory_with_writeable_keys(directory)
data_disk = dict(write_info.data_disk) data_disk = dict(write_info.data_disk)
for delete_path in delete_set: for delete_path in delete_set:
delete_by_path(data_disk, delete_path) delete_by_path(data_disk, delete_path)
inventory_file = get_inventory_path(flake) inventory_file = get_inventory_path(directory)
with inventory_file.open("w") as f: with inventory_file.open("w") as f:
json.dump(data_disk, f, indent=2) json.dump(data_disk, f, indent=2)
commit_file( commit_file(
inventory_file, inventory_file,
flake.path, Path(directory),
commit_message=f"Delete inventory keys {delete_set}", commit_message=f"Delete inventory keys {delete_set}",
) )
def init_inventory(flake: Flake, init: Inventory | None = None) -> None: def init_inventory(directory: str, init: Inventory | None = None) -> None:
inventory = None inventory = None
# Try reading the current flake # Try reading the current flake
if init is None: if init is None:
with contextlib.suppress(ClanCmdError): with contextlib.suppress(ClanCmdError):
inventory = load_inventory_eval(flake) inventory = load_inventory_eval(directory)
if init is not None: if init is not None:
inventory = init inventory = init
@@ -599,9 +596,9 @@ def init_inventory(flake: Flake, init: Inventory | None = None) -> None:
# Write inventory.json file # Write inventory.json file
if inventory is not None: if inventory is not None:
# Persist creates a commit message for each change # Persist creates a commit message for each change
set_inventory(inventory, flake, "Init inventory") set_inventory(inventory, directory, "Init inventory")
@API.register @API.register
def get_inventory(flake: Flake) -> Inventory: def get_inventory(base_path: str | Path) -> Inventory:
return load_inventory_eval(flake) return load_inventory_eval(base_path)

View File

@@ -110,7 +110,7 @@ def create_machine(opts: CreateOptions, commit: bool = True) -> None:
new_machine["deploy"] = {"targetHost": target_host} new_machine["deploy"] = {"targetHost": target_host}
patch_inventory_with( patch_inventory_with(
opts.clan_dir, f"machines.{machine_name}", dataclass_to_dict(new_machine) clan_dir, f"machines.{machine_name}", dataclass_to_dict(new_machine)
) )
# Commit at the end in that order to avoid committing halve-baked machines # Commit at the end in that order to avoid committing halve-baked machines

View File

@@ -5,10 +5,9 @@ from pathlib import Path
from clan_lib.api import API from clan_lib.api import API
from clan_cli import inventory from clan_cli import Flake, inventory
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.dirs import specific_machine_dir 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.folders import sops_secrets_folder
from clan_cli.secrets.machines import has_machine as secrets_has_machine from clan_cli.secrets.machines import has_machine as secrets_has_machine
from clan_cli.secrets.machines import remove_machine as secrets_machine_remove from clan_cli.secrets.machines import remove_machine as secrets_machine_remove
@@ -16,46 +15,49 @@ from clan_cli.secrets.secrets import (
list_secrets, list_secrets,
) )
from .machines import Machine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@API.register @API.register
def delete_machine(machine: Machine) -> None: def delete_machine(flake: Flake, name: str) -> None:
try: try:
inventory.delete(machine.flake, {f"machines.{machine.name}"}) inventory.delete(str(flake.path), {f"machines.{name}"})
except KeyError as exc: except KeyError as exc:
# louis@(2025-03-09): test infrastructure does not seem to set the # louis@(2025-03-09): test infrastructure does not seem to set the
# inventory properly, but more importantly only one machine in my # inventory properly, but more importantly only one machine in my
# personal clan ended up in the inventory for some reason, so I think # personal clan ended up in the inventory for some reason, so I think
# it makes sense to eat the exception here. # it makes sense to eat the exception here.
log.warning( log.warning(
f"{machine.name} was missing or already deleted from the machines inventory: {exc}" f"{name} was missing or already deleted from the machines inventory: {exc}"
) )
changed_paths: list[Path] = [] changed_paths: list[Path] = []
folder = specific_machine_dir(machine) folder = specific_machine_dir(flake.path, name)
if folder.exists(): if folder.exists():
changed_paths.append(folder) changed_paths.append(folder)
shutil.rmtree(folder) shutil.rmtree(folder)
# louis@(2025-02-04): clean-up legacy (pre-vars) secrets: # louis@(2025-02-04): clean-up legacy (pre-vars) secrets:
sops_folder = sops_secrets_folder(machine.flake.path) sops_folder = sops_secrets_folder(flake.path)
filter_fn = lambda secret_name: secret_name.startswith(f"{machine.name}-") filter_fn = lambda secret_name: secret_name.startswith(f"{name}-")
for secret_name in list_secrets(machine.flake.path, filter_fn): for secret_name in list_secrets(flake.path, filter_fn):
secret_path = sops_folder / secret_name secret_path = sops_folder / secret_name
changed_paths.append(secret_path) changed_paths.append(secret_path)
shutil.rmtree(secret_path) shutil.rmtree(secret_path)
machine = Machine(name, flake)
changed_paths.extend(machine.public_vars_store.delete_store()) changed_paths.extend(machine.public_vars_store.delete_store())
changed_paths.extend(machine.secret_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: # Remove the machine's key, and update secrets & vars that referenced it:
if secrets_has_machine(machine.flake.path, machine.name): if secrets_has_machine(flake.path, name):
secrets_machine_remove(machine.flake.path, machine.name) secrets_machine_remove(flake.path, name)
def delete_command(args: argparse.Namespace) -> None: def delete_command(args: argparse.Namespace) -> None:
delete_machine(Machine(flake=args.flake, name=args.name)) delete_machine(args.flake, args.name)
def register_delete_parser(parser: argparse.ArgumentParser) -> None: def register_delete_parser(parser: argparse.ArgumentParser) -> None:

View File

@@ -7,10 +7,11 @@ from pathlib import Path
from clan_lib.api import API from clan_lib.api import API
from clan_cli.cmd import RunOpts, run from clan_cli.cmd import RunOpts, run_no_stdout
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.dirs import specific_machine_dir from clan_cli.dirs import specific_machine_dir
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.flake import Flake
from clan_cli.git import commit_file from clan_cli.git import commit_file
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_config, nix_eval from clan_cli.nix import nix_config, nix_eval
@@ -25,35 +26,61 @@ class HardwareConfig(Enum):
NIXOS_GENERATE_CONFIG = "nixos-generate-config" NIXOS_GENERATE_CONFIG = "nixos-generate-config"
NONE = "none" NONE = "none"
def config_path(self, machine: Machine) -> Path: def config_path(self, clan_dir: Path, machine_name: str) -> Path:
machine_dir = specific_machine_dir(machine) machine_dir = specific_machine_dir(clan_dir, machine_name)
if self == HardwareConfig.NIXOS_FACTER: if self == HardwareConfig.NIXOS_FACTER:
return machine_dir / "facter.json" return machine_dir / "facter.json"
return machine_dir / "hardware-configuration.nix" return machine_dir / "hardware-configuration.nix"
@classmethod @classmethod
def detect_type(cls: type["HardwareConfig"], machine: Machine) -> "HardwareConfig": def detect_type(
hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path(machine) cls: type["HardwareConfig"], clan_dir: Path, machine_name: str
) -> "HardwareConfig":
hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path(
clan_dir, machine_name
)
if hardware_config.exists() and "throw" not in hardware_config.read_text(): if hardware_config.exists() and "throw" not in hardware_config.read_text():
return HardwareConfig.NIXOS_GENERATE_CONFIG return HardwareConfig.NIXOS_GENERATE_CONFIG
if HardwareConfig.NIXOS_FACTER.config_path(machine).exists(): if HardwareConfig.NIXOS_FACTER.config_path(clan_dir, machine_name).exists():
return HardwareConfig.NIXOS_FACTER return HardwareConfig.NIXOS_FACTER
return HardwareConfig.NONE return HardwareConfig.NONE
@API.register @API.register
def show_machine_hardware_config(machine: Machine) -> HardwareConfig: def show_machine_hardware_config(clan_dir: Path, machine_name: str) -> HardwareConfig:
""" """
Show hardware information for a machine returns None if none exist. Show hardware information for a machine returns None if none exist.
""" """
return HardwareConfig.detect_type(machine) return HardwareConfig.detect_type(clan_dir, machine_name)
@API.register @API.register
def show_machine_hardware_platform(machine: Machine) -> str | None: def show_machine_deployment_target(clan_dir: Path, machine_name: str) -> str | None:
"""
Show deployment target for a machine returns None if none exist.
"""
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{clan_dir}#clanInternals.machines.{system}.{machine_name}",
"--apply",
"machine: { inherit (machine.config.clan.core.networking) targetHost; }",
"--json",
]
)
proc = run_no_stdout(cmd, RunOpts(prefix=machine_name))
res = proc.stdout.strip()
target_host = json.loads(res)
return target_host.get("targetHost", None)
@API.register
def show_machine_hardware_platform(clan_dir: Path, machine_name: str) -> str | None:
""" """
Show hardware information for a machine returns None if none exist. Show hardware information for a machine returns None if none exist.
""" """
@@ -61,13 +88,13 @@ def show_machine_hardware_platform(machine: Machine) -> str | None:
system = config["system"] system = config["system"]
cmd = nix_eval( cmd = nix_eval(
[ [
f"{machine.flake}#clanInternals.machines.{system}.{machine.name}", f"{clan_dir}#clanInternals.machines.{system}.{machine_name}",
"--apply", "--apply",
"machine: { inherit (machine.pkgs) system; }", "machine: { inherit (machine.pkgs) system; }",
"--json", "--json",
] ]
) )
proc = run(cmd, RunOpts(prefix=machine.name)) proc = run_no_stdout(cmd, RunOpts(prefix=machine_name))
res = proc.stdout.strip() res = proc.stdout.strip()
host_platform = json.loads(res) host_platform = json.loads(res)
@@ -76,8 +103,11 @@ def show_machine_hardware_platform(machine: Machine) -> str | None:
@dataclass @dataclass
class HardwareGenerateOptions: class HardwareGenerateOptions:
machine: Machine flake: Flake
machine: str
backend: HardwareConfig backend: HardwareConfig
target_host: str | None = None
keyfile: str | None = None
password: str | None = None password: str | None = None
@@ -88,9 +118,15 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
and place the resulting *.nix file in the machine's directory. and place the resulting *.nix file in the machine's directory.
""" """
machine = opts.machine machine = Machine(opts.machine, flake=opts.flake)
hw_file = opts.backend.config_path(opts.machine) if opts.keyfile is not None:
machine.private_key = Path(opts.keyfile)
if opts.target_host is not None:
machine.override_target_host = opts.target_host
hw_file = opts.backend.config_path(opts.flake.path, opts.machine)
hw_file.parent.mkdir(parents=True, exist_ok=True) hw_file.parent.mkdir(parents=True, exist_ok=True)
if opts.backend == HardwareConfig.NIXOS_FACTER: if opts.backend == HardwareConfig.NIXOS_FACTER:
@@ -103,26 +139,26 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
"--show-hardware-config", "--show-hardware-config",
] ]
with machine.target_host() as host: host = machine.target_host
host.ssh_options["StrictHostKeyChecking"] = "accept-new" host.ssh_options["StrictHostKeyChecking"] = "accept-new"
host.ssh_options["UserKnownHostsFile"] = "/dev/null" host.ssh_options["UserKnownHostsFile"] = "/dev/null"
if opts.password: if opts.password:
host.password = opts.password host.password = opts.password
out = host.run(config_command, become_root=True, opts=RunOpts(check=False)) out = host.run(config_command, become_root=True, opts=RunOpts(check=False))
if out.returncode != 0: if out.returncode != 0:
if "nixos-facter" in out.stderr and "not found" in out.stderr: if "nixos-facter" in out.stderr and "not found" in out.stderr:
machine.error(str(out.stderr)) machine.error(str(out.stderr))
msg = ( msg = (
"Please use our custom nixos install images from https://github.com/nix-community/nixos-images/releases/tag/nixos-unstable. " "Please use our custom nixos install images from https://github.com/nix-community/nixos-images/releases/tag/nixos-unstable. "
"nixos-factor only works on nixos / clan systems currently." "nixos-factor only works on nixos / clan systems currently."
) )
raise ClanError(msg)
machine.error(str(out))
msg = f"Failed to inspect {opts.machine}. Address: {host.target}"
raise ClanError(msg) raise ClanError(msg)
machine.error(str(out))
msg = f"Failed to inspect {opts.machine}. Address: {host.target}"
raise ClanError(msg)
backup_file = None backup_file = None
if hw_file.exists(): if hw_file.exists():
backup_file = hw_file.with_suffix(".bak") backup_file = hw_file.with_suffix(".bak")
@@ -135,11 +171,11 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
commit_file( commit_file(
hw_file, hw_file,
opts.machine.flake.path, opts.flake.path,
f"machines/{opts.machine}/{hw_file.name}: update hardware configuration", f"machines/{opts.machine}/{hw_file.name}: update hardware configuration",
) )
try: try:
show_machine_hardware_platform(opts.machine) show_machine_hardware_platform(opts.flake.path, opts.machine)
if backup_file: if backup_file:
backup_file.unlink(missing_ok=True) backup_file.unlink(missing_ok=True)
except ClanCmdError as e: except ClanCmdError as e:
@@ -160,13 +196,10 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
def update_hardware_config_command(args: argparse.Namespace) -> None: def update_hardware_config_command(args: argparse.Namespace) -> None:
machine = Machine(
flake=args.flake,
name=args.machine,
override_target_host=args.target_host,
)
opts = HardwareGenerateOptions( opts = HardwareGenerateOptions(
machine=machine, flake=args.flake,
machine=args.machine,
target_host=args.target_host,
password=args.password, password=args.password,
backend=HardwareConfig(args.backend), backend=HardwareConfig(args.backend),
) )

View File

@@ -36,6 +36,7 @@ class BuildOn(Enum):
@dataclass @dataclass
class InstallOptions: class InstallOptions:
machine: Machine machine: Machine
target_host: str
kexec: str | None = None kexec: str | None = None
debug: bool = False debug: bool = False
no_reboot: bool = False no_reboot: bool = False
@@ -51,16 +52,17 @@ class InstallOptions:
@API.register @API.register
def install_machine(opts: InstallOptions) -> None: def install_machine(opts: InstallOptions) -> None:
machine = opts.machine machine = opts.machine
machine.override_target_host = opts.target_host
machine.debug(f"installing {machine.name}") machine.info(f"installing {machine.name}")
h = machine.target_host
machine.info(f"target host: {h.target}")
generate_facts([machine]) generate_facts([machine])
generate_vars([machine]) generate_vars([machine])
with ( with TemporaryDirectory(prefix="nixos-install-") as _base_directory:
TemporaryDirectory(prefix="nixos-install-") as _base_directory,
machine.target_host() as host,
):
base_directory = Path(_base_directory).resolve() base_directory = Path(_base_directory).resolve()
activation_secrets = base_directory / "activation_secrets" activation_secrets = base_directory / "activation_secrets"
upload_dir = activation_secrets / machine.secrets_upload_directory.lstrip("/") upload_dir = activation_secrets / machine.secrets_upload_directory.lstrip("/")
@@ -111,7 +113,11 @@ def install_machine(opts: InstallOptions) -> None:
[ [
"--generate-hardware-config", "--generate-hardware-config",
str(opts.update_hardware_config.value), str(opts.update_hardware_config.value),
str(opts.update_hardware_config.config_path(machine)), str(
opts.update_hardware_config.config_path(
machine.flake.path, machine.name
)
),
] ]
) )
@@ -128,14 +134,14 @@ def install_machine(opts: InstallOptions) -> None:
if opts.build_on: if opts.build_on:
cmd += ["--build-on", opts.build_on.value] cmd += ["--build-on", opts.build_on.value]
if host.port: if h.port:
cmd += ["--ssh-port", str(host.port)] cmd += ["--ssh-port", str(h.port)]
if opts.kexec: if opts.kexec:
cmd += ["--kexec", opts.kexec] cmd += ["--kexec", opts.kexec]
if opts.debug: if opts.debug:
cmd.append("--debug") cmd.append("--debug")
cmd.append(host.target) cmd.append(h.target)
if opts.use_tor: if opts.use_tor:
# nix copy does not support tor socks proxy # nix copy does not support tor socks proxy
# cmd.append("--ssh-option") # cmd.append("--ssh-option")
@@ -158,32 +164,7 @@ def install_machine(opts: InstallOptions) -> None:
def install_command(args: argparse.Namespace) -> None: def install_command(args: argparse.Namespace) -> None:
host_key_check = HostKeyCheck.from_str(args.host_key_check) host_key_check = HostKeyCheck.from_str(args.host_key_check)
try: try:
# Only if the caller did not specify a target_host via args.target_host machine = Machine(name=args.machine, flake=args.flake, nix_options=args.option)
# Find a suitable target_host that is reachable
target_host = args.target_host
deploy_info: DeployInfo | None = ssh_command_parse(args)
if deploy_info and not args.target_host:
host = find_reachable_host(deploy_info, host_key_check)
if host is None:
use_tor = True
target_host = f"root@{deploy_info.tor}"
else:
target_host = host.target
if args.password:
password = args.password
elif deploy_info and deploy_info.pwd:
password = deploy_info.pwd
else:
password = None
machine = Machine(
name=args.machine,
flake=args.flake,
nix_options=args.option,
override_target_host=target_host,
)
use_tor = False use_tor = False
if machine._class_ == "darwin": if machine._class_ == "darwin":
@@ -194,16 +175,41 @@ def install_command(args: argparse.Namespace) -> None:
msg = "Could not find clan flake toplevel directory" msg = "Could not find clan flake toplevel directory"
raise ClanError(msg) raise ClanError(msg)
deploy_info: DeployInfo | None = ssh_command_parse(args)
if args.target_host:
target_host = args.target_host
elif deploy_info:
host = find_reachable_host(deploy_info, host_key_check)
if host is None:
use_tor = True
target_host = f"root@{deploy_info.tor}"
else:
target_host = host.target
password = deploy_info.pwd
else:
target_host = machine.target_host.target
if args.password:
password = args.password
elif deploy_info and deploy_info.pwd:
password = deploy_info.pwd
else:
password = None
if not target_host:
msg = "No target host provided, please provide a target host."
raise ClanError(msg)
if not args.yes: if not args.yes:
ask = input( ask = input(f"Install {args.machine} to {target_host}? [y/N] ")
f"Install {args.machine} to {machine.target_host_address}? [y/N] "
)
if ask != "y": if ask != "y":
return None return None
return install_machine( return install_machine(
InstallOptions( InstallOptions(
machine=machine, machine=machine,
target_host=target_host,
kexec=args.kexec, kexec=args.kexec,
phases=args.phases, phases=args.phases,
debug=args.debug, debug=args.debug,

View File

@@ -2,7 +2,6 @@ import argparse
import json import json
import logging import logging
import re import re
import time
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
@@ -12,38 +11,38 @@ from clan_lib.api.disk import MachineDiskMatter
from clan_lib.api.modules import parse_frontmatter from clan_lib.api.modules import parse_frontmatter
from clan_lib.api.serde import dataclass_to_dict from clan_lib.api.serde import dataclass_to_dict
from clan_cli.cmd import RunOpts, run from clan_cli.cmd import RunOpts, run_no_stdout
from clan_cli.completions import add_dynamic_completer, complete_tags from clan_cli.completions import add_dynamic_completer, complete_tags
from clan_cli.dirs import specific_machine_dir from clan_cli.dirs import specific_machine_dir
from clan_cli.errors import ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.flake import Flake
from clan_cli.inventory import ( from clan_cli.inventory import (
Machine,
load_inventory_eval, load_inventory_eval,
patch_inventory_with, patch_inventory_with,
) )
from clan_cli.inventory.classes import Machine as InventoryMachine
from clan_cli.machines.hardware import HardwareConfig from clan_cli.machines.hardware import HardwareConfig
from clan_cli.machines.machines import Machine from clan_cli.nix import nix_eval, nix_shell
from clan_cli.nix import nix_eval
from clan_cli.tags import list_nixos_machines_by_tags from clan_cli.tags import list_nixos_machines_by_tags
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@API.register @API.register
def set_machine(flake: Flake, machine_name: str, machine: InventoryMachine) -> None: def set_machine(flake_url: Path, machine_name: str, machine: Machine) -> None:
patch_inventory_with(flake, f"machines.{machine_name}", dataclass_to_dict(machine)) patch_inventory_with(
flake_url, f"machines.{machine_name}", dataclass_to_dict(machine)
)
@API.register @API.register
def list_machines(flake: Flake) -> dict[str, InventoryMachine]: def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
inventory = load_inventory_eval(flake) inventory = load_inventory_eval(flake_url)
return inventory.get("machines", {}) return inventory.get("machines", {})
@dataclass @dataclass
class MachineDetails: class MachineDetails:
machine: InventoryMachine machine: Machine
hw_config: HardwareConfig | None = None hw_config: HardwareConfig | None = None
disk_schema: MachineDiskMatter | None = None disk_schema: MachineDiskMatter | None = None
@@ -60,16 +59,16 @@ def extract_header(c: str) -> str:
@API.register @API.register
def get_machine_details(machine: Machine) -> MachineDetails: def get_inventory_machine_details(flake_url: Path, machine_name: str) -> MachineDetails:
inventory = load_inventory_eval(machine.flake) inventory = load_inventory_eval(flake_url)
machine_inv = inventory.get("machines", {}).get(machine.name) machine = inventory.get("machines", {}).get(machine_name)
if machine_inv is None: if machine is None:
msg = f"Machine {machine.name} not found in inventory" msg = f"Machine {machine_name} not found in inventory"
raise ClanError(msg) raise ClanError(msg)
hw_config = HardwareConfig.detect_type(machine) hw_config = HardwareConfig.detect_type(flake_url, machine_name)
machine_dir = specific_machine_dir(machine) machine_dir = specific_machine_dir(flake_url, machine_name)
disk_schema: MachineDiskMatter | None = None disk_schema: MachineDiskMatter | None = None
disk_path = machine_dir / "disko.nix" disk_path = machine_dir / "disko.nix"
if disk_path.exists(): if disk_path.exists():
@@ -80,9 +79,7 @@ def get_machine_details(machine: Machine) -> MachineDetails:
if data: if data:
disk_schema = data # type: ignore disk_schema = data # type: ignore
return MachineDetails( return MachineDetails(machine=machine, hw_config=hw_config, disk_schema=disk_schema)
machine=machine_inv, hw_config=hw_config, disk_schema=disk_schema
)
def list_nixos_machines(flake_url: str | Path) -> list[str]: def list_nixos_machines(flake_url: str | Path) -> list[str]:
@@ -95,7 +92,7 @@ def list_nixos_machines(flake_url: str | Path) -> list[str]:
] ]
) )
proc = run(cmd) proc = run_no_stdout(cmd)
try: try:
res = proc.stdout.strip() res = proc.stdout.strip()
@@ -109,36 +106,53 @@ def list_nixos_machines(flake_url: str | Path) -> list[str]:
@dataclass @dataclass
class ConnectionOptions: class ConnectionOptions:
keyfile: str | None = None
timeout: int = 2 timeout: int = 2
retries: int = 10
from clan_cli.machines.machines import Machine
@API.register @API.register
def check_machine_online( def check_machine_online(
machine: Machine, opts: ConnectionOptions | None = None flake_url: str | Path, machine_name: str, opts: ConnectionOptions | None
) -> Literal["Online", "Offline"]: ) -> Literal["Online", "Offline"]:
hostname = machine.target_host_address machine = load_inventory_eval(flake_url).get("machines", {}).get(machine_name)
if not hostname: if not machine:
msg = f"Machine {machine.name} does not specify a targetHost" msg = f"Machine {machine_name} not found in inventory"
raise ClanError(msg) raise ClanError(msg)
timeout = opts.timeout if opts and opts.timeout else 2 hostname = machine.get("deploy", {}).get("targetHost")
for _ in range(opts.retries if opts and opts.retries else 10): if not hostname:
with machine.target_host() as target: msg = f"Machine {machine_name} does not specify a targetHost"
res = target.run( raise ClanError(msg)
["true"],
RunOpts(timeout=timeout, check=False, needs_user_terminal=True),
)
if res.returncode == 0: timeout = opts.timeout if opts and opts.timeout else 20
return "Online"
time.sleep(timeout)
return "Offline" cmd = nix_shell(
["util-linux", *(["openssh"] if hostname else [])],
[
"ssh",
*(["-i", f"{opts.keyfile}"] if opts and opts.keyfile else []),
# Disable strict host key checking
"-o",
"StrictHostKeyChecking=accept-new",
# Disable known hosts file
"-o",
"UserKnownHostsFile=/dev/null",
"-o",
f"ConnectTimeout={timeout}",
f"{hostname}",
"true",
"&> /dev/null",
],
)
try:
proc = run_no_stdout(cmd, RunOpts(needs_user_terminal=True))
if proc.returncode != 0:
return "Offline"
except ClanCmdError:
return "Offline"
else:
return "Online"
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:

View File

@@ -2,14 +2,12 @@ import importlib
import json import json
import logging import logging
import re import re
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from clan_cli.cmd import Log, RunOpts, run from clan_cli.cmd import Log, RunOpts, run_no_stdout
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.facts import public_modules as facts_public_modules from clan_cli.facts import public_modules as facts_public_modules
from clan_cli.facts import secret_modules as facts_secret_modules from clan_cli.facts import secret_modules as facts_secret_modules
@@ -26,7 +24,7 @@ if TYPE_CHECKING:
from clan_cli.vars.generate import Generator from clan_cli.vars.generate import Generator
@dataclass(frozen=True) @dataclass
class Machine: class Machine:
name: str name: str
flake: Flake flake: Flake
@@ -147,37 +145,34 @@ class Machine:
def flake_dir(self) -> Path: def flake_dir(self) -> Path:
return self.flake.path return self.flake.path
@contextmanager @property
def target_host(self) -> Iterator[Host]: def target_host(self) -> Host:
with parse_deployment_address( return parse_deployment_address(
self.name, self.name,
self.target_host_address, self.target_host_address,
self.host_key_check, self.host_key_check,
private_key=self.private_key, private_key=self.private_key,
meta={"machine": self}, meta={"machine": self},
) as target_host: )
yield target_host
@contextmanager @property
def build_host(self) -> Iterator[Host | None]: def build_host(self) -> Host:
""" """
The host where the machine is built and deployed from. The host where the machine is built and deployed from.
Can be the same as the target host. Can be the same as the target host.
""" """
build_host = self.override_build_host or self.deployment.get("buildHost") build_host = self.override_build_host or self.deployment.get("buildHost")
if build_host is None: if build_host is None:
yield None return self.target_host
return
# 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
with parse_deployment_address( return parse_deployment_address(
self.name, self.name,
build_host, build_host,
self.host_key_check, self.host_key_check,
forward_agent=True, forward_agent=True,
private_key=self.private_key, private_key=self.private_key,
meta={"machine": self}, meta={"machine": self, "target_host": self.target_host},
) as build_host: )
yield build_host
@cached_property @cached_property
def deploy_as_root(self) -> bool: def deploy_as_root(self) -> bool:
@@ -188,7 +183,7 @@ class Machine:
# however there is a soon to be merged PR that requires deployment # 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 # as root to match NixOS: https://github.com/nix-darwin/nix-darwin/pull/1341
return json.loads( return json.loads(
run( run_no_stdout(
nix_eval( nix_eval(
[ [
f"{self.flake}#darwinConfigurations.{self.name}.options.system", f"{self.flake}#darwinConfigurations.{self.name}.options.system",

View File

@@ -5,12 +5,11 @@ import os
import re import re
import shlex import shlex
import sys import sys
from contextlib import ExitStack
from clan_lib.api import API from clan_lib.api import API
from clan_cli.async_run import AsyncContext, AsyncOpts, AsyncRuntime, is_async_cancelled from clan_cli.async_run import AsyncContext, AsyncOpts, AsyncRuntime, is_async_cancelled
from clan_cli.cmd import Log, MsgColor, RunOpts, run from clan_cli.cmd import MsgColor, RunOpts, run
from clan_cli.colors import AnsiColor from clan_cli.colors import AnsiColor
from clan_cli.completions import ( from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
@@ -21,13 +20,14 @@ from clan_cli.facts.generate import generate_facts
from clan_cli.facts.upload import upload_secrets from clan_cli.facts.upload import upload_secrets
from clan_cli.flake import Flake from clan_cli.flake import Flake
from clan_cli.inventory import Machine as InventoryMachine from clan_cli.inventory import Machine as InventoryMachine
from clan_cli.machines.list import list_machines
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_command, nix_config, nix_metadata from clan_cli.nix import nix_command, nix_config, nix_metadata
from clan_cli.ssh.host import Host, HostKeyCheck from clan_cli.ssh.host import Host, HostKeyCheck
from clan_cli.vars.generate import generate_vars from clan_cli.vars.generate import generate_vars
from clan_cli.vars.upload import upload_secret_vars from clan_cli.vars.upload import upload_secret_vars
from .inventory import get_all_machines, get_selected_machines
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -43,7 +43,8 @@ def is_local_input(node: dict[str, dict[str, str]]) -> bool:
) )
def upload_sources(machine: Machine, host: Host) -> str: def upload_sources(machine: Machine) -> str:
host = machine.build_host
env = host.nix_ssh_env(os.environ.copy()) env = host.nix_ssh_env(os.environ.copy())
flake_url = ( flake_url = (
@@ -110,41 +111,37 @@ def update_machines(base_path: str, machines: list[InventoryMachine]) -> None:
flake = Flake(base_path) flake = Flake(base_path)
for machine in machines: for machine in machines:
name = machine.get("name") name = machine.get("name")
# prefer target host set via inventory, but fallback to the one set in the machine
target_host = machine.get("deploy", {}).get("targetHost")
if not name: if not name:
msg = "Machine name is not set" msg = "Machine name is not set"
raise ClanError(msg) raise ClanError(msg)
m = Machine( m = Machine(
name, name,
flake=flake, flake=flake,
override_target_host=target_host,
) )
# prefer target host set via inventory, but fallback to the one set in the machine
if target_host := machine.get("deploy", {}).get("targetHost"):
m.override_target_host = target_host
group_machines.append(m) group_machines.append(m)
deploy_machines(group_machines) deploy_machines(group_machines)
def deploy_machine(machine: Machine) -> None: def deploy_machines(machines: list[Machine]) -> None:
with ExitStack() as stack: """
target_host = stack.enter_context(machine.target_host()) Deploy to all hosts in parallel
build_host = stack.enter_context(machine.build_host()) """
if machine._class_ == "darwin":
if not machine.deploy_as_root and target_host.user == "root":
msg = f"'targetHost' should be set to a non-root user for deploying to nix-darwin on machine '{machine.name}'"
raise ClanError(msg)
host = build_host or target_host
def deploy(machine: Machine) -> None:
host = machine.build_host
generate_facts([machine], service=None, regenerate=False) generate_facts([machine], service=None, regenerate=False)
generate_vars([machine], generator_name=None, regenerate=False) generate_vars([machine], generator_name=None, regenerate=False)
upload_secrets(machine) upload_secrets(machine)
upload_secret_vars(machine, target_host) upload_secret_vars(machine)
path = upload_sources(machine, host) path = upload_sources(
machine=machine,
)
nix_options = [ nix_options = [
"--show-trace", "--show-trace",
@@ -169,7 +166,8 @@ def deploy_machine(machine: Machine) -> None:
"", "",
] ]
if build_host: target_host: Host | None = host.meta.get("target_host")
if target_host:
become_root = False become_root = False
nix_options += ["--target-host", target_host.target] nix_options += ["--target-host", target_host.target]
@@ -179,19 +177,19 @@ def deploy_machine(machine: Machine) -> None:
switch_cmd = [f"{machine._class_}-rebuild", "switch", *nix_options] switch_cmd = [f"{machine._class_}-rebuild", "switch", *nix_options]
test_cmd = [f"{machine._class_}-rebuild", "test", *nix_options] test_cmd = [f"{machine._class_}-rebuild", "test", *nix_options]
remote_env = host.nix_ssh_env(None, local_ssh=False) env = host.nix_ssh_env(None)
ret = host.run( ret = host.run(
switch_cmd, switch_cmd,
RunOpts( RunOpts(check=False, msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
check=False, extra_env=env,
log=Log.BOTH,
msg_color=MsgColor(stderr=AnsiColor.DEFAULT),
needs_user_terminal=True,
),
extra_env=remote_env,
become_root=become_root, 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(): if is_async_cancelled():
return return
@@ -206,27 +204,26 @@ def deploy_machine(machine: Machine) -> None:
ret = host.run( ret = host.run(
test_cmd if is_mobile else switch_cmd, test_cmd if is_mobile else switch_cmd,
RunOpts( RunOpts(
log=Log.BOTH,
msg_color=MsgColor(stderr=AnsiColor.DEFAULT), msg_color=MsgColor(stderr=AnsiColor.DEFAULT),
needs_user_terminal=True, needs_user_terminal=True,
), ),
extra_env=remote_env, extra_env=env,
become_root=become_root, become_root=become_root,
) )
def deploy_machines(machines: list[Machine]) -> None:
"""
Deploy to all hosts in parallel
"""
with AsyncRuntime() as runtime: with AsyncRuntime() as runtime:
for machine in machines: for machine in machines:
if machine._class_ == "darwin":
if not machine.deploy_as_root and machine.target_host.user == "root":
msg = f"'targetHost' should be set to a non-root user for deploying to nix-darwin on machine '{machine.name}'"
raise ClanError(msg)
machine.info(f"Updating {machine.name}")
runtime.async_run( runtime.async_run(
AsyncOpts( AsyncOpts(
tid=machine.name, async_ctx=AsyncContext(prefix=machine.name) tid=machine.name, async_ctx=AsyncContext(prefix=machine.name)
), ),
deploy_machine, deploy,
machine, machine,
) )
runtime.join_all() runtime.join_all()
@@ -238,73 +235,61 @@ def update_command(args: argparse.Namespace) -> None:
if args.flake is None: if args.flake is None:
msg = "Could not find clan flake toplevel directory" msg = "Could not find clan flake toplevel directory"
raise ClanError(msg) raise ClanError(msg)
machines = []
machines: list[Machine] = [] if len(args.machines) == 1 and args.target_host is not None:
# if no machines are passed, we will update all machines
selected_machines = (
args.machines if args.machines else list_machines(args.flake).keys()
)
if args.target_host is not None and len(args.machines) > 1:
msg = "Target Host can only be set for one machines"
raise ClanError(msg)
for machine_name in selected_machines:
machine = Machine( machine = Machine(
name=machine_name, name=args.machines[0], flake=args.flake, nix_options=args.option
flake=args.flake,
nix_options=args.option,
override_target_host=args.target_host,
override_build_host=args.build_host,
host_key_check=HostKeyCheck.from_str(args.host_key_check),
) )
machine.override_target_host = args.target_host
machine.override_build_host = args.build_host
machine.host_key_check = HostKeyCheck.from_str(args.host_key_check)
machines.append(machine) machines.append(machine)
def filter_machine(m: Machine) -> bool: elif args.target_host is not None:
if m.deployment.get("requireExplicitUpdate", False): print("target host can only be specified for a single machine")
return False exit(1)
else:
if len(args.machines) == 0:
ignored_machines = []
for machine in get_all_machines(args.flake, args.option):
if machine.deployment.get("requireExplicitUpdate", False):
continue
try:
machine.build_host # noqa: B018
except ClanError: # check if we have a build host set
ignored_machines.append(machine)
continue
machine.host_key_check = HostKeyCheck.from_str(args.host_key_check)
machine.override_build_host = args.build_host
machines.append(machine)
try: if not machines and ignored_machines != []:
# check if the machine has a target host set print(
m.target_host # noqa: B018 "WARNING: No machines to update."
except ClanError: "The following defined machines were ignored because they"
return False "do not have the `clan.core.networking.targetHost` nixos option set:",
file=sys.stderr,
)
for machine in ignored_machines:
print(machine, file=sys.stderr)
return True else:
machines = get_selected_machines(args.flake, args.option, args.machines)
for machine in machines:
machine.override_build_host = args.build_host
machine.host_key_check = HostKeyCheck.from_str(args.host_key_check)
machines_to_update = machines config = nix_config()
implicit_all: bool = len(args.machines) == 0 system = config["system"]
if implicit_all: machine_names = [machine.name for machine in machines]
machines_to_update = list(filter(filter_machine, machines)) args.flake.precache(
[
# machines that are in the list but not included in the update list f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.clan.core.vars.generators.*.validationHash",
ignored_machines = {m.name for m in machines if m not in machines_to_update} f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.system.clan.deployment.file",
]
if not machines_to_update and ignored_machines: )
print(
"WARNING: No machines to update.\n"
"The following defined machines were ignored because they\n"
"- Require explicit update (see 'requireExplicitUpdate')\n",
"- Might not have the `clan.core.networking.targetHost` nixos option set:\n",
file=sys.stderr,
)
for m in ignored_machines:
print(m, file=sys.stderr)
if machines_to_update:
# Prepopulate the cache
config = nix_config()
system = config["system"]
machine_names = [machine.name for machine in machines_to_update]
args.flake.precache(
[
f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.clan.core.vars.generators.*.validationHash",
f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.system.clan.deployment.file",
]
)
# Run the deplyoyment
deploy_machines(machines_to_update)
deploy_machines(machines)
except KeyboardInterrupt: except KeyboardInterrupt:
log.warning("Interrupted by user") log.warning("Interrupted by user")
sys.exit(1) sys.exit(1)

View File

@@ -1,13 +1,12 @@
import json import json
import logging import logging
import os import os
import shutil
import tempfile import tempfile
from functools import cache from functools import cache
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from clan_cli.cmd import run from clan_cli.cmd import run, run_no_stdout
from clan_cli.dirs import nixpkgs_flake, nixpkgs_source from clan_cli.dirs import nixpkgs_flake, nixpkgs_source
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.locked_open import locked_open from clan_cli.locked_open import locked_open
@@ -56,7 +55,7 @@ def nix_add_to_gcroots(nix_path: Path, dest: Path) -> None:
@cache @cache
def nix_config() -> dict[str, Any]: def nix_config() -> dict[str, Any]:
cmd = nix_command(["config", "show", "--json"]) cmd = nix_command(["config", "show", "--json"])
proc = run(cmd) proc = run_no_stdout(cmd)
data = json.loads(proc.stdout) data = json.loads(proc.stdout)
config = {} config = {}
for key, value in data.items(): for key, value in data.items():
@@ -132,16 +131,7 @@ class Packages:
cls.static_packages = set( cls.static_packages = set(
os.environ.get("CLAN_PROVIDED_PACKAGES", "").split(":") 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: # Features:

View File

@@ -6,7 +6,6 @@
"age-plugin-sss", "age-plugin-sss",
"age-plugin-tpm", "age-plugin-tpm",
"age-plugin-yubikey", "age-plugin-yubikey",
"age-plugin-1p",
"avahi", "avahi",
"bash", "bash",
"bubblewrap", "bubblewrap",

View File

@@ -5,11 +5,11 @@ import os
import shlex import shlex
import socket import socket
import subprocess import subprocess
import types import errno
import stat
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from shlex import quote from shlex import quote
from tempfile import TemporaryDirectory
from typing import Any from typing import Any
from clan_cli.cmd import CmdOut, RunOpts, run from clan_cli.cmd import CmdOut, RunOpts, run
@@ -40,29 +40,35 @@ class Host:
ssh_options: dict[str, str] = field(default_factory=dict) ssh_options: dict[str, str] = field(default_factory=dict)
tor_socks: bool = False tor_socks: bool = False
_temp_dir: TemporaryDirectory | None = None def setup_control_master(self) -> None:
home = Path.home()
def __enter__(self) -> "Host": if not home.exists():
self._temp_dir = TemporaryDirectory(prefix="clan-ssh-") return
return self control_path = home / ".ssh"
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: types.TracebackType | None,
) -> None:
try: try:
if self._temp_dir: if not stat.S_ISDIR(control_path.stat().st_mode):
self._temp_dir.cleanup() return
except OSError: except OSError as e:
pass if e.errno == errno.ENOENT:
try:
control_path.mkdir(exist_ok=True)
except OSError:
return
else:
return
self.ssh_options["ControlMaster"] = "auto"
# Can we make this a temporary directory?
self.ssh_options["ControlPath"] = str(control_path / "clan-%h-%p-%r")
# We use a short ttl because we want to mainly re-use the connection during the cli run
self.ssh_options["ControlPersist"] = "1m"
def __post_init__(self) -> None: def __post_init__(self) -> None:
if not self.command_prefix: if not self.command_prefix:
self.command_prefix = self.host self.command_prefix = self.host
if not self.user: if not self.user:
self.user = "root" self.user = "root"
self.setup_control_master()
def __str__(self) -> str: def __str__(self) -> str:
return self.target return self.target
@@ -182,17 +188,15 @@ class Host:
# Run the ssh command # Run the ssh command
return run(ssh_cmd, opts) return run(ssh_cmd, opts)
def nix_ssh_env( def nix_ssh_env(self, env: dict[str, str] | None) -> dict[str, str]:
self, env: dict[str, str] | None, local_ssh: bool = True
) -> dict[str, str]:
if env is None: if env is None:
env = {} env = {}
env["NIX_SSHOPTS"] = " ".join(self.ssh_cmd_opts(local_ssh=local_ssh)) env["NIX_SSHOPTS"] = " ".join(self.ssh_cmd_opts)
return env return env
@property
def ssh_cmd_opts( def ssh_cmd_opts(
self, self,
local_ssh: bool = True,
) -> list[str]: ) -> list[str]:
ssh_opts = ["-A"] if self.forward_agent else [] ssh_opts = ["-A"] if self.forward_agent else []
if self.port: if self.port:
@@ -206,16 +210,6 @@ class Host:
if self.private_key: if self.private_key:
ssh_opts.extend(["-i", str(self.private_key)]) ssh_opts.extend(["-i", str(self.private_key)])
if local_ssh and self._temp_dir:
ssh_opts.extend(["-o", "ControlPersist=30m"])
ssh_opts.extend(
[
"-o",
f"ControlPath={Path(self._temp_dir.name) / 'clan-%h-%p-%r'}",
]
)
ssh_opts.extend(["-o", "ControlMaster=auto"])
return ssh_opts return ssh_opts
def ssh_cmd( def ssh_cmd(
@@ -233,7 +227,7 @@ class Host:
self.password, self.password,
] ]
ssh_opts = self.ssh_cmd_opts() ssh_opts = self.ssh_cmd_opts
if verbose_ssh or self.verbose_ssh: if verbose_ssh or self.verbose_ssh:
ssh_opts.extend(["-v"]) ssh_opts.extend(["-v"])
if tty: if tty:

View File

@@ -63,8 +63,7 @@ def upload(
for mdir in dirs: for mdir in dirs:
dir_path = Path(root) / mdir dir_path = Path(root) / mdir
tarinfo = tar.gettarinfo( tarinfo = tar.gettarinfo(
dir_path, dir_path, arcname=str(dir_path.relative_to(str(local_src)))
arcname=str(dir_path.relative_to(str(local_src))),
) )
tarinfo.mode = dir_mode tarinfo.mode = dir_mode
tarinfo.uname = file_user tarinfo.uname = file_user

View File

@@ -3,7 +3,7 @@ import json
import logging import logging
from pathlib import Path from pathlib import Path
from clan_cli.cmd import RunOpts, run from clan_cli.cmd import RunOpts, run_no_stdout
from clan_cli.completions import ( from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
@@ -32,7 +32,7 @@ def list_state_folders(machine: Machine, service: None | str = None) -> None:
res = "{}" res = "{}"
try: try:
proc = run(cmd, RunOpts(prefix=machine.name)) proc = run_no_stdout(cmd, opts=RunOpts(prefix=machine.name))
res = proc.stdout.strip() res = proc.stdout.strip()
except ClanCmdError as e: except ClanCmdError as e:
msg = "Clan might not have meta attributes" msg = "Clan might not have meta attributes"

View File

@@ -2,7 +2,7 @@ import json
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from clan_cli.cmd import run from clan_cli.cmd import run_no_stdout
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
@@ -18,7 +18,7 @@ def list_tagged_machines(flake_url: str | Path) -> dict[str, Any]:
"--json", "--json",
] ]
) )
proc = run(cmd) proc = run_no_stdout(cmd)
try: try:
res = proc.stdout.strip() res = proc.stdout.strip()

View File

@@ -10,15 +10,8 @@ from pathlib import Path
from typing import Any, NamedTuple from typing import Any, NamedTuple
import pytest import pytest
from clan_cli.dirs import ( from clan_cli.dirs import TemplateType, clan_templates, nixpkgs_source
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.locked_open import locked_open
from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_test_store from clan_cli.nix import nix_test_store
from clan_cli.tests import age_keys from clan_cli.tests import age_keys
from clan_cli.tests.fixture_error import FixtureError from clan_cli.tests.fixture_error import FixtureError
@@ -77,10 +70,11 @@ class FlakeForTest(NamedTuple):
def set_machine_settings( def set_machine_settings(
machine: Machine, flake: Path,
machine_name: str,
machine_settings: dict, machine_settings: dict,
) -> None: ) -> None:
config_path = specific_machine_dir(machine) / "configuration.json" config_path = flake / "machines" / machine_name / "configuration.json"
config_path.write_text(json.dumps(machine_settings, indent=2)) config_path.write_text(json.dumps(machine_settings, indent=2))
@@ -208,8 +202,7 @@ class ClanFlake:
}} }}
""" """
) )
machine = Machine(name=machine_name, flake=Flake(str(self.path))) set_machine_settings(self.path, machine_name, machine_config)
set_machine_settings(machine, machine_config)
sp.run(["git", "add", "."], cwd=self.path, check=True) sp.run(["git", "add", "."], cwd=self.path, check=True)
sp.run( sp.run(
["git", "commit", "-a", "-m", "Update by flake generator"], ["git", "commit", "-a", "-m", "Update by flake generator"],

View File

@@ -27,14 +27,6 @@ def test_root() -> Path:
return TEST_ROOT return TEST_ROOT
@pytest.fixture(scope="session")
def test_lib_root() -> Path:
"""
Root directory of the clan-lib tests
"""
return PROJECT_ROOT.parent / "clan_lib" / "tests"
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def clan_core() -> Path: def clan_core() -> Path:
""" """

View File

@@ -1,5 +1,4 @@
import pytest import pytest
from clan_cli.flake import Flake
from clan_cli.inventory import load_inventory_json from clan_cli.inventory import load_inventory_json
from clan_cli.secrets.folders import sops_machines_folder from clan_cli.secrets.folders import sops_machines_folder
from clan_cli.tests import fixtures_flakes from clan_cli.tests import fixtures_flakes
@@ -25,7 +24,7 @@ def test_machine_subcommands(
] ]
) )
inventory: dict = dict(load_inventory_json(Flake(str(test_flake_with_core.path)))) inventory: dict = dict(load_inventory_json(str(test_flake_with_core.path)))
assert "machine1" in inventory["machines"] assert "machine1" in inventory["machines"]
assert "service" not in inventory assert "service" not in inventory
@@ -41,7 +40,7 @@ def test_machine_subcommands(
["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"] ["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"]
) )
inventory_2: dict = dict(load_inventory_json(Flake(str(test_flake_with_core.path)))) inventory_2: dict = dict(load_inventory_json(str(test_flake_with_core.path)))
assert "machine1" not in inventory_2["machines"] assert "machine1" not in inventory_2["machines"]
assert "service" not in inventory_2 assert "service" not in inventory_2

View File

@@ -11,7 +11,7 @@ from clan_cli.inventory import (
set_inventory, set_inventory,
) )
from clan_cli.machines.create import CreateOptions, create_machine from clan_cli.machines.create import CreateOptions, create_machine
from clan_cli.nix import nix_eval, run from clan_cli.nix import nix_eval, run_no_stdout
from clan_cli.tests.fixtures_flakes import FlakeForTest from clan_cli.tests.fixtures_flakes import FlakeForTest
from clan_lib.api.modules import list_modules from clan_lib.api.modules import list_modules
@@ -27,8 +27,10 @@ def test_list_modules(test_flake_with_core: FlakeForTest) -> None:
base_path = test_flake_with_core.path base_path = test_flake_with_core.path
modules_info = list_modules(str(base_path)) modules_info = list_modules(str(base_path))
assert "localModules" in modules_info assert len(modules_info.items()) > 1
assert "modulesPerSource" in modules_info # Random test for those two modules
assert "borgbackup" in modules_info
assert "syncthing" in modules_info
@pytest.mark.impure @pytest.mark.impure
@@ -86,7 +88,7 @@ def test_add_module_to_inventory(
} }
} }
set_inventory(inventory, Flake(str(base_path)), "Add borgbackup service") set_inventory(inventory, base_path, "Add borgbackup service")
# cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "machine1"] # cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "machine1"]
cmd = [ cmd = [
@@ -120,7 +122,7 @@ def test_add_module_to_inventory(
"--json", "--json",
] ]
) )
proc = run(cmd) proc = run_no_stdout(cmd)
res = json.loads(proc.stdout.strip()) res = json.loads(proc.stdout.strip())
assert res["machine1"]["authorizedKeys"] == [ssh_key.decode()] assert res["machine1"]["authorizedKeys"] == [ssh_key.decode()]

View File

@@ -12,12 +12,7 @@ from clan_cli.tests.age_keys import SopsSetup
from clan_cli.tests.fixtures_flakes import ClanFlake from clan_cli.tests.fixtures_flakes import ClanFlake
from clan_cli.tests.helpers import cli from clan_cli.tests.helpers import cli
from clan_cli.vars.check import check_vars from clan_cli.vars.check import check_vars
from clan_cli.vars.generate import ( from clan_cli.vars.generate import Generator, generate_vars_for_machine_interactive
Generator,
generate_vars_for_machine,
generate_vars_for_machine_interactive,
get_generators_closure,
)
from clan_cli.vars.get import get_var from clan_cli.vars.get import get_var
from clan_cli.vars.graph import all_missing_closure, requested_closure from clan_cli.vars.graph import all_missing_closure, requested_closure
from clan_cli.vars.list import stringify_all_vars from clan_cli.vars.list import stringify_all_vars
@@ -645,6 +640,9 @@ def test_api_set_prompts(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake, flake: ClanFlake,
) -> None: ) -> None:
from clan_cli.vars._types import GeneratorUpdate
from clan_cli.vars.list import get_generators, set_prompts
config = flake.machines["my_machine"] config = flake.machines["my_machine"]
config["nixpkgs"]["hostPlatform"] = "x86_64-linux" config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
@@ -654,39 +652,33 @@ def test_api_set_prompts(
flake.refresh() flake.refresh()
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
params = {"machine_name": "my_machine", "base_dir": str(flake.path)}
generate_vars_for_machine( set_prompts(
machine_name="my_machine", **params,
base_dir=flake.path, updates=[
generators=["my_generator"], GeneratorUpdate(
all_prompt_values={ generator="my_generator",
"my_generator": { prompt_values={"prompt1": "input1"},
"prompt1": "input1", )
} ],
},
) )
machine = Machine(name="my_machine", flake=Flake(str(flake.path))) machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
store = in_repo.FactStore(machine) store = in_repo.FactStore(machine)
assert store.exists(Generator("my_generator"), "prompt1") assert store.exists(Generator("my_generator"), "prompt1")
assert store.get(Generator("my_generator"), "prompt1").decode() == "input1" assert store.get(Generator("my_generator"), "prompt1").decode() == "input1"
generate_vars_for_machine( set_prompts(
machine_name="my_machine", **params,
base_dir=flake.path, updates=[
generators=["my_generator"], GeneratorUpdate(
all_prompt_values={ generator="my_generator",
"my_generator": { prompt_values={"prompt1": "input2"},
"prompt1": "input2", )
} ],
},
) )
assert store.get(Generator("my_generator"), "prompt1").decode() == "input2" assert store.get(Generator("my_generator"), "prompt1").decode() == "input2"
generators = get_generators_closure( generators = get_generators(**params)
machine_name="my_machine",
base_dir=flake.path,
regenerate=True,
include_previous_values=True,
)
assert len(generators) == 1 assert len(generators) == 1
assert generators[0].name == "my_generator" assert generators[0].name == "my_generator"
assert generators[0].prompts[0].name == "prompt1" assert generators[0].prompts[0].name == "prompt1"

View File

@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines import machines from clan_cli.machines import machines
from clan_cli.ssh.host import Host
if TYPE_CHECKING: if TYPE_CHECKING:
from .generate import Generator, Var from .generate import Generator, Var
@@ -184,5 +183,5 @@ class StoreBase(ABC):
pass pass
@abstractmethod @abstractmethod
def upload(self, host: Host, phases: list[str]) -> None: def upload(self, phases: list[str]) -> None:
pass pass

View File

@@ -1,7 +1,6 @@
import argparse import argparse
import logging import logging
import os import os
import shutil
import sys import sys
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property from functools import cached_property
@@ -87,11 +86,6 @@ class Generator:
def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]: def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
test_store = nix_test_store() test_store = nix_test_store()
real_bash_path = Path("bash")
if os.environ.get("IN_NIX_SANDBOX"):
bash_executable_path = Path(str(shutil.which("bash")))
real_bash_path = bash_executable_path.resolve()
# fmt: off # fmt: off
return nix_shell( return nix_shell(
[ [
@@ -103,7 +97,6 @@ def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
"--unshare-all", "--unshare-all",
"--tmpfs", "/", "--tmpfs", "/",
"--ro-bind", "/nix/store", "/nix/store", "--ro-bind", "/nix/store", "/nix/store",
"--ro-bind", "/bin/sh", "/bin/sh",
*(["--ro-bind", str(test_store), str(test_store)] if test_store else []), *(["--ro-bind", str(test_store), str(test_store)] if test_store else []),
"--dev", "/dev", "--dev", "/dev",
# not allowed to bind procfs in some sandboxes # not allowed to bind procfs in some sandboxes
@@ -116,8 +109,8 @@ def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
"--uid", "1000", "--uid", "1000",
"--gid", "1000", "--gid", "1000",
"--", "--",
str(real_bash_path), "-c", generator "bash", "-c", generator
] ],
) )
# fmt: on # fmt: on
@@ -295,28 +288,10 @@ def _ask_prompts(
return prompt_values return prompt_values
def _get_previous_value(
machine: "Machine",
generator: Generator,
prompt: Prompt,
) -> str | None:
if not prompt.persist:
return None
pub_store = machine.public_vars_store
if pub_store.exists(generator, prompt.name):
return pub_store.get(generator, prompt.name).decode()
sec_store = machine.secret_vars_store
if sec_store.exists(generator, prompt.name):
return sec_store.get(generator, prompt.name).decode()
return None
def get_closure( def get_closure(
machine: "Machine", machine: "Machine",
generator_name: str | None, generator_name: str | None,
regenerate: bool, regenerate: bool,
include_previous_values: bool = False,
) -> list[Generator]: ) -> list[Generator]:
from .graph import all_missing_closure, full_closure from .graph import all_missing_closure, full_closure
@@ -329,24 +304,14 @@ def get_closure(
for generator in vars_generators: for generator in vars_generators:
generator.machine(machine) generator.machine(machine)
result_closure = []
if generator_name is None: # all generators selected if generator_name is None: # all generators selected
if regenerate: if regenerate:
result_closure = full_closure(generators) return full_closure(generators)
else: return all_missing_closure(generators)
result_closure = all_missing_closure(generators)
# specific generator selected # specific generator selected
elif regenerate: if regenerate:
result_closure = requested_closure([generator_name], generators) return requested_closure([generator_name], generators)
else: return minimal_closure([generator_name], generators)
result_closure = minimal_closure([generator_name], generators)
if include_previous_values:
for generator in result_closure:
for prompt in generator.prompts:
prompt.previous_value = _get_previous_value(machine, generator, prompt)
return result_closure
@API.register @API.register
@@ -354,7 +319,6 @@ def get_generators_closure(
machine_name: str, machine_name: str,
base_dir: Path, base_dir: Path,
regenerate: bool = False, regenerate: bool = False,
include_previous_values: bool = False,
) -> list[Generator]: ) -> list[Generator]:
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
@@ -362,14 +326,13 @@ def get_generators_closure(
machine=Machine(name=machine_name, flake=Flake(str(base_dir))), machine=Machine(name=machine_name, flake=Flake(str(base_dir))),
generator_name=None, generator_name=None,
regenerate=regenerate, regenerate=regenerate,
include_previous_values=include_previous_values,
) )
def _generate_vars_for_machine( def _generate_vars_for_machine(
machine: "Machine", machine: "Machine",
generators: list[Generator], generators: list[Generator],
all_prompt_values: dict[str, dict[str, str]], all_prompt_values: dict[str, dict],
no_sandbox: bool = False, no_sandbox: bool = False,
) -> bool: ) -> bool:
for generator in generators: for generator in generators:
@@ -381,7 +344,7 @@ def _generate_vars_for_machine(
generator=generator, generator=generator,
secret_vars_store=machine.secret_vars_store, secret_vars_store=machine.secret_vars_store,
public_vars_store=machine.public_vars_store, public_vars_store=machine.public_vars_store,
prompt_values=all_prompt_values.get(generator.name, {}), prompt_values=all_prompt_values[generator.name],
no_sandbox=no_sandbox, no_sandbox=no_sandbox,
) )
return True return True
@@ -390,20 +353,19 @@ def _generate_vars_for_machine(
@API.register @API.register
def generate_vars_for_machine( def generate_vars_for_machine(
machine_name: str, machine_name: str,
generators: list[str], generators: list[Generator],
all_prompt_values: dict[str, dict[str, str]], all_prompt_values: dict[str, dict[str, str]],
base_dir: Path, base_dir: Path,
no_sandbox: bool = False, no_sandbox: bool = False,
) -> bool: ) -> bool:
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
machine = Machine(name=machine_name, flake=Flake(str(base_dir)))
generators_set = set(generators)
generators_ = [g for g in machine.vars_generators if g.name in generators_set]
return _generate_vars_for_machine( return _generate_vars_for_machine(
machine=machine, machine=Machine(
generators=generators_, name=machine_name,
flake=Flake(str(base_dir)),
),
generators=generators,
all_prompt_values=all_prompt_values, all_prompt_values=all_prompt_values,
no_sandbox=no_sandbox, no_sandbox=no_sandbox,
) )

View File

@@ -4,7 +4,6 @@ from pathlib import Path
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
from clan_cli.vars._types import StoreBase from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var from clan_cli.vars.generate import Generator, Var
@@ -73,6 +72,6 @@ class FactStore(StoreBase):
msg = "populate_dir is not implemented for public vars stores" msg = "populate_dir is not implemented for public vars stores"
raise NotImplementedError(msg) raise NotImplementedError(msg)
def upload(self, host: Host, phases: list[str]) -> None: def upload(self, phases: list[str]) -> None:
msg = "upload is not implemented for public vars stores" msg = "upload is not implemented for public vars stores"
raise NotImplementedError(msg) raise NotImplementedError(msg)

View File

@@ -6,7 +6,6 @@ from pathlib import Path
from clan_cli.dirs import vm_state_dir from clan_cli.dirs import vm_state_dir
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
from clan_cli.vars._types import StoreBase from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var from clan_cli.vars.generate import Generator, Var
@@ -70,6 +69,6 @@ class FactStore(StoreBase):
msg = "populate_dir is not implemented for public vars stores" msg = "populate_dir is not implemented for public vars stores"
raise NotImplementedError(msg) raise NotImplementedError(msg)
def upload(self, host: Host, phases: list[str]) -> None: def upload(self, phases: list[str]) -> None:
msg = "upload is not implemented for public vars stores" msg = "upload is not implemented for public vars stores"
raise NotImplementedError(msg) raise NotImplementedError(msg)

View File

@@ -3,7 +3,6 @@ import tempfile
from pathlib import Path from pathlib import Path
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
from clan_cli.vars._types import StoreBase from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var from clan_cli.vars.generate import Generator, Var
@@ -46,6 +45,6 @@ class SecretStore(StoreBase):
shutil.copytree(self.dir, output_dir) shutil.copytree(self.dir, output_dir)
shutil.rmtree(self.dir) shutil.rmtree(self.dir)
def upload(self, host: Host, phases: list[str]) -> None: def upload(self, phases: list[str]) -> None:
msg = "Cannot upload secrets with FS backend" msg = "Cannot upload secrets with FS backend"
raise NotImplementedError(msg) raise NotImplementedError(msg)

View File

@@ -10,7 +10,6 @@ from tempfile import TemporaryDirectory
from clan_cli.cmd import CmdOut, Log, RunOpts, run from clan_cli.cmd import CmdOut, Log, RunOpts, run
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
from clan_cli.ssh.host import Host
from clan_cli.ssh.upload import upload from clan_cli.ssh.upload import upload
from clan_cli.vars._types import StoreBase from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var from clan_cli.vars.generate import Generator, Var
@@ -147,9 +146,9 @@ class SecretStore(StoreBase):
manifest += hashes manifest += hashes
return b"\n".join(manifest) return b"\n".join(manifest)
def needs_upload(self, host: Host) -> bool: def needs_upload(self) -> bool:
local_hash = self.generate_hash() local_hash = self.generate_hash()
remote_hash = host.run( remote_hash = self.machine.target_host.run(
# TODO get the path to the secrets from the machine # TODO get the path to the secrets from the machine
[ [
"cat", "cat",
@@ -225,11 +224,11 @@ class SecretStore(StoreBase):
(output_dir / f".{self._store_backend}_info").write_bytes(self.generate_hash()) (output_dir / f".{self._store_backend}_info").write_bytes(self.generate_hash())
def upload(self, host: Host, phases: list[str]) -> None: def upload(self, phases: list[str]) -> None:
if "partitioning" in phases: if "partitioning" in phases:
msg = "Cannot upload partitioning secrets" msg = "Cannot upload partitioning secrets"
raise NotImplementedError(msg) raise NotImplementedError(msg)
if not self.needs_upload(host): if not self.needs_upload():
log.info("Secrets already uploaded") log.info("Secrets already uploaded")
return return
with TemporaryDirectory(prefix="vars-upload-") as _tempdir: with TemporaryDirectory(prefix="vars-upload-") as _tempdir:
@@ -238,4 +237,4 @@ class SecretStore(StoreBase):
upload_dir = Path( upload_dir = Path(
self.machine.deployment["password-store"]["secretLocation"] self.machine.deployment["password-store"]["secretLocation"]
) )
upload(host, pass_dir, upload_dir) upload(self.machine.target_host, pass_dir, upload_dir)

Some files were not shown because too many files have changed in this diff Show More