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()} 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))) 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 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/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: 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") 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) 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/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( 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/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) 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()) diff --git a/pkgs/clan-cli/clan_lib/ssh/remote.py b/pkgs/clan-cli/clan_lib/ssh/remote.py index ec8c4e5d0..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() @@ -470,6 +485,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() 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." )