Merge pull request 'expose an option to generate hardware configuration during installation' (#2313) from hardware into main
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -52,8 +52,8 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
||||
target_host: info?.deploy.targetHost,
|
||||
debug: true,
|
||||
nix_options: [],
|
||||
},
|
||||
password: null,
|
||||
},
|
||||
}),
|
||||
{
|
||||
loading: "Installing...",
|
||||
|
||||
@@ -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", {
|
||||
const result = await callApi("show_machine_hardware_config", {
|
||||
clan_dir: curr,
|
||||
machine_name: props.name,
|
||||
machine_name: name,
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data?.backend === "nixos-facter" || null;
|
||||
}
|
||||
return null;
|
||||
return result.data === "NIXOS_FACTER";
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -107,8 +84,8 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
},
|
||||
machine: props.name,
|
||||
target_host: props.targetHost,
|
||||
},
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user