Merge pull request 'clan machines generations' (#4848) from Qubasa/clan-core:add_generate_cli into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4848
This commit is contained in:
Luis Hebendanz
2025-09-19 23:30:19 +00:00
20 changed files with 573 additions and 106 deletions

View File

@@ -10,15 +10,15 @@
{ lib, ... }:
{
options.allowAllInterfaces = lib.mkOption {
type = lib.types.bool;
default = false;
description = "If true, Telegraf will listen on all interfaces. Otherwise, it will only listen on the interfaces specified in `interfaces`";
type = lib.types.nullOr lib.types.bool;
default = null;
description = "Deprecated. Has no effect.";
};
options.interfaces = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "zt+" ];
description = "List of interfaces to expose the metrics to";
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = "Deprecated. Has no effect.";
};
};
};

View File

@@ -14,30 +14,51 @@
auth_user = "prometheus";
in
{
warnings =
lib.optionals (settings.allowAllInterfaces != null) [
"monitoring.settings.allowAllInterfaces is deprecated and and has no effect. Please remove it from your inventory."
"The monitoring service will now always listen on all interfaces over https."
]
++ (lib.optionals (settings.interfaces != null) [
"monitoring.settings.interfaces is deprecated and and has no effect. Please remove it from your inventory."
"The monitoring service will now always listen on all interfaces over https."
]);
networking.firewall.interfaces = lib.mkIf (settings.allowAllInterfaces == false) (
builtins.listToAttrs (
map (name: {
inherit name;
value.allowedTCPPorts = [
9273
9990
];
}) settings.interfaces
)
);
networking.firewall.allowedTCPPorts = lib.mkIf (settings.allowAllInterfaces == true) [
networking.firewall.allowedTCPPorts = [
9273
9990
];
clan.core.vars.generators."telegraf" = {
clan.core.vars.generators."telegraf-certs" = {
files.crt = {
restartUnits = [ "telegraf.service" ];
deploy = true;
secret = false;
};
files.key = {
mode = "0600";
restartUnits = [ "telegraf.service" ];
};
runtimeInputs = [
pkgs.openssl
];
script = ''
openssl req -x509 -nodes -newkey rsa:4096 \
-keyout "$out"/key \
-out "$out"/crt \
-subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com"
'';
};
clan.core.vars.generators."telegraf" = {
files.password.restartUnits = [ "telegraf.service" ];
files.password-env.restartUnits = [ "telegraf.service" ];
files.miniserve-auth.restartUnits = [ "telegraf.service" ];
dependencies = [ "telegraf-certs" ];
runtimeInputs = [
pkgs.coreutils
pkgs.xkcdpass
@@ -60,16 +81,33 @@
serviceConfig = {
LoadCredential = [
"auth_file_path:${config.clan.core.vars.generators.telegraf.files.miniserve-auth.path}"
"telegraf_crt_path:${config.clan.core.vars.generators.telegraf-certs.files.crt.path}"
"telegraf_key_path:${config.clan.core.vars.generators.telegraf-certs.files.key.path}"
];
Environment = [
"AUTH_FILE_PATH=%d/auth_file_path"
"CRT_PATH=%d/telegraf_crt_path"
"KEY_PATH=%d/telegraf_key_path"
];
Restart = "on-failure";
User = "telegraf";
Group = "telegraf";
RuntimeDirectory = "telegraf-www";
};
script = "${pkgs.miniserve}/bin/miniserve -p 9990 /run/telegraf-www --auth-file \"$AUTH_FILE_PATH\"";
script = "${pkgs.miniserve}/bin/miniserve -p 9990 /run/telegraf-www --auth-file \"$AUTH_FILE_PATH\" --tls-cert \"$CRT_PATH\" --tls-key \"$KEY_PATH\"";
};
systemd.services.telegraf = {
serviceConfig = {
LoadCredential = [
"telegraf_crt_path:${config.clan.core.vars.generators.telegraf-certs.files.crt.path}"
"telegraf_key_path:${config.clan.core.vars.generators.telegraf-certs.files.key.path}"
];
Environment = [
"CRT_PATH=%d/telegraf_crt_path"
"KEY_PATH=%d/telegraf_key_path"
];
};
};
services.telegraf = {
@@ -77,6 +115,7 @@
environmentFiles = [
(builtins.toString config.clan.core.vars.generators.telegraf.files.password-env.path)
];
extraConfig = {
agent.interval = "60s";
inputs = {
@@ -112,6 +151,8 @@
metric_version = 2;
basic_username = "${auth_user}";
basic_password = "$${BASIC_AUTH_PWD}";
tls_cert = "$${CRT_PATH}";
tls_key = "$${KEY_PATH}";
};
outputs.file = {

View File

@@ -1,4 +1,4 @@
{ packages, pkgs, ... }:
{ ... }:
{
name = "monitoring";
@@ -14,9 +14,6 @@
module.input = "self";
roles.telegraf.machines.peer1 = { };
roles.telegraf.settings = {
allowAllInterfaces = true;
};
};
};
};
@@ -28,6 +25,8 @@
services.telegraf.extraConfig = {
agent.interval = lib.mkForce "1s";
outputs.prometheus_client = {
# BUG: We have to disable basic auth here because the prometheus_client
# output plugin will otherwise deadlock Telegraf on startup.
basic_password = lib.mkForce "";
basic_username = lib.mkForce "";
};
@@ -35,17 +34,16 @@
};
};
extraPythonPackages = _p: [
(pkgs.python3.pkgs.toPythonModule packages.${pkgs.system}.clan-cli)
];
# !!! ANY CHANGES HERE MUST BE REFLECTED IN:
# clan_lib/metrics/telegraf.py::get_metrics
testScript =
{ ... }:
{ nodes, ... }:
''
import time
import os
import sys
import subprocess
import ssl
import json
import shlex
import urllib.request
@@ -54,45 +52,44 @@
peer1.wait_for_unit("network-online.target")
peer1.wait_for_unit("telegraf.service")
peer1.wait_for_unit("telegraf-json.service")
peer1.succeed("curl http://localhost:9990/telegraf.json")
peer1.succeed("curl http://localhost:9273/metrics")
# Fetch the basic auth password from the secret file
password = peer1.succeed("cat /var/run/secrets/vars/telegraf/password")
url = f"http://192.168.1.1:9990/telegraf.json"
password = peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.telegraf.files.password.path}").strip()
credentials = f"prometheus:{password}"
print("Using credentials:", credentials)
time.sleep(10) # wait a bit for telegraf to collect some data
# Fetch the json output from miniserve
print("Using credentials:", credentials)
peer1.succeed(f"curl -k -u {credentials} https://localhost:9990/telegraf.json")
peer1.succeed(f"curl -k -u {credentials} https://localhost:9273/metrics")
cert_path = "${nodes.peer1.clan.core.vars.generators.telegraf-certs.files.crt.path}"
url = "https://192.168.1.1:9990/telegraf.json" # HTTPS required
print("Waiting for /var/run/telegraf-www/telegraf.json to be bigger then 200 bytes")
peer1.wait_until_succeeds(f"test \"$(stat -c%s /var/run/telegraf-www/telegraf.json)\" -ge 200", timeout=30)
encoded_credentials = b64encode(credentials.encode("utf-8")).decode("utf-8")
headers = {"Authorization": f"Basic {encoded_credentials}"}
req = urllib.request.Request(url, headers=headers) # noqa: S310
response = urllib.request.urlopen(req)
# Look for the nixos_systems metric in the json output
# Trust the provided CA/server certificate
context = ssl.create_default_context(cafile=cert_path)
context.check_hostname = False
context.verify_mode = ssl.CERT_REQUIRED
found_system = False
for line in response:
line_str = line.decode("utf-8").strip()
line = json.loads(line_str)
if line["name"] == "nixos_systems":
found_system = True
print("Found nixos_systems metric in json output")
break
assert found_system, "nixos_systems metric not found in json output"
with urllib.request.urlopen(req, context=context, timeout=5) as response:
for raw_line in response:
line_str = raw_line.decode("utf-8").strip()
if not line_str:
continue
obj = json.loads(line_str)
if obj.get("name") == "nixos_systems":
found_system = True
print("Found nixos_systems metric in json output")
break
# TODO: I would like to test the python code here but it's not working yet
# Missing: I need a way to get the encrypted var from the clan
#from clan_lib.metrics.version import get_nixos_systems
#from clan_lib.machines.machines import Machine as ClanMachine
#from clan_lib.flake import Flake
#from clan_lib.ssh.remote import Remote
#target_host = Remote("peer1", "192.168.1.1")
#machine = ClanMachine("peer1", flake=Flake("${./.}"))
# data = get_nixos_systems(mymachine, target_host)
# assert data["current_system"] is not None
assert found_system, "nixos_systems metric not found in json output"
'';
}

View File

@@ -1,6 +1,6 @@
[
{
"publickey": "age18vspwr3de7jp0awyu66kk9psd5x4sy9suv0zt7ux2kqw0s6h2ueqwkgjxm",
"publickey": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"type": "age"
}
]

View File

@@ -1,14 +1,14 @@
{
"data": "ENC[AES256_GCM,data:raon/RshBCUAHgav5phuv5ZrXQDed03F/ZdjW6iUyj3UJJ/mYdkSdBmmYwhFh32Yluqyy9bUajHoEgDQqV27IE567o9M5YqmnVw=,iv:rpyv2/fAAg77JLyOUeWGkTmd8TYIrrb8pS89/AjSc+4=,tag:WA+UaqrhEhcbg6K21JgAsA==,type:str]",
"data": "ENC[AES256_GCM,data:ACFpRJRDIgVPurZwHYW0J1MnvyuiRGnXMeQj1nb9rDAIqHbZzZk8+E0Nu1+EdXwk78ziP6tHR1GQP2ILTtpLME4lXXRVjouW5Eo=,iv:ctR1HENO3XGIq1/gzYi47nateYzsSK317EKn92ptqDI=,tag:q1yuk/ZMx3nuORkiT/XXqg==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4SzVMVHhUMzAzcFdTSjEx\nRWZabEZEZzQzUzJuZkVYSzZBcFFYM3JaVmpBCm9xY1pJdUdNWGRaMFprdXhkdysw\nZXh4Q1dva1lMbmtVT2MzNGhQWEd1cUUKLS0tIFZhM1NjYnA1SldmcTFwQnZYZkpF\nVmtzbGIyL3JQbklPVk5ESFhHcWtPWUEKf04KPkSts5hiF4uImepznlfzbkb48YD4\nbtQ3toBSzW0wUnbxEHfA9nmuZFb6DF6majNCd1pVVr02c0MMinxVPw==\n-----END AGE ENCRYPTED FILE-----\n"
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvMUtabnp3V0dzNFFYRzk0\nd0ZJbUtDMXRPRGxpRjhYR1MyQzdJYWdJTUFrCjBNV0pPTTlIOHBBbzlEQkFzVy92\ndENxcDdIZlNDSm1oZTNveUtIeVc3MXcKLS0tIGtocENjMFNYT0s1LzhYNy92QU5G\nREVEdjErb0xPSE1yb0g5bGlackh6bEUKwxBoDteD7+JfnlFF71CHx4oEdV/TFYcF\n3JPYUbTWAIyMtUu/CLbX+Pn9Mv+McrEIqhwT7TWL/YbELKVadX/k5Q==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-08-20T19:56:05Z",
"mac": "ENC[AES256_GCM,data:/P15LdZuAUOrKagmmq2+EZ+70GZ5sGT/QbkPrOw/t1P281kFQXTL/OjXEyQcLUMSYElPjK+qEdDuIN4hsh24lOEe2ChJ/xEo/Gm0NvPRwb3CeIsfYs7mdOAVitUgc0q5kjpEBvljaGwN3SUvVD15I7jZkgY2BkVHXHTRxueKcvw=,iv:dmb7qnNR6hKBDHANwS2LHXdJrVBicdsOoJ42uhYd9Fo=,tag:OiHWr4nCK+9BvKnn/2LlBQ==,type:str]",
"lastmodified": "2025-09-18T14:33:37Z",
"mac": "ENC[AES256_GCM,data:4631iJmioJ2vZ2PTFbdEJu7UqtyQbp43XBlgEbFAviGZdugb3weVI24rJ8m1Rdnxq8uciEeiX6YHBhURdWQY4JNm2wTGnjz7e2PwQ8FCwOmxCcIQPpdKKsziq/M4HArgD66eUxIWfTt1yJfHgBcUuuANbrbH8MirllT+hJTBhqE=,iv:rM8a/MpKbK7DlqjuR4BG77XDHLK11Q+E2rzZLDJalhk=,tag:bbGMn4anXrLHg4eLA0/CXA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}

View File

@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFuTCCA6GgAwIBAgIUMXnA00bMrYvYSq0PjU5/HhXTpmcwDQYJKoZIhvcNAQEL
BQAwbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMRUwEwYDVQQKDAxFeGFtcGxlIENvcnAxCzAJBgNVBAsMAklUMRQwEgYD
VQQDDAtleGFtcGxlLmNvbTAeFw0yNTA5MTgxNDMzMzZaFw0yNTEwMTgxNDMzMzZa
MGwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5j
aXNjbzEVMBMGA1UECgwMRXhhbXBsZSBDb3JwMQswCQYDVQQLDAJJVDEUMBIGA1UE
AwwLZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC7
sdy27E/XMAyKrgeFcXY70R/vX0gx6EcZlWGp2vZSUVAfW1ni/Vq/LVC02sxGEGwv
10+42yP2yghi89doKo8oCoLsbVu+Pi+TmRsgAijy4jN8pHqbn9/Vk8M8utLa1u4z
VonSIx9pzCYd2+IIdwVuWoyPAAnK/JIKS3n0A8KWkZ/1lq6YDl2whj8iY4YF2Ekg
M0SWhquLZiaApAs7STTYvcP7iLfL4U6cH65dRAbwWMpMErPuLf/CedkXiSUp8Zqx
YIXXE5lf7wqt7tM6k6BHic9FEzAo1HnBWBXV5eB5fs1lX9M1VPmx43XINCfzKwxE
xODtIBrmvj+qOp6/ihBsu3LlOoOikxmL+T9Wgvf7fOuFC4BgmX85mGUV+EMZCDoJ
44jlwFF8wgrfG/ZawkP+opNsQLsdOm9DbAdWpx5+JYdgWBahjxuH4z2eIiBmMKgj
puqDgXdZzcERiYtOEEn0p0tvIkVLO3Tm2GjtHbmg1yF2nwsZjupGfcOGTVX4Zi5x
ZCs7vYgBtZy96kNAuyZcFl8eBUr/oVg//i3Zc9Vnw/UJryB7I6dvj228hlrSz0Ve
pGoeZXbcCzRv8NX2V0V1VTtrblSA3w5WRxVzK7UAVetPZ4dlJX+eyx3x2wiC3TiW
ZYH8haFubQqr1h9oXFHgDE5xYZKr51T3SRGfpn6KvQIDAQABo1MwUTAdBgNVHQ4E
FgQUJHOErJYWaGdla1XhxWha4XBKFYgwHwYDVR0jBBgwFoAUJHOErJYWaGdla1Xh
xWha4XBKFYgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXqcg
DW6qzFccR+JTqNR5HBOneB07LxaUqfBTAzU5GTRljY3mVpnTa6vVvXlStChqdmwU
JJdRhWzTpzE4K92l4UKiYKy486PT1ff34aPLPX5BB9OzL4dgvC3gO0MYDJ84AFZl
6BN/MRTinioG+s14SsxmgcUTl+HXsxt75r3WKjXvqECqhONLPXEXDJ6TVmfb2yd5
X9cE6HLS2IXqfvs0EdXmQhSQVS7AlUQWZPDeoBTDUA1tT6ZKCcG0BuHEFnHxg4Yg
W9xp/wMJCEly+9eNJYZYzyK1AHRGnTMRCSifTJEybwI4A35v68FyRLfAC0lM2qVL
yQIGjj55+r4yGCK7bySSKjs59LLLxi6Px3S61OxAYq9KMT65nBLK9JAPFyTnikw9
q/xW208lL+kcRtG+ARo5ycx5QUjWdsHn7TCnqxnDhHznwSV4KGbJFaGQZTtgfcz0
g5a1GwxqHjEZ9IWiN38f2l4kpLLybKhwVQMYeG000s7rDa5hgjbh13qtQN6vUvI6
VozzZPnFcR1Rsa8RR9njDugxbVwlJQfGkoMiMZwNGgXnZRC2XaI6SCyPwqTPBuVP
ZR1eWv4qwsIGKJzJYcdChb5dimlTuVSfZmONpnrOP/4mhQLyaWr3XLqxxP3mIXsz
k1PNWTkgLsXO8DNkCudxcvPElXfmaw6zwaLrZys=
-----END CERTIFICATE-----

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data: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,iv:q3j8EAokyyxiszf+wyRqxEr2igaD1bX7YnFx/NbsGg8=,tag:HKKYWRJEUwW2/TxL+5dSng==,type:str]",
"sops": {
"age": [
{
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaeXRjU214aWk5ajl1aW9E\naGJlb1ViaVRmMTBHdkFDQUNDZS94WFZiNUNvCllmWTJBck9hR3U3V09VWDZwQ2xI\nd3ZEQnBIUG5ZSTVIdS8rQ2FMYVhyNk0KLS0tIEE1UG8rSzFyU01sVXhGVHpoaE9i\nSis4Qi9tMGFqbTNMTDZUVk1ZdXkrM28Km4VkfaOsZ69ckjvrg+os43H/O1IoWHzC\nt4LqZRz1Tk7/d1aLWavSPPjVYrCOMZeNBqGbQpGfjjuXrafClRNQdQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3R1RHTGViTnRLVVkyM3J0\nbm96cGVPTlo4NXBNL0g1eEVSNG9DUkgwVFRBCmRKVTlMRmV3Tmg2RTZIclBlWlcr\ndzI5MUxhcllzbE1IMDNxa08zVkpITmsKLS0tIG01Y2dyQkY3UmRudFk2d0p6bThn\nemlaWnZoS3p4VHhMTFFwTm9VN0ttYzQKVbLFgtK6NIRIiryWHeeOPD45iwUds4QD\n7b8xYYoxlo+DETggxK6Vz3IdT/BSK5bFtgAxl864b5gW+Aw4c6AO5w==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-18T14:33:37Z",
"mac": "ENC[AES256_GCM,data:XKCnd0QrAlOCECSeSvbLYHMLbmUh4fMRnLaTb5ARoP4Zc9joWGsCaRZxokc2/sG4BXA/6pkbQXHyIOudKbcBpVjjvs9E+6Mnzt53nfRoH/iOkYPbN2EO49okVZJXW0M1rlBxrxvGuiDlz2p2p6L7neKLy4EB482pYea5+dUr2Yw=,iv:oj/MkZCfkvCmAb79uzEvKwEAm1bKtWhS4rPRAWSgRgw=,tag:h5TPPILXkhJplnDT2Gqtfw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

@@ -1,18 +1,18 @@
{
"data": "ENC[AES256_GCM,data:ePdnRA2Rkwl3C1Ugp0GjZ3gncdgu+vxTMZe87tI23FKRX7KxJoobKeivWH4=,iv:h3Mjf+zfWMC98KarOYKdAr1/I0HSDd7iSxnlgxIFL7Y=,tag:GVave5z1hT2MG1WW0p6H4g==,type:str]",
"data": "ENC[AES256_GCM,data:Q0Vn7J0nERccBYT8HZxHF0Zi5qxmMu40n0H1c+L2SCRF6vRLdURxXKDwvh8xtTU=,iv:ucExjoYDFYy19GsBbNNldJRPBSpT+L+x4PrwTG+m2K8=,tag:/Quupyy/nnUNZsDudEMmNA==,type:str]",
"sops": {
"age": [
{
"recipient": "age18vspwr3de7jp0awyu66kk9psd5x4sy9suv0zt7ux2kqw0s6h2ueqwkgjxm",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUOXJ3UVJkdHl1T0REODBB\nUU9pamtPQ092L3FWT3pHeWpoNitLNlllSzFrClNLSlRIYlMxRUZIQ1JZaXpiclpm\nZlV4SUo0USs3NmluV2ZHVkdtZjZXUGMKLS0tIFJscmkxVG03dUNiUkJLZ1F6UEkw\nQmVZQndkNm9TczcwaXRoN2hTVGVPSVkKJHjTSZrR0VTPh3IiIfvoRAsWBA4lvXp5\n3+9x3zN802z4+62SmI1y7497GEUe4iVcMIvxup1az+sbFpN31eC9+w==\n-----END AGE ENCRYPTED FILE-----\n"
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQWWo5OEJ5N1RTR0xMaDhL\nQnlUV2RrRXIzM01OemhQWjVkd3FNZjRhR2dzCi9IeE56b3VZTkNkdW9DMzVia3Zx\nbklxWmFpenRjdEIrc0ZDTGdmSTAxRTQKLS0tIHZJdjdYUzhhY0YzQjRqS0psZmpI\nVHJpUjNZNHRpc2ZWSml1TVNNejhiT28K8TTP/J+XspXZ7TVYj9YaBhEodPIXjojB\nRLqAIgJXRaK4NCLukC6l0IMii6w5J/512RnO2ZBTGhKfbdLfyLOFqg==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJZEhBSW0rWFRDb2tCMzln\nVzZEZFIxdXBlQkVxaEl1N1p3UUI4elpTSEVFCnYvUC9iV0RZVURDODRiNGFjZnY1\nSUVNeUo3UUg3YVV4OXdsbGtaMGlaY1UKLS0tIHFhdXpOdzd5TVBSQm5NdGtxZzdr\nL01odEF4QnRReTNRVkZkMk44cHk3SzAKHUs4hgsOMK9ZIIyUDbTqbWmk1GHGBa+B\nENSaH5UL8AYnOvGd3vV4VQcznVmhYh+VPkJUbu7gXkrYyVNBjsWx0Q==\n-----END AGE ENCRYPTED FILE-----\n"
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrZVc5b0FhbzNXcG1zUDlD\neEVWcWpSRkRCMkxBTHdBM3dCbjVpR3FBa0VjCitlTmx4eUJOMHlaU0dFZEhpK3ZD\nZzlMQXVuZWpnaUNmQW9kOGtOaGVDMU0KLS0tIFNlUi9LSzF0UEJCSVBiRlRSNFQz\nNHhMbmNlRXd4ZEJQWVcvTWdCRWEzMUkKls7RbmNOdPDx8z15F+7qay9qIWx6jNsN\nTahT+GgbG29t1aGQCb0yEzKuUyAp39maxxSWToPsfCgJSYJ8RYiUng==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-08-20T19:56:05Z",
"mac": "ENC[AES256_GCM,data:0g+PNdDOZ+pQnt3d5cfYVOtToIbuBnyMnqfk86HjK4YDMObFRkyLe9k0aTDAH2NvERBX/BPb+lTffBlwM2Fl6p/EDXh+x0Q3IELjzcOfhX6jbR45rMRJF+CcU8pbNSGApp153QWai2ku60SUUOdpe5tIbmTah/QfdMO4x4/fM+M=,iv:vLhzyZpfcuShncd0K0+GzFNNmhBOOeNxnboe+3VkJGY=,tag:VgbJA1xfISWK9JJPIgxHew==,type:str]",
"lastmodified": "2025-09-18T14:33:39Z",
"mac": "ENC[AES256_GCM,data:g+9/fRiqom2+W28ZpiF+oBj9V6ieq5Xz3sRz3GyzvHoLr6yw51JvpG2QuYNYANW0WCiUjFDkU0qPj/9gLHcuX52nc+gNaTzznb1QGPg7WCGSQI7xaMzyYsPxHpg/BOdj5CL8GyLiOWstD1ch0kc3bJmyu68sJUs04uGtHAADzsE=,iv:oASrYaZarEPDu0R3hd/jMazLgwG5r//hIdMyU/tN15o=,tag:o1fgf5oy+rlWXg88FN5Nfw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}

View File

@@ -1,18 +1,18 @@
{
"data": "ENC[AES256_GCM,data:u9lTWO6Z4wgw35zBhhfbPDv3bc1MIWttbWUzkS3Hjzgwq06Zp4z61e4Wgt1QBkCC,iv:1uD5R0hQ/6Su1bg4nqL0MjJ22HvHjLGmgrL02DxegpY=,tag:mypBmsq27JHUujCdNYpR+Q==,type:str]",
"data": "ENC[AES256_GCM,data:4NIUEK05kEQAKjR8F9mU3M/XvtZXw+X6CejVI0usMcb4WzagNz7XTVDhLWXZ9St5Ev0Y,iv:bD2+rDLMoWSqUAIZRJof0wRrJVya1xwZUTIJBdCs98I=,tag:g2s4byFHTzwU3ikcBGMElA==,type:str]",
"sops": {
"age": [
{
"recipient": "age18vspwr3de7jp0awyu66kk9psd5x4sy9suv0zt7ux2kqw0s6h2ueqwkgjxm",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKMElLR3dhWmZJd1NmUytD\nVitaeW1JbkpnMGt0VFk4M0VRWnpCazRWeTFnCis4eWxnZVdVajVPWVUwMGFKQkRB\nbDVkTk95RzZZVW9BQll0M09VekZCNkkKLS0tIHFpZm5JRDlueTRsMGUxekxxWjVz\nRW5KVEtZRE85KzNFM1BINUtJcExtME0K5aOLpzy9Y35McN19UEm1Wy6bU2oeXGxZ\nCjw5tLHHzxUOzfoE1RfIZinRmBXRZpCpQVH6iK3IaIq8aouK36Pa1Q==\n-----END AGE ENCRYPTED FILE-----\n"
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQeVh2M2tqSGlOVkpzNlhU\nd0pMd1R0c0tQWnZzdXViWmtxcjl1Wk1Ka0FNCnBUUWJVbjlyR1hSNGpXNWlPRHJB\nNnMzN3BMQ2NDamFBMlhHbVdJUEZ6cjQKLS0tIEJjWmI0ZDl1NXgrSW9uc0R0LzAr\neEwwOC9DdDg2RTJHQ0M3QTFlcVBaSE0K2Du4NguefdEyY1gS6OuVdO3gHga4omcR\n8B+K1wUfIQbArxZLawPxrj7WNDoW5d4mF9fA3MeV1DFyc4KwtYZmUw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2dEJkTnZzekhaSjRsaFJw\na2xPK1BoMkVkc3N0UnNqZTFZZWpWQ2lIYUJFClBaemplMjhPYkJsRmIvWmsyQm56\nYUswOVkzTUd1cTRtNVFoV1RZQlBtNzAKLS0tIC9ERjl1Q2lBaXFHcHMyRGt3VTRs\nV25tTEZsak5KQ2lNOEFLUVR5c1lnNjgKaGDYoK6UJSbkBs8+eiqEFEx/tbzlNGPz\nw96ttHtR3j/jkCbwOpAb/D5yChfJf9mQlpjbtKvEJ0SJSEPT5fniiw==\n-----END AGE ENCRYPTED FILE-----\n"
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvWkdBakVrMVR4RU8xdDlF\nRDkvL0Mrb3ltazhIMjRLZDVlSTVlaFY2ODBBCnlQM2s0SGEvZjFDN3dGWDhIN0dK\nenhQbjZ1ZS9QZzg5SE5XazZXS3dFSkkKLS0tIHJhKzhadGpjTXd4L3hOQkhpR0Fy\nYzhTN2dxVSt3OE5uZFpuWmVlYW4vd1kKwHOxP0C5mLcm4oIT/sGQtUsdsmu3LSN0\nSola5+N+IrAZ+HKnuZlDLZ5JmJSc5j/YhGNn7KR1xhkhfGSS1e3UZw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-08-20T19:56:05Z",
"mac": "ENC[AES256_GCM,data:GH6B/83FRG4KDdD/ZHmAVjeOlzVFMbuoaq+yyc/XRVIdTi5t5uLMGZePd1AiHzAz4lLubxJWVp2bqw56m9G9ICPCCtmCpE5SEoXllZ8cRZ9H0yb8ywAT/66pRnWT60cNCLYrX0LJyPw5HLbA09xETXhNt3HYhom6tt3VA3/ghW4=,iv:UcZoY2P8g8KS/NvsOd3B983vu5D6fC/ClF6hDXkjvq0=,tag:J1KxVXW/EpNiOo4mhwqokw==,type:str]",
"lastmodified": "2025-09-18T14:33:39Z",
"mac": "ENC[AES256_GCM,data:ehbrYqTJcsBKGHUB25JHFnKXrJ6z3LkcElZ89xVr4XxLet+odbhsjIoP2FCcxex7PlXcegMduhHBpXwNGUbX+IUNAXTxlWA9CLDmYhWuS2WLiEVXrS11NE03/zUyHdVx/C38dbIPrWD9iaYSrAiuOyfqDTh9k/Bn7vehLTtadoE=,iv:Nk2WVuJydi5tfsb1Mib4A6NocBCDp9QoIbSadq3bIDI=,tag:IaoyfCv3SkmtemXMR9XnkA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}

View File

@@ -1,18 +1,18 @@
{
"data": "ENC[AES256_GCM,data:rXxZC/HU9O/3CR6lDR+twbgtq3SjPHR7mz7iUnHF4MA=,iv:SpRivg1us5RutN7h/YR5dh3QG8/wBYM4GgE1t7u0YVM=,tag:miFjFfqYWy0yFdPPUB+T6Q==,type:str]",
"data": "ENC[AES256_GCM,data:0BmP+NwG/NGe6R5yU55/MdPEQ8E5u+VXWtvstHc4GpDtmBY=,iv:vo8XBcN7KcYjiyKvvp+XDOdP9yR9B7wJi0XlaiCdVbk=,tag:brK9ntAPSuOvw/C+oDo51g==,type:str]",
"sops": {
"age": [
{
"recipient": "age18vspwr3de7jp0awyu66kk9psd5x4sy9suv0zt7ux2kqw0s6h2ueqwkgjxm",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMMllDWGZvSXRMTFM4SFBQ\ncEo4dFZ4SHdVL3hYQisrQTlFOTBWQ2JPTmowCktIak55S29YVnZ2OGtJa3hVcURx\nWDdEa1hmZGt1UkZxdmpnang4NThMbHMKLS0tIFU4UTVaSnpQdzZCNFNJWnRSaUNU\nSDRuYnpvbDJsL2d0OEcxQXVxVDExTHcKsijOA0GChkmjNGuPiD4/5ohXuBcTmxxD\nTOC6jdf3TEo0b9ZRmNk/TpJpUhe7PQiv48oqFfbyj7VTicNMKbtw1A==\n-----END AGE ENCRYPTED FILE-----\n"
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4Tk1INGtybUVlejlNNlZE\nVms3TkdRVVF1T0E4TmV3NmxvYWVEL2U3WVhNCjJIaHhBcWVlMEYxRjg5bzJpTWdJ\neUhaRTNRTmtlTW0zUXQxTVZEMkQ2MFEKLS0tIFNGWDI4b2FXTE8xQ2xqb0cyK3FI\ncktHWnE5c1ZSVFpmQU1HZmU2VVB1QmcK/s1fVmwpMMg4BYkkAJzSY7hVQWae1F7g\nmfH8EGlr74mifWUNEbd49/K13nl8atQx6bcau83JIEQR+yyihuY4Jw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVZzNsNGJBbEU2eXhpenBj\nTkRlRThJbHpMd1NlMWVXNU9EcGRvemYzQXhjCi9Xc2gzQW82T3A2WGs0KzE5M1JV\nb1ZZT0ptVHhubjlTTTM3MkUydUN2d28KLS0tIFJScGZudjBVTFpnenllaW96NEVi\nZmNyaU1PUDY0QWZFNnNuaVBYdmlXdzQKfmGQ5EUjUxGzZddENlu4ZSaHYuT33Kfj\nBMJsbYovtgJA4UsBufcMY+ohN86C2Xo23JxYpgA2Qzu1KA46qXlrHw==\n-----END AGE ENCRYPTED FILE-----\n"
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsL2FXVytUUVZnVU90bG5L\nYURiYjgwN3RuTldWMGl4clpUWmxkeUsrVzM0CkhKZFgwWHl4dWhNSWRQRXVPNDR6\na3hHNmp2RG9YNDhNM2MyV2FuOGY2UlUKLS0tIFpNU2tNOHdhRDRTdHhYWVh2NGZa\nU3J3S0hpclZzWGIwTlFyczdNZkZSZTAKXCZrLaIOVq90ejoKMaRiK0xNw8WOPcnm\nz2uxProEYvQhY8k29mhCFX5HCN0tGn1XTtHeDL7uHuKuFsnSG/fgYQ==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-08-20T19:56:05Z",
"mac": "ENC[AES256_GCM,data:vtO+KbN36c38DkFyqlQAnz22CodxE8S6yfadRLtjbKPaBD4cn60R6Md90jHB25uqiXyr48J+3mMbpqBVwl2Sjxt/PtcsS4UBz/xt9pGtdEssX5veQnIdEA4dlBpAeYWba/aAZn/rxYYsm+yswLAd1DCwQf/moetF6asQCZSXTBc=,iv:DI5m3qopcdPSvpVSaNDIhUoNDcVpumkMI/nz1MV/fF4=,tag:HNG0pupG1tzQh7bQJobVDA==,type:str]",
"lastmodified": "2025-09-18T14:33:39Z",
"mac": "ENC[AES256_GCM,data:QkGJKj/H+MI9Mr9Up5NDUToSddY5eTz47egc2+IatfxR8RebKJ2/mYaeLV26vPdmY60bIac4N/nZkoa6IVBhkHHMvsEHsx3nD6Lro9Wf/pWP8Zddzr90LF5p2+wusq25JutKQiPKOb2gmrcagmSsH/7V/UqI/my3PMeKmw6irhw=,iv:hOtHF/cDFdNfvqCKRhJsOwAHEiQmCPjENzsg23sKG+Q=,tag:K7qG9b4fQD0VbAV8OYp3vw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}

View File

@@ -549,11 +549,11 @@ def main() -> None:
try:
args.func(args)
except ClanError:
except ClanError as e:
if debug:
log.exception("Exited with error")
else:
log.exception("Exited with error")
log.error(e) # noqa: TRY400
sys.exit(1)
except KeyboardInterrupt as ex:
log.warning("Interrupted by user", exc_info=ex)

View File

@@ -3,6 +3,7 @@ import argparse
from .create import register_create_parser
from .delete import register_delete_parser
from .generations import register_generations_parser
from .hardware import register_update_hardware_config
from .install import register_install_parser
from .list import register_list_parser
@@ -145,3 +146,19 @@ For more detailed information, visit: https://docs.clan.lol/guides/getting-start
formatter_class=argparse.RawTextHelpFormatter,
)
register_install_parser(install_parser)
generations_parser = subparser.add_parser(
"generations",
help="list generations of machines",
description="list generations of machines",
epilog=(
"""
List NixOS generations of the machine.
The generations are the different versions of the machine that are installed on the target host.
Examples:
$ clan generations [MACHINE]
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
register_generations_parser(generations_parser)

View File

@@ -0,0 +1,285 @@
import argparse
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal, TypeVar, get_args
from clan_lib.async_run import AsyncContext, AsyncFuture, AsyncOpts, AsyncRuntime
from clan_lib.errors import ClanError, text_heading
from clan_lib.flake import require_flake
from clan_lib.machines.generations import MachineGeneration, get_machine_generations
from clan_lib.machines.machines import Machine
from clan_lib.metrics.telegraf import MonitoringNotEnabledError
from clan_lib.metrics.version import check_machine_up_to_date
from clan_lib.network.network import get_best_remote
from clan_lib.ssh.host_key import HostKeyCheck
from clan_lib.ssh.localhost import LocalHost
from clan_lib.ssh.remote import Remote
from clan_cli.completions import (
add_dynamic_completer,
complete_machines,
complete_tags,
)
from clan_cli.machines.update import get_machines_for_update
if TYPE_CHECKING:
from clan_lib.ssh.host import Host
log = logging.getLogger(__name__)
UpToDateType = Literal["up-to-date", "out-of-date", "unknown"]
def print_generations(
generations: list[MachineGeneration],
needs_update: UpToDateType = "unknown",
) -> None:
headers = [
"Generation (Up-To-Date)",
"Date",
"NixOS Version",
"Kernel Version",
]
rows = []
for gen in generations:
gen_marker = f" ← ({needs_update})" if gen.current else ""
gen_str = f"{gen.generation}{gen_marker}"
row = [
gen_str,
gen.date,
gen.nixos_version,
gen.kernel_version,
]
rows.append(row)
elided_rows = rows
col_widths = [
max(len(str(item)) for item in [header] + [row[i] for row in elided_rows])
for i, header in enumerate(headers)
]
# Print header
header_row = " | ".join(
header.ljust(col_widths[i]) for i, header in enumerate(headers)
)
print(header_row)
print("-+-".join("-" * w for w in col_widths))
# Print rows
for row in elided_rows:
print(" | ".join(row[i].ljust(col_widths[i]) for i in range(len(headers))))
print()
def print_summary_table(
machine_data: dict[Machine, tuple[list[MachineGeneration], UpToDateType]],
) -> None:
print(text_heading("Current Generations Summary"))
headers = ["Machine", "Current Generation", "Date", "NixOS Version", "Up-To-Date"]
rows = []
for machine, (generations, needs_update) in machine_data.items():
current_gen = None
for gen in generations:
if gen.current:
current_gen = gen
break
if current_gen is None:
continue
status = needs_update
row = [
machine.name,
str(current_gen.generation),
current_gen.date,
current_gen.nixos_version,
status,
]
rows.append(row)
if not rows:
print("Couldn't retrieve data from any machine.")
return
col_widths = [
max(len(str(item)) for item in [header] + [row[i] for row in rows])
for i, header in enumerate(headers)
]
# Print header
header_row = " | ".join(
header.ljust(col_widths[i]) for i, header in enumerate(headers)
)
print(header_row)
print("-+-".join("-" * w for w in col_widths))
# Print rows
for row in rows:
print(" | ".join(row[i].ljust(col_widths[i]) for i in range(len(headers))))
print()
@dataclass(frozen=True)
class MachineVersionData:
generations: AsyncFuture[list[MachineGeneration]]
machine_update: AsyncFuture[bool] | None
def generations_command(args: argparse.Namespace) -> None:
flake = require_flake(args.flake)
machines_to_update = get_machines_for_update(flake, args.machines, args.tags)
if args.target_host is not None and len(machines_to_update) > 1:
msg = "Target Host can only be set for one machines"
raise ClanError(msg)
host_key_check = args.host_key_check
machine_generations: dict[Machine, MachineVersionData] = {}
with AsyncRuntime() as runtime:
for machine in machines_to_update:
if args.target_host:
target_host: Host | None = None
if args.target_host == "localhost":
target_host = LocalHost()
else:
target_host = Remote.from_ssh_uri(
machine_name=machine.name,
address=args.target_host,
).override(host_key_check=host_key_check)
else:
try:
with get_best_remote(machine) as _remote:
target_host = machine.target_host().override(
host_key_check=host_key_check
)
except ClanError:
log.warning(
f"Skipping {machine.name} as it has no target host configured."
)
continue
generations = runtime.async_run(
AsyncOpts(
tid=machine.name,
async_ctx=AsyncContext(prefix=machine.name),
),
get_machine_generations,
target_host=target_host,
)
if args.skip_outdated_check:
machine_update = None
else:
machine_update = runtime.async_run(
AsyncOpts(
tid=machine.name + "-needs-update",
async_ctx=AsyncContext(prefix=machine.name),
),
check_machine_up_to_date,
machine=machine,
target_host=target_host,
)
machine_generations[machine] = MachineVersionData(
generations, machine_update
)
runtime.join_all()
R = TypeVar("R")
errors: dict[Machine, Exception] = {}
successful_machines: dict[
Machine, tuple[list[MachineGeneration], UpToDateType]
] = {}
for machine, async_version_data in machine_generations.items():
def get_result(async_future: AsyncFuture[R]) -> R | Exception:
aresult = async_future.get_result()
if aresult is None:
msg = "Generations result should never be None"
raise ClanError(msg)
if aresult.error is not None:
return aresult.error
return aresult.result
mgenerations = get_result(async_version_data.generations)
if isinstance(mgenerations, Exception):
errors[machine] = mgenerations
continue
if async_version_data.machine_update is None:
needs_update: UpToDateType = "unknown"
else:
eneeds_update = get_result(async_version_data.machine_update)
if isinstance(eneeds_update, MonitoringNotEnabledError):
log.warning(
f"Skipping up-to-date check for {machine.name} as monitoring is not enabled."
)
needs_update = "unknown"
elif isinstance(eneeds_update, Exception):
errors[machine] = eneeds_update
continue
else:
needs_update = "out-of-date" if eneeds_update else "up-to-date"
successful_machines[machine] = (mgenerations, needs_update)
# Check if specific machines were requested
specific_machines_requested = bool(args.machines or args.tags)
if specific_machines_requested:
# Print detailed generations for each machine
for mgenerations, needs_update in successful_machines.values():
print_generations(
generations=mgenerations,
needs_update=needs_update,
)
else:
# Print summary table
print_summary_table(successful_machines)
for machine, error in errors.items():
msg = f"Failed for machine {machine.name}: {error}"
raise ClanError(msg) from error
def register_generations_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
"machines",
type=str,
nargs="*",
default=[],
metavar="MACHINE",
help="Machine to update. If no machines are specified, all machines that don't require explicit updates will be updated.",
)
add_dynamic_completer(machines_parser, complete_machines)
tag_parser = parser.add_argument(
"--tags",
nargs="+",
default=[],
help="Tags that machines should be queried for. Multiple tags will intersect.",
)
add_dynamic_completer(tag_parser, complete_tags)
parser.add_argument(
"--host-key-check",
choices=list(get_args(HostKeyCheck)),
default="ask",
help="Host key (.ssh/known_hosts) check mode.",
)
parser.add_argument(
"--target-host",
type=str,
help="Address of the machine to update, in the format of user@host:1234.",
)
parser.add_argument(
"--skip-outdated-check",
action="store_true",
help="Skip checking if the current generation is outdated (faster).",
)
parser.set_defaults(func=generations_command)

View File

@@ -17,6 +17,10 @@ from .list import get_machine_vars
log = logging.getLogger(__name__)
class VarNotFoundError(ClanError):
pass
def get_machine_var(machine: Machine, var_id: str) -> Var:
log.debug(f"getting var: {var_id} from machine: {machine.name}")
vars_ = get_machine_vars(machine)
@@ -29,11 +33,14 @@ def get_machine_var(machine: Machine, var_id: str) -> Var:
if var.id.startswith(var_id):
results.append(var)
if len(results) == 0:
msg = f"Couldn't find var: {var_id} for machine: {machine}"
raise ClanError(msg)
msg = f"Couldn't find var: {var_id} for machine: {machine.name}"
raise VarNotFoundError(msg)
if len(results) > 1:
error = f"Found multiple vars in {machine} for {var_id}:\n - " + "\n - ".join(
[str(var) for var in results],
error = (
f"Found multiple vars in {machine.name} for {var_id}:\n - "
+ "\n - ".join(
[str(var) for var in results],
)
)
raise ClanError(error)
# we have exactly one result at this point

View File

@@ -0,0 +1,44 @@
import json
from dataclasses import dataclass, field
from clan_lib.api import API
from clan_lib.ssh.localhost import LocalHost
from clan_lib.ssh.remote import Remote
@dataclass(order=True, frozen=True)
class MachineGeneration:
generation: int
date: str
nixos_version: str
kernel_version: str
configuration_revision: str
specialisations: list[str] = field(default_factory=list)
current: bool = False
@API.register
def get_machine_generations(target_host: Remote | LocalHost) -> list[MachineGeneration]:
"""Get the nix generations installed on the target host and compare them with the machine."""
with target_host.host_connection() as target_host_conn:
cmd = [
"nixos-rebuild",
"list-generations",
"--json",
]
res = target_host_conn.run(cmd)
data = json.loads(res.stdout.strip())
sorted_data = sorted(data, key=lambda gen: gen.get("generation", 0))
return [
MachineGeneration(
generation=gen.get("generation"),
date=gen.get("date"),
nixos_version=gen.get("nixosVersion", ""),
kernel_version=gen.get("kernelVersion", ""),
configuration_revision=gen.get("configurationRevision", ""),
specialisations=gen.get("specialisations", []),
current=gen.get("current", False),
)
for gen in sorted_data
]

View File

@@ -1,11 +1,12 @@
import json
import logging
import ssl
import urllib.request
from base64 import b64encode
from collections.abc import Iterator
from typing import Any, TypedDict, cast
from clan_cli.vars.get import get_machine_var
from clan_cli.vars.get import VarNotFoundError, get_machine_var
from clan_lib.errors import ClanError
from clan_lib.machines.machines import Machine
@@ -21,6 +22,11 @@ class MetricSample(TypedDict):
timestamp: int
class MonitoringNotEnabledError(ClanError):
pass
# Tests for this function are in the 'monitoring' clanService tests
def get_metrics(
machine: Machine,
target_host: Host,
@@ -36,14 +42,20 @@ def get_metrics(
"""
# Example: fetch Prometheus metrics with basic auth
url = f"http://{target_host.address}:9990/telegraf.json"
url = f"https://{target_host.address}:9990/telegraf.json"
username = "prometheus"
var_name = "telegraf/password"
password_var = get_machine_var(machine, var_name)
if not password_var.exists:
try:
password_var = get_machine_var(machine, "telegraf/password")
cert_var = get_machine_var(machine, "telegraf-certs/crt")
except VarNotFoundError as e:
msg = "Module 'monitoring' is required to fetch metrics from machine."
raise MonitoringNotEnabledError(msg) from e
if not password_var.exists or not cert_var.exists:
msg = (
f"Missing required var '{var_name}' for machine '{machine.name}'.\n"
"Ensure the 'monitoring' clanService is enabled and run `clan machines update {machine.name}`."
f"Missing required var.\n"
f"Ensure the 'monitoring' clanService is enabled and run `clan machines update {machine.name}`."
"For more information, see: https://docs.clan.lol/reference/clanServices/monitoring/"
)
raise ClanError(msg)
@@ -53,21 +65,30 @@ def get_metrics(
encoded_credentials = b64encode(credentials.encode("utf-8")).decode("utf-8")
headers = {"Authorization": f"Basic {encoded_credentials}"}
cert_path = machine.select(
"config.clan.core.vars.generators.telegraf-certs.files.crt.path"
)
context = ssl.create_default_context(cafile=cert_path)
context.check_hostname = False
context.verify_mode = ssl.CERT_REQUIRED
req = urllib.request.Request(url, headers=headers) # noqa: S310
try:
response = urllib.request.urlopen(req) # noqa: S310
for line in response:
line_str = line.decode("utf-8").strip()
if line_str:
try:
yield cast("MetricSample", json.loads(line_str))
except json.JSONDecodeError:
log.warning(f"Skipping invalid JSON line: {line_str}")
continue
machine.info(f"Fetching Prometheus metrics from {url}")
with urllib.request.urlopen(req, context=context, timeout=10) as response: # noqa: S310
for line in response:
line_str = line.decode("utf-8").strip()
if line_str:
try:
yield cast("MetricSample", json.loads(line_str))
except json.JSONDecodeError:
machine.warn(f"Skipping invalid JSON line: {line_str}")
continue
except Exception as e:
msg = (
f"Failed to fetch Prometheus metrics from {url} for machine '{machine.name}': {e}\n"
f"Failed to fetch Prometheus metrics from {url}: {e}\n"
"Ensure the telegraf.service is running and accessible."
)
raise ClanError(msg) from e

View File

@@ -67,8 +67,8 @@ def check_machine_up_to_date(
],
)
log.debug(
f"Checking if {machine.name} needs an update:\n"
machine.debug(
f"Checking up-to-date:\n"
f"Machine outPath: {nixos_systems.current_system}\n"
f"Git outPath : {git_out_path}\n",
)