Merge pull request 'expose an option to generate hardware configuration during installation' (#2313) from hardware into main

This commit is contained in:
clan-bot
2024-11-05 15:09:19 +00:00
4 changed files with 108 additions and 124 deletions

View File

@@ -2,13 +2,14 @@ import argparse
import json import json
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Literal
from clan_cli.api import API from clan_cli.api import API
from clan_cli.clan_uri import FlakeId from clan_cli.clan_uri import FlakeId
from clan_cli.cmd import run, run_no_stdout from clan_cli.cmd import run, run_no_stdout
from clan_cli.completions import add_dynamic_completer, complete_machines 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.errors import ClanCmdError, ClanError
from clan_cli.git import commit_file from clan_cli.git import commit_file
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
@@ -19,33 +20,40 @@ from .types import machine_name_type
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@dataclass class HardwareConfig(Enum):
class HardwareReport: NIXOS_FACTER = "nixos-facter"
backend: Literal["nixos-generate-config", "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" @classmethod
facter_file = "facter.json" 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 @API.register
def show_machine_hardware_info( def show_machine_hardware_config(clan_dir: Path, machine_name: str) -> HardwareConfig:
clan_dir: Path, machine_name: str
) -> HardwareReport | None:
""" """
Show hardware information for a machine returns None if none exist. Show hardware information for a machine returns None if none exist.
""" """
return HardwareConfig.detect_type(clan_dir, machine_name)
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
@API.register @API.register
@@ -96,14 +104,14 @@ def show_machine_hardware_platform(clan_dir: Path, machine_name: str) -> str | N
class HardwareGenerateOptions: class HardwareGenerateOptions:
flake: FlakeId flake: FlakeId
machine: str machine: str
backend: Literal["nixos-generate-config", "nixos-facter"] backend: HardwareConfig
target_host: str | None = None target_host: str | None = None
keyfile: str | None = None keyfile: str | None = None
password: str | None = None password: str | None = None
@API.register @API.register
def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareReport: def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareConfig:
""" """
Generate hardware information for a machine Generate hardware information for a machine
and place the resulting *.nix file in the machine's directory. 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: if opts.target_host is not None:
machine.override_target_host = opts.target_host machine.override_target_host = opts.target_host
hw_file = opts.flake.path / "machines" / opts.machine hw_file = opts.backend.config_path(opts.flake.path, opts.machine)
if opts.backend == "nixos-generate-config":
hw_file /= hw_nix_file
else:
hw_file /= facter_file
hw_file.parent.mkdir(parents=True, exist_ok=True) hw_file.parent.mkdir(parents=True, exist_ok=True)
if opts.backend == "nixos-facter": if opts.backend == HardwareConfig.NIXOS_FACTER:
config_command = ["nixos-facter"] config_command = ["nixos-facter"]
else: else:
config_command = [ 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.", description=f"Configuration at '{hw_file}' is invalid. Please check the file and try again.",
) from e ) from e
return HardwareReport(opts.backend) return opts.backend
def update_hardware_config_command(args: argparse.Namespace) -> None: 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, machine=args.machine,
target_host=args.target_host, target_host=args.target_host,
password=args.password, password=args.password,
backend=args.backend, backend=HardwareConfig(args.backend),
) )
generate_machine_hardware_info(opts) generate_machine_hardware_info(opts)

View File

@@ -12,6 +12,7 @@ from clan_cli.clan_uri import FlakeId
from clan_cli.cmd import Log, run from clan_cli.cmd import Log, run
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.facts.generate import generate_facts 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.machines.machines import Machine
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
from clan_cli.ssh.cli import is_ipv6, is_reachable, qrcode_scan from clan_cli.ssh.cli import is_ipv6, is_reachable, qrcode_scan
@@ -24,17 +25,27 @@ class ClanError(Exception):
pass pass
def install_nixos( @dataclass
machine: Machine, class InstallOptions:
kexec: str | None = None, # flake to install
debug: bool = False, flake: FlakeId
password: str | None = None, machine: str
no_reboot: bool = False, target_host: str
extra_args: list[str] | None = None, kexec: str | None = None
build_on_remote: bool = False, debug: bool = False
) -> None: no_reboot: bool = False
if extra_args is None: json_ssh_deploy: dict[str, str] | None = None
extra_args = [] 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) secret_facts_module = importlib.import_module(machine.secret_facts_module)
log.info(f"installing {machine.name}") log.info(f"installing {machine.name}")
secret_facts_store = secret_facts_module.SecretStore(machine=machine) secret_facts_store = secret_facts_module.SecretStore(machine=machine)
@@ -56,8 +67,8 @@ def install_nixos(
upload_dir.mkdir(parents=True) upload_dir.mkdir(parents=True)
secret_facts_store.upload(upload_dir) secret_facts_store.upload(upload_dir)
if password: if opts.password:
os.environ["SSHPASS"] = password os.environ["SSHPASS"] = opts.password
cmd = [ cmd = [
"nixos-anywhere", "nixos-anywhere",
@@ -65,16 +76,28 @@ def install_nixos(
f"{machine.flake}#{machine.name}", f"{machine.flake}#{machine.name}",
"--extra-files", "--extra-files",
str(tmpdir), str(tmpdir),
*extra_args,
] ]
if no_reboot: if opts.no_reboot:
cmd.append("--no-reboot") cmd.append("--no-reboot")
if build_on_remote: if opts.build_on_remote:
cmd.append("--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 += [ cmd += [
"--env-password", "--env-password",
"--ssh-option", "--ssh-option",
@@ -83,9 +106,9 @@ def install_nixos(
if machine.target_host.port: if machine.target_host.port:
cmd += ["--ssh-port", str(machine.target_host.port)] cmd += ["--ssh-port", str(machine.target_host.port)]
if kexec: if opts.kexec:
cmd += ["--kexec", kexec] cmd += ["--kexec", opts.kexec]
if debug: if opts.debug:
cmd.append("--debug") cmd.append("--debug")
cmd.append(target_host) 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: 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 json_ssh_deploy = None
if args.json: if args.json:
json_file = Path(args.json) json_file = Path(args.json)
@@ -166,8 +162,9 @@ def install_command(args: argparse.Namespace) -> None:
json_ssh_deploy=json_ssh_deploy, json_ssh_deploy=json_ssh_deploy,
nix_options=args.option, nix_options=args.option,
build_on_remote=args.build_on_remote, 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", help="do not ask for confirmation",
default=False, 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( machines_parser = parser.add_argument(
"machine", "machine",

View File

@@ -52,8 +52,8 @@ export const MachineListItem = (props: MachineListItemProps) => {
target_host: info?.deploy.targetHost, target_host: info?.deploy.targetHost,
debug: true, debug: true,
nix_options: [], nix_options: [],
password: null,
}, },
password: null,
}), }),
{ {
loading: "Installing...", loading: "Installing...",

View File

@@ -1,10 +1,4 @@
import { import { callApi, ClanService, SuccessData, SuccessQuery } from "@/src/api";
callApi,
ClanService,
Services,
SuccessData,
SuccessQuery,
} from "@/src/api";
import { set_single_disk_id } from "@/src/api/disk"; import { set_single_disk_id } from "@/src/api/disk";
import { get_iwd_service } from "@/src/api/wifi"; import { get_iwd_service } from "@/src/api/wifi";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
@@ -17,25 +11,11 @@ import {
createForm, createForm,
FieldValues, FieldValues,
getValue, getValue,
reset,
setValue, setValue,
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import { useParams } from "@solidjs/router"; import { useParams } from "@solidjs/router";
import { import { createQuery, useQueryClient } from "@tanstack/solid-query";
createQuery, import { createSignal, For, Show, Switch, Match, JSXElement } from "solid-js";
QueryObserver,
useQueryClient,
} from "@tanstack/solid-query";
import {
createSignal,
For,
Show,
Switch,
Match,
JSXElement,
createEffect,
createMemo,
} from "solid-js";
import toast from "solid-toast"; import toast from "solid-toast";
type MachineFormInterface = MachineData & { type MachineFormInterface = MachineData & {
@@ -58,6 +38,12 @@ interface InstallMachineProps {
disks: Disks; disks: Disks;
} }
const InstallMachine = (props: InstallMachineProps) => { const InstallMachine = (props: InstallMachineProps) => {
const curr = activeURI();
const { name } = props;
if (!curr || !name) {
return <span>No Clan selected</span>;
}
const diskPlaceholder = "Select the boot disk of the remote machine"; const diskPlaceholder = "Select the boot disk of the remote machine";
const [formStore, { Form, Field }] = createForm<InstallForm>(); const [formStore, { Form, Field }] = createForm<InstallForm>();
@@ -67,23 +53,14 @@ const InstallMachine = (props: InstallMachineProps) => {
const [confirmDisk, setConfirmDisk] = createSignal(!hasDisk()); const [confirmDisk, setConfirmDisk] = createSignal(!hasDisk());
const hwInfoQuery = createQuery(() => ({ const hwInfoQuery = createQuery(() => ({
queryKey: [ queryKey: [curr, "machine", name, "show_machine_hardware_config"],
activeURI(),
"machine",
props.name,
"show_machine_hardware_info",
],
queryFn: async () => { queryFn: async () => {
const curr = activeURI(); const result = await callApi("show_machine_hardware_config", {
if (curr && props.name) { clan_dir: curr,
const result = await callApi("show_machine_hardware_info", { machine_name: name,
clan_dir: curr, });
machine_name: props.name, if (result.status === "error") throw new Error("Failed to fetch data");
}); return result.data === "NIXOS_FACTER";
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data?.backend === "nixos-facter" || null;
}
return null;
}, },
})); }));
@@ -107,8 +84,8 @@ const InstallMachine = (props: InstallMachineProps) => {
}, },
machine: props.name, machine: props.name,
target_host: props.targetHost, target_host: props.targetHost,
password: "",
}, },
password: "",
}); });
toast.dismiss(loading_toast); toast.dismiss(loading_toast);
@@ -158,7 +135,7 @@ const InstallMachine = (props: InstallMachineProps) => {
machine: props.name, machine: props.name,
keyfile: props.sshKey?.name, keyfile: props.sshKey?.name,
target_host: props.targetHost, target_host: props.targetHost,
backend: "nixos-facter", backend: "NIXOS_FACTER",
}, },
}); });
toast.dismiss(loading_toast); toast.dismiss(loading_toast);