Merge pull request 'expose nixos-facter in cli' (#2172) from nixos-facter into main
This commit is contained in:
@@ -50,6 +50,7 @@
|
|||||||
services.openssh.enable = true;
|
services.openssh.enable = true;
|
||||||
users.users.root.openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ];
|
users.users.root.openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ];
|
||||||
system.nixos.variant_id = "installer";
|
system.nixos.variant_id = "installer";
|
||||||
|
environment.systemPackages = [ pkgs.nixos-facter ];
|
||||||
virtualisation.emptyDiskImages = [ 4096 ];
|
virtualisation.emptyDiskImages = [ 4096 ];
|
||||||
nix.settings = {
|
nix.settings = {
|
||||||
substituters = lib.mkForce [ ];
|
substituters = lib.mkForce [ ];
|
||||||
@@ -101,6 +102,8 @@
|
|||||||
client.fail("test -f test-flake/machines/test-install-machine/hardware-configuration.nix")
|
client.fail("test -f test-flake/machines/test-install-machine/hardware-configuration.nix")
|
||||||
client.succeed("clan machines update-hardware-config --flake test-flake test-install-machine root@target>&2")
|
client.succeed("clan machines update-hardware-config --flake test-flake test-install-machine root@target>&2")
|
||||||
client.succeed("test -f test-flake/machines/test-install-machine/hardware-configuration.nix")
|
client.succeed("test -f test-flake/machines/test-install-machine/hardware-configuration.nix")
|
||||||
|
client.succeed("clan machines update-hardware-config --backend nixos-facter --flake test-flake test-install-machine root@target>&2")
|
||||||
|
client.succeed("test -f test-flake/machines/test-install-machine/facter.json")
|
||||||
client.succeed("clan machines install --debug --flake ${../..} --yes test-install-machine root@target >&2")
|
client.succeed("clan machines install --debug --flake ${../..} --yes test-install-machine root@target >&2")
|
||||||
try:
|
try:
|
||||||
target.shutdown()
|
target.shutdown()
|
||||||
|
|||||||
@@ -414,13 +414,19 @@ def main() -> None:
|
|||||||
args.func(args)
|
args.func(args)
|
||||||
except ClanError as e:
|
except ClanError as e:
|
||||||
if isinstance(e, ClanCmdError):
|
if isinstance(e, ClanCmdError):
|
||||||
|
msg = ""
|
||||||
if e.cmd.msg:
|
if e.cmd.msg:
|
||||||
log.fatal(e.cmd.msg)
|
msg += f"{e.cmd.msg}: "
|
||||||
|
msg += f"command exited with code {e.cmd.returncode}: {e.cmd.command}"
|
||||||
|
log.error(msg) # noqa: TRY400
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
log.fatal(e.msg)
|
if not e.msg: # should not be empty, print stack trace
|
||||||
|
raise
|
||||||
|
msg = e.msg
|
||||||
if e.description:
|
if e.description:
|
||||||
print(f"========> {e.description}", file=sys.stderr)
|
msg += f": {e.description}"
|
||||||
|
log.error(msg) # noqa: TRY400
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log.warning("Interrupted by user")
|
log.warning("Interrupted by user")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class HardwareReport:
|
class HardwareReport:
|
||||||
file: Literal["nixos-generate-config", "nixos-facter"]
|
backend: Literal["nixos-generate-config", "nixos-facter"]
|
||||||
|
|
||||||
|
|
||||||
hw_nix_file = "hardware-configuration.nix"
|
hw_nix_file = "hardware-configuration.nix"
|
||||||
@@ -30,13 +30,13 @@ facter_file = "facter.json"
|
|||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def show_machine_hardware_info(
|
def show_machine_hardware_info(
|
||||||
clan_dir: str | Path, machine_name: str
|
clan_dir: Path, machine_name: str
|
||||||
) -> HardwareReport | None:
|
) -> HardwareReport | None:
|
||||||
"""
|
"""
|
||||||
Show hardware information for a machine returns None if none exist.
|
Show hardware information for a machine returns None if none exist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
hw_file = Path(f"{clan_dir}/machines/{machine_name}/{hw_nix_file}")
|
hw_file = Path(clan_dir) / "machines" / machine_name / hw_nix_file
|
||||||
is_template = hw_file.exists() and "throw" in hw_file.read_text()
|
is_template = hw_file.exists() and "throw" in hw_file.read_text()
|
||||||
|
|
||||||
if hw_file.exists() and not is_template:
|
if hw_file.exists() and not is_template:
|
||||||
@@ -49,9 +49,7 @@ def show_machine_hardware_info(
|
|||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def show_machine_deployment_target(
|
def show_machine_deployment_target(clan_dir: Path, machine_name: str) -> str | None:
|
||||||
clan_dir: str | Path, machine_name: str
|
|
||||||
) -> str | None:
|
|
||||||
"""
|
"""
|
||||||
Show deployment target for a machine returns None if none exist.
|
Show deployment target for a machine returns None if none exist.
|
||||||
"""
|
"""
|
||||||
@@ -73,9 +71,7 @@ def show_machine_deployment_target(
|
|||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def show_machine_hardware_platform(
|
def show_machine_hardware_platform(clan_dir: Path, machine_name: str) -> str | None:
|
||||||
clan_dir: str | Path, machine_name: str
|
|
||||||
) -> str | None:
|
|
||||||
"""
|
"""
|
||||||
Show hardware information for a machine returns None if none exist.
|
Show hardware information for a machine returns None if none exist.
|
||||||
"""
|
"""
|
||||||
@@ -85,7 +81,7 @@ def show_machine_hardware_platform(
|
|||||||
[
|
[
|
||||||
f"{clan_dir}#clanInternals.machines.{system}.{machine_name}",
|
f"{clan_dir}#clanInternals.machines.{system}.{machine_name}",
|
||||||
"--apply",
|
"--apply",
|
||||||
"machine: { inherit (machine.config.nixpkgs.hostPlatform) system; }",
|
"machine: { inherit (machine.pkgs) system; }",
|
||||||
"--json",
|
"--json",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -96,37 +92,44 @@ def show_machine_hardware_platform(
|
|||||||
return host_platform.get("system", None)
|
return host_platform.get("system", None)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HardwareGenerateOptions:
|
||||||
|
flake: FlakeId
|
||||||
|
machine: str
|
||||||
|
backend: Literal["nixos-generate-config", "nixos-facter"]
|
||||||
|
target_host: str | None = None
|
||||||
|
keyfile: str | None = None
|
||||||
|
password: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def generate_machine_hardware_info(
|
def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareReport:
|
||||||
clan_dir: FlakeId,
|
|
||||||
machine_name: str,
|
|
||||||
hostname: str | None = None,
|
|
||||||
password: str | None = None,
|
|
||||||
keyfile: str | None = None,
|
|
||||||
force: bool | None = False,
|
|
||||||
report_type: Literal[
|
|
||||||
"nixos-generate-config", "nixos-facter"
|
|
||||||
] = "nixos-generate-config",
|
|
||||||
) -> HardwareReport:
|
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
machine = Machine(machine_name, flake=clan_dir)
|
machine = Machine(opts.machine, flake=opts.flake)
|
||||||
if hostname is not None:
|
if opts.target_host is not None:
|
||||||
machine.target_host_address = hostname
|
machine.target_host_address = opts.target_host
|
||||||
|
|
||||||
config_command = (
|
hw_file = opts.flake.path / "machines" / opts.machine
|
||||||
["nixos-facter"]
|
if opts.backend == "nixos-generate-config":
|
||||||
if report_type == "nixos-facter"
|
hw_file /= hw_nix_file
|
||||||
else [
|
else:
|
||||||
|
hw_file /= facter_file
|
||||||
|
|
||||||
|
hw_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if opts.backend == "nixos-facter":
|
||||||
|
config_command = ["nixos-facter"]
|
||||||
|
else:
|
||||||
|
config_command = [
|
||||||
"nixos-generate-config",
|
"nixos-generate-config",
|
||||||
# Filesystems are managed by disko
|
# Filesystems are managed by disko
|
||||||
"--no-filesystems",
|
"--no-filesystems",
|
||||||
"--show-hardware-config",
|
"--show-hardware-config",
|
||||||
]
|
]
|
||||||
)
|
|
||||||
|
|
||||||
host = machine.target_host
|
host = machine.target_host
|
||||||
target_host = f"{host.user or 'root'}@{host.host}"
|
target_host = f"{host.user or 'root'}@{host.host}"
|
||||||
@@ -136,9 +139,9 @@ def generate_machine_hardware_info(
|
|||||||
"nixpkgs#sshpass",
|
"nixpkgs#sshpass",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
*(["sshpass", "-p", f"{password}"] if password else []),
|
*(["sshpass", "-p", opts.password] if opts.password else []),
|
||||||
"ssh",
|
"ssh",
|
||||||
*(["-i", f"{keyfile}"] if keyfile else []),
|
*(["-i", f"{opts.keyfile}"] if opts.keyfile else []),
|
||||||
# Disable known hosts file
|
# Disable known hosts file
|
||||||
"-o",
|
"-o",
|
||||||
"UserKnownHostsFile=/dev/null",
|
"UserKnownHostsFile=/dev/null",
|
||||||
@@ -156,36 +159,15 @@ def generate_machine_hardware_info(
|
|||||||
)
|
)
|
||||||
out = run(cmd)
|
out = run(cmd)
|
||||||
if out.returncode != 0:
|
if out.returncode != 0:
|
||||||
log.error(f"Failed to inspect {machine_name}. Address: {hostname}")
|
|
||||||
log.error(out)
|
log.error(out)
|
||||||
msg = f"Failed to inspect {machine_name}. Address: {hostname}"
|
msg = f"Failed to inspect {opts.machine}. Address: {opts.target_host}"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
hw_file = Path(
|
|
||||||
f"{clan_dir}/machines/{machine_name}/{hw_nix_file if report_type == 'nixos-generate-config' else facter_file}"
|
|
||||||
)
|
|
||||||
hw_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Check if the hardware-configuration.nix file is a template
|
|
||||||
is_template = hw_file.exists() and "throw" in hw_file.read_text()
|
|
||||||
|
|
||||||
if hw_file.exists() and not force and not is_template:
|
|
||||||
msg = "File exists."
|
|
||||||
raise ClanError(
|
|
||||||
msg,
|
|
||||||
description="Hardware file already exists. To force overwrite the existing configuration use '--force'.",
|
|
||||||
location=f"{__name__} {hw_file}",
|
|
||||||
)
|
|
||||||
|
|
||||||
backup_file = None
|
backup_file = None
|
||||||
if hw_file.exists() and force:
|
if hw_file.exists():
|
||||||
# Backup the existing file
|
|
||||||
backup_file = hw_file.with_suffix(".bak")
|
backup_file = hw_file.with_suffix(".bak")
|
||||||
hw_file.replace(backup_file)
|
hw_file.replace(backup_file)
|
||||||
print(f"Backed up existing {hw_file} to {backup_file}")
|
hw_file.write_text(out.stdout)
|
||||||
|
|
||||||
with hw_file.open("w") as f:
|
|
||||||
f.write(out.stdout)
|
|
||||||
print(f"Successfully generated: {hw_file}")
|
print(f"Successfully generated: {hw_file}")
|
||||||
|
|
||||||
# try to evaluate the machine
|
# try to evaluate the machine
|
||||||
@@ -193,11 +175,11 @@ def generate_machine_hardware_info(
|
|||||||
|
|
||||||
commit_file(
|
commit_file(
|
||||||
hw_file,
|
hw_file,
|
||||||
clan_dir.path,
|
opts.flake.path,
|
||||||
f"HW/report: Hardware configuration for {machine_name}",
|
f"machines/{opts.machine}/{hw_file.name}: update hardware configuration",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
show_machine_hardware_platform(clan_dir.path, machine_name)
|
show_machine_hardware_platform(opts.flake.path, opts.machine)
|
||||||
except ClanCmdError as e:
|
except ClanCmdError as e:
|
||||||
log.exception("Failed to evaluate hardware-configuration.nix")
|
log.exception("Failed to evaluate hardware-configuration.nix")
|
||||||
# Restore the backup file
|
# Restore the backup file
|
||||||
@@ -209,20 +191,10 @@ def generate_machine_hardware_info(
|
|||||||
msg = "Invalid hardware-configuration.nix file"
|
msg = "Invalid hardware-configuration.nix file"
|
||||||
raise ClanError(
|
raise ClanError(
|
||||||
msg,
|
msg,
|
||||||
description="The hardware-configuration.nix 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.",
|
||||||
location=f"{__name__} {hw_file}",
|
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
return HardwareReport(report_type)
|
return HardwareReport(opts.backend)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HardwareGenerateOptions:
|
|
||||||
flake: FlakeId
|
|
||||||
machine: str
|
|
||||||
target_host: str | None
|
|
||||||
password: str | None
|
|
||||||
force: bool | None
|
|
||||||
|
|
||||||
|
|
||||||
def update_hardware_config_command(args: argparse.Namespace) -> None:
|
def update_hardware_config_command(args: argparse.Namespace) -> None:
|
||||||
@@ -231,14 +203,9 @@ 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,
|
||||||
force=args.force,
|
backend=args.backend,
|
||||||
)
|
)
|
||||||
hw_info = generate_machine_hardware_info(
|
generate_machine_hardware_info(opts)
|
||||||
opts.flake, opts.machine, opts.target_host, opts.password
|
|
||||||
)
|
|
||||||
print("Successfully generated hardware information.")
|
|
||||||
print(f"Target: {opts.machine} ({opts.target_host})")
|
|
||||||
print(f"Type: {hw_info.file}")
|
|
||||||
|
|
||||||
|
|
||||||
def register_update_hardware_config(parser: argparse.ArgumentParser) -> None:
|
def register_update_hardware_config(parser: argparse.ArgumentParser) -> None:
|
||||||
@@ -261,8 +228,9 @@ def register_update_hardware_config(parser: argparse.ArgumentParser) -> None:
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
machine_parser = parser.add_argument(
|
machine_parser = parser.add_argument(
|
||||||
"--force",
|
"--backend",
|
||||||
help="Will overwrite the hardware-configuration.nix file.",
|
help="The type of hardware report to generate.",
|
||||||
action="store_true",
|
choices=["nixos-generate-config", "nixos-facter"],
|
||||||
|
default="nixos-generate-config",
|
||||||
)
|
)
|
||||||
add_dynamic_completer(machine_parser, complete_machines)
|
add_dynamic_completer(machine_parser, complete_machines)
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
machine_name: props.name,
|
machine_name: props.name,
|
||||||
});
|
});
|
||||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
return result.data?.file === "nixos-facter" || null;
|
return result.data?.backend === "nixos-facter" || null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -153,11 +153,13 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
|
|
||||||
const loading_toast = toast.loading("Generating hardware report...");
|
const loading_toast = toast.loading("Generating hardware report...");
|
||||||
const r = await callApi("generate_machine_hardware_info", {
|
const r = await callApi("generate_machine_hardware_info", {
|
||||||
clan_dir: { loc: curr_uri },
|
opts: {
|
||||||
machine_name: props.name,
|
flake: { loc: curr_uri },
|
||||||
|
machine: props.name,
|
||||||
keyfile: props.sshKey?.name,
|
keyfile: props.sshKey?.name,
|
||||||
hostname: props.targetHost,
|
target_host: props.targetHost,
|
||||||
report_type: "nixos-facter",
|
backend: "nixos-facter",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
toast.dismiss(loading_toast);
|
toast.dismiss(loading_toast);
|
||||||
hwInfoQuery.refetch();
|
hwInfoQuery.refetch();
|
||||||
|
|||||||
Reference in New Issue
Block a user