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 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)

View File

@@ -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",

View File

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

View File

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