Compare commits
2 Commits
pinned-cla
...
control-ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160f7d2cf5 | ||
|
|
4c9aaa09d5 |
@@ -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
1
.gitignore
vendored
@@ -14,7 +14,6 @@ example_clan
|
||||
nixos.qcow2
|
||||
**/*.glade~
|
||||
/docs/out
|
||||
/pkgs/clan-cli/clan_cli/select
|
||||
**/.local.env
|
||||
|
||||
# MacOS stuff
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
nixosModules/clanCore/vars/.* @lopter
|
||||
pkgs/clan-cli/clan_cli/(secrets|vars)/.* @lopter
|
||||
@@ -147,7 +147,25 @@
|
||||
perSystem =
|
||||
{ pkgs, ... }:
|
||||
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
|
||||
{
|
||||
checks = pkgs.lib.mkIf pkgs.stdenv.isLinux {
|
||||
@@ -164,6 +182,11 @@
|
||||
# import the inventory generated nixosModules
|
||||
self.clanInternals.inventoryClass.machines.test-backup.machineImports;
|
||||
clan.core.settings.directory = ./.;
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeShellScriptBin "foo" ''
|
||||
echo ${clanCore}
|
||||
'')
|
||||
];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{ fetchgit }:
|
||||
fetchgit {
|
||||
url = "https://git.clan.lol/clan/clan-core.git";
|
||||
rev = "1e8b9def2a021877342491ca1f4c45533a580759";
|
||||
sha256 = "0f12vwr1abwa1iwjbb5z5xx8jlh80d9njwdm6iaw1z1h2m76xgzc";
|
||||
}
|
||||
@@ -26,7 +26,6 @@ clanLib.test.makeTestClan {
|
||||
roles.admin.machines = [ "admin1" ];
|
||||
};
|
||||
};
|
||||
|
||||
instances."test" = {
|
||||
module.name = "new-service";
|
||||
roles.peer.machines.peer1 = { };
|
||||
@@ -34,33 +33,25 @@ clanLib.test.makeTestClan {
|
||||
|
||||
modules = {
|
||||
legacy-module = ./legacy-module;
|
||||
};
|
||||
};
|
||||
modules.new-service = {
|
||||
_class = "clan.service";
|
||||
manifest.name = "new-service";
|
||||
roles.peer = { };
|
||||
perMachine = {
|
||||
nixosModule = {
|
||||
# This should be generated by:
|
||||
# nix run .#generate-test-vars -- checks/dummy-inventory-test dummy-inventory-test
|
||||
clan.core.vars.generators.new-service = {
|
||||
files.not-a-secret = {
|
||||
secret = false;
|
||||
deploy = true;
|
||||
new-service = {
|
||||
_class = "clan.service";
|
||||
manifest.name = "new-service";
|
||||
roles.peer = { };
|
||||
perMachine = {
|
||||
nixosModule = {
|
||||
# This should be generated by:
|
||||
# ./pkgs/scripts/update-vars.py
|
||||
clan.core.vars.generators.new-service = {
|
||||
files.hello = {
|
||||
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"))
|
||||
|
||||
# peer1 should have the 'hello' file
|
||||
peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.not-a-secret.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}"
|
||||
peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.hello.path}")
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age12yt078p9ewxy2sh0a36nxdpgglv8wqqftmj4dkj9rgy5fuyn4p0q5nje9m",
|
||||
"publickey": "age1hd2exjq88h7538y6mvjvexx3u5gp6a03yfn5nj32h2667yyksyaqcuk5qs",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age12w2ld4vxfyf3hdq2d8la4cu0tye4pq97egvv3me4wary7xkdnq2snh0zx2",
|
||||
"publickey": "age19urkt89q45a2wk6a4yaramzufjtnw6nq2snls0v7hmf7tqf73axsfx50tk",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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": {
|
||||
"age": [
|
||||
{
|
||||
"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",
|
||||
"mac": "ENC[AES256_GCM,data:fWxLHXBWolHVxv6Q7utcy6OVLV13ziswrIYyNKiwy1vsU8i7xvvuGO1HlnE+q43D2WuHR53liKq1UHuf1JMrWzTwZ0PYe+CVugtoEtbR2qu3rK/jAkOyMyhmmHzmf6Rp4ZMCzKgZeC/X2bDKY/z0firHAvjWydEyogutHpvtznM=,iv:OQI3FfkLneqbdztAXVQB3UkHwDPK+0hWu5hZ9m8Oczg=,tag:em6GfS2QHsXs391QKPxfmA==,type:str]",
|
||||
"lastmodified": "2025-04-09T15:10:16Z",
|
||||
"mac": "ENC[AES256_GCM,data:xuXj4833G6nhvcRo2ekDxz8G5phltmU8h1GgGofH9WndzrqLKeRSqm/n03IHRW0f4F68XxnyAkfvokVh6vW3LRQAFkqIlXz5U4+zFNcaVaPobS5gHTgxsCoTUoalWPvHWtXd50hUVXeAt8rPfTfeveVGja8bOERk8mvwUPxb6h4=,iv:yP1usA9m8tKl6Z/UK9PaVMJlZlF5qpY4EiM4+ByVlik=,tag:8DgoIhLstp3MRki90VfEvw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
"version": "3.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
"age": [
|
||||
{
|
||||
"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",
|
||||
"mac": "ENC[AES256_GCM,data:yTkQeFvKrN1+5FP+yInsaRWSAG+ZGG0uWF3+gVRvzJTFxab8kT2XkAMc+4D7SKgcjsmwBBb77GNoAKaKByhZ92UaCfZ2X66i7ZmYUwLM1NVVmm+xiwwjsh7PJXlZO/70anTzd1evtlZse0jEmRnV5Y0F0M6YqXmuwU+qGUJU2F8=,iv:sy6ozhXonWVruaQfa7pdEoV5GkNZR/UbbINKAPbgWeg=,tag:VMruQ1KExmlMR7TsGNgMlg==,type:str]",
|
||||
"lastmodified": "2025-04-09T15:10:41Z",
|
||||
"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",
|
||||
"version": "3.10.2"
|
||||
"version": "3.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age12yt078p9ewxy2sh0a36nxdpgglv8wqqftmj4dkj9rgy5fuyn4p0q5nje9m",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPNUhiYkZWK3dPMHNiRTVM\nRHNvaHFsOFp1c0UxQitwVG0zY01MNDZRV1E4CjEybENoTVIzN29vQ3FtUTRSYmFU\nNXIzQllVSllXRGN2M1B6WXJLdHZSajgKLS0tIDllZ0ZmZUcxMHhDQUpUOEdWbmkv\neUQweHArYTdFSmNteVpuQ3BKdnh0Y0UKs8Hm3D+rXRRfpUVSZM3zYjs6b9z8g10D\nGTkvreUMim4CS22pjdQ3eNA9TGeDXfWXE7XzwXLCb+wVcf7KwbDmvg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age1hd2exjq88h7538y6mvjvexx3u5gp6a03yfn5nj32h2667yyksyaqcuk5qs",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvZDZYYXdpcXVqRFRnQ2Jx\nTFhFWEJTR290cHZhTXZadFFvcHM4MHVIN3lFCmJhOEZrL3g4TFBZVllxdDFZakJn\nR3NxdXo0eE8vTDh3QlhWOFpVZ0lNUHcKLS0tIEE4dkpCalNzaXJ0Qks3VHJSUzZF\nb2N3NGdjNHJnSUN6bW8welZ1VDdJakEKGKZ7nn1p11IyJB6DMxu2HJMvZ+0+5WpE\nPLWh2NlGJO3XrrL4Fw7xetwbqE+QUZPNl/JbEbu4KLIUGLjqk9JDhQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"mac": "ENC[AES256_GCM,data:6fKrS1eLLUWlHkQpxLFXBRk6f2wa5ADLMViVvYXXGU24ayl9UuNSKrCRHp9cbzhqhti3HdwyNt6TM+2X6qhiiAQanKEB2PF7JRYX74NfNKil9BEDjt5AqqtpSgVv5l7Ku/uSHaPkd2sDmzHsy5Q4bSGxJQokStk1kidrwle+mbc=,iv:I/Aad82L/TCxStM8d8IZICUrwdjRbGx2fuGWqexr21o=,tag:BfgRbGUxhPZzK2fLik1kxA==,type:str]",
|
||||
"lastmodified": "2025-04-09T15:10:30Z",
|
||||
"mac": "ENC[AES256_GCM,data:cIwWctUbAFI8TRMxYWy5xqlKDVLMqBIxVv4LInnLqi3AauL0rJ3Z7AxK/wb2dCQM07E1N7YaORNqgUpFC1xo0hObAA8mrPaToPotKDkjua0zuyTUNS1COoraYjZpI/LKwmik/qtk399LMhiC7aHs+IliT9Dd41B8LSMBXwdMldY=,iv:sZ+//BrYH5Ay2JJAGs7K+WfO2ASK82syDlilQjGmgFs=,tag:nY+Af9eQRLwkiHZe85dQ9A==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
"version": "3.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
18650
|
||||
13898
|
||||
|
||||
@@ -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": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age12w2ld4vxfyf3hdq2d8la4cu0tye4pq97egvv3me4wary7xkdnq2snh0zx2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjaFVNMEd2YUxpSm5XVVRi\nY2ZUc3NTOStJUFNMWWVPQTgxZ2tCK1QrMW1ZCjYwMlA4dkIzSlc0TGtvZjcyK3Bi\nM3pob2JOOFUyeVJ6M2JpaTRCZlc1R0kKLS0tIDJMb1dFcVRWckhwYWNCQng0RlFO\nTkw3OGt4dkFIZVY5aVEzZE5mMzJSM0EKUv8bUqg48L2FfYVUVlpXvyZvPye699of\nG6PcjLh1ZMbNCfnsCzr+P8Vdk/F4J/ifxL66lRGfu2xOLxwciwQ+5Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age19urkt89q45a2wk6a4yaramzufjtnw6nq2snls0v7hmf7tqf73axsfx50tk",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpTEROZjh6NjBhSlJSc1Av\nSHhjdkhwVUd3VzBZemhQb3dhMlJXalBmZlFjCkZPYkhZZGVOVTNjUWdFU0s4cWFn\nL2NXbkRCdUlMdElnK2lGbG5iV0w1cHMKLS0tIFREcmxDdHlUNVBFVGRVZSt0c0E5\nbnpHaW1Vb3R3ZFFnZVMxY3djSjJmOU0KIwqCSQf5S9oA59BXu7yC/V6yqvCh88pa\nYgmNyBjulytPh1aAfOuNWIGdIxBpcEf+gFjz3EiJY9Kft3fTmhp2bw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"mac": "ENC[AES256_GCM,data:1ZZ+ZI1JsHmxTov1bRijfol3kTkyheg2o3ivLsMHRhCmScsUry97hQJchF78+y2Izt7avaQEHYn6pVbYt/0rLrSYD7Ru7ITVxXoYHOiN5Qb98masUzpibZjrdyg5nO+LW5/Hmmwsc3yn/+o3IH1AUYpsxlJRdnHHCmoSOFaiFFM=,iv:OQlgmpOTw4ljujNzqwQ5/0Mz8pQpCSUtqRvj3FJAxDs=,tag:foZvdeW7gK9ZVKkWqnlxGA==,type:str]",
|
||||
"lastmodified": "2025-04-09T15:11:04Z",
|
||||
"mac": "ENC[AES256_GCM,data:JdJzocQZWVprOmZ4Ni04k1tpD1TpFcK5neKy3+0/c3+uPBwjwaMayISKRaa/ILUXlalg60oTqxB4fUFoYVm8KGQVhDwPhO/T1hyYVQqidonrcYfJfCYg00mVSREV/AWqXb7RTnaEBfrdnRJvaAQF9g2qDXGVgzp3eACdlItclv4=,iv:nOw1jQjIWHWwU3SiKpuQgMKXyu8MZYI+zI9UYYd9fCI=,tag:ewUkemIPm/5PkmuUD0EcAQ==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
"version": "3.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
6745
|
||||
30661
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../../../../sops/machines/peer1
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
This is a dummy script
|
||||
@@ -1 +0,0 @@
|
||||
not-a-secret
|
||||
@@ -14,14 +14,13 @@ in
|
||||
./installation/flake-module.nix
|
||||
./morph/flake-module.nix
|
||||
./nixos-documentation/flake-module.nix
|
||||
./dont-depend-on-repo-root.nix
|
||||
./sanity-checks/dont-depend-on-repo-root.nix
|
||||
];
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
self',
|
||||
system,
|
||||
...
|
||||
}:
|
||||
{
|
||||
@@ -84,10 +83,7 @@ in
|
||||
schema =
|
||||
(self.clanLib.inventory.evalClanService {
|
||||
modules = [ m ];
|
||||
prefix = [
|
||||
"checks"
|
||||
system
|
||||
];
|
||||
key = "checks";
|
||||
}).config.result.api.schema;
|
||||
in
|
||||
schema
|
||||
@@ -101,12 +97,6 @@ in
|
||||
mkdir -p $out
|
||||
cat $schemaFile > $out/allSchemas.json
|
||||
'';
|
||||
|
||||
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
|
||||
cp -r ${pkgs.callPackage ./clan-core-for-checks.nix { }} $out
|
||||
chmod +w $out/flake.lock
|
||||
cp ${../flake.lock} $out/flake.lock
|
||||
'';
|
||||
};
|
||||
legacyPackages = {
|
||||
nixosTests =
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
let
|
||||
dependencies = [
|
||||
pkgs.disko
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles
|
||||
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp
|
||||
|
||||
@@ -81,7 +80,7 @@
|
||||
|
||||
# Some distros like to automount disks with spaces
|
||||
machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdb && mount /dev/vdb "/mnt/with spaces"')
|
||||
machine.succeed("clan flash write --debug --flake ${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; };
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ let
|
||||
pkgs.bash.drvPath
|
||||
pkgs.nixos-anywhere
|
||||
pkgs.bubblewrap
|
||||
pkgs.buildPackages.xorg.lndir
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
in
|
||||
@@ -198,7 +197,7 @@ in
|
||||
installer.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../assets/ssh/privkey} /root/.ssh/id_ed25519")
|
||||
|
||||
installer.wait_until_succeeds("timeout 2 ssh -o StrictHostKeyChecking=accept-new -v nonrootuser@localhost hostname")
|
||||
installer.succeed("cp -r ${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.shutdown()
|
||||
@@ -218,7 +217,7 @@ in
|
||||
installer.start()
|
||||
installer.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../assets/ssh/privkey} /root/.ssh/id_ed25519")
|
||||
installer.wait_until_succeeds("timeout 2 ssh -o StrictHostKeyChecking=accept-new -v nonrootuser@localhost hostname")
|
||||
installer.succeed("cp -r ${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/facter.json")
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
dependencies = [
|
||||
self
|
||||
pkgs.stdenv.drvPath
|
||||
pkgs.stdenvNoCC
|
||||
self.nixosConfigurations.test-morph-machine.config.system.build.toplevel
|
||||
@@ -54,7 +55,7 @@
|
||||
testScript = ''
|
||||
start_all()
|
||||
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"
|
||||
'';
|
||||
} { inherit pkgs self; };
|
||||
|
||||
@@ -105,7 +105,10 @@ in
|
||||
private_key = {
|
||||
inherit owner;
|
||||
};
|
||||
public_key.secret = false;
|
||||
public_key = {
|
||||
inherit owner;
|
||||
secret = false;
|
||||
};
|
||||
};
|
||||
|
||||
runtimeInputs = [
|
||||
@@ -131,7 +134,10 @@ in
|
||||
private_key = {
|
||||
inherit owner;
|
||||
};
|
||||
public_key.secret = false;
|
||||
public_key = {
|
||||
inherit owner;
|
||||
secret = false;
|
||||
};
|
||||
};
|
||||
|
||||
runtimeInputs = [
|
||||
|
||||
@@ -4,15 +4,7 @@
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/hello-word";
|
||||
|
||||
roles.peer = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.foo = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
};
|
||||
};
|
||||
};
|
||||
roles.peer = { };
|
||||
|
||||
perMachine =
|
||||
{ machine, ... }:
|
||||
|
||||
@@ -10,6 +10,9 @@ let
|
||||
};
|
||||
in
|
||||
{
|
||||
clan.inventory.modules = {
|
||||
hello-world = module;
|
||||
};
|
||||
clan.modules = {
|
||||
hello-world = module;
|
||||
};
|
||||
@@ -47,7 +50,6 @@ in
|
||||
hello-service = import ./tests/vm/default.nix {
|
||||
inherit module;
|
||||
inherit self inputs pkgs;
|
||||
# clanLib is exposed from inputs.clan-core
|
||||
clanLib = self.clanLib;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ let
|
||||
};
|
||||
|
||||
# Register the module for the test
|
||||
modules.hello-world = module;
|
||||
inventory.modules.hello-world = module;
|
||||
|
||||
# Use the module in the test
|
||||
inventory.instances = {
|
||||
|
||||
@@ -14,9 +14,6 @@ clanLib.test.makeTestClan {
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
modules = {
|
||||
hello-service = module;
|
||||
};
|
||||
inventory = {
|
||||
machines.peer1 = { };
|
||||
|
||||
@@ -24,6 +21,10 @@ clanLib.test.makeTestClan {
|
||||
module.name = "hello-service";
|
||||
roles.peer.machines.peer1 = { };
|
||||
};
|
||||
|
||||
modules = {
|
||||
hello-service = module;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ We might not be sure whether all of those will exist but the architecture should
|
||||
## Decision
|
||||
|
||||
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.
|
||||
|
||||
*Note: The next person who wants implement any new frontend should do this first. Currently it looks like the TUI is the next one.*
|
||||
|
||||
@@ -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/
|
||||
@@ -58,7 +58,7 @@ nav:
|
||||
- Autoincludes: manual/adding-machines.md
|
||||
- Inventory:
|
||||
- Inventory: manual/inventory.md
|
||||
- Services: manual/distributed-services.md
|
||||
- Instances: manual/distributed-services.md
|
||||
- Secure Boot: manual/secure-boot.md
|
||||
- Flake-parts: manual/flake-parts.md
|
||||
- Authoring:
|
||||
|
||||
@@ -12,7 +12,7 @@ We discussed the initial architecture in [01-clan-service-modules](https://git.c
|
||||
|
||||
### A Minimal module
|
||||
|
||||
First of all we need to register our module into the `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.
|
||||
|
||||
@@ -22,15 +22,20 @@ i.e. `@hsjobeki/customNetworking`
|
||||
|
||||
```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 ];
|
||||
# ...
|
||||
clan = {
|
||||
inventory = {
|
||||
# We could also inline the complete module spec here
|
||||
# For example
|
||||
# {...}: { _class = "clan.service"; ... };
|
||||
modules."@hsjobeki/customNetworking" = import ./service-modules/networking.nix;
|
||||
};
|
||||
|
||||
# If needed: Exporting the module for other people
|
||||
modules."@hsjobeki/customNetworking" = import ./service-modules/networking.nix;
|
||||
# We could also inline the complete module spec here
|
||||
# For example
|
||||
# {...}: { _class = "clan.service"; ... };
|
||||
};
|
||||
})
|
||||
```
|
||||
@@ -216,6 +221,9 @@ outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({self, lib, ...}:
|
||||
# ...
|
||||
clan = {
|
||||
# Register the module
|
||||
inventory.modules."@hsjobeki/messaging" = lib.importApply ./service-modules/messaging.nix { inherit self; };
|
||||
|
||||
# Expose the module for downstream users, 'self' would always point to this flake.
|
||||
modules."@hsjobeki/messaging" = lib.importApply ./service-modules/messaging.nix { inherit self; };
|
||||
};
|
||||
})
|
||||
@@ -242,7 +250,7 @@ outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({self, lib, ...}:
|
||||
# ...
|
||||
clan = {
|
||||
# Register the module
|
||||
modules."@hsjobeki/messaging" = {
|
||||
inventory.modules."@hsjobeki/messaging" = {
|
||||
# Create an option 'myClan' and assign it to 'self'
|
||||
options.myClan = lib.mkOption {
|
||||
default = self;
|
||||
|
||||
@@ -32,7 +32,7 @@ VM tests should be avoided wherever it is possible to implement a cheaper unit t
|
||||
|
||||
Existing nixos vm tests in clan-core can be found by using ripgrep:
|
||||
```shellSession
|
||||
rg self.clanLib.test.baseTest
|
||||
rg "import.*/lib/test-base.nix"
|
||||
```
|
||||
|
||||
### Locating definitions of failing VM tests
|
||||
@@ -50,7 +50,7 @@ example: locating the vm test named `borgbackup`:
|
||||
```shellSession
|
||||
$ rg "borgbackup =" ./checks
|
||||
./checks/flake-module.nix
|
||||
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`.
|
||||
@@ -99,15 +99,15 @@ Basically everything stated under the NixOS VM tests sections applies here, exce
|
||||
|
||||
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.
|
||||
- setuid binaries don't work
|
||||
- does not yet support networking
|
||||
- supports only one machine as of now
|
||||
|
||||
|
||||
### Where to find examples for NixOS container tests
|
||||
|
||||
Existing nixos container tests in clan-core can be found by using ripgrep:
|
||||
|
||||
```shellSession
|
||||
rg self.clanLib.test.containerTest
|
||||
rg "import.*/lib/container-test.nix"
|
||||
```
|
||||
|
||||
|
||||
|
||||
38
flake.lock
generated
38
flake.lock
generated
@@ -16,11 +16,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1747008053,
|
||||
"narHash": "sha256-rob/qftmEuk+/JVGCIrOpv+LWjdmayFtebEKqRZXVAI=",
|
||||
"rev": "2666bb11f4287cfbdf3b7c5f55231c6b5772a436",
|
||||
"lastModified": 1746334246,
|
||||
"narHash": "sha256-YU4wtH9Y5yRjqbMwczOdDakOjSiTkOUP/JAYd1f3jBc=",
|
||||
"rev": "607ce65fbfe20bb38170b76826a11006f526c05d",
|
||||
"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": {
|
||||
"type": "tarball",
|
||||
@@ -34,11 +34,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746729224,
|
||||
"narHash": "sha256-9R4sOLAK1w3Bq54H3XOJogdc7a6C2bLLmatOQ+5pf5w=",
|
||||
"lastModified": 1745812220,
|
||||
"narHash": "sha256-hotBG0EJ9VmAHJYF0yhWuTVZpENHvwcJ2SxvIPrXm+g=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "85555d27ded84604ad6657ecca255a03fd878607",
|
||||
"rev": "d0c543d740fad42fe2c035b43c9d41127e073c78",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -74,11 +74,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746708654,
|
||||
"narHash": "sha256-GeC99gu5H6+AjBXsn5dOhP4/ApuioGCBkufdmEIWPRs=",
|
||||
"lastModified": 1746254942,
|
||||
"narHash": "sha256-Y062AuRx6l+TJNX8wxZcT59SSLsqD9EedAY0mqgTtQE=",
|
||||
"owner": "nix-darwin",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "6cb36e8327421c61e5a3bbd08ed63491b616364a",
|
||||
"rev": "760a11c87009155afa0140d55c40e7c336d62d7a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -118,10 +118,10 @@
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 315532800,
|
||||
"narHash": "sha256-EbVl0wIdDYZWrxpQoxPlXfliaR4KHA9xP5dVjG1CZxI=",
|
||||
"rev": "ed30f8aba41605e3ab46421e3dcb4510ec560ff8",
|
||||
"narHash": "sha256-pxwYhAgOyComW58BCfboADZWr4b5oS8hP9E9fQ489HM=",
|
||||
"rev": "f21e4546e3ede7ae34d12a84602a22246b31f7e0",
|
||||
"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": {
|
||||
"type": "tarball",
|
||||
@@ -149,11 +149,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746485181,
|
||||
"narHash": "sha256-PxrrSFLaC7YuItShxmYbMgSuFFuwxBB+qsl9BZUnRvg=",
|
||||
"lastModified": 1745310711,
|
||||
"narHash": "sha256-ePyTpKEJTgX0gvgNQWd7tQYQ3glIkbqcW778RpHlqgA=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "e93ee1d900ad264d65e9701a5c6f895683433386",
|
||||
"rev": "5e3e92b16d6fdf9923425a8d4df7496b2434f39c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -184,11 +184,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746989248,
|
||||
"narHash": "sha256-uoQ21EWsAhyskNo8QxrTVZGjG/dV4x5NM1oSgrmNDJY=",
|
||||
"lastModified": 1746216483,
|
||||
"narHash": "sha256-4h3s1L/kKqt3gMDcVfN8/4v2jqHrgLIe4qok4ApH5x4=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "708ec80ca82e2bbafa93402ccb66a35ff87900c5",
|
||||
"rev": "29ec5026372e0dec56f890e50dbe4f45930320fd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -37,7 +37,7 @@ let
|
||||
done
|
||||
if ! test -e ~/clan-core; then
|
||||
# git clone https://git.clan.lol/clan/clan-core.git ~/clan-core
|
||||
cp -rv ${self.checks.x86_64-linux.clan-core-for-checks} clan-core
|
||||
cp -rv ${self} clan-core
|
||||
fi
|
||||
cd clan-core
|
||||
clan machines morph demo-template --i-will-be-fired-for-using-this
|
||||
|
||||
@@ -10,11 +10,6 @@ let
|
||||
in
|
||||
{
|
||||
options = {
|
||||
_prefix = lib.mkOption {
|
||||
type = types.listOf types.str;
|
||||
internal = true;
|
||||
default = [ ];
|
||||
};
|
||||
self = lib.mkOption {
|
||||
type = types.raw;
|
||||
default = self;
|
||||
@@ -173,7 +168,6 @@ in
|
||||
inventoryFile = lib.mkOption { type = lib.types.raw; };
|
||||
# The machine 'imports' generated by the inventory per machine
|
||||
inventoryClass = lib.mkOption { type = lib.types.raw; };
|
||||
evalServiceSchema = lib.mkOption { };
|
||||
# clan-core's modules
|
||||
clanModules = lib.mkOption { type = lib.types.raw; };
|
||||
source = lib.mkOption { type = lib.types.raw; };
|
||||
|
||||
@@ -44,10 +44,6 @@ let
|
||||
buildInventory {
|
||||
inherit inventory directory;
|
||||
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
|
||||
{
|
||||
inventory.modules = clan-core.clanModules;
|
||||
inventory._legacyModules = clan-core.clanModules;
|
||||
}
|
||||
# config.inventory.meta <- config.meta
|
||||
{ inventory.meta = config.meta; }
|
||||
@@ -209,9 +204,6 @@ in
|
||||
|
||||
inherit inventoryClass;
|
||||
|
||||
# Endpoint that can be called to get a service schema
|
||||
evalServiceSchema = clan-core.clanLib.evalServiceSchema config.self;
|
||||
|
||||
# TODO: unify this interface
|
||||
# We should have only clan.modules. (consistent with clan.templates)
|
||||
inherit (clan-core) clanModules clanLib;
|
||||
|
||||
@@ -15,27 +15,10 @@ lib.fix (clanLib: {
|
||||
*/
|
||||
callLib = file: args: import file ({ inherit lib clanLib; } // args);
|
||||
|
||||
# ------------------------------------
|
||||
buildClan = clanLib.buildClanModule.buildClanWith {
|
||||
clan-core = self;
|
||||
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
|
||||
evalClan = clanLib.callLib ./inventory/eval-clan-modules { };
|
||||
|
||||
@@ -12,35 +12,25 @@ let
|
||||
inventory,
|
||||
directory,
|
||||
flakeInputs,
|
||||
prefix ? [ ],
|
||||
localModuleSet ? { },
|
||||
}:
|
||||
(lib.evalModules {
|
||||
# TODO: remove clanLib from specialArgs
|
||||
specialArgs = {
|
||||
inherit clanLib;
|
||||
};
|
||||
modules = [
|
||||
./builder
|
||||
(lib.modules.importApply ./service-list-from-inputs.nix {
|
||||
inherit flakeInputs clanLib localModuleSet;
|
||||
})
|
||||
{ inherit directory inventory; }
|
||||
(
|
||||
# config.distributedServices.allMachines.${name} or [ ];
|
||||
{ config, ... }:
|
||||
{
|
||||
|
||||
distributedServices = clanLib.inventory.mapInstances {
|
||||
inherit (config) inventory;
|
||||
inherit localModuleSet;
|
||||
inherit flakeInputs;
|
||||
prefix = prefix ++ [ "distributedServices" ];
|
||||
};
|
||||
machines = lib.mapAttrs (_machineName: v: {
|
||||
machineImports = v;
|
||||
}) config.distributedServices.allMachines;
|
||||
|
||||
}
|
||||
)
|
||||
(lib.modules.importApply ./inventory-introspection.nix { inherit clanLib; })
|
||||
|
||||
@@ -96,12 +96,6 @@ in
|
||||
./assertions.nix
|
||||
];
|
||||
options = {
|
||||
_legacyModules = lib.mkOption {
|
||||
internal = true;
|
||||
visible = false;
|
||||
default = { };
|
||||
};
|
||||
|
||||
options = lib.mkOption {
|
||||
internal = true;
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ let
|
||||
services = clanLib.callLib ./distributed-service/inventory-adapter.nix { };
|
||||
in
|
||||
{
|
||||
inherit (services) evalClanService mapInstances resolveModule;
|
||||
inherit (services) evalClanService mapInstances;
|
||||
inherit (import ./build-inventory { inherit lib clanLib; }) buildInventory;
|
||||
interface = ./build-inventory/interface.nix;
|
||||
# Returns the list of machine names
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This module enables itself if
|
||||
# manifest.features.API = true
|
||||
# It converts the roles.interface to a json-schema
|
||||
{ clanLib, prefix }:
|
||||
{ clanLib, attrName }:
|
||||
let
|
||||
converter = clanLib.jsonschema {
|
||||
includeDefaults = true;
|
||||
@@ -45,7 +45,7 @@ in
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -16,73 +16,27 @@
|
||||
}:
|
||||
let
|
||||
evalClanService =
|
||||
{ modules, prefix }:
|
||||
{ modules, key }:
|
||||
(lib.evalModules {
|
||||
class = "clan.service";
|
||||
modules = [
|
||||
./service-module.nix
|
||||
# feature modules
|
||||
(lib.modules.importApply ./api-feature.nix {
|
||||
inherit clanLib prefix;
|
||||
inherit clanLib;
|
||||
attrName = key;
|
||||
})
|
||||
] ++ 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
|
||||
{
|
||||
inherit evalClanService resolveModule;
|
||||
inherit evalClanService;
|
||||
mapInstances =
|
||||
{
|
||||
# This is used to resolve the module imports from 'flake.inputs'
|
||||
flakeInputs,
|
||||
# The clan inventory
|
||||
inventory,
|
||||
localModuleSet,
|
||||
prefix ? [ ],
|
||||
}:
|
||||
let
|
||||
# machineHasTag = machineName: tagName: lib.elem tagName inventory.machines.${machineName}.tags;
|
||||
@@ -91,11 +45,42 @@ in
|
||||
importedModuleWithInstances = lib.mapAttrs (
|
||||
instanceName: instance:
|
||||
let
|
||||
resolvedModule = resolveModule {
|
||||
moduleSpec = instance.module;
|
||||
inherit localModuleSet;
|
||||
inherit flakeInputs;
|
||||
};
|
||||
# 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 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
|
||||
# :: { client :: ... }
|
||||
@@ -153,7 +138,7 @@ in
|
||||
importedModulesEvaluated = lib.mapAttrs (
|
||||
module_ident: instances:
|
||||
evalClanService {
|
||||
prefix = prefix ++ [ module_ident ];
|
||||
key = module_ident;
|
||||
modules =
|
||||
[
|
||||
# Import the resolved module.
|
||||
|
||||
@@ -255,9 +255,7 @@ in
|
||||
{
|
||||
options.API = mkOption {
|
||||
type = types.bool;
|
||||
# This is read only, because we don't support turning it off yet
|
||||
readOnly = true;
|
||||
default = true;
|
||||
default = false;
|
||||
description = ''
|
||||
Enables automatic API schema conversion for the interface of this module.
|
||||
'';
|
||||
|
||||
@@ -41,13 +41,9 @@ let
|
||||
|
||||
callInventoryAdapter =
|
||||
inventoryModule:
|
||||
let
|
||||
inventory = evalInventory inventoryModule;
|
||||
in
|
||||
clanLib.inventory.mapInstances {
|
||||
flakeInputs = flakeInputsFixture;
|
||||
inherit inventory;
|
||||
localModuleSet = inventory.modules;
|
||||
inventory = evalInventory inventoryModule;
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
@@ -92,7 +92,7 @@ in
|
||||
lib.lazyDerivation {
|
||||
# lazyDerivation improves performance when only passthru items and/or meta are used.
|
||||
derivation = hostPkgs.stdenv.mkDerivation {
|
||||
name = "container-test-run-${config.name}";
|
||||
name = "vm-test-run-${config.name}";
|
||||
|
||||
requiredSystemFeatures = [ "uid-range" ];
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ import argparse
|
||||
import ctypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import types
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from contextlib import _GeneratorContextManager
|
||||
from dataclasses import dataclass
|
||||
@@ -15,8 +13,6 @@ from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any
|
||||
|
||||
from colorama import Fore, Style
|
||||
|
||||
from .logger import AbstractLogger, CompositeLogger, TerminalLogger
|
||||
|
||||
# Load the C library
|
||||
@@ -191,22 +187,6 @@ class Machine:
|
||||
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(
|
||||
self,
|
||||
command: str,
|
||||
@@ -248,10 +228,23 @@ class Machine:
|
||||
"""
|
||||
|
||||
# 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(
|
||||
self.nsenter_command(command),
|
||||
[
|
||||
"nsenter",
|
||||
"--target",
|
||||
str(self.container_pid),
|
||||
"--mount",
|
||||
"--uts",
|
||||
"--ipc",
|
||||
"--net",
|
||||
"--pid",
|
||||
"--cgroup",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
command,
|
||||
],
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -472,43 +465,6 @@ class Driver:
|
||||
print(f"Starting {machine.name}")
|
||||
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]:
|
||||
general_symbols = {
|
||||
"start_all": self.start_all,
|
||||
|
||||
@@ -22,9 +22,6 @@ in
|
||||
pkgs,
|
||||
self,
|
||||
useContainers ? true,
|
||||
# Displayed for better error messages, otherwise the placeholder
|
||||
system ? "<system>",
|
||||
attrName ? "<check_name>",
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -38,7 +35,7 @@ in
|
||||
{
|
||||
imports = [
|
||||
nixosTest
|
||||
] ++ lib.optionals useContainers [ ./container-test-driver/driver-module.nix ];
|
||||
] ++ lib.optionals (useContainers) [ ./container-test-driver/driver-module.nix ];
|
||||
options = {
|
||||
clanSettings = mkOption {
|
||||
default = { };
|
||||
@@ -63,15 +60,6 @@ in
|
||||
};
|
||||
modules = [
|
||||
clanLib.buildClanModule.flakePartsModule
|
||||
{
|
||||
_prefix = [
|
||||
"checks"
|
||||
system
|
||||
attrName
|
||||
"config"
|
||||
"clan"
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
21
module.nix
21
module.nix
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -39,44 +39,9 @@ in
|
||||
type = submodule { imports = [ ./interface.nix ]; };
|
||||
};
|
||||
|
||||
config = {
|
||||
# check all that all non-secret files have no owner/group/mode set
|
||||
warnings = lib.foldl' (
|
||||
warnings: generator:
|
||||
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;
|
||||
};
|
||||
config.system.clan.deployment.data = {
|
||||
vars = config.clan.core.vars._serialized;
|
||||
inherit (config.clan.core.networking) targetHost buildHost;
|
||||
inherit (config.clan.core.deployment) requireExplicitUpdate;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ in
|
||||
internal = true;
|
||||
description = ''
|
||||
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 = {
|
||||
# TODO: We don't support per-machine choice of backends
|
||||
@@ -258,7 +258,7 @@ in
|
||||
defaultText = ''
|
||||
builtins.path {
|
||||
name = "$${generator.config._module.args.name}_$${file.config._module.args.name}";
|
||||
path = file.config.flakePath;
|
||||
path = file.config.inRepoPath;
|
||||
}
|
||||
'';
|
||||
default = builtins.path {
|
||||
|
||||
@@ -18,7 +18,7 @@ let
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${machineName}/${secret.generator}/${secret.name}/secret";
|
||||
|
||||
vars = collectFiles config.clan.core.vars.generators;
|
||||
vars = collectFiles config.clan.core.vars;
|
||||
in
|
||||
{
|
||||
config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") {
|
||||
|
||||
@@ -13,7 +13,7 @@ in
|
||||
{
|
||||
|
||||
collectFiles =
|
||||
generators:
|
||||
vars:
|
||||
let
|
||||
relevantFiles =
|
||||
generator:
|
||||
@@ -30,7 +30,7 @@ in
|
||||
inherit (generator) share;
|
||||
inherit (file) owner group mode;
|
||||
}) (relevantFiles generator)
|
||||
) generators
|
||||
) vars.generators
|
||||
);
|
||||
in
|
||||
allFiles;
|
||||
|
||||
@@ -29,8 +29,6 @@ mkShell {
|
||||
export GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||
export PKG_ROOT=$GIT_ROOT/pkgs/clan-app
|
||||
|
||||
export CLAN_CORE_PATH="$GIT_ROOT"
|
||||
|
||||
# Add current package to PYTHONPATH
|
||||
export PYTHONPATH="$PKG_ROOT''${PYTHONPATH:+:$PYTHONPATH:}"
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
# 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 .clan import show, update
|
||||
|
||||
# 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 (
|
||||
backups,
|
||||
|
||||
@@ -19,23 +19,21 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
|
||||
if not backup_scripts["providers"]:
|
||||
msg = "No providers specified"
|
||||
raise ClanError(msg)
|
||||
with machine.target_host() as host:
|
||||
for provider in backup_scripts["providers"]:
|
||||
proc = host.run(
|
||||
[backup_scripts["providers"][provider]["create"]],
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
msg = "failed to start backup"
|
||||
raise ClanError(msg)
|
||||
print("successfully started backup")
|
||||
for provider in backup_scripts["providers"]:
|
||||
proc = machine.target_host.run(
|
||||
[backup_scripts["providers"][provider]["create"]],
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
msg = "failed to start backup"
|
||||
raise ClanError(msg)
|
||||
print("successfully started backup")
|
||||
else:
|
||||
if provider not in backup_scripts["providers"]:
|
||||
msg = f"provider {provider} not found"
|
||||
raise ClanError(msg)
|
||||
with machine.target_host() as host:
|
||||
proc = host.run(
|
||||
[backup_scripts["providers"][provider]["create"]],
|
||||
)
|
||||
proc = machine.target_host.run(
|
||||
[backup_scripts["providers"][provider]["create"]],
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
msg = "failed to start backup"
|
||||
raise ClanError(msg)
|
||||
|
||||
@@ -10,7 +10,6 @@ from clan_cli.completions import (
|
||||
)
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.ssh.host import Host
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -19,11 +18,11 @@ class Backup:
|
||||
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 = []
|
||||
backup_metadata = machine.eval_nix("config.clan.core.backups")
|
||||
list_command = backup_metadata["providers"][provider]["list"]
|
||||
proc = host.run(
|
||||
proc = machine.target_host.run(
|
||||
[list_command],
|
||||
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]:
|
||||
backup_metadata = machine.eval_nix("config.clan.core.backups")
|
||||
results = []
|
||||
with machine.target_host() as host:
|
||||
if provider is None:
|
||||
for _provider in backup_metadata["providers"]:
|
||||
results += list_provider(machine, host, _provider)
|
||||
if provider is None:
|
||||
for _provider in backup_metadata["providers"]:
|
||||
results += list_provider(machine, _provider)
|
||||
|
||||
else:
|
||||
results += list_provider(machine, host, provider)
|
||||
else:
|
||||
results += list_provider(machine, provider)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@@ -8,12 +8,9 @@ from clan_cli.completions import (
|
||||
)
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.ssh.host import Host
|
||||
|
||||
|
||||
def restore_service(
|
||||
machine: Machine, host: Host, name: str, provider: str, service: str
|
||||
) -> None:
|
||||
def restore_service(machine: Machine, name: str, provider: str, service: str) -> None:
|
||||
backup_metadata = machine.eval_nix("config.clan.core.backups")
|
||||
backup_folders = machine.eval_nix("config.clan.core.state")
|
||||
|
||||
@@ -28,7 +25,7 @@ def restore_service(
|
||||
env["FOLDERS"] = ":".join(set(folders))
|
||||
|
||||
if pre_restore := backup_folders[service]["preRestoreCommand"]:
|
||||
proc = host.run(
|
||||
proc = machine.target_host.run(
|
||||
[pre_restore],
|
||||
RunOpts(log=Log.STDERR),
|
||||
extra_env=env,
|
||||
@@ -37,7 +34,7 @@ def restore_service(
|
||||
msg = f"failed to run preRestoreCommand: {pre_restore}, error was: {proc.stdout}"
|
||||
raise ClanError(msg)
|
||||
|
||||
proc = host.run(
|
||||
proc = machine.target_host.run(
|
||||
[backup_metadata["providers"][provider]["restore"]],
|
||||
RunOpts(log=Log.STDERR),
|
||||
extra_env=env,
|
||||
@@ -47,7 +44,7 @@ def restore_service(
|
||||
raise ClanError(msg)
|
||||
|
||||
if post_restore := backup_folders[service]["postRestoreCommand"]:
|
||||
proc = host.run(
|
||||
proc = machine.target_host.run(
|
||||
[post_restore],
|
||||
RunOpts(log=Log.STDERR),
|
||||
extra_env=env,
|
||||
@@ -64,19 +61,18 @@ def restore_backup(
|
||||
service: str | None = None,
|
||||
) -> None:
|
||||
errors = []
|
||||
with machine.target_host() as host:
|
||||
if service is None:
|
||||
backup_folders = machine.eval_nix("config.clan.core.state")
|
||||
for _service in backup_folders:
|
||||
try:
|
||||
restore_service(machine, host, name, provider, _service)
|
||||
except ClanError as e:
|
||||
errors.append(f"{_service}: {e}")
|
||||
else:
|
||||
if service is None:
|
||||
backup_folders = machine.eval_nix("config.clan.core.state")
|
||||
for _service in backup_folders:
|
||||
try:
|
||||
restore_service(machine, host, name, provider, service)
|
||||
restore_service(machine, name, provider, _service)
|
||||
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:
|
||||
raise ClanError(
|
||||
"Restore failed for the following services:\n" + "\n".join(errors)
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.cmd import Log, RunOpts, run
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.nix import nix_shell
|
||||
|
||||
_works: bool | None = None
|
||||
@@ -16,11 +12,6 @@ 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
|
||||
cmd = nix_shell(
|
||||
[
|
||||
@@ -39,10 +30,13 @@ def _bubblewrap_works() -> bool:
|
||||
"--gid", "1000",
|
||||
"--",
|
||||
# do nothing, just test if bash executes
|
||||
str(real_bash_path), "-c", ":"
|
||||
"bash", "-c", ":"
|
||||
],
|
||||
)
|
||||
|
||||
# fmt: on
|
||||
res = run(cmd, RunOpts(log=Log.BOTH, check=False))
|
||||
return res.returncode == 0
|
||||
try:
|
||||
run(cmd)
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@@ -107,7 +107,7 @@ def create_clan(opts: CreateOptions) -> CreateClanResponse:
|
||||
response.flake_update = flake_update
|
||||
|
||||
if opts.initial:
|
||||
init_inventory(Flake(str(opts.dest)), init=opts.initial)
|
||||
init_inventory(str(opts.dest), init=opts.initial)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -6,9 +6,8 @@ from urllib.parse import urlparse
|
||||
|
||||
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.flake import Flake
|
||||
from clan_cli.inventory import Meta
|
||||
from clan_cli.nix import nix_eval
|
||||
|
||||
@@ -16,26 +15,26 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@API.register
|
||||
def show_clan_meta(flake: Flake) -> Meta:
|
||||
if flake.is_local and not flake.path.exists():
|
||||
msg = f"Path {flake} does not exist"
|
||||
def show_clan_meta(uri: str) -> Meta:
|
||||
if uri.startswith("/") and not Path(uri).exists():
|
||||
msg = f"Path {uri} does not exist"
|
||||
raise ClanError(msg, description="clan directory does not exist")
|
||||
cmd = nix_eval(
|
||||
[
|
||||
f"{flake}#clanInternals.inventory.meta",
|
||||
f"{uri}#clanInternals.inventory.meta",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
res = "{}"
|
||||
|
||||
try:
|
||||
proc = run(cmd)
|
||||
proc = run_no_stdout(cmd)
|
||||
res = proc.stdout.strip()
|
||||
except ClanCmdError as e:
|
||||
msg = "Evaluation failed on meta attribute"
|
||||
raise ClanError(
|
||||
msg,
|
||||
location=f"show_clan {flake}",
|
||||
location=f"show_clan {uri}",
|
||||
description=str(e.cmd),
|
||||
) from e
|
||||
|
||||
@@ -54,16 +53,16 @@ def show_clan_meta(flake: Flake) -> Meta:
|
||||
msg = "Invalid absolute path"
|
||||
raise ClanError(
|
||||
msg,
|
||||
location=f"show_clan {flake}",
|
||||
location=f"show_clan {uri}",
|
||||
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:
|
||||
msg = "Invalid schema"
|
||||
raise ClanError(
|
||||
msg,
|
||||
location=f"show_clan {flake}",
|
||||
location=f"show_clan {uri}",
|
||||
description="Icon path must be a URL or a relative path",
|
||||
)
|
||||
|
||||
|
||||
@@ -2,21 +2,20 @@ from dataclasses import dataclass
|
||||
|
||||
from clan_lib.api import API
|
||||
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.inventory import Inventory, Meta, load_inventory_json, set_inventory
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpdateOptions:
|
||||
flake: Flake
|
||||
directory: str
|
||||
meta: Meta
|
||||
|
||||
|
||||
@API.register
|
||||
def update_clan_meta(options: UpdateOptions) -> Inventory:
|
||||
inventory = load_inventory_json(options.flake)
|
||||
inventory = load_inventory_json(options.directory)
|
||||
inventory["meta"] = options.meta
|
||||
|
||||
set_inventory(inventory, options.flake, "Update clan metadata")
|
||||
set_inventory(inventory, options.directory, "Update clan metadata")
|
||||
|
||||
return inventory
|
||||
|
||||
@@ -403,3 +403,23 @@ def run(
|
||||
raise ClanCmdError(cmd_out)
|
||||
|
||||
return cmd_out
|
||||
|
||||
|
||||
def run_no_stdout(
|
||||
cmd: list[str],
|
||||
opts: RunOpts | None = None,
|
||||
) -> CmdOut:
|
||||
"""
|
||||
Like run, but automatically suppresses all output, if not in DEBUG log level.
|
||||
If in DEBUG log level the stdout of commands will be shown.
|
||||
"""
|
||||
if opts is None:
|
||||
opts = RunOpts()
|
||||
|
||||
if cmdlog.isEnabledFor(logging.DEBUG):
|
||||
opts.log = opts.log if opts.log.value > Log.STDERR.value else Log.STDERR
|
||||
|
||||
return run(
|
||||
cmd,
|
||||
opts,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from clan_cli.custom_logger import setup_logging
|
||||
|
||||
# collect_ignore = ["./nixpkgs"]
|
||||
|
||||
pytest_plugins = [
|
||||
"clan_cli.tests.temporary_dir",
|
||||
"clan_cli.tests.root",
|
||||
@@ -13,3 +19,13 @@ pytest_plugins = [
|
||||
"clan_cli.tests.stdout",
|
||||
"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")
|
||||
|
||||
@@ -4,14 +4,9 @@ import sys
|
||||
import urllib
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .errors import ClanError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.machines.machines import Machine
|
||||
|
||||
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
|
||||
|
||||
|
||||
def machines_dir(flake: "Flake") -> Path:
|
||||
if flake.is_local:
|
||||
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 machines_dir(flake_dir: Path) -> Path:
|
||||
return flake_dir / "machines"
|
||||
|
||||
|
||||
def specific_machine_dir(machine: "Machine") -> Path:
|
||||
return machines_dir(machine.flake) / machine.name
|
||||
def specific_machine_dir(flake_dir: Path, machine: str) -> Path:
|
||||
return machines_dir(flake_dir) / machine
|
||||
|
||||
|
||||
def module_root() -> Path:
|
||||
|
||||
@@ -48,7 +48,6 @@ def bubblewrap_cmd(generator: str, facts_dir: Path, secrets_dir: Path) -> list[s
|
||||
"--unshare-all",
|
||||
"--tmpfs", "/",
|
||||
"--ro-bind", "/nix/store", "/nix/store",
|
||||
"--ro-bind", "/bin/sh", "/bin/sh",
|
||||
"--dev", "/dev",
|
||||
# not allowed to bind procfs in some sandboxes
|
||||
"--bind", str(facts_dir), str(facts_dir),
|
||||
|
||||
@@ -4,7 +4,6 @@ from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
|
||||
import clan_cli.machines.machines as machines
|
||||
from clan_cli.ssh.host import Host
|
||||
|
||||
|
||||
class SecretStoreBase(ABC):
|
||||
@@ -26,7 +25,7 @@ class SecretStoreBase(ABC):
|
||||
def exists(self, service: str, name: str) -> bool:
|
||||
pass
|
||||
|
||||
def needs_upload(self, host: Host) -> bool:
|
||||
def needs_upload(self) -> bool:
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import override
|
||||
from clan_cli.cmd import Log, RunOpts
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.nix import nix_shell
|
||||
from clan_cli.ssh.host import Host
|
||||
|
||||
from . import SecretStoreBase
|
||||
|
||||
@@ -94,9 +93,9 @@ class SecretStore(SecretStoreBase):
|
||||
return b"\n".join(hashes)
|
||||
|
||||
@override
|
||||
def needs_upload(self, host: Host) -> bool:
|
||||
def needs_upload(self) -> bool:
|
||||
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
|
||||
["cat", f"{self.machine.secrets_upload_directory}/.pass_info"],
|
||||
RunOpts(log=Log.STDERR, check=False),
|
||||
|
||||
@@ -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.secrets import decrypt_secret, encrypt_secret, has_secret
|
||||
from clan_cli.secrets.sops import generate_private_key
|
||||
from clan_cli.ssh.host import Host
|
||||
|
||||
from . import SecretStoreBase
|
||||
|
||||
@@ -61,7 +60,7 @@ class SecretStore(SecretStoreBase):
|
||||
)
|
||||
|
||||
@override
|
||||
def needs_upload(self, host: Host) -> bool:
|
||||
def needs_upload(self) -> bool:
|
||||
return False
|
||||
|
||||
# We rely now on the vars backend to upload the age key
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
from clan_cli.dirs import vm_state_dir
|
||||
from clan_cli.machines.machines import Machine
|
||||
@@ -29,7 +28,6 @@ class SecretStore(SecretStoreBase):
|
||||
def exists(self, service: str, name: str) -> bool:
|
||||
return (self.dir / service / name).exists()
|
||||
|
||||
@override
|
||||
def upload(self, output_dir: Path) -> None:
|
||||
if output_dir.exists():
|
||||
shutil.rmtree(output_dir)
|
||||
|
||||
@@ -11,16 +11,16 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upload_secrets(machine: Machine) -> None:
|
||||
with machine.target_host() as host:
|
||||
if not machine.secret_facts_store.needs_upload(host):
|
||||
machine.info("Secrets already uploaded")
|
||||
return
|
||||
if not machine.secret_facts_store.needs_upload():
|
||||
machine.info("Secrets already uploaded")
|
||||
return
|
||||
|
||||
with TemporaryDirectory(prefix="facts-upload-") as _tempdir:
|
||||
local_secret_dir = Path(_tempdir).resolve()
|
||||
machine.secret_facts_store.upload(local_secret_dir)
|
||||
remote_secret_dir = Path(machine.secrets_upload_directory)
|
||||
upload(host, local_secret_dir, remote_secret_dir)
|
||||
with TemporaryDirectory(prefix="facts-upload-") as _tempdir:
|
||||
local_secret_dir = Path(_tempdir).resolve()
|
||||
machine.secret_facts_store.upload(local_secret_dir)
|
||||
remote_secret_dir = Path(machine.secrets_upload_directory)
|
||||
|
||||
upload(machine.target_host, local_secret_dir, remote_secret_dir)
|
||||
|
||||
|
||||
def upload_command(args: argparse.Namespace) -> None:
|
||||
|
||||
@@ -14,7 +14,6 @@ from clan_cli.nix import (
|
||||
nix_build,
|
||||
nix_command,
|
||||
nix_config,
|
||||
nix_eval,
|
||||
nix_metadata,
|
||||
nix_test_store,
|
||||
)
|
||||
@@ -575,12 +574,12 @@ class Flake:
|
||||
identifier: str
|
||||
inputs_from: str | None = None
|
||||
hash: str | None = None
|
||||
flake_cache_path: Path | None = None
|
||||
store_path: str | None = None
|
||||
|
||||
_flake_cache_path: Path | None = field(init=False, default=None)
|
||||
_cache: FlakeCache | None = field(init=False, default=None)
|
||||
_path: Path | None = field(init=False, default=None)
|
||||
_is_local: bool | None = field(init=False, default=None)
|
||||
cache: FlakeCache | None = None
|
||||
_cache: FlakeCache | None = None
|
||||
_path: Path | None = None
|
||||
_is_local: bool | None = None
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: type["Flake"], data: dict[str, Any]) -> "Flake":
|
||||
@@ -620,9 +619,11 @@ class Flake:
|
||||
except Exception as e:
|
||||
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 = [
|
||||
"flake",
|
||||
@@ -641,15 +642,6 @@ class Flake:
|
||||
flake_metadata = json.loads(flake_prefetch.stdout)
|
||||
self.store_path = flake_metadata["storePath"]
|
||||
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()
|
||||
assert self.hash is not None
|
||||
@@ -659,17 +651,17 @@ class Flake:
|
||||
)
|
||||
self.load_cache()
|
||||
|
||||
if "original" not in self.flake_metadata:
|
||||
self.flake_metadata = nix_metadata(self.identifier)
|
||||
if "original" not in flake_metadata:
|
||||
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
|
||||
path = self.flake_metadata["original"]["url"].removeprefix("file://")
|
||||
path = flake_metadata["original"]["url"].removeprefix("file://")
|
||||
path = path.removeprefix("file:")
|
||||
self._path = Path(path)
|
||||
elif self.flake_metadata["original"].get("path"):
|
||||
elif flake_metadata["original"].get("path"):
|
||||
self._is_local = True
|
||||
self._path = Path(self.flake_metadata["original"]["path"])
|
||||
self._path = Path(flake_metadata["original"]["path"])
|
||||
else:
|
||||
self._is_local = False
|
||||
assert self.store_path is not None
|
||||
@@ -763,56 +755,6 @@ class Flake:
|
||||
if 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(
|
||||
self,
|
||||
selectors: list[str],
|
||||
|
||||
@@ -14,7 +14,7 @@ from clan_cli.facts.generate import generate_facts
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.nix import nix_shell
|
||||
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 .list import list_possible_keymaps, list_possible_languages
|
||||
@@ -35,7 +35,6 @@ class Disk:
|
||||
device: str
|
||||
|
||||
|
||||
# TODO: unify this with machine install
|
||||
@API.register
|
||||
def flash_machine(
|
||||
machine: Machine,
|
||||
@@ -108,7 +107,7 @@ def flash_machine(
|
||||
|
||||
local_dir.mkdir(parents=True)
|
||||
machine.secret_facts_store.upload(local_dir)
|
||||
populate_secret_vars(machine, local_dir)
|
||||
upload_secret_vars(machine, local_dir)
|
||||
disko_install = []
|
||||
|
||||
if os.geteuid() != 0:
|
||||
|
||||
@@ -21,9 +21,8 @@ from typing import Any
|
||||
|
||||
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.flake import Flake
|
||||
from clan_cli.git import commit_file
|
||||
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
|
||||
"""
|
||||
inventory_file = (flake.path / "inventory.json").resolve()
|
||||
inventory_file = (Path(flake_dir) / "inventory.json").resolve()
|
||||
return inventory_file
|
||||
|
||||
|
||||
@@ -62,7 +61,8 @@ def get_inventory_path(flake: Flake) -> Path:
|
||||
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.
|
||||
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:
|
||||
res = proc.stdout.strip()
|
||||
@@ -355,7 +355,7 @@ def determine_writeability(
|
||||
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
|
||||
|
||||
@@ -375,12 +375,12 @@ def get_inventory_current_priority(flake: Flake) -> dict:
|
||||
"""
|
||||
cmd = nix_eval(
|
||||
[
|
||||
f"{flake}#clanInternals.inventoryClass.introspection",
|
||||
f"{flake_dir}#clanInternals.inventoryClass.introspection",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
|
||||
proc = run(cmd)
|
||||
proc = run_no_stdout(cmd)
|
||||
|
||||
try:
|
||||
res = proc.stdout.strip()
|
||||
@@ -393,7 +393,7 @@ def get_inventory_current_priority(flake: Flake) -> dict:
|
||||
|
||||
|
||||
@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
|
||||
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
|
||||
"""
|
||||
|
||||
inventory_file = get_inventory_path(flake)
|
||||
inventory_file = get_inventory_path(flake_dir)
|
||||
|
||||
if not inventory_file.exists():
|
||||
return {}
|
||||
@@ -473,14 +473,14 @@ def patch(d: dict[str, Any], path: str, content: Any) -> None:
|
||||
|
||||
|
||||
@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.
|
||||
Make sure you pass only attributes that you would like to persist.
|
||||
ATTENTION: Don't pass nix eval values unintentionally.
|
||||
"""
|
||||
|
||||
inventory_file = get_inventory_path(flake)
|
||||
inventory_file = get_inventory_path(base_dir)
|
||||
|
||||
curr_inventory = {}
|
||||
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:
|
||||
json.dump(curr_inventory, f, indent=2)
|
||||
|
||||
commit_file(
|
||||
inventory_file, flake.path, commit_message=f"inventory.{section}: Update"
|
||||
)
|
||||
commit_file(inventory_file, base_dir, commit_message=f"inventory.{section}: Update")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -506,16 +504,16 @@ class WriteInfo:
|
||||
|
||||
@API.register
|
||||
def get_inventory_with_writeable_keys(
|
||||
flake: Flake,
|
||||
flake_dir: str | Path,
|
||||
) -> WriteInfo:
|
||||
"""
|
||||
Load the inventory and determine the writeable keys
|
||||
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_disk: Inventory = load_inventory_json(flake)
|
||||
data_eval: Inventory = load_inventory_eval(flake_dir)
|
||||
data_disk: Inventory = load_inventory_json(flake_dir)
|
||||
|
||||
writeables = determine_writeability(
|
||||
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)
|
||||
|
||||
|
||||
# TODO: remove this function in favor of a proper read/write API
|
||||
@API.register
|
||||
def set_inventory(
|
||||
inventory: Inventory, flake: Flake, message: str, commit: bool = True
|
||||
inventory: Inventory, flake_dir: str | Path, message: str, commit: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Write the inventory to the flake directory
|
||||
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
|
||||
inventory.pop("tags", None) # type: ignore
|
||||
@@ -555,43 +552,43 @@ def set_inventory(
|
||||
for delete_path in delete_set:
|
||||
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:
|
||||
json.dump(persisted, f, indent=2)
|
||||
|
||||
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
|
||||
def delete(flake: Flake, delete_set: set[str]) -> None:
|
||||
@API.register
|
||||
def delete(directory: str | Path, delete_set: set[str]) -> None:
|
||||
"""
|
||||
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)
|
||||
|
||||
for delete_path in delete_set:
|
||||
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:
|
||||
json.dump(data_disk, f, indent=2)
|
||||
|
||||
commit_file(
|
||||
inventory_file,
|
||||
flake.path,
|
||||
Path(directory),
|
||||
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
|
||||
# Try reading the current flake
|
||||
if init is None:
|
||||
with contextlib.suppress(ClanCmdError):
|
||||
inventory = load_inventory_eval(flake)
|
||||
inventory = load_inventory_eval(directory)
|
||||
|
||||
if init is not None:
|
||||
inventory = init
|
||||
@@ -599,9 +596,9 @@ def init_inventory(flake: Flake, init: Inventory | None = None) -> None:
|
||||
# Write inventory.json file
|
||||
if inventory is not None:
|
||||
# Persist creates a commit message for each change
|
||||
set_inventory(inventory, flake, "Init inventory")
|
||||
set_inventory(inventory, directory, "Init inventory")
|
||||
|
||||
|
||||
@API.register
|
||||
def get_inventory(flake: Flake) -> Inventory:
|
||||
return load_inventory_eval(flake)
|
||||
def get_inventory(base_path: str | Path) -> Inventory:
|
||||
return load_inventory_eval(base_path)
|
||||
|
||||
@@ -110,7 +110,7 @@ def create_machine(opts: CreateOptions, commit: bool = True) -> None:
|
||||
new_machine["deploy"] = {"targetHost": target_host}
|
||||
|
||||
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
|
||||
|
||||
@@ -5,10 +5,9 @@ from pathlib import Path
|
||||
|
||||
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.dirs import specific_machine_dir
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.secrets.folders import sops_secrets_folder
|
||||
from clan_cli.secrets.machines import has_machine as secrets_has_machine
|
||||
from clan_cli.secrets.machines import remove_machine as secrets_machine_remove
|
||||
@@ -16,46 +15,49 @@ from clan_cli.secrets.secrets import (
|
||||
list_secrets,
|
||||
)
|
||||
|
||||
from .machines import Machine
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@API.register
|
||||
def delete_machine(machine: Machine) -> None:
|
||||
def delete_machine(flake: Flake, name: str) -> None:
|
||||
try:
|
||||
inventory.delete(machine.flake, {f"machines.{machine.name}"})
|
||||
inventory.delete(str(flake.path), {f"machines.{name}"})
|
||||
except KeyError as exc:
|
||||
# louis@(2025-03-09): test infrastructure does not seem to set the
|
||||
# inventory properly, but more importantly only one machine in my
|
||||
# personal clan ended up in the inventory for some reason, so I think
|
||||
# it makes sense to eat the exception here.
|
||||
log.warning(
|
||||
f"{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] = []
|
||||
|
||||
folder = specific_machine_dir(machine)
|
||||
folder = specific_machine_dir(flake.path, name)
|
||||
if folder.exists():
|
||||
changed_paths.append(folder)
|
||||
shutil.rmtree(folder)
|
||||
|
||||
# louis@(2025-02-04): clean-up legacy (pre-vars) secrets:
|
||||
sops_folder = sops_secrets_folder(machine.flake.path)
|
||||
filter_fn = lambda secret_name: secret_name.startswith(f"{machine.name}-")
|
||||
for secret_name in list_secrets(machine.flake.path, filter_fn):
|
||||
sops_folder = sops_secrets_folder(flake.path)
|
||||
filter_fn = lambda secret_name: secret_name.startswith(f"{name}-")
|
||||
for secret_name in list_secrets(flake.path, filter_fn):
|
||||
secret_path = sops_folder / secret_name
|
||||
changed_paths.append(secret_path)
|
||||
shutil.rmtree(secret_path)
|
||||
|
||||
machine = Machine(name, flake)
|
||||
changed_paths.extend(machine.public_vars_store.delete_store())
|
||||
changed_paths.extend(machine.secret_vars_store.delete_store())
|
||||
# Remove the machine's key, and update secrets & vars that referenced it:
|
||||
if secrets_has_machine(machine.flake.path, machine.name):
|
||||
secrets_machine_remove(machine.flake.path, machine.name)
|
||||
if secrets_has_machine(flake.path, name):
|
||||
secrets_machine_remove(flake.path, name)
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -7,10 +7,11 @@ from pathlib import Path
|
||||
|
||||
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.dirs import specific_machine_dir
|
||||
from clan_cli.errors import ClanCmdError, ClanError
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.git import commit_file
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.nix import nix_config, nix_eval
|
||||
@@ -25,35 +26,61 @@ class HardwareConfig(Enum):
|
||||
NIXOS_GENERATE_CONFIG = "nixos-generate-config"
|
||||
NONE = "none"
|
||||
|
||||
def config_path(self, machine: Machine) -> Path:
|
||||
machine_dir = specific_machine_dir(machine)
|
||||
def config_path(self, clan_dir: Path, machine_name: str) -> Path:
|
||||
machine_dir = specific_machine_dir(clan_dir, machine_name)
|
||||
if self == HardwareConfig.NIXOS_FACTER:
|
||||
return machine_dir / "facter.json"
|
||||
return machine_dir / "hardware-configuration.nix"
|
||||
|
||||
@classmethod
|
||||
def detect_type(cls: type["HardwareConfig"], machine: Machine) -> "HardwareConfig":
|
||||
hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path(machine)
|
||||
def detect_type(
|
||||
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():
|
||||
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.NONE
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
return HardwareConfig.detect_type(machine)
|
||||
return HardwareConfig.detect_type(clan_dir, machine_name)
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
@@ -61,13 +88,13 @@ def show_machine_hardware_platform(machine: Machine) -> str | None:
|
||||
system = config["system"]
|
||||
cmd = nix_eval(
|
||||
[
|
||||
f"{machine.flake}#clanInternals.machines.{system}.{machine.name}",
|
||||
f"{clan_dir}#clanInternals.machines.{system}.{machine_name}",
|
||||
"--apply",
|
||||
"machine: { inherit (machine.pkgs) system; }",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
proc = run(cmd, RunOpts(prefix=machine.name))
|
||||
proc = run_no_stdout(cmd, RunOpts(prefix=machine_name))
|
||||
res = proc.stdout.strip()
|
||||
|
||||
host_platform = json.loads(res)
|
||||
@@ -76,8 +103,11 @@ def show_machine_hardware_platform(machine: Machine) -> str | None:
|
||||
|
||||
@dataclass
|
||||
class HardwareGenerateOptions:
|
||||
machine: Machine
|
||||
flake: Flake
|
||||
machine: str
|
||||
backend: HardwareConfig
|
||||
target_host: str | None = None
|
||||
keyfile: 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.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
if opts.backend == HardwareConfig.NIXOS_FACTER:
|
||||
@@ -103,26 +139,26 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
|
||||
"--show-hardware-config",
|
||||
]
|
||||
|
||||
with machine.target_host() as host:
|
||||
host.ssh_options["StrictHostKeyChecking"] = "accept-new"
|
||||
host.ssh_options["UserKnownHostsFile"] = "/dev/null"
|
||||
if opts.password:
|
||||
host.password = opts.password
|
||||
host = machine.target_host
|
||||
host.ssh_options["StrictHostKeyChecking"] = "accept-new"
|
||||
host.ssh_options["UserKnownHostsFile"] = "/dev/null"
|
||||
if opts.password:
|
||||
host.password = opts.password
|
||||
|
||||
out = host.run(config_command, become_root=True, opts=RunOpts(check=False))
|
||||
if out.returncode != 0:
|
||||
if "nixos-facter" in out.stderr and "not found" in out.stderr:
|
||||
machine.error(str(out.stderr))
|
||||
msg = (
|
||||
"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."
|
||||
)
|
||||
raise ClanError(msg)
|
||||
|
||||
machine.error(str(out))
|
||||
msg = f"Failed to inspect {opts.machine}. Address: {host.target}"
|
||||
out = host.run(config_command, become_root=True, opts=RunOpts(check=False))
|
||||
if out.returncode != 0:
|
||||
if "nixos-facter" in out.stderr and "not found" in out.stderr:
|
||||
machine.error(str(out.stderr))
|
||||
msg = (
|
||||
"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."
|
||||
)
|
||||
raise ClanError(msg)
|
||||
|
||||
machine.error(str(out))
|
||||
msg = f"Failed to inspect {opts.machine}. Address: {host.target}"
|
||||
raise ClanError(msg)
|
||||
|
||||
backup_file = None
|
||||
if hw_file.exists():
|
||||
backup_file = hw_file.with_suffix(".bak")
|
||||
@@ -135,11 +171,11 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
|
||||
|
||||
commit_file(
|
||||
hw_file,
|
||||
opts.machine.flake.path,
|
||||
opts.flake.path,
|
||||
f"machines/{opts.machine}/{hw_file.name}: update hardware configuration",
|
||||
)
|
||||
try:
|
||||
show_machine_hardware_platform(opts.machine)
|
||||
show_machine_hardware_platform(opts.flake.path, opts.machine)
|
||||
if backup_file:
|
||||
backup_file.unlink(missing_ok=True)
|
||||
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:
|
||||
machine = Machine(
|
||||
flake=args.flake,
|
||||
name=args.machine,
|
||||
override_target_host=args.target_host,
|
||||
)
|
||||
opts = HardwareGenerateOptions(
|
||||
machine=machine,
|
||||
flake=args.flake,
|
||||
machine=args.machine,
|
||||
target_host=args.target_host,
|
||||
password=args.password,
|
||||
backend=HardwareConfig(args.backend),
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ class BuildOn(Enum):
|
||||
@dataclass
|
||||
class InstallOptions:
|
||||
machine: Machine
|
||||
target_host: str
|
||||
kexec: str | None = None
|
||||
debug: bool = False
|
||||
no_reboot: bool = False
|
||||
@@ -51,16 +52,17 @@ class InstallOptions:
|
||||
@API.register
|
||||
def install_machine(opts: InstallOptions) -> None:
|
||||
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_vars([machine])
|
||||
|
||||
with (
|
||||
TemporaryDirectory(prefix="nixos-install-") as _base_directory,
|
||||
machine.target_host() as host,
|
||||
):
|
||||
with TemporaryDirectory(prefix="nixos-install-") as _base_directory:
|
||||
base_directory = Path(_base_directory).resolve()
|
||||
activation_secrets = base_directory / "activation_secrets"
|
||||
upload_dir = activation_secrets / machine.secrets_upload_directory.lstrip("/")
|
||||
@@ -111,7 +113,11 @@ def install_machine(opts: InstallOptions) -> None:
|
||||
[
|
||||
"--generate-hardware-config",
|
||||
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:
|
||||
cmd += ["--build-on", opts.build_on.value]
|
||||
|
||||
if host.port:
|
||||
cmd += ["--ssh-port", str(host.port)]
|
||||
if h.port:
|
||||
cmd += ["--ssh-port", str(h.port)]
|
||||
if opts.kexec:
|
||||
cmd += ["--kexec", opts.kexec]
|
||||
|
||||
if opts.debug:
|
||||
cmd.append("--debug")
|
||||
cmd.append(host.target)
|
||||
cmd.append(h.target)
|
||||
if opts.use_tor:
|
||||
# nix copy does not support tor socks proxy
|
||||
# cmd.append("--ssh-option")
|
||||
@@ -158,32 +164,7 @@ def install_machine(opts: InstallOptions) -> None:
|
||||
def install_command(args: argparse.Namespace) -> None:
|
||||
host_key_check = HostKeyCheck.from_str(args.host_key_check)
|
||||
try:
|
||||
# Only if the caller did not specify a target_host via args.target_host
|
||||
# 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,
|
||||
)
|
||||
machine = Machine(name=args.machine, flake=args.flake, nix_options=args.option)
|
||||
use_tor = False
|
||||
|
||||
if machine._class_ == "darwin":
|
||||
@@ -194,16 +175,41 @@ def install_command(args: argparse.Namespace) -> None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
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:
|
||||
ask = input(
|
||||
f"Install {args.machine} to {machine.target_host_address}? [y/N] "
|
||||
)
|
||||
ask = input(f"Install {args.machine} to {target_host}? [y/N] ")
|
||||
if ask != "y":
|
||||
return None
|
||||
|
||||
return install_machine(
|
||||
InstallOptions(
|
||||
machine=machine,
|
||||
target_host=target_host,
|
||||
kexec=args.kexec,
|
||||
phases=args.phases,
|
||||
debug=args.debug,
|
||||
|
||||
@@ -2,7 +2,6 @@ import argparse
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
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.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.dirs import specific_machine_dir
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.errors import ClanCmdError, ClanError
|
||||
from clan_cli.inventory import (
|
||||
Machine,
|
||||
load_inventory_eval,
|
||||
patch_inventory_with,
|
||||
)
|
||||
from clan_cli.inventory.classes import Machine as InventoryMachine
|
||||
from clan_cli.machines.hardware import HardwareConfig
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.nix import nix_eval
|
||||
from clan_cli.nix import nix_eval, nix_shell
|
||||
from clan_cli.tags import list_nixos_machines_by_tags
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@API.register
|
||||
def set_machine(flake: Flake, machine_name: str, machine: InventoryMachine) -> None:
|
||||
patch_inventory_with(flake, f"machines.{machine_name}", dataclass_to_dict(machine))
|
||||
def set_machine(flake_url: Path, machine_name: str, machine: Machine) -> None:
|
||||
patch_inventory_with(
|
||||
flake_url, f"machines.{machine_name}", dataclass_to_dict(machine)
|
||||
)
|
||||
|
||||
|
||||
@API.register
|
||||
def list_machines(flake: Flake) -> dict[str, InventoryMachine]:
|
||||
inventory = load_inventory_eval(flake)
|
||||
def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
|
||||
inventory = load_inventory_eval(flake_url)
|
||||
return inventory.get("machines", {})
|
||||
|
||||
|
||||
@dataclass
|
||||
class MachineDetails:
|
||||
machine: InventoryMachine
|
||||
machine: Machine
|
||||
hw_config: HardwareConfig | None = None
|
||||
disk_schema: MachineDiskMatter | None = None
|
||||
|
||||
@@ -60,16 +59,16 @@ def extract_header(c: str) -> str:
|
||||
|
||||
|
||||
@API.register
|
||||
def get_machine_details(machine: Machine) -> MachineDetails:
|
||||
inventory = load_inventory_eval(machine.flake)
|
||||
machine_inv = inventory.get("machines", {}).get(machine.name)
|
||||
if machine_inv is None:
|
||||
msg = f"Machine {machine.name} not found in inventory"
|
||||
def get_inventory_machine_details(flake_url: Path, machine_name: str) -> MachineDetails:
|
||||
inventory = load_inventory_eval(flake_url)
|
||||
machine = inventory.get("machines", {}).get(machine_name)
|
||||
if machine is None:
|
||||
msg = f"Machine {machine_name} not found in inventory"
|
||||
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_path = machine_dir / "disko.nix"
|
||||
if disk_path.exists():
|
||||
@@ -80,9 +79,7 @@ def get_machine_details(machine: Machine) -> MachineDetails:
|
||||
if data:
|
||||
disk_schema = data # type: ignore
|
||||
|
||||
return MachineDetails(
|
||||
machine=machine_inv, hw_config=hw_config, disk_schema=disk_schema
|
||||
)
|
||||
return MachineDetails(machine=machine, hw_config=hw_config, disk_schema=disk_schema)
|
||||
|
||||
|
||||
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:
|
||||
res = proc.stdout.strip()
|
||||
@@ -109,36 +106,53 @@ def list_nixos_machines(flake_url: str | Path) -> list[str]:
|
||||
|
||||
@dataclass
|
||||
class ConnectionOptions:
|
||||
keyfile: str | None = None
|
||||
timeout: int = 2
|
||||
retries: int = 10
|
||||
|
||||
|
||||
from clan_cli.machines.machines import Machine
|
||||
|
||||
|
||||
@API.register
|
||||
def check_machine_online(
|
||||
machine: Machine, opts: ConnectionOptions | None = None
|
||||
flake_url: str | Path, machine_name: str, opts: ConnectionOptions | None
|
||||
) -> Literal["Online", "Offline"]:
|
||||
hostname = machine.target_host_address
|
||||
if not hostname:
|
||||
msg = f"Machine {machine.name} does not specify a targetHost"
|
||||
machine = load_inventory_eval(flake_url).get("machines", {}).get(machine_name)
|
||||
if not machine:
|
||||
msg = f"Machine {machine_name} not found in inventory"
|
||||
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):
|
||||
with machine.target_host() as target:
|
||||
res = target.run(
|
||||
["true"],
|
||||
RunOpts(timeout=timeout, check=False, needs_user_terminal=True),
|
||||
)
|
||||
if not hostname:
|
||||
msg = f"Machine {machine_name} does not specify a targetHost"
|
||||
raise ClanError(msg)
|
||||
|
||||
if res.returncode == 0:
|
||||
return "Online"
|
||||
time.sleep(timeout)
|
||||
timeout = opts.timeout if opts and opts.timeout else 20
|
||||
|
||||
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:
|
||||
|
||||
@@ -2,14 +2,12 @@ import importlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
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.facts import public_modules as facts_public_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
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class Machine:
|
||||
name: str
|
||||
flake: Flake
|
||||
@@ -147,37 +145,34 @@ class Machine:
|
||||
def flake_dir(self) -> Path:
|
||||
return self.flake.path
|
||||
|
||||
@contextmanager
|
||||
def target_host(self) -> Iterator[Host]:
|
||||
with parse_deployment_address(
|
||||
@property
|
||||
def target_host(self) -> Host:
|
||||
return parse_deployment_address(
|
||||
self.name,
|
||||
self.target_host_address,
|
||||
self.host_key_check,
|
||||
private_key=self.private_key,
|
||||
meta={"machine": self},
|
||||
) as target_host:
|
||||
yield target_host
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def build_host(self) -> Iterator[Host | None]:
|
||||
@property
|
||||
def build_host(self) -> Host:
|
||||
"""
|
||||
The host where the machine is built and deployed from.
|
||||
Can be the same as the target host.
|
||||
"""
|
||||
build_host = self.override_build_host or self.deployment.get("buildHost")
|
||||
if build_host is None:
|
||||
yield None
|
||||
return
|
||||
return self.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,
|
||||
build_host,
|
||||
self.host_key_check,
|
||||
forward_agent=True,
|
||||
private_key=self.private_key,
|
||||
meta={"machine": self},
|
||||
) as build_host:
|
||||
yield build_host
|
||||
meta={"machine": self, "target_host": self.target_host},
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def deploy_as_root(self) -> bool:
|
||||
@@ -188,7 +183,7 @@ class Machine:
|
||||
# however there is a soon to be merged PR that requires deployment
|
||||
# as root to match NixOS: https://github.com/nix-darwin/nix-darwin/pull/1341
|
||||
return json.loads(
|
||||
run(
|
||||
run_no_stdout(
|
||||
nix_eval(
|
||||
[
|
||||
f"{self.flake}#darwinConfigurations.{self.name}.options.system",
|
||||
|
||||
@@ -5,12 +5,11 @@ import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from contextlib import ExitStack
|
||||
|
||||
from clan_lib.api import API
|
||||
|
||||
from clan_cli.async_run import AsyncContext, AsyncOpts, AsyncRuntime, is_async_cancelled
|
||||
from clan_cli.cmd import Log, MsgColor, RunOpts, run
|
||||
from clan_cli.cmd import MsgColor, RunOpts, run
|
||||
from clan_cli.colors import AnsiColor
|
||||
from clan_cli.completions import (
|
||||
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.flake import Flake
|
||||
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.nix import nix_command, nix_config, nix_metadata
|
||||
from clan_cli.ssh.host import Host, HostKeyCheck
|
||||
from clan_cli.vars.generate import generate_vars
|
||||
from clan_cli.vars.upload import upload_secret_vars
|
||||
|
||||
from .inventory import get_all_machines, get_selected_machines
|
||||
|
||||
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())
|
||||
|
||||
flake_url = (
|
||||
@@ -110,41 +111,37 @@ def update_machines(base_path: str, machines: list[InventoryMachine]) -> None:
|
||||
flake = Flake(base_path)
|
||||
for machine in machines:
|
||||
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:
|
||||
msg = "Machine name is not set"
|
||||
raise ClanError(msg)
|
||||
m = Machine(
|
||||
name,
|
||||
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)
|
||||
|
||||
deploy_machines(group_machines)
|
||||
|
||||
|
||||
def deploy_machine(machine: Machine) -> None:
|
||||
with ExitStack() as stack:
|
||||
target_host = stack.enter_context(machine.target_host())
|
||||
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_machines(machines: list[Machine]) -> None:
|
||||
"""
|
||||
Deploy to all hosts in parallel
|
||||
"""
|
||||
|
||||
def deploy(machine: Machine) -> None:
|
||||
host = machine.build_host
|
||||
generate_facts([machine], service=None, regenerate=False)
|
||||
generate_vars([machine], generator_name=None, regenerate=False)
|
||||
|
||||
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 = [
|
||||
"--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
|
||||
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]
|
||||
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(
|
||||
switch_cmd,
|
||||
RunOpts(
|
||||
check=False,
|
||||
log=Log.BOTH,
|
||||
msg_color=MsgColor(stderr=AnsiColor.DEFAULT),
|
||||
needs_user_terminal=True,
|
||||
),
|
||||
extra_env=remote_env,
|
||||
RunOpts(check=False, msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
|
||||
extra_env=env,
|
||||
become_root=become_root,
|
||||
)
|
||||
|
||||
# Last output line (config store path) is printed to stdout instead of stderr
|
||||
lines = ret.stdout.splitlines()
|
||||
if lines:
|
||||
print(lines[-1])
|
||||
|
||||
if is_async_cancelled():
|
||||
return
|
||||
|
||||
@@ -206,27 +204,26 @@ def deploy_machine(machine: Machine) -> None:
|
||||
ret = host.run(
|
||||
test_cmd if is_mobile else switch_cmd,
|
||||
RunOpts(
|
||||
log=Log.BOTH,
|
||||
msg_color=MsgColor(stderr=AnsiColor.DEFAULT),
|
||||
needs_user_terminal=True,
|
||||
),
|
||||
extra_env=remote_env,
|
||||
extra_env=env,
|
||||
become_root=become_root,
|
||||
)
|
||||
|
||||
|
||||
def deploy_machines(machines: list[Machine]) -> None:
|
||||
"""
|
||||
Deploy to all hosts in parallel
|
||||
"""
|
||||
|
||||
with AsyncRuntime() as runtime:
|
||||
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(
|
||||
AsyncOpts(
|
||||
tid=machine.name, async_ctx=AsyncContext(prefix=machine.name)
|
||||
),
|
||||
deploy_machine,
|
||||
deploy,
|
||||
machine,
|
||||
)
|
||||
runtime.join_all()
|
||||
@@ -238,73 +235,61 @@ def update_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
|
||||
machines: list[Machine] = []
|
||||
# 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:
|
||||
machines = []
|
||||
if len(args.machines) == 1 and args.target_host is not None:
|
||||
machine = Machine(
|
||||
name=machine_name,
|
||||
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),
|
||||
name=args.machines[0], flake=args.flake, nix_options=args.option
|
||||
)
|
||||
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)
|
||||
|
||||
def filter_machine(m: Machine) -> bool:
|
||||
if m.deployment.get("requireExplicitUpdate", False):
|
||||
return False
|
||||
elif args.target_host is not None:
|
||||
print("target host can only be specified for a single machine")
|
||||
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:
|
||||
# check if the machine has a target host set
|
||||
m.target_host # noqa: B018
|
||||
except ClanError:
|
||||
return False
|
||||
if not machines and ignored_machines != []:
|
||||
print(
|
||||
"WARNING: No machines to update."
|
||||
"The following defined machines were ignored because they"
|
||||
"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
|
||||
implicit_all: bool = len(args.machines) == 0
|
||||
if implicit_all:
|
||||
machines_to_update = list(filter(filter_machine, machines))
|
||||
|
||||
# machines that are in the list but not included in the update list
|
||||
ignored_machines = {m.name for m in machines if m not in machines_to_update}
|
||||
|
||||
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)
|
||||
config = nix_config()
|
||||
system = config["system"]
|
||||
machine_names = [machine.name for machine in machines]
|
||||
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",
|
||||
]
|
||||
)
|
||||
|
||||
deploy_machines(machines)
|
||||
except KeyboardInterrupt:
|
||||
log.warning("Interrupted by user")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.cmd import run, run_no_stdout
|
||||
from clan_cli.dirs import nixpkgs_flake, nixpkgs_source
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.locked_open import locked_open
|
||||
@@ -56,7 +55,7 @@ def nix_add_to_gcroots(nix_path: Path, dest: Path) -> None:
|
||||
@cache
|
||||
def nix_config() -> dict[str, Any]:
|
||||
cmd = nix_command(["config", "show", "--json"])
|
||||
proc = run(cmd)
|
||||
proc = run_no_stdout(cmd)
|
||||
data = json.loads(proc.stdout)
|
||||
config = {}
|
||||
for key, value in data.items():
|
||||
@@ -132,16 +131,7 @@ class Packages:
|
||||
cls.static_packages = set(
|
||||
os.environ.get("CLAN_PROVIDED_PACKAGES", "").split(":")
|
||||
)
|
||||
|
||||
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
|
||||
return program in cls.static_packages
|
||||
|
||||
|
||||
# Features:
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"age-plugin-sss",
|
||||
"age-plugin-tpm",
|
||||
"age-plugin-yubikey",
|
||||
"age-plugin-1p",
|
||||
"avahi",
|
||||
"bash",
|
||||
"bubblewrap",
|
||||
|
||||
@@ -5,11 +5,11 @@ import os
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import types
|
||||
import errno
|
||||
import stat
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from shlex import quote
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.cmd import CmdOut, RunOpts, run
|
||||
@@ -40,29 +40,35 @@ class Host:
|
||||
ssh_options: dict[str, str] = field(default_factory=dict)
|
||||
tor_socks: bool = False
|
||||
|
||||
_temp_dir: TemporaryDirectory | None = None
|
||||
|
||||
def __enter__(self) -> "Host":
|
||||
self._temp_dir = TemporaryDirectory(prefix="clan-ssh-")
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: types.TracebackType | None,
|
||||
) -> None:
|
||||
def setup_control_master(self) -> None:
|
||||
home = Path.home()
|
||||
if not home.exists():
|
||||
return
|
||||
control_path = home / ".ssh"
|
||||
try:
|
||||
if self._temp_dir:
|
||||
self._temp_dir.cleanup()
|
||||
except OSError:
|
||||
pass
|
||||
if not stat.S_ISDIR(control_path.stat().st_mode):
|
||||
return
|
||||
except OSError as e:
|
||||
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:
|
||||
if not self.command_prefix:
|
||||
self.command_prefix = self.host
|
||||
if not self.user:
|
||||
self.user = "root"
|
||||
self.setup_control_master()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.target
|
||||
@@ -182,17 +188,15 @@ class Host:
|
||||
# Run the ssh command
|
||||
return run(ssh_cmd, opts)
|
||||
|
||||
def nix_ssh_env(
|
||||
self, env: dict[str, str] | None, local_ssh: bool = True
|
||||
) -> dict[str, str]:
|
||||
def nix_ssh_env(self, env: dict[str, str] | None) -> dict[str, str]:
|
||||
if env is None:
|
||||
env = {}
|
||||
env["NIX_SSHOPTS"] = " ".join(self.ssh_cmd_opts(local_ssh=local_ssh))
|
||||
env["NIX_SSHOPTS"] = " ".join(self.ssh_cmd_opts)
|
||||
return env
|
||||
|
||||
@property
|
||||
def ssh_cmd_opts(
|
||||
self,
|
||||
local_ssh: bool = True,
|
||||
) -> list[str]:
|
||||
ssh_opts = ["-A"] if self.forward_agent else []
|
||||
if self.port:
|
||||
@@ -206,16 +210,6 @@ class Host:
|
||||
if 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
|
||||
|
||||
def ssh_cmd(
|
||||
@@ -233,7 +227,7 @@ class Host:
|
||||
self.password,
|
||||
]
|
||||
|
||||
ssh_opts = self.ssh_cmd_opts()
|
||||
ssh_opts = self.ssh_cmd_opts
|
||||
if verbose_ssh or self.verbose_ssh:
|
||||
ssh_opts.extend(["-v"])
|
||||
if tty:
|
||||
|
||||
@@ -63,8 +63,7 @@ def upload(
|
||||
for mdir in dirs:
|
||||
dir_path = Path(root) / mdir
|
||||
tarinfo = tar.gettarinfo(
|
||||
dir_path,
|
||||
arcname=str(dir_path.relative_to(str(local_src))),
|
||||
dir_path, arcname=str(dir_path.relative_to(str(local_src)))
|
||||
)
|
||||
tarinfo.mode = dir_mode
|
||||
tarinfo.uname = file_user
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.cmd import RunOpts, run
|
||||
from clan_cli.cmd import RunOpts, run_no_stdout
|
||||
from clan_cli.completions import (
|
||||
add_dynamic_completer,
|
||||
complete_machines,
|
||||
@@ -32,7 +32,7 @@ def list_state_folders(machine: Machine, service: None | str = None) -> None:
|
||||
res = "{}"
|
||||
|
||||
try:
|
||||
proc = run(cmd, RunOpts(prefix=machine.name))
|
||||
proc = run_no_stdout(cmd, opts=RunOpts(prefix=machine.name))
|
||||
res = proc.stdout.strip()
|
||||
except ClanCmdError as e:
|
||||
msg = "Clan might not have meta attributes"
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.cmd import run_no_stdout
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.nix import nix_eval
|
||||
|
||||
@@ -18,7 +18,7 @@ def list_tagged_machines(flake_url: str | Path) -> dict[str, Any]:
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
proc = run(cmd)
|
||||
proc = run_no_stdout(cmd)
|
||||
|
||||
try:
|
||||
res = proc.stdout.strip()
|
||||
|
||||
@@ -10,15 +10,8 @@ from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
from clan_cli.dirs import (
|
||||
TemplateType,
|
||||
clan_templates,
|
||||
nixpkgs_source,
|
||||
specific_machine_dir,
|
||||
)
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.dirs import TemplateType, clan_templates, nixpkgs_source
|
||||
from clan_cli.locked_open import locked_open
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.nix import nix_test_store
|
||||
from clan_cli.tests import age_keys
|
||||
from clan_cli.tests.fixture_error import FixtureError
|
||||
@@ -77,10 +70,11 @@ class FlakeForTest(NamedTuple):
|
||||
|
||||
|
||||
def set_machine_settings(
|
||||
machine: Machine,
|
||||
flake: Path,
|
||||
machine_name: str,
|
||||
machine_settings: dict,
|
||||
) -> 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))
|
||||
|
||||
|
||||
@@ -208,8 +202,7 @@ class ClanFlake:
|
||||
}}
|
||||
"""
|
||||
)
|
||||
machine = Machine(name=machine_name, flake=Flake(str(self.path)))
|
||||
set_machine_settings(machine, machine_config)
|
||||
set_machine_settings(self.path, machine_name, machine_config)
|
||||
sp.run(["git", "add", "."], cwd=self.path, check=True)
|
||||
sp.run(
|
||||
["git", "commit", "-a", "-m", "Update by flake generator"],
|
||||
|
||||
@@ -27,14 +27,6 @@ def test_root() -> Path:
|
||||
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")
|
||||
def clan_core() -> Path:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import pytest
|
||||
from clan_cli.flake import Flake
|
||||
from clan_cli.inventory import load_inventory_json
|
||||
from clan_cli.secrets.folders import sops_machines_folder
|
||||
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 "service" not in inventory
|
||||
|
||||
@@ -41,7 +40,7 @@ def test_machine_subcommands(
|
||||
["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 "service" not in inventory_2
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from clan_cli.inventory import (
|
||||
set_inventory,
|
||||
)
|
||||
from clan_cli.machines.create import CreateOptions, create_machine
|
||||
from clan_cli.nix import nix_eval, run
|
||||
from clan_cli.nix import nix_eval, run_no_stdout
|
||||
from clan_cli.tests.fixtures_flakes import FlakeForTest
|
||||
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
|
||||
modules_info = list_modules(str(base_path))
|
||||
|
||||
assert "localModules" in modules_info
|
||||
assert "modulesPerSource" in modules_info
|
||||
assert len(modules_info.items()) > 1
|
||||
# Random test for those two modules
|
||||
assert "borgbackup" in modules_info
|
||||
assert "syncthing" in modules_info
|
||||
|
||||
|
||||
@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 = [
|
||||
@@ -120,7 +122,7 @@ def test_add_module_to_inventory(
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
proc = run(cmd)
|
||||
proc = run_no_stdout(cmd)
|
||||
res = json.loads(proc.stdout.strip())
|
||||
|
||||
assert res["machine1"]["authorizedKeys"] == [ssh_key.decode()]
|
||||
|
||||
@@ -12,12 +12,7 @@ from clan_cli.tests.age_keys import SopsSetup
|
||||
from clan_cli.tests.fixtures_flakes import ClanFlake
|
||||
from clan_cli.tests.helpers import cli
|
||||
from clan_cli.vars.check import check_vars
|
||||
from clan_cli.vars.generate import (
|
||||
Generator,
|
||||
generate_vars_for_machine,
|
||||
generate_vars_for_machine_interactive,
|
||||
get_generators_closure,
|
||||
)
|
||||
from clan_cli.vars.generate import Generator, generate_vars_for_machine_interactive
|
||||
from clan_cli.vars.get import get_var
|
||||
from clan_cli.vars.graph import all_missing_closure, requested_closure
|
||||
from clan_cli.vars.list import stringify_all_vars
|
||||
@@ -645,6 +640,9 @@ def test_api_set_prompts(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
flake: ClanFlake,
|
||||
) -> None:
|
||||
from clan_cli.vars._types import GeneratorUpdate
|
||||
from clan_cli.vars.list import get_generators, set_prompts
|
||||
|
||||
config = flake.machines["my_machine"]
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||
@@ -654,39 +652,33 @@ def test_api_set_prompts(
|
||||
flake.refresh()
|
||||
|
||||
monkeypatch.chdir(flake.path)
|
||||
params = {"machine_name": "my_machine", "base_dir": str(flake.path)}
|
||||
|
||||
generate_vars_for_machine(
|
||||
machine_name="my_machine",
|
||||
base_dir=flake.path,
|
||||
generators=["my_generator"],
|
||||
all_prompt_values={
|
||||
"my_generator": {
|
||||
"prompt1": "input1",
|
||||
}
|
||||
},
|
||||
set_prompts(
|
||||
**params,
|
||||
updates=[
|
||||
GeneratorUpdate(
|
||||
generator="my_generator",
|
||||
prompt_values={"prompt1": "input1"},
|
||||
)
|
||||
],
|
||||
)
|
||||
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
|
||||
store = in_repo.FactStore(machine)
|
||||
assert store.exists(Generator("my_generator"), "prompt1")
|
||||
assert store.get(Generator("my_generator"), "prompt1").decode() == "input1"
|
||||
generate_vars_for_machine(
|
||||
machine_name="my_machine",
|
||||
base_dir=flake.path,
|
||||
generators=["my_generator"],
|
||||
all_prompt_values={
|
||||
"my_generator": {
|
||||
"prompt1": "input2",
|
||||
}
|
||||
},
|
||||
set_prompts(
|
||||
**params,
|
||||
updates=[
|
||||
GeneratorUpdate(
|
||||
generator="my_generator",
|
||||
prompt_values={"prompt1": "input2"},
|
||||
)
|
||||
],
|
||||
)
|
||||
assert store.get(Generator("my_generator"), "prompt1").decode() == "input2"
|
||||
|
||||
generators = get_generators_closure(
|
||||
machine_name="my_machine",
|
||||
base_dir=flake.path,
|
||||
regenerate=True,
|
||||
include_previous_values=True,
|
||||
)
|
||||
generators = get_generators(**params)
|
||||
assert len(generators) == 1
|
||||
assert generators[0].name == "my_generator"
|
||||
assert generators[0].prompts[0].name == "prompt1"
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.machines import machines
|
||||
from clan_cli.ssh.host import Host
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .generate import Generator, Var
|
||||
@@ -184,5 +183,5 @@ class StoreBase(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def upload(self, host: Host, phases: list[str]) -> None:
|
||||
def upload(self, phases: list[str]) -> None:
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
@@ -87,11 +86,6 @@ class Generator:
|
||||
def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
|
||||
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
|
||||
return nix_shell(
|
||||
[
|
||||
@@ -103,7 +97,6 @@ def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
|
||||
"--unshare-all",
|
||||
"--tmpfs", "/",
|
||||
"--ro-bind", "/nix/store", "/nix/store",
|
||||
"--ro-bind", "/bin/sh", "/bin/sh",
|
||||
*(["--ro-bind", str(test_store), str(test_store)] if test_store else []),
|
||||
"--dev", "/dev",
|
||||
# not allowed to bind procfs in some sandboxes
|
||||
@@ -116,8 +109,8 @@ def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
|
||||
"--uid", "1000",
|
||||
"--gid", "1000",
|
||||
"--",
|
||||
str(real_bash_path), "-c", generator
|
||||
]
|
||||
"bash", "-c", generator
|
||||
],
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
@@ -295,28 +288,10 @@ def _ask_prompts(
|
||||
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(
|
||||
machine: "Machine",
|
||||
generator_name: str | None,
|
||||
regenerate: bool,
|
||||
include_previous_values: bool = False,
|
||||
) -> list[Generator]:
|
||||
from .graph import all_missing_closure, full_closure
|
||||
|
||||
@@ -329,24 +304,14 @@ def get_closure(
|
||||
for generator in vars_generators:
|
||||
generator.machine(machine)
|
||||
|
||||
result_closure = []
|
||||
if generator_name is None: # all generators selected
|
||||
if regenerate:
|
||||
result_closure = full_closure(generators)
|
||||
else:
|
||||
result_closure = all_missing_closure(generators)
|
||||
return full_closure(generators)
|
||||
return all_missing_closure(generators)
|
||||
# specific generator selected
|
||||
elif regenerate:
|
||||
result_closure = requested_closure([generator_name], generators)
|
||||
else:
|
||||
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
|
||||
if regenerate:
|
||||
return requested_closure([generator_name], generators)
|
||||
return minimal_closure([generator_name], generators)
|
||||
|
||||
|
||||
@API.register
|
||||
@@ -354,7 +319,6 @@ def get_generators_closure(
|
||||
machine_name: str,
|
||||
base_dir: Path,
|
||||
regenerate: bool = False,
|
||||
include_previous_values: bool = False,
|
||||
) -> list[Generator]:
|
||||
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))),
|
||||
generator_name=None,
|
||||
regenerate=regenerate,
|
||||
include_previous_values=include_previous_values,
|
||||
)
|
||||
|
||||
|
||||
def _generate_vars_for_machine(
|
||||
machine: "Machine",
|
||||
generators: list[Generator],
|
||||
all_prompt_values: dict[str, dict[str, str]],
|
||||
all_prompt_values: dict[str, dict],
|
||||
no_sandbox: bool = False,
|
||||
) -> bool:
|
||||
for generator in generators:
|
||||
@@ -381,7 +344,7 @@ def _generate_vars_for_machine(
|
||||
generator=generator,
|
||||
secret_vars_store=machine.secret_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,
|
||||
)
|
||||
return True
|
||||
@@ -390,20 +353,19 @@ def _generate_vars_for_machine(
|
||||
@API.register
|
||||
def generate_vars_for_machine(
|
||||
machine_name: str,
|
||||
generators: list[str],
|
||||
generators: list[Generator],
|
||||
all_prompt_values: dict[str, dict[str, str]],
|
||||
base_dir: Path,
|
||||
no_sandbox: bool = False,
|
||||
) -> bool:
|
||||
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(
|
||||
machine=machine,
|
||||
generators=generators_,
|
||||
machine=Machine(
|
||||
name=machine_name,
|
||||
flake=Flake(str(base_dir)),
|
||||
),
|
||||
generators=generators,
|
||||
all_prompt_values=all_prompt_values,
|
||||
no_sandbox=no_sandbox,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from pathlib import Path
|
||||
|
||||
from clan_cli.errors import ClanError
|
||||
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.generate import Generator, Var
|
||||
|
||||
@@ -73,6 +72,6 @@ class FactStore(StoreBase):
|
||||
msg = "populate_dir is not implemented for public vars stores"
|
||||
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"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
@@ -6,7 +6,6 @@ from pathlib import Path
|
||||
from clan_cli.dirs import vm_state_dir
|
||||
from clan_cli.errors import ClanError
|
||||
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.generate import Generator, Var
|
||||
|
||||
@@ -70,6 +69,6 @@ class FactStore(StoreBase):
|
||||
msg = "populate_dir is not implemented for public vars stores"
|
||||
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"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
@@ -3,7 +3,6 @@ import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
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.generate import Generator, Var
|
||||
|
||||
@@ -46,6 +45,6 @@ class SecretStore(StoreBase):
|
||||
shutil.copytree(self.dir, output_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"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
@@ -10,7 +10,6 @@ from tempfile import TemporaryDirectory
|
||||
from clan_cli.cmd import CmdOut, Log, RunOpts, run
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.nix import nix_shell
|
||||
from clan_cli.ssh.host import Host
|
||||
from clan_cli.ssh.upload import upload
|
||||
from clan_cli.vars._types import StoreBase
|
||||
from clan_cli.vars.generate import Generator, Var
|
||||
@@ -147,9 +146,9 @@ class SecretStore(StoreBase):
|
||||
manifest += hashes
|
||||
return b"\n".join(manifest)
|
||||
|
||||
def needs_upload(self, host: Host) -> bool:
|
||||
def needs_upload(self) -> bool:
|
||||
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
|
||||
[
|
||||
"cat",
|
||||
@@ -225,11 +224,11 @@ class SecretStore(StoreBase):
|
||||
|
||||
(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:
|
||||
msg = "Cannot upload partitioning secrets"
|
||||
raise NotImplementedError(msg)
|
||||
if not self.needs_upload(host):
|
||||
if not self.needs_upload():
|
||||
log.info("Secrets already uploaded")
|
||||
return
|
||||
with TemporaryDirectory(prefix="vars-upload-") as _tempdir:
|
||||
@@ -238,4 +237,4 @@ class SecretStore(StoreBase):
|
||||
upload_dir = Path(
|
||||
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
Reference in New Issue
Block a user