Merge remote-tracking branch 'origin/main' into rework-installation

This commit is contained in:
Jörg Thalheim
2024-08-21 13:33:27 +02:00
196 changed files with 10069 additions and 2432 deletions

View File

@@ -6,7 +6,6 @@ from .delete import register_delete_parser
from .hardware import register_hw_generate
from .install import register_install_parser
from .list import register_list_parser
from .show import register_show_parser
from .update import register_update_parser
@@ -86,17 +85,6 @@ For more detailed information, visit: https://docs.clan.lol/getting-started/conf
)
register_hw_generate(generate_hw_parser)
show_parser = subparser.add_parser(
"show",
help="Show a machine",
epilog=(
"""
This subcommand shows the details of a machine managed by this clan like icon, description, etc
"""
),
)
register_show_parser(show_parser)
install_parser = subparser.add_parser(
"install",
help="Install a machine",

View File

@@ -31,6 +31,8 @@ def create_machine(flake: FlakeId, machine: Machine) -> None:
if machine.name in full_inventory.machines.keys():
raise ClanError(f"Machine with the name {machine.name} already exists")
print(f"Define machine {machine.name}", machine)
inventory.machines.update({machine.name: machine})
save_inventory(inventory, flake.path, f"Create machine {machine.name}")

View File

@@ -94,6 +94,7 @@ def generate_machine_hardware_info(
machine_name: str,
hostname: str | None = None,
password: str | None = None,
keyfile: str | None = None,
force: bool | None = False,
) -> HardwareInfo:
"""
@@ -117,15 +118,15 @@ def generate_machine_hardware_info(
[
*(["sshpass", "-p", f"{password}"] if password else []),
"ssh",
# Disable strict host key checking
"-o",
"StrictHostKeyChecking=no",
*(["-i", f"{keyfile}"] if keyfile else []),
# Disable known hosts file
"-o",
"UserKnownHostsFile=/dev/null",
"-p",
str(machine.target_host.port),
target_host,
"-o UserKnownHostsFile=/dev/null",
f"{hostname}",
"nixos-generate-config",
# Filesystems are managed by disko
"--no-filesystems",

View File

@@ -3,10 +3,12 @@ import importlib
import json
import logging
import os
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import TemporaryDirectory
from clan_cli.api import API
from ..clan_uri import FlakeId
from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines
@@ -91,15 +93,30 @@ def install_nixos(
@dataclass
class InstallOptions:
# flake to install
flake: FlakeId
machine: str
target_host: str
kexec: str | None
confirm: bool
debug: bool
no_reboot: bool
json_ssh_deploy: dict[str, str] | None
nix_options: list[str]
kexec: str | None = None
debug: bool = False
no_reboot: bool = False
json_ssh_deploy: dict[str, str] | None = None
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.target_host_address = opts.target_host
install_nixos(
machine,
kexec=opts.kexec,
debug=opts.debug,
password=password,
no_reboot=opts.no_reboot,
extra_args=opts.nix_options,
)
def install_command(args: argparse.Namespace) -> None:
@@ -123,32 +140,23 @@ def install_command(args: argparse.Namespace) -> None:
target_host = args.target_host
password = None
opts = InstallOptions(
flake=args.flake,
machine=args.machine,
target_host=target_host,
kexec=args.kexec,
confirm=not args.yes,
debug=args.debug,
no_reboot=args.no_reboot,
json_ssh_deploy=json_ssh_deploy,
nix_options=args.option,
)
machine = Machine(opts.machine, flake=opts.flake)
machine.target_host_address = opts.target_host
if opts.confirm:
ask = input(f"Install {machine.name} to {opts.target_host}? [y/N] ")
if not args.yes:
ask = input(f"Install {args.machine} to {target_host}? [y/N] ")
if ask != "y":
return
install_nixos(
machine,
kexec=opts.kexec,
debug=opts.debug,
password=password,
no_reboot=opts.no_reboot,
extra_args=opts.nix_options,
return install_machine(
InstallOptions(
flake=args.flake,
machine=args.machine,
target_host=target_host,
kexec=args.kexec,
debug=args.debug,
no_reboot=args.no_reboot,
json_ssh_deploy=json_ssh_deploy,
nix_options=args.option,
),
password,
)

View File

@@ -1,22 +1,121 @@
import argparse
import json
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
from clan_cli.api import API
from clan_cli.cmd import run_no_stdout
from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.inventory import Machine, load_inventory_eval
from clan_cli.nix import nix_eval, nix_shell
log = logging.getLogger(__name__)
@API.register
def list_machines(flake_url: str | Path, debug: bool = False) -> dict[str, Machine]:
def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
inventory = load_inventory_eval(flake_url)
return inventory.machines
@dataclass
class MachineDetails:
machine: Machine
has_hw_specs: bool = False
# TODO:
# has_disk_specs: bool = False
@API.register
def get_inventory_machine_details(
flake_url: str | Path, machine_name: str
) -> MachineDetails:
inventory = load_inventory_eval(flake_url)
machine = inventory.machines.get(machine_name)
if machine is None:
raise ClanError(f"Machine {machine_name} not found in inventory")
hw_config_path = (
Path(flake_url) / "machines" / Path(machine_name) / "hardware-configuration.nix"
)
return MachineDetails(
machine=machine,
has_hw_specs=hw_config_path.exists(),
)
@API.register
def list_nixos_machines(flake_url: str | Path) -> list[str]:
cmd = nix_eval(
[
f"{flake_url}#nixosConfigurations",
"--apply",
"builtins.attrNames",
"--json",
]
)
proc = run_no_stdout(cmd)
try:
res = proc.stdout.strip()
data = json.loads(res)
return data
except json.JSONDecodeError as e:
raise ClanError(f"Error decoding machines from flake: {e}")
@dataclass
class ConnectionOptions:
keyfile: str | None = None
timeout: int = 2
@API.register
def check_machine_online(
flake_url: str | Path, machine_name: str, opts: ConnectionOptions | None
) -> Literal["Online", "Offline"]:
machine = load_inventory_eval(flake_url).machines.get(machine_name)
if not machine:
raise ClanError(f"Machine {machine_name} not found in inventory")
hostname = machine.deploy.targetHost
if not hostname:
raise ClanError(f"Machine {machine_name} does not specify a targetHost")
timeout = opts.timeout if opts and opts.timeout else 2
cmd = nix_shell(
["nixpkgs#util-linux", *(["nixpkgs#openssh"] if hostname else [])],
[
"ssh",
*(["-i", f"{opts.keyfile}"] if opts and opts.keyfile else []),
# Disable strict host key checking
"-o StrictHostKeyChecking=no",
# Disable known hosts file
"-o UserKnownHostsFile=/dev/null",
f"-o ConnectTimeout={timeout}",
f"{hostname}",
"true",
"&> /dev/null",
],
)
try:
proc = run_no_stdout(cmd)
if proc.returncode != 0:
return "Offline"
return "Online"
except ClanCmdError:
return "Offline"
def list_command(args: argparse.Namespace) -> None:
flake_path = args.flake.path
for name in list_machines(flake_path, args.debug).keys():
for name in list_nixos_machines(flake_path):
print(name)

View File

@@ -1,59 +0,0 @@
import argparse
import dataclasses
import json
import logging
from pathlib import Path
from clan_cli.api import API
from ..cmd import run_no_stdout
from ..completions import add_dynamic_completer, complete_machines
from ..nix import nix_config, nix_eval
from .types import machine_name_type
log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@API.register
def show_machine(flake_url: str | Path, machine_name: str) -> MachineInfo:
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{flake_url}#clanInternals.machines.{system}.{machine_name}",
"--apply",
"machine: { inherit (machine.config.clan.core) machineDescription machineIcon machineName; }",
"--json",
]
)
proc = run_no_stdout(cmd)
res = proc.stdout.strip()
machine = json.loads(res)
return MachineInfo(
machine_name=machine.get("machineName"),
machine_description=machine.get("machineDescription", None),
machine_icon=machine.get("machineIcon", None),
)
def show_command(args: argparse.Namespace) -> None:
machine = show_machine(args.flake.path, args.machine)
print(f"Name: {machine.machine_name}")
print(f"Description: {machine.machine_description or ''}")
print(f"Icon: {machine.machine_icon or ''}")
def register_show_parser(parser: argparse.ArgumentParser) -> None:
parser.set_defaults(func=show_command)
machine_parser = parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
)
add_dynamic_completer(machine_parser, complete_machines)

View File

@@ -5,11 +5,15 @@ import os
import shlex
import sys
from clan_cli.api import API
from clan_cli.clan_uri import FlakeId
from ..cmd import run
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
from ..facts.generate import generate_facts
from ..facts.upload import upload_secrets
from ..inventory import Machine as InventoryMachine
from ..machines.machines import Machine
from ..nix import nix_command, nix_metadata
from ..ssh import HostKeyCheck
@@ -81,6 +85,25 @@ def upload_sources(
)
@API.register
def update_machines(base_path: str, machines: list[InventoryMachine]) -> None:
group_machines: list[Machine] = []
# Convert InventoryMachine to Machine
for machine in machines:
m = Machine(
name=machine.name,
flake=FlakeId(base_path),
)
if not machine.deploy.targetHost:
raise ClanError(f"'TargetHost' is not set for machine '{machine.name}'")
# Copy targetHost to machine
m.target_host_address = machine.deploy.targetHost
group_machines.append(m)
deploy_machine(MachineGroup(group_machines))
def deploy_machine(machines: MachineGroup) -> None:
"""
Deploy to all hosts in parallel
@@ -97,8 +120,10 @@ def deploy_machine(machines: MachineGroup) -> None:
generate_vars([machine], None, False)
upload_secrets(machine)
path = upload_sources(".", target)
path = upload_sources(
str(machine.flake.path) if machine.flake.is_local() else machine.flake.url,
target,
)
if host.host_key_check != HostKeyCheck.STRICT:
ssh_arg += " -o StrictHostKeyChecking=no"
if host.host_key_check == HostKeyCheck.NONE:
@@ -109,6 +134,7 @@ def deploy_machine(machines: MachineGroup) -> None:
cmd = [
"nixos-rebuild",
"switch",
"--show-trace",
"--fast",
"--option",
"keep-going",