From 5e39514251e00b195de03b7a946fa68b1b54d4a7 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 15 Jun 2024 17:35:50 +0200 Subject: [PATCH] CLI: init hw-generate command --- pkgs/clan-cli/clan_cli/errors.py | 4 +- pkgs/clan-cli/clan_cli/machines/__init__.py | 22 ++- pkgs/clan-cli/clan_cli/machines/hardware.py | 137 ++++++++++++++++++ pkgs/clan-cli/clan_cli/machines/inventory.py | 2 +- pkgs/clan-cli/clan_cli/machines/show.py | 4 +- .../app/src/components/MachineListItem.tsx | 1 - 6 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/machines/hardware.py diff --git a/pkgs/clan-cli/clan_cli/errors.py b/pkgs/clan-cli/clan_cli/errors.py index 84c3957f..ead0ad5c 100644 --- a/pkgs/clan-cli/clan_cli/errors.py +++ b/pkgs/clan-cli/clan_cli/errors.py @@ -58,9 +58,9 @@ class ClanError(Exception): self.location = location or "Unknown location" self.msg = msg or "" if self.description: - exception_msg = f"{self.location}: {self.msg} - {self.description}" + exception_msg = f"{self.location}: \n{self.msg} - {self.description}" else: - exception_msg = f"{self.location}: {self.msg}" + exception_msg = f"{self.location}: \n{self.msg}" super().__init__(exception_msg) diff --git a/pkgs/clan-cli/clan_cli/machines/__init__.py b/pkgs/clan-cli/clan_cli/machines/__init__.py index 449f6903..5851d37a 100644 --- a/pkgs/clan-cli/clan_cli/machines/__init__.py +++ b/pkgs/clan-cli/clan_cli/machines/__init__.py @@ -3,6 +3,7 @@ import argparse from .create import register_create_parser from .delete import register_delete_parser +from .hardware import register_hw_generate from .install import register_install_parser from .list import register_list_parser from .show import register_show_parser @@ -63,6 +64,25 @@ Examples: ) register_list_parser(list_parser) + generate_hw_parser = subparser.add_parser( + "hw-generate", + help="Generate hardware specifics for a machine", + epilog=( + """ +This subcommand generates hardware specifics for a machine. Such as the host platform, available kernel modules, etc. + +Examples: + + $ clan machines hw-generate [MACHINE] [TARGET_HOST] + Will generate hardware specifics for the the specified `[TARGET_HOST]` and place the result in hardware.nix for the given machine `[MACHINE]`. + +For more detailed information, visit: https://docs.clan.lol/getting-started/configure/#machine-configuration + +""" + ), + ) + register_hw_generate(generate_hw_parser) + show_parser = subparser.add_parser( "show", help="Show a machine", @@ -92,7 +112,7 @@ Examples: Will install the specified machine [MACHINE], to the specified [TARGET_HOST]. $ clan machines install [MACHINE] --json [JSON] - Will install the specified machine [MACHINE] to the host exposed by + Will install the specified machine [MACHINE] to the host exposed by the deployment information of the [JSON] deployment string. For information on how to set up the installer see: https://docs.clan.lol/getting-started/installer/ diff --git a/pkgs/clan-cli/clan_cli/machines/hardware.py b/pkgs/clan-cli/clan_cli/machines/hardware.py new file mode 100644 index 00000000..3cd73c63 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/machines/hardware.py @@ -0,0 +1,137 @@ +import argparse +import dataclasses +import json +import logging +from pathlib import Path + +from clan_cli.api import API +from clan_cli.errors import ClanError + +from ..cmd import run, run_no_stdout +from ..completions import add_dynamic_completer, complete_machines +from ..nix import nix_config, nix_eval, nix_shell +from .types import machine_name_type + +log = logging.getLogger(__name__) + + +@dataclasses.dataclass +class HardwareInfo: + system: str | None + + +@API.register +def generate_machine_hardware_info( + clan_dir: str | Path, + machine_name: str, + hostname: str, + password: str | None = None, + force: bool | None = False, +) -> HardwareInfo: + """ + Generate hardware information for a machine + and place the resulting *.nix file in the machine's directory. + """ + cmd = nix_shell( + [ + "nixpkgs#openssh", + "nixpkgs#sshpass", + # Provides nixos-generate-config on non-NixOS systems + "nixpkgs#nixos-install-tools", + ], + [ + *(["sshpass", "-p", f"{password}"] if password else []), + "ssh", + # Disable strict host key checking + "-o StrictHostKeyChecking=no", + # Disable known hosts file + "-o UserKnownHostsFile=/dev/null", + f"root@{hostname}", + "nixos-generate-config", + # Filesystems are managed by disko + "--no-filesystems", + "--show-hardware-config", + ], + ) + out = run(cmd) + if out.returncode != 0: + log.error(f"Failed to inspect {machine_name}. Address: {hostname}") + log.error(out) + raise ClanError(f"Failed to inspect {machine_name}. Address: {hostname}") + + hw_file = Path(f"{clan_dir}/machines/{machine_name}/hardware-configuration.nix") + hw_file.parent.mkdir(parents=True, exist_ok=True) + + # Check if the hardware-configuration.nix file is a template + is_template = False + if hw_file.exists(): + is_template = "throw" in hw_file.read_text() + + if hw_file.exists() and not force and not is_template: + raise ClanError( + "File exists.", + description="Hardware file already exists. To force overwrite the existing configuration use '--force'.", + location=f"{__name__} {hw_file}", + ) + + with open(hw_file, "w") as f: + f.write(out.stdout) + print(f"Successfully generated: {hw_file}") + + # TODO: This could be its own API function? + config = nix_config() + system = config["system"] + cmd = nix_eval( + [ + f"{clan_dir}#clanInternals.machines.{system}.{machine_name}", + "--apply", + "machine: { inherit (machine.config.nixpkgs.hostPlatform) system; }", + "--json", + ] + ) + proc = run_no_stdout(cmd) + res = proc.stdout.strip() + + host_platform = json.loads(res) + + return HardwareInfo( + system=host_platform.get("system", None), + ) + + +def hw_generate_command(args: argparse.Namespace) -> None: + flake_path = Path(args.flake).resolve() + hw_info = generate_machine_hardware_info( + flake_path, args.machine, args.hostname, args.password, args.force + ) + print("----") + print("Successfully generated hardware information.") + print(f"Target: {args.machine} ({args.hostname})") + print(f"System: {hw_info.system}") + print("----") + + +def register_hw_generate(parser: argparse.ArgumentParser) -> None: + parser.set_defaults(func=hw_generate_command) + machine_parser = parser.add_argument( + "machine", + help="the name of the machine", + type=machine_name_type, + ) + machine_parser = parser.add_argument( + "hostname", + help="hostname of the machine", + type=str, + ) + machine_parser = parser.add_argument( + "--password", + help="Pre-provided password the cli will prompt otherwise if needed.", + type=str, + required=False, + ) + machine_parser = parser.add_argument( + "--force", + help="Will overwrite the hardware-configuration.nix file.", + action="store_true", + ) + add_dynamic_completer(machine_parser, complete_machines) diff --git a/pkgs/clan-cli/clan_cli/machines/inventory.py b/pkgs/clan-cli/clan_cli/machines/inventory.py index 84873aab..089c5d9b 100644 --- a/pkgs/clan-cli/clan_cli/machines/inventory.py +++ b/pkgs/clan-cli/clan_cli/machines/inventory.py @@ -6,7 +6,7 @@ from ..nix import nix_build, nix_config from .machines import Machine -# function to speedup eval if we want to evauluate all machines +# function to speedup eval if we want to evaluate all machines def get_all_machines(flake_dir: Path, nix_options: list[str]) -> list[Machine]: config = nix_config() system = config["system"] diff --git a/pkgs/clan-cli/clan_cli/machines/show.py b/pkgs/clan-cli/clan_cli/machines/show.py index 5909f1b0..a77cd2b2 100644 --- a/pkgs/clan-cli/clan_cli/machines/show.py +++ b/pkgs/clan-cli/clan_cli/machines/show.py @@ -22,7 +22,7 @@ class MachineInfo: @API.register -def show_machine(flake_url: str | Path, machine_name: str, debug: bool) -> MachineInfo: +def show_machine(flake_url: str | Path, machine_name: str) -> MachineInfo: config = nix_config() system = config["system"] cmd = nix_eval( @@ -46,7 +46,7 @@ def show_machine(flake_url: str | Path, machine_name: str, debug: bool) -> Machi def show_command(args: argparse.Namespace) -> None: flake_path = Path(args.flake).resolve() - machine = show_machine(flake_path, args.machine, args.debug) + machine = show_machine(flake_path, args.machine) print(f"Name: {machine.machine_name}") print(f"Description: {machine.machine_description or ''}") print(f"Icon: {machine.machine_icon or ''}") diff --git a/pkgs/webview-ui/app/src/components/MachineListItem.tsx b/pkgs/webview-ui/app/src/components/MachineListItem.tsx index 9bddf0dc..964585d3 100644 --- a/pkgs/webview-ui/app/src/components/MachineListItem.tsx +++ b/pkgs/webview-ui/app/src/components/MachineListItem.tsx @@ -32,7 +32,6 @@ export const MachineListItem = (props: MachineListItemProps) => { pyApi.show_machine.dispatch({ op_key: name, machine_name: name, - debug: false, flake_url: currClanURI(), });