Compare commits

..

1 Commits

Author SHA1 Message Date
Johannes Kirschbauer
9e0efcef8b vars: move logic from vars-to-sops into single file 2025-10-13 16:27:31 +02:00
42 changed files with 344 additions and 293 deletions

View File

@@ -2,6 +2,7 @@
self, self,
lib, lib,
inputs, inputs,
privateInputs ? { },
... ...
}: }:
let let
@@ -128,7 +129,7 @@ in
// flakeOutputs // flakeOutputs
// { // {
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } '' clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
cp -r ${self} $out cp -r ${privateInputs.clan-core-for-checks} $out
chmod -R +w $out chmod -R +w $out
cp ${../flake.lock} $out/flake.lock cp ${../flake.lock} $out/flake.lock

View File

@@ -50,13 +50,13 @@
dns = dns =
{ pkgs, ... }: { pkgs, ... }:
{ {
environment.systemPackages = [ pkgs.nettools ]; environment.systemPackages = [ pkgs.net-tools ];
}; };
client = client =
{ pkgs, ... }: { pkgs, ... }:
{ {
environment.systemPackages = [ pkgs.nettools ]; environment.systemPackages = [ pkgs.net-tools ];
}; };
server01 = { server01 = {

View File

@@ -1,39 +1,91 @@
The `sshd` Clan service manages SSH to make it easy to securely access your # Clan service: sshd
machines over the internet. The service uses `vars` to store the SSH host keys What it does
for each machine to ensure they remain stable across deployments. - Generates and persists SSH host keys via `vars`.
- Optionally issues CAsigned host certificates for servers.
- Installs the `server` CA public key into `clients` `known_hosts` for TOFUless verification.
`sshd` also generates SSH certificates for both servers and clients allowing for
certificate-based authentication for SSH.
The service also disables password-based authentication over SSH, to access your When to use it
machines you'll need to use public key authentication or certificate-based - ZeroTOFU SSH for dynamic fleets: admins/CI can connect to frequently rebuilt hosts (e.g., server-1.example.com) without prompts or perhost `known_hosts` churn.
authentication.
## Usage Roles
- Server: runs sshd, presents a CAsigned host certificate for `<machine>.<domain>`.
- Client: trusts the CA for the given domains to verify servers certificates.
Tip: assign both roles to a machine if it should both present a cert and verify others.
Quick start (with host certificates)
Useful if you never want to get a prompt about trusting the ssh fingerprint.
```nix
{
inventory.instances = {
sshd-with-certs = {
module = { name = "sshd"; input = "clan-core"; };
# Servers present certificates for <machine>.example.com
roles.server.tags.all = { };
roles.server.settings = {
certificate.searchDomains = [ "example.com" ];
# Optional: also add RSA host keys
# hostKeys.rsa.enable = true;
};
# Clients trust the CA for *.example.com
roles.client.tags.all = { };
roles.client.settings = {
certificate.searchDomains = [ "example.com" ];
};
};
};
}
```
Basic: only add persistent host keys (ed25519), no certificates
Useful if you want to get an ssh "trust this server" prompt once and then never again.
```nix ```nix
{ {
inventory.instances = { inventory.instances = {
# By default this service only generates ed25519 host keys
sshd-basic = { sshd-basic = {
module = { module = {
name = "sshd"; name = "sshd";
input = "clan-core"; input = "clan-core";
}; };
roles.server.tags.all = { }; roles.server.tags.all = { };
roles.client.tags.all = { };
};
# Also generate RSA host keys for all servers
sshd-with-rsa = {
module = {
name = "sshd";
input = "clan-core";
};
roles.server.tags.all = { };
roles.server.settings = {
hostKeys.rsa.enable = true;
};
roles.client.tags.all = { };
}; };
}; };
} }
``` ```
Example: selective trust per environment
Admins should trust only production; CI should trust prod and staging. Servers are reachable under both domains.
```nix
{
inventory.instances = {
sshd-env-scoped = {
module = { name = "sshd"; input = "clan-core"; };
# Servers present certs for both prod and staging FQDNs
roles.server.tags.all = { };
roles.server.settings = {
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
};
# Admin laptop: trust prod only
roles.client.machines."admin-laptop".settings = {
certificate.searchDomains = [ "prod.example.com" ];
};
# CI runner: trust prod and staging
roles.client.machines."ci-runner-1".settings = {
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
};
};
};
}
```
- Admin -> server1.prod.example.com: zeroTOFU (verified via cert).
- Admin -> server1.staging.example.com: falls back to TOFU (or is blocked by policy).
- CI -> either prod or staging: zeroTOFU for both.
Note: server and client searchDomains dont have to be identical; they only need to overlap for the hostnames you actually use.
Notes
- Connect using a name that matches a cert principal (e.g., `server1.example.com`); wildcards are not allowed inside the certificate.
- CA private key stays in `vars` (not deployed); only the CA public key is distributed.
- Logins still require your user SSH keys on the server (passwords are disabled).

View File

@@ -11,7 +11,9 @@
pkgs.syncthing pkgs.syncthing
]; ];
script = '' script = ''
syncthing generate --config "$out" export TMPDIR=/tmp
TEMPORARY=$(mktemp -d)
syncthing generate --config "$out" --data "$TEMPORARY"
mv "$out"/key.pem "$out"/key mv "$out"/key.pem "$out"/key
mv "$out"/cert.pem "$out"/cert mv "$out"/cert.pem "$out"/cert
cat "$out"/config.xml | grep -oP '(?<=<device id=")[^"]+' | uniq > "$out"/id cat "$out"/config.xml | grep -oP '(?<=<device id=")[^"]+' | uniq > "$out"/id

18
devFlake/flake.lock generated
View File

@@ -3,16 +3,16 @@
"clan-core-for-checks": { "clan-core-for-checks": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1760368011, "lastModified": 1760213549,
"narHash": "sha256-mLK2nwbfklfOGIVAKVNDwGyYz8mPh4fzsAqSK3BlCiI=", "narHash": "sha256-XosVRUEcdsoEdRtXyz9HrRc4Dt9Ke+viM5OVF7tLK50=",
"ref": "clan-25.05", "ref": "main",
"rev": "1b3c129aa9741d99b27810652ca888b3fbfc3a11", "rev": "9c8797e77031d8d472d057894f18a53bdc9bbe1e",
"shallow": true, "shallow": true,
"type": "git", "type": "git",
"url": "https://git.clan.lol/clan/clan-core" "url": "https://git.clan.lol/clan/clan-core"
}, },
"original": { "original": {
"ref": "clan-25.05", "ref": "main",
"shallow": true, "shallow": true,
"type": "git", "type": "git",
"url": "https://git.clan.lol/clan/clan-core" "url": "https://git.clan.lol/clan/clan-core"
@@ -105,16 +105,16 @@
}, },
"nixpkgs-dev": { "nixpkgs-dev": {
"locked": { "locked": {
"lastModified": 1760309387, "lastModified": 1760161054,
"narHash": "sha256-e0lvQ7+B1Y8zjykYHAj9tBv10ggLqK0nmxwvMU3J0Eo=", "narHash": "sha256-PO3cKHFIQEPI0dr/SzcZwG50cHXfjoIqP2uS5W78OXg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6cd95994a9c8f7c6f8c1f1161be94119afdcb305", "rev": "e18d8ec6fafaed55561b7a1b54eb1c1ce3ffa2c5",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-25.05-small", "ref": "nixos-unstable-small",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@@ -2,7 +2,7 @@
description = "private dev inputs"; description = "private dev inputs";
# Dev dependencies # Dev dependencies
inputs.nixpkgs-dev.url = "github:NixOS/nixpkgs/nixos-25.05-small"; inputs.nixpkgs-dev.url = "github:NixOS/nixpkgs/nixos-unstable-small";
inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.flake-utils.inputs.systems.follows = "systems"; inputs.flake-utils.inputs.systems.follows = "systems";
@@ -15,7 +15,7 @@
inputs.systems.url = "github:nix-systems/default"; inputs.systems.url = "github:nix-systems/default";
inputs.clan-core-for-checks.url = "git+https://git.clan.lol/clan/clan-core?ref=clan-25.05&shallow=1"; inputs.clan-core-for-checks.url = "git+https://git.clan.lol/clan/clan-core?ref=main&shallow=1";
inputs.clan-core-for-checks.flake = false; inputs.clan-core-for-checks.flake = false;
inputs.test-fixtures.url = "git+https://git.clan.lol/clan/test-fixtures"; inputs.test-fixtures.url = "git+https://git.clan.lol/clan/test-fixtures";

17
flake.lock generated
View File

@@ -71,16 +71,15 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1759509947, "lastModified": 1758805352,
"narHash": "sha256-4XifSIHfpJKcCf5bZZRhj8C4aCpjNBaE3kXr02s4rHU=", "narHash": "sha256-BHdc43Lkayd+72W/NXRKHzX5AZ+28F3xaUs3a88/Uew=",
"owner": "nix-darwin", "owner": "nix-darwin",
"repo": "nix-darwin", "repo": "nix-darwin",
"rev": "000eadb231812ad6ea6aebd7526974aaf4e79355", "rev": "c48e963a5558eb1c3827d59d21c5193622a1477c",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-darwin", "owner": "nix-darwin",
"ref": "nix-darwin-25.05",
"repo": "nix-darwin", "repo": "nix-darwin",
"type": "github" "type": "github"
} }
@@ -115,15 +114,15 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1760324802, "lastModified": 315532800,
"narHash": "sha256-VWlJtLQ5EQQj45Wj0yTExtSjwRyZ59/qMqEwus/Exlg=", "narHash": "sha256-1tUpklZsKzMGI3gjo/dWD+hS8cf+5Jji8TF5Cfz7i3I=",
"rev": "7e297ddff44a3cc93673bb38d0374df8d0ad73e4", "rev": "08b8f92ac6354983f5382124fef6006cade4a1c1",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.811135.7e297ddff44a/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre862603.08b8f92ac635/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
"url": "https://nixos.org/channels/nixos-25.05/nixexprs.tar.xz" "url": "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz"
} }
}, },
"root": { "root": {

View File

@@ -2,9 +2,9 @@
description = "clan.lol base operating system"; description = "clan.lol base operating system";
inputs = { inputs = {
nixpkgs.url = "https://nixos.org/channels/nixos-25.05/nixexprs.tar.xz"; nixpkgs.url = "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz";
nix-darwin.url = "github:nix-darwin/nix-darwin/nix-darwin-25.05"; nix-darwin.url = "github:nix-darwin/nix-darwin";
nix-darwin.inputs.nixpkgs.follows = "nixpkgs"; nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.url = "github:hercules-ci/flake-parts";

View File

@@ -11,6 +11,8 @@
treefmt.programs.nixfmt.enable = true; treefmt.programs.nixfmt.enable = true;
treefmt.programs.nixfmt.package = pkgs.nixfmt-rfc-style; treefmt.programs.nixfmt.package = pkgs.nixfmt-rfc-style;
treefmt.programs.deadnix.enable = true; treefmt.programs.deadnix.enable = true;
treefmt.programs.sizelint.enable = true;
treefmt.programs.sizelint.failOnWarn = true;
treefmt.programs.clang-format.enable = true; treefmt.programs.clang-format.enable = true;
treefmt.settings.global.excludes = [ treefmt.settings.global.excludes = [
"*.png" "*.png"

View File

@@ -28,6 +28,7 @@ lib.fix (
# Plain imports. # Plain imports.
introspection = import ./introspection { inherit lib; }; introspection = import ./introspection { inherit lib; };
jsonschema = import ./jsonschema { inherit lib; }; jsonschema = import ./jsonschema { inherit lib; };
facts = import ./facts.nix { inherit lib; };
docs = import ./docs.nix { inherit lib; }; docs = import ./docs.nix { inherit lib; };
# flakes # flakes

71
lib/facts.nix Normal file
View File

@@ -0,0 +1,71 @@
{ lib, ... }:
clanDir:
let
allMachineNames = lib.mapAttrsToList (name: _: name) (builtins.readDir clanDir);
getFactPath = machine: fact: "${clanDir}/machines/${machine}/facts/${fact}";
readFact =
machine: fact:
let
path = getFactPath machine fact;
in
if builtins.pathExists path then builtins.readFile path else null;
# Example:
#
# readFactFromAllMachines zerotier-ip
# => {
# machineA = "1.2.3.4";
# machineB = "5.6.7.8";
# };
readFactFromAllMachines =
fact:
let
machines = allMachineNames;
facts = lib.genAttrs machines (machine: readFact machine fact);
filteredFacts = lib.filterAttrs (_machine: fact: fact != null) facts;
in
filteredFacts;
# all given facts are are set and factvalues are never null.
#
# Example:
#
# readFactsFromAllMachines [ "zerotier-ip" "syncthing.pub" ]
# => {
# machineA =
# {
# "zerotier-ip" = "1.2.3.4";
# "synching.pub" = "1234";
# };
# machineB =
# {
# "zerotier-ip" = "5.6.7.8";
# "synching.pub" = "23456719";
# };
# };
readFactsFromAllMachines =
facts:
let
# machine -> fact -> factvalue
machinesFactsAttrs = lib.genAttrs allMachineNames (
machine: lib.genAttrs facts (fact: readFact machine fact)
);
# remove all machines which don't have all facts set
filteredMachineFactAttrs = lib.filterAttrs (
_machine: values: builtins.all (fact: values.${fact} != null) facts
) machinesFactsAttrs;
in
filteredMachineFactAttrs;
in
{
inherit
allMachineNames
getFactPath
readFact
readFactFromAllMachines
readFactsFromAllMachines
;
}

View File

@@ -1,35 +0,0 @@
# collectFiles helper function
{
lib ? import <nixpkgs/lib>,
}:
let
inherit (lib)
filterAttrs
mapAttrsToList
;
relevantFiles = filterAttrs (
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
);
collectFiles =
generators:
builtins.concatLists (
mapAttrsToList (
gen_name: generator:
mapAttrsToList (fname: file: {
name = fname;
generator = gen_name;
neededForUsers = file.neededFor == "users";
inherit (generator) share;
inherit (file)
owner
group
mode
restartUnits
;
}) (relevantFiles generator.files)
) generators
);
in
collectFiles

View File

@@ -7,19 +7,9 @@
}: }:
let let
collectFiles = import ./collectFiles.nix { inherit lib; }; mapGeneratorsToSopsSecrets = import ./generators-to-sops.nix { inherit lib; };
machineName = config.clan.core.settings.machine.name; machineName = config.clan.core.settings.machine.name;
secretPath =
secret:
if secret.share then
config.clan.core.settings.directory + "/vars/shared/${secret.generator}/${secret.name}/secret"
else
config.clan.core.settings.directory
+ "/vars/per-machine/${machineName}/${secret.generator}/${secret.name}/secret";
vars = collectFiles config.clan.core.vars.generators;
in in
{ {
config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") { config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") {
@@ -39,28 +29,13 @@ in
}; };
config.sops = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") { config.sops = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") {
#
secrets = lib.listToAttrs ( secrets = mapGeneratorsToSopsSecrets {
map (secret: { inherit machineName;
name = "vars/${secret.generator}/${secret.name}"; directory = config.clan.core.settings.directory;
value = { class = _class;
inherit (secret) generators = config.clan.core.vars.generators;
owner
group
mode
neededForUsers
;
sopsFile = builtins.path {
name = "${secret.generator}_${secret.name}";
path = secretPath secret;
}; };
format = "binary";
}
// (lib.optionalAttrs (_class == "nixos") {
inherit (secret) restartUnits;
});
}) (builtins.filter (x: builtins.pathExists (secretPath x)) vars)
);
# To get proper error messages about missing secrets we need a dummy secret file that is always present # To get proper error messages about missing secrets we need a dummy secret file that is always present
defaultSopsFile = lib.mkIf config.sops.validateSopsFiles ( defaultSopsFile = lib.mkIf config.sops.validateSopsFiles (

View File

@@ -0,0 +1,77 @@
# This file maps generators to sops.secrets
# TODO(@davHau): add tests
{
lib ? import <nixpkgs/lib>,
# Can be mocked for testing
pathExists ? builtins.pathExists,
}:
let
inherit (lib)
filterAttrs
mapAttrsToList
;
relevantFiles = filterAttrs (
_name: f: f.secret && f.deploy && (f.neededFor == "users" || f.neededFor == "services")
);
extractSecretDefinitions =
generators:
builtins.concatLists (
mapAttrsToList (
gen_name: generator:
mapAttrsToList (fname: file: {
name = fname;
generator = gen_name;
neededForUsers = file.neededFor == "users";
inherit (generator) share;
inherit (file)
owner
group
mode
restartUnits
;
}) (relevantFiles generator.files)
) generators
);
mapGeneratorsToSopsSecrets =
{
machineName,
directory,
class,
generators,
}:
assert lib.assertMsg (class == "nixos" || class == "darwin")
"Error trying to map 'var.generators' to 'sops.secrets': class must be 'nixos' or 'darwin', got: ${class}";
let
getSecretPath =
secret:
let
scope = if secret.share then "shared" else "per-machine/${machineName}";
in
"${directory}/vars/${scope}/${secret.generator}/${secret.name}/secret";
in
lib.listToAttrs (
map (secret: {
name = "vars/${secret.generator}/${secret.name}";
value = {
inherit (secret)
owner
group
mode
neededForUsers
;
sopsFile = builtins.path {
name = "${secret.generator}_${secret.name}";
path = getSecretPath secret;
};
format = "binary";
}
// (lib.optionalAttrs (class == "nixos") {
inherit (secret) restartUnits;
});
}) (builtins.filter (x: pathExists (getSecretPath x)) (extractSecretDefinitions generators))
);
in
mapGeneratorsToSopsSecrets

View File

@@ -41,7 +41,7 @@ class ApiBridge(Protocol):
def process_request(self, request: BackendRequest) -> None: def process_request(self, request: BackendRequest) -> None:
"""Process an API request through the middleware chain.""" """Process an API request through the middleware chain."""
from clan_app.middleware.base import MiddlewareContext from clan_app.middleware.base import MiddlewareContext # noqa: PLC0415
with ExitStack() as stack: with ExitStack() as stack:
# Capture the current call stack up to this point # Capture the current call stack up to this point
@@ -62,7 +62,7 @@ class ApiBridge(Protocol):
) )
middleware.process(context) middleware.process(context)
except Exception as e: except Exception as e:
from clan_app.middleware.base import ( from clan_app.middleware.base import ( # noqa: PLC0415
MiddlewareError, MiddlewareError,
) )

View File

@@ -191,13 +191,13 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
return file_data return file_data
def do_OPTIONS(self) -> None: # noqa: N802 def do_OPTIONS(self) -> None:
"""Handle CORS preflight requests.""" """Handle CORS preflight requests."""
self.send_response_only(200) self.send_response_only(200)
self._send_cors_headers() self._send_cors_headers()
self.end_headers() self.end_headers()
def do_GET(self) -> None: # noqa: N802 def do_GET(self) -> None:
"""Handle GET requests.""" """Handle GET requests."""
parsed_url = urlparse(self.path) parsed_url = urlparse(self.path)
path = parsed_url.path path = parsed_url.path
@@ -211,7 +211,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
else: else:
self.send_api_error_response("info", "Not Found", ["http_bridge", "GET"]) self.send_api_error_response("info", "Not Found", ["http_bridge", "GET"])
def do_POST(self) -> None: # noqa: N802 def do_POST(self) -> None:
"""Handle POST requests.""" """Handle POST requests."""
parsed_url = urlparse(self.path) parsed_url = urlparse(self.path)
path = parsed_url.path path = parsed_url.path

View File

@@ -34,7 +34,7 @@ class WebviewBridge(ApiBridge):
log.debug(f"Sending response: {serialized}") log.debug(f"Sending response: {serialized}")
# Import FuncStatus locally to avoid circular import # Import FuncStatus locally to avoid circular import
from .webview import FuncStatus from .webview import FuncStatus # noqa: PLC0415
self.webview.return_(response._op_key, FuncStatus.SUCCESS, serialized) # noqa: SLF001 self.webview.return_(response._op_key, FuncStatus.SUCCESS, serialized) # noqa: SLF001

View File

@@ -9,7 +9,7 @@ def main() -> None:
load_in_all_api_functions() load_in_all_api_functions()
# import lazily since we otherwise we do not have all api functions loaded according to Qubasa # import lazily since we otherwise we do not have all api functions loaded according to Qubasa
from clan_lib.api import API from clan_lib.api import API # noqa: PLC0415
schema = API.to_json_schema() schema = API.to_json_schema()
print(f"""{json.dumps(schema, indent=2)}""") print(f"""{json.dumps(schema, indent=2)}""")

View File

@@ -102,7 +102,7 @@ class TestFlake(Flake):
opts: "ListOptions | None" = None, # noqa: ARG002 opts: "ListOptions | None" = None, # noqa: ARG002
) -> "dict[str, MachineResponse]": ) -> "dict[str, MachineResponse]":
"""List machines of a clan""" """List machines of a clan"""
from clan_lib.machines.actions import ( from clan_lib.machines.actions import ( # noqa: PLC0415
InventoryMachine, InventoryMachine,
MachineResponse, MachineResponse,
) )

View File

@@ -231,7 +231,7 @@ def remove_machine_command(args: argparse.Namespace) -> None:
def add_group_argument(parser: argparse.ArgumentParser) -> None: def add_group_argument(parser: argparse.ArgumentParser) -> None:
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_groups, complete_groups,
) )
@@ -334,7 +334,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machines to add", help="the name of the machines to add",
type=machine_name_type, type=machine_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
) )
@@ -353,7 +353,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machines to remove", help="the name of the machines to remove",
type=machine_name_type, type=machine_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
) )
@@ -369,7 +369,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user to add", help="the name of the user to add",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )
@@ -388,7 +388,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user to remove", help="the name of the user to remove",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )
@@ -407,7 +407,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the secret", help="the name of the secret",
type=secret_name_type, type=secret_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_secrets, complete_secrets,
) )
@@ -426,7 +426,7 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the secret", help="the name of the secret",
type=secret_name_type, type=secret_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_secrets, complete_secrets,
) )

View File

@@ -69,7 +69,7 @@ def register_import_sops_parser(parser: argparse.ArgumentParser) -> None:
default=[], default=[],
help="the group to import the secrets to", help="the group to import the secrets to",
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_groups, complete_groups,
) )
@@ -82,7 +82,7 @@ def register_import_sops_parser(parser: argparse.ArgumentParser) -> None:
default=[], default=[],
help="the machine to import the secrets to", help="the machine to import the secrets to",
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
) )
@@ -95,7 +95,7 @@ def register_import_sops_parser(parser: argparse.ArgumentParser) -> None:
default=[], default=[],
help="the user to import the secrets to", help="the user to import the secrets to",
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )

View File

@@ -172,7 +172,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine", help="the name of the machine",
type=machine_name_type, type=machine_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
) )
@@ -192,7 +192,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine", help="the name of the machine",
type=machine_name_type, type=machine_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
) )
@@ -207,7 +207,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine", help="the name of the machine",
type=machine_name_type, type=machine_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
) )
@@ -225,7 +225,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine", help="the name of the machine",
type=machine_name_type, type=machine_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
complete_secrets, complete_secrets,
@@ -250,7 +250,7 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the machine", help="the name of the machine",
type=machine_name_type, type=machine_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
complete_secrets, complete_secrets,

View File

@@ -255,7 +255,7 @@ def add_secret_argument(parser: argparse.ArgumentParser, autocomplete: bool) ->
type=secret_name_type, type=secret_name_type,
) )
if autocomplete: if autocomplete:
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_secrets, complete_secrets,
) )
@@ -467,7 +467,7 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
default=[], default=[],
help="the group to import the secrets to (can be repeated)", help="the group to import the secrets to (can be repeated)",
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_groups, complete_groups,
) )
@@ -480,7 +480,7 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
default=[], default=[],
help="the machine to import the secrets to (can be repeated)", help="the machine to import the secrets to (can be repeated)",
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_machines, complete_machines,
) )
@@ -493,7 +493,7 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
default=[], default=[],
help="the user to import the secrets to (can be repeated)", help="the user to import the secrets to (can be repeated)",
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )

View File

@@ -281,7 +281,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )
@@ -295,7 +295,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )
@@ -312,7 +312,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_secrets, complete_secrets,
complete_users, complete_users,
@@ -336,7 +336,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the group", help="the name of the group",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_secrets, complete_secrets,
complete_users, complete_users,
@@ -360,7 +360,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )
@@ -378,7 +378,7 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
help="the name of the user", help="the name of the user",
type=user_name_type, type=user_name_type,
) )
from clan_cli.completions import ( from clan_cli.completions import ( # noqa: PLC0415
add_dynamic_completer, add_dynamic_completer,
complete_users, complete_users,
) )

View File

@@ -1,6 +1,5 @@
import json import json
import logging import logging
import os
import shutil import shutil
import subprocess import subprocess
import time import time
@@ -430,43 +429,9 @@ def test_generated_shared_secret_sops(
machine1 = Machine(name="machine1", flake=Flake(str(flake.path))) machine1 = Machine(name="machine1", flake=Flake(str(flake.path)))
machine2 = Machine(name="machine2", flake=Flake(str(flake.path))) machine2 = Machine(name="machine2", flake=Flake(str(flake.path)))
cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"]) cli.run(["vars", "generate", "--flake", str(flake.path), "machine1"])
assert check_vars(machine1.name, machine1.flake)
# Get the initial state of the flake directory after generation
def get_file_mtimes(path: str) -> dict[str, float]:
"""Get modification times of all files in a directory tree."""
mtimes = {}
for root, _dirs, files in os.walk(path):
# Skip .git directory
if ".git" in root:
continue
for file in files:
filepath = Path(root) / file
mtimes[str(filepath)] = filepath.stat().st_mtime
return mtimes
initial_mtimes = get_file_mtimes(str(flake.path))
# First check_vars should not write anything
assert check_vars(machine1.name, machine1.flake), (
"machine1 has already generated vars, so check_vars should return True\n"
f"Check result:\n{check_vars(machine1.name, machine1.flake)}"
)
# Verify no files were modified
after_check_mtimes = get_file_mtimes(str(flake.path))
assert initial_mtimes == after_check_mtimes, (
"check_vars should not modify any files when vars are already valid"
)
assert not check_vars(machine2.name, machine2.flake), (
"machine2 has not generated vars yet, so check_vars should return False"
)
# Verify no files were modified
after_check_mtimes_2 = get_file_mtimes(str(flake.path))
assert initial_mtimes == after_check_mtimes_2, (
"check_vars should not modify any files when vars are not valid"
)
cli.run(["vars", "generate", "--flake", str(flake.path), "machine2"]) cli.run(["vars", "generate", "--flake", str(flake.path), "machine2"])
assert check_vars(machine2.name, machine2.flake)
m1_sops_store = sops.SecretStore(machine1.flake) m1_sops_store = sops.SecretStore(machine1.flake)
m2_sops_store = sops.SecretStore(machine2.flake) m2_sops_store = sops.SecretStore(machine2.flake)
# Create generators with machine context for testing # Create generators with machine context for testing

View File

@@ -171,7 +171,7 @@ class StoreBase(ABC):
if generator.share: if generator.share:
log_info = log.info log_info = log.info
else: else:
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine # noqa: PLC0415
machine_obj = Machine(name=generator.machines[0], flake=self.flake) machine_obj = Machine(name=generator.machines[0], flake=self.flake)
log_info = machine_obj.info log_info = machine_obj.info

View File

@@ -3,7 +3,6 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.vars.secret_modules import sops
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
from clan_lib.flake import Flake, require_flake from clan_lib.flake import Flake, require_flake
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
@@ -27,33 +26,13 @@ class VarStatus:
self.unfixed_secret_vars = unfixed_secret_vars self.unfixed_secret_vars = unfixed_secret_vars
self.invalid_generators = invalid_generators self.invalid_generators = invalid_generators
def text(self) -> str:
log = ""
if self.missing_secret_vars:
log += "Missing secret vars:\n"
for var in self.missing_secret_vars:
log += f" - {var.id}\n"
if self.missing_public_vars:
log += "Missing public vars:\n"
for var in self.missing_public_vars:
log += f" - {var.id}\n"
if self.unfixed_secret_vars:
log += "Unfixed secret vars:\n"
for var in self.unfixed_secret_vars:
log += f" - {var.id}\n"
if self.invalid_generators:
log += "Invalid generators (outdated invalidation hash):\n"
for gen in self.invalid_generators:
log += f" - {gen}\n"
return log if log else "All vars are present and valid."
def vars_status( def vars_status(
machine_name: str, machine_name: str,
flake: Flake, flake: Flake,
generator_name: None | str = None, generator_name: None | str = None,
) -> VarStatus: ) -> VarStatus:
from clan_cli.vars.generator import Generator from clan_cli.vars.generator import Generator # noqa: PLC0415
machine = Machine(name=machine_name, flake=flake) machine = Machine(name=machine_name, flake=flake)
missing_secret_vars = [] missing_secret_vars = []
@@ -87,32 +66,15 @@ def vars_status(
f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} is missing.", f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} is missing.",
) )
missing_secret_vars.append(file) missing_secret_vars.append(file)
if (
isinstance(machine.secret_vars_store, sops.SecretStore)
and generator.share
and file.exists
and not machine.secret_vars_store.machine_has_access(
generator=generator,
secret_name=file.name,
machine=machine.name,
)
):
msg = (
f"Secret var '{generator.name}/{file.name}' is marked for deployment to machine '{machine.name}', but the machine does not have access to it.\n"
f"Run 'clan vars generate {machine.name}' to fix this.\n"
)
machine.info(msg)
missing_secret_vars.append(file)
else: else:
health_msg = machine.secret_vars_store.health_check( msg = machine.secret_vars_store.health_check(
machine=machine.name, machine=machine.name,
generators=[generator], generators=[generator],
file_name=file.name, file_name=file.name,
) )
if health_msg is not None: if msg:
machine.info( machine.info(
f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} needs update: {health_msg}", f"Secret var '{file.name}' for service '{generator.name}' in machine {machine.name} needs update: {msg}",
) )
unfixed_secret_vars.append(file) unfixed_secret_vars.append(file)
@@ -144,7 +106,6 @@ def check_vars(
generator_name: None | str = None, generator_name: None | str = None,
) -> bool: ) -> bool:
status = vars_status(machine_name, flake, generator_name=generator_name) status = vars_status(machine_name, flake, generator_name=generator_name)
log.info(f"Check results for machine '{machine_name}': \n{status.text()}")
return not ( return not (
status.missing_secret_vars status.missing_secret_vars
or status.missing_public_vars or status.missing_public_vars

View File

@@ -259,10 +259,6 @@ class Generator:
_secret_store=sec_store, _secret_store=sec_store,
) )
# link generator to its files
for file in files:
file.generator(generator)
if share: if share:
# For shared generators, check if we already created it # For shared generators, check if we already created it
existing = next( existing = next(
@@ -482,7 +478,7 @@ class Generator:
if sys.platform == "linux" and bwrap.bubblewrap_works(): if sys.platform == "linux" and bwrap.bubblewrap_works():
cmd = bubblewrap_cmd(str(final_script), tmpdir) cmd = bubblewrap_cmd(str(final_script), tmpdir)
elif sys.platform == "darwin": elif sys.platform == "darwin":
from clan_lib.sandbox_exec import sandbox_exec_cmd from clan_lib.sandbox_exec import sandbox_exec_cmd # noqa: PLC0415
cmd = stack.enter_context(sandbox_exec_cmd(str(final_script), tmpdir)) cmd = stack.enter_context(sandbox_exec_cmd(str(final_script), tmpdir))
else: else:

View File

@@ -54,7 +54,7 @@ class SecretStore(StoreBase):
def ensure_machine_key(self, machine: str) -> None: def ensure_machine_key(self, machine: str) -> None:
"""Ensure machine has sops keys initialized.""" """Ensure machine has sops keys initialized."""
# no need to generate keys if we don't manage secrets # no need to generate keys if we don't manage secrets
from clan_cli.vars.generator import Generator from clan_cli.vars.generator import Generator # noqa: PLC0415
vars_generators = Generator.get_machine_generators([machine], self.flake) vars_generators = Generator.get_machine_generators([machine], self.flake)
if not vars_generators: if not vars_generators:
@@ -98,8 +98,7 @@ class SecretStore(StoreBase):
def machine_has_access( def machine_has_access(
self, generator: Generator, secret_name: str, machine: str self, generator: Generator, secret_name: str, machine: str
) -> bool: ) -> bool:
if not has_machine(self.flake.path, machine): self.ensure_machine_key(machine)
return False
key_dir = sops_machines_folder(self.flake.path) / machine key_dir = sops_machines_folder(self.flake.path) / machine
return self.key_has_access(key_dir, generator, secret_name) return self.key_has_access(key_dir, generator, secret_name)
@@ -143,7 +142,7 @@ class SecretStore(StoreBase):
""" """
if generators is None: if generators is None:
from clan_cli.vars.generator import Generator from clan_cli.vars.generator import Generator # noqa: PLC0415
generators = Generator.get_machine_generators([machine], self.flake) generators = Generator.get_machine_generators([machine], self.flake)
file_found = False file_found = False
@@ -157,6 +156,8 @@ class SecretStore(StoreBase):
else: else:
continue continue
if file.secret and self.exists(generator, file.name): if file.secret and self.exists(generator, file.name):
if file.deploy:
self.ensure_machine_has_access(generator, file.name, machine)
needs_update, msg = self.needs_fix(generator, file.name, machine) needs_update, msg = self.needs_fix(generator, file.name, machine)
if needs_update: if needs_update:
outdated.append((generator.name, file.name, msg)) outdated.append((generator.name, file.name, msg))
@@ -218,7 +219,7 @@ class SecretStore(StoreBase):
return [store_folder] return [store_folder]
def populate_dir(self, machine: str, output_dir: Path, phases: list[str]) -> None: def populate_dir(self, machine: str, output_dir: Path, phases: list[str]) -> None:
from clan_cli.vars.generator import Generator from clan_cli.vars.generator import Generator # noqa: PLC0415
vars_generators = Generator.get_machine_generators([machine], self.flake) vars_generators = Generator.get_machine_generators([machine], self.flake)
if "users" in phases or "services" in phases: if "users" in phases or "services" in phases:
@@ -282,7 +283,6 @@ class SecretStore(StoreBase):
) -> None: ) -> None:
if self.machine_has_access(generator, name, machine): if self.machine_has_access(generator, name, machine):
return return
self.ensure_machine_key(machine)
secret_folder = self.secret_path(generator, name) secret_folder = self.secret_path(generator, name)
add_secret( add_secret(
self.flake.path, self.flake.path,
@@ -292,7 +292,7 @@ class SecretStore(StoreBase):
) )
def collect_keys_for_secret(self, machine: str, path: Path) -> set[sops.SopsKey]: def collect_keys_for_secret(self, machine: str, path: Path) -> set[sops.SopsKey]:
from clan_cli.secrets.secrets import ( from clan_cli.secrets.secrets import ( # noqa: PLC0415
collect_keys_for_path, collect_keys_for_path,
collect_keys_for_type, collect_keys_for_type,
) )
@@ -354,10 +354,10 @@ class SecretStore(StoreBase):
ClanError: If the specified file_name is not found ClanError: If the specified file_name is not found
""" """
from clan_cli.secrets.secrets import update_keys from clan_cli.secrets.secrets import update_keys # noqa: PLC0415
if generators is None: if generators is None:
from clan_cli.vars.generator import Generator from clan_cli.vars.generator import Generator # noqa: PLC0415
generators = Generator.get_machine_generators([machine], self.flake) generators = Generator.get_machine_generators([machine], self.flake)
file_found = False file_found = False

View File

@@ -319,9 +319,9 @@ def load_in_all_api_functions() -> None:
We have to make sure python loads every wrapped function at least once. We have to make sure python loads every wrapped function at least once.
This is done by importing all modules from the clan_lib and clan_cli packages. This is done by importing all modules from the clan_lib and clan_cli packages.
""" """
import clan_cli # Avoid circular imports - many modules import from clan_lib.api import clan_cli # noqa: PLC0415 # Avoid circular imports - many modules import from clan_lib.api
import clan_lib # Avoid circular imports - many modules import from clan_lib.api import clan_lib # noqa: PLC0415 # Avoid circular imports - many modules import from clan_lib.api
import_all_modules_from_package(clan_lib) import_all_modules_from_package(clan_lib)
import_all_modules_from_package(clan_cli) import_all_modules_from_package(clan_cli)

View File

@@ -88,7 +88,7 @@ def list_system_storage_devices() -> Blockdevices:
A list of detected block devices with metadata like size, path, type, etc. A list of detected block devices with metadata like size, path, type, etc.
""" """
from clan_lib.nix import nix_shell from clan_lib.nix import nix_shell # noqa: PLC0415
cmd = nix_shell( cmd = nix_shell(
["util-linux"], ["util-linux"],
@@ -124,7 +124,7 @@ def get_clan_directory_relative(flake: Flake) -> str:
ClanError: If the flake evaluation fails or directories cannot be found ClanError: If the flake evaluation fails or directories cannot be found
""" """
from clan_lib.dirs import get_clan_directories from clan_lib.dirs import get_clan_directories # noqa: PLC0415
_, relative_dir = get_clan_directories(flake) _, relative_dir = get_clan_directories(flake)
return relative_dir return relative_dir

View File

@@ -1162,7 +1162,7 @@ class Flake:
opts: "ListOptions | None" = None, opts: "ListOptions | None" = None,
) -> "dict[str, MachineResponse]": ) -> "dict[str, MachineResponse]":
"""List machines of a clan""" """List machines of a clan"""
from clan_lib.machines.actions import list_machines from clan_lib.machines.actions import list_machines # noqa: PLC0415
return list_machines(self, opts) return list_machines(self, opts)

View File

@@ -18,14 +18,14 @@ def locked_open(filename: Path, mode: str = "r") -> Generator:
def write_history_file(data: Any) -> None: def write_history_file(data: Any) -> None:
from clan_lib.dirs import user_history_file from clan_lib.dirs import user_history_file # noqa: PLC0415
with locked_open(user_history_file(), "w+") as f: with locked_open(user_history_file(), "w+") as f:
f.write(json.dumps(data, cls=ClanJSONEncoder, indent=4)) f.write(json.dumps(data, cls=ClanJSONEncoder, indent=4))
def read_history_file() -> list[dict]: def read_history_file() -> list[dict]:
from clan_lib.dirs import user_history_file from clan_lib.dirs import user_history_file # noqa: PLC0415
with locked_open(user_history_file(), "r") as f: with locked_open(user_history_file(), "r") as f:
content: str = f.read() content: str = f.read()

View File

@@ -33,7 +33,7 @@ class Machine:
def get_inv_machine(self) -> "InventoryMachine": def get_inv_machine(self) -> "InventoryMachine":
# Import on demand to avoid circular imports # Import on demand to avoid circular imports
from clan_lib.machines.actions import get_machine from clan_lib.machines.actions import get_machine # noqa: PLC0415
return get_machine(self.flake, self.name) return get_machine(self.flake, self.name)
@@ -95,7 +95,7 @@ class Machine:
@cached_property @cached_property
def secret_vars_store(self) -> StoreBase: def secret_vars_store(self) -> StoreBase:
from clan_cli.vars.secret_modules import password_store from clan_cli.vars.secret_modules import password_store # noqa: PLC0415
secret_module = self.select("config.clan.core.vars.settings.secretModule") secret_module = self.select("config.clan.core.vars.settings.secretModule")
module = importlib.import_module(secret_module) module = importlib.import_module(secret_module)
@@ -126,7 +126,7 @@ class Machine:
return self.flake.path return self.flake.path
def target_host(self) -> Remote: def target_host(self) -> Remote:
from clan_lib.network.network import get_best_remote from clan_lib.network.network import get_best_remote # noqa: PLC0415
with get_best_remote(self) as remote: with get_best_remote(self) as remote:
return remote return remote

View File

@@ -42,7 +42,7 @@ def _suggest_similar_names(
def get_available_machines(flake: Flake) -> list[str]: def get_available_machines(flake: Flake) -> list[str]:
from clan_lib.machines.list import list_machines from clan_lib.machines.list import list_machines # noqa: PLC0415
machines = list_machines(flake) machines = list_machines(flake)
return list(machines.keys()) return list(machines.keys())

View File

@@ -34,7 +34,7 @@ class Peer:
_var: dict[str, str] = self._host["var"] _var: dict[str, str] = self._host["var"]
machine_name = _var["machine"] machine_name = _var["machine"]
generator = _var["generator"] generator = _var["generator"]
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine # noqa: PLC0415
machine = Machine(name=machine_name, flake=self.flake) machine = Machine(name=machine_name, flake=self.flake)
var = get_machine_var( var = get_machine_var(

View File

@@ -88,7 +88,7 @@ def nix_eval(flags: list[str]) -> list[str]:
], ],
) )
if os.environ.get("IN_NIX_SANDBOX"): if os.environ.get("IN_NIX_SANDBOX"):
from clan_lib.dirs import nixpkgs_source from clan_lib.dirs import nixpkgs_source # noqa: PLC0415
return [ return [
*default_flags, *default_flags,
@@ -169,7 +169,7 @@ def nix_shell(packages: list[str], cmd: list[str]) -> list[str]:
if not missing_packages: if not missing_packages:
return cmd return cmd
from clan_lib.dirs import nixpkgs_flake from clan_lib.dirs import nixpkgs_flake # noqa: PLC0415
return [ return [
*nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]), *nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]),

View File

@@ -464,12 +464,12 @@ class Remote:
self, self,
opts: "ConnectionOptions | None" = None, opts: "ConnectionOptions | None" = None,
) -> None: ) -> None:
from clan_lib.network.check import check_machine_ssh_reachable from clan_lib.network.check import check_machine_ssh_reachable # noqa: PLC0415
return check_machine_ssh_reachable(self, opts) return check_machine_ssh_reachable(self, opts)
def check_machine_ssh_login(self) -> None: def check_machine_ssh_login(self) -> None:
from clan_lib.network.check import check_machine_ssh_login from clan_lib.network.check import check_machine_ssh_login # noqa: PLC0415
return check_machine_ssh_login(self) return check_machine_ssh_login(self)

View File

@@ -5,7 +5,6 @@ from clan_cli.vars import graph
from clan_cli.vars.generator import Generator from clan_cli.vars.generator import Generator
from clan_cli.vars.graph import requested_closure from clan_cli.vars.graph import requested_closure
from clan_cli.vars.migration import check_can_migrate, migrate_files from clan_cli.vars.migration import check_can_migrate, migrate_files
from clan_cli.vars.secret_modules import sops
from clan_lib.api import API from clan_lib.api import API
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
@@ -153,15 +152,15 @@ def run_generators(
if not machines: if not machines:
msg = "At least one machine must be provided" msg = "At least one machine must be provided"
raise ClanError(msg) raise ClanError(msg)
all_generators = get_generators(machines, full_closure=True)
if isinstance(generators, list): if isinstance(generators, list):
# List of generator names - use them exactly as provided # List of generator names - use them exactly as provided
if len(generators) == 0: if len(generators) == 0:
return return
generators_to_run = [g for g in all_generators if g.key.name in generators] all_generators = get_generators(machines, full_closure=True)
generator_objects = [g for g in all_generators if g.key.name in generators]
else: else:
# None or single string - use get_generators with closure parameter # None or single string - use get_generators with closure parameter
generators_to_run = get_generators( generator_objects = get_generators(
machines, machines,
full_closure=full_closure, full_closure=full_closure,
generator_name=generators, generator_name=generators,
@@ -171,30 +170,13 @@ def run_generators(
# TODO: make this more lazy and ask for every generator on execution # TODO: make this more lazy and ask for every generator on execution
if callable(prompt_values): if callable(prompt_values):
prompt_values = { prompt_values = {
generator.name: prompt_values(generator) for generator in generators_to_run generator.name: prompt_values(generator) for generator in generator_objects
} }
# execute health check # execute health check
for machine in machines: for machine in machines:
_ensure_healthy(machine=machine) _ensure_healthy(machine=machine)
# ensure all selected machines have access to all selected shared generators
for machine in machines:
# This is only relevant for the sops store
# TODO: improve store abstraction to use Protocols and introduce a proper SecretStore interface
if not isinstance(machine.secret_vars_store, sops.SecretStore):
continue
for generator in all_generators:
if generator.share:
for file in generator.files:
if not file.secret or not file.exists:
continue
machine.secret_vars_store.ensure_machine_has_access(
generator,
file.name,
machine.name,
)
# get the flake via any machine (they are all the same) # get the flake via any machine (they are all the same)
flake = machines[0].flake flake = machines[0].flake
@@ -206,13 +188,13 @@ def run_generators(
# preheat the select cache, to reduce repeated calls during execution # preheat the select cache, to reduce repeated calls during execution
selectors = [] selectors = []
for generator in generators_to_run: for generator in generator_objects:
machine = get_generator_machine(generator) machine = get_generator_machine(generator)
selectors.append(generator.final_script_selector(machine.name)) selectors.append(generator.final_script_selector(machine.name))
flake.precache(selectors) flake.precache(selectors)
# execute generators # execute generators
for generator in generators_to_run: for generator in generator_objects:
machine = get_generator_machine(generator) machine = get_generator_machine(generator)
if check_can_migrate(machine, generator): if check_can_migrate(machine, generator):
migrate_files(machine, generator) migrate_files(machine, generator)

View File

@@ -290,7 +290,9 @@ def collect_commands() -> list[Category]:
# 3. sort by title alphabetically # 3. sort by title alphabetically
return (c.title.split(" ")[0], c.title, weight) return (c.title.split(" ")[0], c.title, weight)
return sorted(result, key=weight_cmd_groups) result = sorted(result, key=weight_cmd_groups)
return result
def build_command_reference() -> None: def build_command_reference() -> None:

View File

@@ -36,7 +36,7 @@ class MPProcess:
def _set_proc_name(name: str) -> None: def _set_proc_name(name: str) -> None:
if sys.platform != "linux": if sys.platform != "linux":
return return
import ctypes import ctypes # noqa: PLC0415
# Define the prctl function with the appropriate arguments and return type # Define the prctl function with the appropriate arguments and return type
libc = ctypes.CDLL("libc.so.6") libc = ctypes.CDLL("libc.so.6")

View File

@@ -759,12 +759,12 @@ class Win32Implementation(BaseImplementation):
SM_CXSMICON = 49 SM_CXSMICON = 49
if sys.platform == "win32": if sys.platform == "win32":
from ctypes import Structure from ctypes import Structure # noqa: PLC0415
class WNDCLASSW(Structure): class WNDCLASSW(Structure):
"""Windows class structure for window registration.""" """Windows class structure for window registration."""
from ctypes import CFUNCTYPE, wintypes from ctypes import CFUNCTYPE, wintypes # noqa: PLC0415
LPFN_WND_PROC = CFUNCTYPE( LPFN_WND_PROC = CFUNCTYPE(
wintypes.INT, wintypes.INT,
@@ -789,7 +789,7 @@ class Win32Implementation(BaseImplementation):
class MENUITEMINFOW(Structure): class MENUITEMINFOW(Structure):
"""Windows menu item information structure.""" """Windows menu item information structure."""
from ctypes import wintypes from ctypes import wintypes # noqa: PLC0415
_fields_: ClassVar = [ _fields_: ClassVar = [
("cb_size", wintypes.UINT), ("cb_size", wintypes.UINT),
@@ -809,7 +809,7 @@ class Win32Implementation(BaseImplementation):
class NOTIFYICONDATAW(Structure): class NOTIFYICONDATAW(Structure):
"""Windows notification icon data structure.""" """Windows notification icon data structure."""
from ctypes import wintypes from ctypes import wintypes # noqa: PLC0415
_fields_: ClassVar = [ _fields_: ClassVar = [
("cb_size", wintypes.DWORD), ("cb_size", wintypes.DWORD),
@@ -1061,7 +1061,7 @@ class Win32Implementation(BaseImplementation):
if sys.platform != "win32": if sys.platform != "win32":
return return
from ctypes import wintypes from ctypes import wintypes # noqa: PLC0415
if self._menu is None: if self._menu is None:
self.update_menu() self.update_menu()
@@ -1110,7 +1110,7 @@ class Win32Implementation(BaseImplementation):
if sys.platform != "win32": if sys.platform != "win32":
return 0 return 0
from ctypes import wintypes from ctypes import wintypes # noqa: PLC0415
if msg == self.WM_TRAYICON: if msg == self.WM_TRAYICON:
if l_param == self.WM_RBUTTONUP: if l_param == self.WM_RBUTTONUP: