From b8ba8b79cad98726b3737c4a43ba9d6dc79e42ca Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:02:35 +0200 Subject: [PATCH 01/12] api/check_machine_ssh_reachable: add function docs --- pkgs/clan-cli/clan_lib/ssh/remote.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/ssh/remote.py b/pkgs/clan-cli/clan_lib/ssh/remote.py index ec8c4e5d0..5356cf9ee 100644 --- a/pkgs/clan-cli/clan_lib/ssh/remote.py +++ b/pkgs/clan-cli/clan_lib/ssh/remote.py @@ -470,6 +470,22 @@ def check_machine_ssh_login( def check_machine_ssh_reachable( remote: Remote, opts: ConnectionOptions | None = None ) -> CheckResult: + """ + Checks if a remote machine is reachable via SSH by attempting to open a TCP connection + to the specified address and port. + Args: + remote (Remote): The remote host to check for SSH reachability. + opts (ConnectionOptions, optional): Connection options such as timeout and number of retries. + If not provided, default values are used. + Returns: + CheckResult: An object indicating whether the SSH port is reachable (`ok=True`) or not (`ok=False`), + and a reason if the check failed. + Usage: + result = check_machine_ssh_reachable(remote) + if result.ok: + print("SSH port is reachable") + print(f"SSH port is not reachable: {result.reason}") + """ if opts is None: opts = ConnectionOptions() From ec28c5c3070d5f35faa055940008594959c51d83 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:13:23 +0200 Subject: [PATCH 02/12] api/machines: document {get_machine,get_machine_details} --- pkgs/clan-cli/clan_lib/machines/actions.py | 13 +++++++++++++ pkgs/clan-cli/clan_lib/machines/list.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/machines/actions.py b/pkgs/clan-cli/clan_lib/machines/actions.py index 4c19e7a23..93c599a1b 100644 --- a/pkgs/clan-cli/clan_lib/machines/actions.py +++ b/pkgs/clan-cli/clan_lib/machines/actions.py @@ -54,6 +54,19 @@ def list_machines( @API.register def get_machine(flake: Flake, name: str) -> InventoryMachine: + """ + Retrieve a machine's inventory details by name from the given flake. + + Args: + flake (Flake): The flake object representing the configuration source. + name (str): The name of the machine to retrieve from the inventory. + + Returns: + InventoryMachine: An instance representing the machine's inventory details. + + Raises: + ClanError: If the machine with the specified name is not found in the inventory. + """ inventory_store = InventoryStore(flake=flake) inventory = inventory_store.read() diff --git a/pkgs/clan-cli/clan_lib/machines/list.py b/pkgs/clan-cli/clan_lib/machines/list.py index 0fc152f6e..7fbe4b3ac 100644 --- a/pkgs/clan-cli/clan_lib/machines/list.py +++ b/pkgs/clan-cli/clan_lib/machines/list.py @@ -52,8 +52,22 @@ def extract_header(c: str) -> str: return "\n".join(header_lines) +# TODO: Remove this function +# Split out the disko schema extraction into a separate function +# get machine returns the machine already @API.register def get_machine_details(machine: Machine) -> MachineDetails: + """Retrieve detailed information about a machine, including its inventory, + hardware configuration, and disk schema if available. + Args: + machine (Machine): The machine instance for which details are to be retrieved. + Returns: + MachineDetails: An instance containing the machine's inventory, hardware configuration, + and disk schema. + Raises: + ClanError: If the machine's inventory cannot be found or if there are issues with the + hardware configuration or disk schema extraction. + """ machine_inv = get_machine(machine.flake, machine.name) hw_config = HardwareConfig.detect_type(machine) From b5a6e809d062255bfb55920cc76bef32a7669d3b Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:22:44 +0200 Subject: [PATCH 03/12] docs/api: add docstrings to {get_generators, run_generators} --- pkgs/clan-cli/clan_cli/vars/generate.py | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pkgs/clan-cli/clan_cli/vars/generate.py b/pkgs/clan-cli/clan_cli/vars/generate.py index 3c3764a75..85c98ff56 100644 --- a/pkgs/clan-cli/clan_cli/vars/generate.py +++ b/pkgs/clan-cli/clan_cli/vars/generate.py @@ -429,6 +429,21 @@ def get_generators( full_closure: bool = False, include_previous_values: bool = False, ) -> list[Generator]: + """ + Get the list of generators for a machine, optionally with previous values. + If `full_closure` is True, it returns the full closure of generators. + If `include_previous_values` is True, it includes the previous values for prompts. + + Args: + machine_name (str): The name of the machine. + base_dir (Path): The base directory of the flake. + full_closure (bool): Whether to return the full closure of generators. If False, + it returns only the generators that are missing or need to be regenerated. + include_previous_values (bool): Whether to include previous values for prompts. + Returns: + list[Generator]: A list of generators for the machine. + + """ from clan_lib.machines.machines import Machine return get_closure( @@ -468,6 +483,20 @@ def run_generators( base_dir: Path, no_sandbox: bool = False, ) -> bool: + """Run the specified generators for a machine. + Args: + machine_name (str): The name of the machine. + generators (list[str]): The list of generator names to run. + all_prompt_values (dict[str, dict[str, str]]): A dictionary mapping generator names + to their prompt values. + base_dir (Path): The base directory of the flake. + no_sandbox (bool): Whether to disable sandboxing when executing the generator. + Returns: + bool: True if any variables were generated, False otherwise. + Raises: + ClanError: If the machine or generator is not found, or if there are issues with + executing the generator. + """ from clan_lib.machines.machines import Machine machine = Machine(name=machine_name, flake=Flake(str(base_dir))) From 2bff7403dfff45873bdb39a1b9fb80f73f8f69af Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:29:19 +0200 Subject: [PATCH 04/12] docs/api: add docstrings to {create_clan} --- pkgs/clan-cli/clan_lib/clan/create.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/clan/create.py b/pkgs/clan-cli/clan_lib/clan/create.py index 81272977b..c00f9f131 100644 --- a/pkgs/clan-cli/clan_lib/clan/create.py +++ b/pkgs/clan-cli/clan_lib/clan/create.py @@ -31,6 +31,15 @@ def git_command(directory: Path, *args: str) -> list[str]: @API.register def create_clan(opts: CreateOptions) -> None: + """Create a new clan repository with the specified template. + Args: + opts: CreateOptions containing the destination path, template name, + source flake, and other options. + Raises: + ClanError: If the source flake is not a valid flake or if the destination + directory already exists. + """ + dest = opts.dest.resolve() if opts.src_flake is not None: From 508cd3c784fa61c2af1f1bab97c296fba0adc25e Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:31:06 +0200 Subject: [PATCH 05/12] docs/api: add docstrings to {get_clan_details} --- pkgs/clan-cli/clan_lib/clan/get.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/clan/get.py b/pkgs/clan-cli/clan_lib/clan/get.py index 5fb43f31d..3127c80bd 100644 --- a/pkgs/clan-cli/clan_lib/clan/get.py +++ b/pkgs/clan-cli/clan_lib/clan/get.py @@ -7,6 +7,14 @@ from clan_lib.persist.inventory_store import InventoryStore @API.register def get_clan_details(flake: Flake) -> InventoryMeta: + """Retrieve the clan details from the inventory of a given flake. + Args: + flake: The Flake instance representing the clan. + Returns: + InventoryMeta: The meta information from the clan's inventory. + Raises: + ClanError: If the flake does not exist, or if the inventory is invalid (missing the meta attribute). + """ if flake.is_local and not flake.path.exists(): msg = f"Path {flake} does not exist" raise ClanError(msg, description="clan directory does not exist") From 3b309ea74b0385072af09a8f3f3b28880c210870 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:34:49 +0200 Subject: [PATCH 06/12] docs/api: add docstrings to {get_flash_options, run_machine_flash} --- pkgs/clan-cli/clan_cli/flash/flash.py | 15 +++++++++++++++ pkgs/clan-cli/clan_cli/flash/list.py | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/pkgs/clan-cli/clan_cli/flash/flash.py b/pkgs/clan-cli/clan_cli/flash/flash.py index 9f2989ba3..d43ffdd9f 100644 --- a/pkgs/clan-cli/clan_cli/flash/flash.py +++ b/pkgs/clan-cli/clan_cli/flash/flash.py @@ -49,6 +49,21 @@ def run_machine_flash( extra_args: list[str] | None = None, graphical: bool = False, ) -> None: + """Flash a machine with the given configuration. + Args: + machine: The Machine instance to flash. + mode: The mode to use for flashing (e.g., "install", "reinstall + disks: List of Disk instances representing the disks to flash. + system_config: SystemConfig instance containing language, keymap, and SSH keys. + dry_run: If True, perform a dry run without making changes. + write_efi_boot_entries: If True, write EFI boot entries. + debug: If True, enable debug mode. + extra_args: Additional arguments to pass to the disko-install command. + graphical: If True, run the command in graphical mode. + Raises: + ClanError: If the language or keymap is invalid, or if there are issues with + reading SSH keys, or if disko-install fails. + """ devices = [Path(disk.device) for disk in disks] with pause_automounting(devices, machine, request_graphical=graphical): if extra_args is None: diff --git a/pkgs/clan-cli/clan_cli/flash/list.py b/pkgs/clan-cli/clan_cli/flash/list.py index 146f0bcb4..cbcfdace2 100644 --- a/pkgs/clan-cli/clan_cli/flash/list.py +++ b/pkgs/clan-cli/clan_cli/flash/list.py @@ -19,6 +19,12 @@ class FlashOptions(TypedDict): @API.register def get_flash_options() -> FlashOptions: + """Retrieve available languages and keymaps for flash configuration. + Returns: + FlashOptions: A dictionary containing lists of available languages and keymaps. + Raises: + ClanError: If the locale file or keymaps directory does not exist. + """ return {"languages": list_languages(), "keymaps": list_keymaps()} From 59a8c402bacd593849a9de6f0c60aba9b9051f16 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:36:16 +0200 Subject: [PATCH 07/12] docs/api: add docstrings to {delete_machine} --- pkgs/clan-cli/clan_lib/machines/delete.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/machines/delete.py b/pkgs/clan-cli/clan_lib/machines/delete.py index 7d642a1d7..86be5abb6 100644 --- a/pkgs/clan-cli/clan_lib/machines/delete.py +++ b/pkgs/clan-cli/clan_lib/machines/delete.py @@ -19,6 +19,13 @@ log = logging.getLogger(__name__) @API.register def delete_machine(machine: Machine) -> None: + """Delete a machine from the clan's inventory and remove its associated files. + Args: + machine: The Machine instance to be deleted. + Raises: + ClanError: If the machine does not exist in the inventory or if there are issues with + removing its files. + """ inventory_store = InventoryStore(machine.flake) try: inventory_store.delete( From d462ae501e45333222f8cd919dfbefd661e2c2f0 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:38:09 +0200 Subject: [PATCH 08/12] docs/api: add docstrings to {check_machine_ssh_login} --- pkgs/clan-cli/clan_lib/ssh/remote.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/ssh/remote.py b/pkgs/clan-cli/clan_lib/ssh/remote.py index 5356cf9ee..83e49b163 100644 --- a/pkgs/clan-cli/clan_lib/ssh/remote.py +++ b/pkgs/clan-cli/clan_lib/ssh/remote.py @@ -444,6 +444,21 @@ class CheckResult: def check_machine_ssh_login( remote: Remote, opts: ConnectionOptions | None = None ) -> CheckResult: + """Checks if a remote machine is reachable via SSH by attempting to run a simple command. + Args: + remote (Remote): The remote host to check for SSH login. + opts (ConnectionOptions, optional): Connection options such as timeout and number of retries. + If not provided, default values are used. + Returns: + CheckResult: An object indicating whether the SSH login is successful (`ok=True`) or not (`ok=False`), + and a reason if the check failed. + Usage: + result = check_machine_ssh_login(remote) + if result.ok: + print("SSH login successful") + else: + print(f"SSH login failed: {result.reason}") + """ if opts is None: opts = ConnectionOptions() From 655b87ad04bdfdcd9a303f38da5c67a89142e34d Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:41:02 +0200 Subject: [PATCH 09/12] docs/api: add docstrings to {run_machine_install,run_machine_deploy} --- pkgs/clan-cli/clan_lib/machines/install.py | 10 ++++++++++ pkgs/clan-cli/clan_lib/machines/update.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/machines/install.py b/pkgs/clan-cli/clan_lib/machines/install.py index ef33e6222..7627c1160 100644 --- a/pkgs/clan-cli/clan_lib/machines/install.py +++ b/pkgs/clan-cli/clan_lib/machines/install.py @@ -40,6 +40,16 @@ class InstallOptions: @API.register def run_machine_install(opts: InstallOptions, target_host: Remote) -> None: + """Install a machine using nixos-anywhere. + Args: + opts: InstallOptions containing the machine to install, kexec option, debug mode, + no-reboot option, phases, build-on option, hardware config update, password, + identity file, and use_tor flag. + target_host: Remote object representing the target host for installation. + Raises: + ClanError: If the machine is not found in the inventory or if there are issues with + generating facts or variables. + """ machine = opts.machine machine.debug(f"installing {machine.name}") diff --git a/pkgs/clan-cli/clan_lib/machines/update.py b/pkgs/clan-cli/clan_lib/machines/update.py index 62cd98c75..dfb725525 100644 --- a/pkgs/clan-cli/clan_lib/machines/update.py +++ b/pkgs/clan-cli/clan_lib/machines/update.py @@ -106,6 +106,16 @@ def upload_sources(machine: Machine, ssh: Remote) -> str: def run_machine_deploy( machine: Machine, target_host: Remote, build_host: Remote | None ) -> None: + """Update an existing machine using nixos-rebuild or darwin-rebuild. + Args: + machine: The Machine instance to deploy. + target_host: Remote object representing the target host for deployment. + build_host: Optional Remote object representing the build host. + Raises: + ClanError: If the machine is not found in the inventory or if there are issues with + generating facts or variables. + """ + with ExitStack() as stack: target_host = stack.enter_context(target_host.ssh_control_master()) From f2cb6fef412f229d506ffaa4f888b634f1fa7102 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:45:51 +0200 Subject: [PATCH 10/12] api: remove unused get_directory --- pkgs/clan-cli/clan_lib/api/directory.py | 53 ------------------------- 1 file changed, 53 deletions(-) diff --git a/pkgs/clan-cli/clan_lib/api/directory.py b/pkgs/clan-cli/clan_lib/api/directory.py index a0cbcb0e9..1b723e716 100644 --- a/pkgs/clan-cli/clan_lib/api/directory.py +++ b/pkgs/clan-cli/clan_lib/api/directory.py @@ -1,12 +1,8 @@ import json -import os from dataclasses import dataclass, field -from pathlib import Path from typing import Any, Literal from clan_lib.cmd import RunOpts, run -from clan_lib.errors import ClanError -from clan_lib.flake import Flake from clan_lib.nix import nix_shell from . import API @@ -42,55 +38,6 @@ def open_file(file_request: FileRequest) -> list[str] | None: raise NotImplementedError(msg) -@dataclass -class File: - path: str - file_type: Literal["file", "directory", "symlink"] - - -@dataclass -class Directory: - path: str - files: list[File] = field(default_factory=list) - - -@API.register -def get_directory(flake: Flake) -> Directory: - curr_dir = flake.path - directory = Directory(path=str(curr_dir)) - - if not curr_dir.is_dir(): - msg = f"Path {curr_dir} is not a directory" - raise ClanError(msg) - - with os.scandir(curr_dir.resolve()) as it: - for entry in it: - if entry.is_symlink(): - directory.files.append( - File( - path=str(curr_dir / Path(entry.name)), - file_type="symlink", - ) - ) - elif entry.is_file(): - directory.files.append( - File( - path=str(curr_dir / Path(entry.name)), - file_type="file", - ) - ) - - elif entry.is_dir(): - directory.files.append( - File( - path=str(curr_dir / Path(entry.name)), - file_type="directory", - ) - ) - - return directory - - @dataclass class BlkInfo: name: str From dab11cb0200d0004836a86a48e1567e91554dee7 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:47:14 +0200 Subject: [PATCH 11/12] docs/api: add docstrings to {list_mdns_services, set_clan_details} --- pkgs/clan-cli/clan_lib/api/mdns_discovery.py | 4 ++++ pkgs/clan-cli/clan_lib/clan/update.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/api/mdns_discovery.py b/pkgs/clan-cli/clan_lib/api/mdns_discovery.py index 93c1bc2c4..dfbaacb30 100644 --- a/pkgs/clan-cli/clan_lib/api/mdns_discovery.py +++ b/pkgs/clan-cli/clan_lib/api/mdns_discovery.py @@ -89,6 +89,10 @@ def parse_avahi_output(output: str) -> DNSInfo: @API.register def list_mdns_services() -> DNSInfo: + """List mDNS/DNS-SD services on the local network. + Returns: + DNSInfo: A dictionary containing discovered mDNS/DNS-SD services. + """ cmd = nix_shell( ["avahi"], [ diff --git a/pkgs/clan-cli/clan_lib/clan/update.py b/pkgs/clan-cli/clan_lib/clan/update.py index 7e735ba4f..59c13147e 100644 --- a/pkgs/clan-cli/clan_lib/clan/update.py +++ b/pkgs/clan-cli/clan_lib/clan/update.py @@ -15,6 +15,14 @@ class UpdateOptions: @API.register def set_clan_details(options: UpdateOptions) -> InventorySnapshot: + """Update the clan metadata in the inventory of a given flake. + Args: + options: UpdateOptions containing the flake and the new metadata. + Returns: + InventorySnapshot: The updated inventory snapshot after modifying the metadata. + Raises: + ClanError: If the flake does not exist or if the inventory is invalid (missing the meta attribute). + """ inventory_store = InventoryStore(options.flake) inventory = inventory_store.read() set_value_by_path(inventory, "meta", options.meta) From d7cf79faa772bd4380f09f993b50cc4be7ee3740 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 7 Jul 2025 15:48:36 +0200 Subject: [PATCH 12/12] openapi: error on missing api function docstring --- pkgs/clan-cli/openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/clan-cli/openapi.py b/pkgs/clan-cli/openapi.py index af70d47cd..bd6c280de 100644 --- a/pkgs/clan-cli/openapi.py +++ b/pkgs/clan-cli/openapi.py @@ -150,7 +150,7 @@ def main() -> None: errors.extend(check_res) if not func_schema.get("description"): - warnings.append( + errors.append( f"{func_name} doesn't have a description. Python docstring is required for an API function." )