From 2b05f902395191cc1cf0965385d4cc14c12f3701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 5 Nov 2024 13:22:05 +0100 Subject: [PATCH] expose an option to generate hardware configuration during installation --- pkgs/clan-cli/clan_cli/machines/hardware.py | 65 ++++++----- pkgs/clan-cli/clan_cli/machines/install.py | 106 +++++++++--------- .../app/src/components/MachineListItem.tsx | 2 +- .../app/src/routes/machines/details.tsx | 8 +- 4 files changed, 94 insertions(+), 87 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/machines/hardware.py b/pkgs/clan-cli/clan_cli/machines/hardware.py index 72138429a..e1cea0ff4 100644 --- a/pkgs/clan-cli/clan_cli/machines/hardware.py +++ b/pkgs/clan-cli/clan_cli/machines/hardware.py @@ -2,13 +2,14 @@ import argparse import json import logging from dataclasses import dataclass +from enum import Enum from pathlib import Path -from typing import Literal from clan_cli.api import API from clan_cli.clan_uri import FlakeId from clan_cli.cmd import run, run_no_stdout from clan_cli.completions import add_dynamic_completer, complete_machines +from clan_cli.dirs import specific_machine_dir from clan_cli.errors import ClanCmdError, ClanError from clan_cli.git import commit_file from clan_cli.machines.machines import Machine @@ -19,33 +20,40 @@ from .types import machine_name_type log = logging.getLogger(__name__) -@dataclass -class HardwareReport: - backend: Literal["nixos-generate-config", "nixos-facter"] +class HardwareConfig(Enum): + NIXOS_FACTER = "nixos-facter" + NIXOS_GENERATE_CONFIG = "nixos-generate-config" + NONE = "none" + def config_path(self, clan_dir: Path, machine_name: str) -> Path: + machine_dir = specific_machine_dir(clan_dir, machine_name) + if self == HardwareConfig.NIXOS_FACTER: + return machine_dir / "facter.json" + return machine_dir / "hardware-configuration.nix" -hw_nix_file = "hardware-configuration.nix" -facter_file = "facter.json" + @classmethod + def detect_type( + cls: type["HardwareConfig"], clan_dir: Path, machine_name: str + ) -> "HardwareConfig": + hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path( + clan_dir, machine_name + ) + + if hardware_config.exists() and "throw" not in hardware_config.read_text(): + return HardwareConfig.NIXOS_GENERATE_CONFIG + + if HardwareConfig.NIXOS_FACTER.config_path(clan_dir, machine_name).exists(): + return HardwareConfig.NIXOS_FACTER + + return HardwareConfig.NONE @API.register -def show_machine_hardware_info( - clan_dir: Path, machine_name: str -) -> HardwareReport | None: +def show_machine_hardware_config(clan_dir: Path, machine_name: str) -> HardwareConfig: """ Show hardware information for a machine returns None if none exist. """ - - hw_file = Path(clan_dir) / "machines" / machine_name / hw_nix_file - is_template = hw_file.exists() and "throw" in hw_file.read_text() - - if hw_file.exists() and not is_template: - return HardwareReport("nixos-generate-config") - - if Path(f"{clan_dir}/machines/{machine_name}/{facter_file}").exists(): - return HardwareReport("nixos-facter") - - return None + return HardwareConfig.detect_type(clan_dir, machine_name) @API.register @@ -96,14 +104,14 @@ def show_machine_hardware_platform(clan_dir: Path, machine_name: str) -> str | N class HardwareGenerateOptions: flake: FlakeId machine: str - backend: Literal["nixos-generate-config", "nixos-facter"] + backend: HardwareConfig target_host: str | None = None keyfile: str | None = None password: str | None = None @API.register -def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareReport: +def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareConfig: """ Generate hardware information for a machine and place the resulting *.nix file in the machine's directory. @@ -113,15 +121,10 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareRep if opts.target_host is not None: machine.override_target_host = opts.target_host - hw_file = opts.flake.path / "machines" / opts.machine - if opts.backend == "nixos-generate-config": - hw_file /= hw_nix_file - else: - hw_file /= facter_file - + hw_file = opts.backend.config_path(opts.flake.path, opts.machine) hw_file.parent.mkdir(parents=True, exist_ok=True) - if opts.backend == "nixos-facter": + if opts.backend == HardwareConfig.NIXOS_FACTER: config_command = ["nixos-facter"] else: config_command = [ @@ -196,7 +199,7 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareRep description=f"Configuration at '{hw_file}' is invalid. Please check the file and try again.", ) from e - return HardwareReport(opts.backend) + return opts.backend def update_hardware_config_command(args: argparse.Namespace) -> None: @@ -205,7 +208,7 @@ def update_hardware_config_command(args: argparse.Namespace) -> None: machine=args.machine, target_host=args.target_host, password=args.password, - backend=args.backend, + backend=HardwareConfig(args.backend), ) generate_machine_hardware_info(opts) diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index 414e836c7..0375420e8 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -12,6 +12,7 @@ from clan_cli.clan_uri import FlakeId from clan_cli.cmd import Log, run from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.facts.generate import generate_facts +from clan_cli.machines.hardware import HardwareConfig from clan_cli.machines.machines import Machine from clan_cli.nix import nix_shell from clan_cli.ssh.cli import is_ipv6, is_reachable, qrcode_scan @@ -24,17 +25,27 @@ class ClanError(Exception): pass -def install_nixos( - machine: Machine, - kexec: str | None = None, - debug: bool = False, - password: str | None = None, - no_reboot: bool = False, - extra_args: list[str] | None = None, - build_on_remote: bool = False, -) -> None: - if extra_args is None: - extra_args = [] +@dataclass +class InstallOptions: + # flake to install + flake: FlakeId + machine: str + target_host: str + kexec: str | None = None + debug: bool = False + no_reboot: bool = False + json_ssh_deploy: dict[str, str] | None = None + build_on_remote: bool = False + nix_options: list[str] = field(default_factory=list) + update_hardware_config: HardwareConfig = HardwareConfig.NONE + password: str | None = None + + +@API.register +def install_machine(opts: InstallOptions) -> None: + machine = Machine(opts.machine, flake=opts.flake) + machine.override_target_host = opts.target_host + secret_facts_module = importlib.import_module(machine.secret_facts_module) log.info(f"installing {machine.name}") secret_facts_store = secret_facts_module.SecretStore(machine=machine) @@ -56,8 +67,8 @@ def install_nixos( upload_dir.mkdir(parents=True) secret_facts_store.upload(upload_dir) - if password: - os.environ["SSHPASS"] = password + if opts.password: + os.environ["SSHPASS"] = opts.password cmd = [ "nixos-anywhere", @@ -65,16 +76,28 @@ def install_nixos( f"{machine.flake}#{machine.name}", "--extra-files", str(tmpdir), - *extra_args, ] - if no_reboot: + if opts.no_reboot: cmd.append("--no-reboot") - if build_on_remote: + if opts.build_on_remote: cmd.append("--build-on-remote") - if password: + if opts.update_hardware_config is not HardwareConfig.NONE: + cmd.extend( + [ + "--generate-hardware-config", + str(opts.update_hardware_config), + str( + opts.update_hardware_config.config_path( + opts.flake.path, machine.name + ) + ), + ] + ) + + if opts.password: cmd += [ "--env-password", "--ssh-option", @@ -83,9 +106,9 @@ def install_nixos( if machine.target_host.port: cmd += ["--ssh-port", str(machine.target_host.port)] - if kexec: - cmd += ["--kexec", kexec] - if debug: + if opts.kexec: + cmd += ["--kexec", opts.kexec] + if opts.debug: cmd.append("--debug") cmd.append(target_host) @@ -98,37 +121,10 @@ def install_nixos( ) -@dataclass -class InstallOptions: - # flake to install - flake: FlakeId - machine: str - target_host: str - kexec: str | None = None - debug: bool = False - no_reboot: bool = False - json_ssh_deploy: dict[str, str] | None = None - build_on_remote: bool = False - nix_options: list[str] = field(default_factory=list) - - -@API.register -def install_machine(opts: InstallOptions, password: str | None) -> None: - machine = Machine(opts.machine, flake=opts.flake) - machine.override_target_host = opts.target_host - - install_nixos( - machine, - kexec=opts.kexec, - debug=opts.debug, - password=password, - no_reboot=opts.no_reboot, - extra_args=opts.nix_options, - build_on_remote=opts.build_on_remote, - ) - - def install_command(args: argparse.Namespace) -> None: + if args.flake is None: + msg = "Could not find clan flake toplevel directory" + raise ClanError(msg) json_ssh_deploy = None if args.json: json_file = Path(args.json) @@ -166,8 +162,9 @@ def install_command(args: argparse.Namespace) -> None: json_ssh_deploy=json_ssh_deploy, nix_options=args.option, build_on_remote=args.build_on_remote, + update_hardware_config=HardwareConfig(args.update_hardware_config), + password=password, ), - password, ) @@ -211,6 +208,13 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None: help="do not ask for confirmation", default=False, ) + parser.add_argument( + "--update-hardware-config", + type=str, + default="none", + help="update the hardware configuration", + choices=[x.value for x in HardwareConfig], + ) machines_parser = parser.add_argument( "machine", diff --git a/pkgs/webview-ui/app/src/components/MachineListItem.tsx b/pkgs/webview-ui/app/src/components/MachineListItem.tsx index cf6e6692d..39f3908ab 100644 --- a/pkgs/webview-ui/app/src/components/MachineListItem.tsx +++ b/pkgs/webview-ui/app/src/components/MachineListItem.tsx @@ -52,8 +52,8 @@ export const MachineListItem = (props: MachineListItemProps) => { target_host: info?.deploy.targetHost, debug: true, nix_options: [], + password: null, }, - password: null, }), { loading: "Installing...", diff --git a/pkgs/webview-ui/app/src/routes/machines/details.tsx b/pkgs/webview-ui/app/src/routes/machines/details.tsx index 3eb690f9c..efb966e5b 100644 --- a/pkgs/webview-ui/app/src/routes/machines/details.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/details.tsx @@ -76,12 +76,12 @@ const InstallMachine = (props: InstallMachineProps) => { queryFn: async () => { const curr = activeURI(); if (curr && props.name) { - const result = await callApi("show_machine_hardware_info", { + const result = await callApi("show_machine_hardware_config", { clan_dir: curr, machine_name: props.name, }); if (result.status === "error") throw new Error("Failed to fetch data"); - return result.data?.backend === "nixos-facter" || null; + return result.data === "NIXOS_FACTER" || null; } return null; }, @@ -107,8 +107,8 @@ const InstallMachine = (props: InstallMachineProps) => { }, machine: props.name, target_host: props.targetHost, + password: "", }, - password: "", }); toast.dismiss(loading_toast); @@ -158,7 +158,7 @@ const InstallMachine = (props: InstallMachineProps) => { machine: props.name, keyfile: props.sshKey?.name, target_host: props.targetHost, - backend: "nixos-facter", + backend: "NIXOS_FACTER", }, }); toast.dismiss(loading_toast);