clan-cli: Fix clan install command and multiple other issues

This commit is contained in:
Qubasa
2024-12-18 15:29:08 +01:00
parent 37dc74d0f7
commit 94b99034c9
5 changed files with 65 additions and 47 deletions

View File

@@ -8,7 +8,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from clan_cli.api import API from clan_cli.api import API
from clan_cli.clan_uri import FlakeId
from clan_cli.cmd import Log, RunOpts, run from clan_cli.cmd import Log, RunOpts, run
from clan_cli.completions import ( from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
@@ -21,6 +20,7 @@ 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.deploy_info import DeployInfo, find_reachable_host, ssh_command_parse from clan_cli.ssh.deploy_info import DeployInfo, find_reachable_host, ssh_command_parse
from clan_cli.ssh.host_key import HostKeyCheck
from clan_cli.vars.generate import generate_vars from clan_cli.vars.generate import generate_vars
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -28,14 +28,11 @@ log = logging.getLogger(__name__)
@dataclass @dataclass
class InstallOptions: class InstallOptions:
# flake to install machine: Machine
flake: FlakeId
machine: str
target_host: str target_host: str
kexec: str | None = None kexec: str | None = None
debug: bool = False debug: bool = False
no_reboot: bool = False no_reboot: bool = False
deploy_info: DeployInfo | None = None
build_on_remote: bool = False build_on_remote: bool = False
nix_options: list[str] = field(default_factory=list) nix_options: list[str] = field(default_factory=list)
update_hardware_config: HardwareConfig = HardwareConfig.NONE update_hardware_config: HardwareConfig = HardwareConfig.NONE
@@ -44,13 +41,7 @@ class InstallOptions:
@API.register @API.register
def install_machine(opts: InstallOptions) -> None: def install_machine(opts: InstallOptions) -> None:
# TODO: Fixme, replace opts.machine and opts.flake with machine object machine = opts.machine
# remove opts.deploy_info, opts.target_host and populate machine object
if opts.deploy_info:
msg = "Deploy info has not been fully implemented yet"
raise NotImplementedError(msg)
machine = Machine(opts.machine, flake=opts.flake)
machine.override_target_host = opts.target_host 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)
@@ -95,7 +86,7 @@ def install_machine(opts: InstallOptions) -> None:
str(opts.update_hardware_config.value), str(opts.update_hardware_config.value),
str( str(
opts.update_hardware_config.config_path( opts.update_hardware_config.config_path(
opts.flake.path, machine.name machine.flake.path, machine.name
) )
), ),
] ]
@@ -130,32 +121,34 @@ def install_machine(opts: InstallOptions) -> None:
def install_command(args: argparse.Namespace) -> None: def install_command(args: argparse.Namespace) -> None:
host_key_check = HostKeyCheck.from_str(args.host_key_check)
try: try:
machine = Machine(name=args.machine, flake=args.flake, nix_options=args.option)
if args.flake is None: if args.flake is None:
msg = "Could not find clan flake toplevel directory" msg = "Could not find clan flake toplevel directory"
raise ClanError(msg) raise ClanError(msg)
deploy_info: DeployInfo | None = ssh_command_parse(args) deploy_info: DeployInfo | None = ssh_command_parse(args)
password = None
if args.target_host: if args.target_host:
target_host = args.target_host target_host = args.target_host
elif deploy_info: elif deploy_info:
host = find_reachable_host(deploy_info) host = find_reachable_host(deploy_info, host_key_check)
if host is None: if host is None:
msg = f"Couldn't reach any host address: {deploy_info.addrs}" msg = f"Couldn't reach any host address: {deploy_info.addrs}"
raise ClanError(msg) raise ClanError(msg)
target_host = host.target target_host = host.target
else:
machine = Machine(
name=args.machine, flake=args.flake, nix_options=args.option
)
target_host = machine.target_host.target
if deploy_info:
password = deploy_info.pwd password = deploy_info.pwd
else:
target_host = machine.target_host.target
if args.password: if args.password:
password = args.password password = args.password
elif deploy_info and deploy_info.pwd:
password = deploy_info.pwd
else:
password = None
if not target_host: if not target_host:
msg = "No target host provided, please provide a target host." msg = "No target host provided, please provide a target host."
@@ -168,13 +161,11 @@ def install_command(args: argparse.Namespace) -> None:
return install_machine( return install_machine(
InstallOptions( InstallOptions(
flake=args.flake, machine=machine,
machine=args.machine,
target_host=target_host, target_host=target_host,
kexec=args.kexec, kexec=args.kexec,
debug=args.debug, debug=args.debug,
no_reboot=args.no_reboot, no_reboot=args.no_reboot,
deploy_info=deploy_info,
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), update_hardware_config=HardwareConfig(args.update_hardware_config),
@@ -198,6 +189,12 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
help="do not reboot after installation", help="do not reboot after installation",
default=False, default=False,
) )
parser.add_argument(
"--host-key-check",
choices=["strict", "ask", "tofu", "none"],
default="ask",
help="Host key (.ssh/known_hosts) check mode.",
)
parser.add_argument( parser.add_argument(
"--build-on-remote", "--build-on-remote",
action="store_true", action="store_true",

View File

@@ -11,6 +11,8 @@ from clan_cli.cmd import run
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
from clan_cli.ssh.host import Host, is_ssh_reachable from clan_cli.ssh.host import Host, is_ssh_reachable
from clan_cli.ssh.host_key import HostKeyCheck
from clan_cli.ssh.parse import parse_deployment_address
from clan_cli.ssh.tor import TorTarget, spawn_tor, ssh_tor_reachable from clan_cli.ssh.tor import TorTarget, spawn_tor, ssh_tor_reachable
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -24,7 +26,9 @@ class DeployInfo:
@staticmethod @staticmethod
def from_json(data: dict[str, Any]) -> "DeployInfo": def from_json(data: dict[str, Any]) -> "DeployInfo":
return DeployInfo(tor=data["tor"], pwd=data["pass"], addrs=data["addrs"]) return DeployInfo(
tor=data.get("tor"), pwd=data.get("pass"), addrs=data.get("addrs", [])
)
def is_ipv6(ip: str) -> bool: def is_ipv6(ip: str) -> bool:
@@ -34,11 +38,15 @@ def is_ipv6(ip: str) -> bool:
return False return False
def find_reachable_host(deploy_info: DeployInfo) -> Host | None: def find_reachable_host(
deploy_info: DeployInfo, host_key_check: HostKeyCheck
) -> Host | None:
host = None host = None
for addr in deploy_info.addrs: for addr in deploy_info.addrs:
host_addr = f"[{addr}]" if is_ipv6(addr) else addr host_addr = f"[{addr}]" if is_ipv6(addr) else addr
host = Host(host=host_addr) host = parse_deployment_address(
machine_name="uknown", host=host_addr, host_key_check=host_key_check
)
if is_ssh_reachable(host): if is_ssh_reachable(host):
break break
return host return host
@@ -63,8 +71,10 @@ def parse_qr_code(picture_file: Path) -> DeployInfo:
return DeployInfo.from_json(json.loads(data)) return DeployInfo.from_json(json.loads(data))
def ssh_shell_from_deploy(deploy_info: DeployInfo, runtime: AsyncRuntime) -> None: def ssh_shell_from_deploy(
if host := find_reachable_host(deploy_info): deploy_info: DeployInfo, runtime: AsyncRuntime, host_key_check: HostKeyCheck
) -> None:
if host := find_reachable_host(deploy_info, host_key_check):
host.connect_ssh_shell(password=deploy_info.pwd) host.connect_ssh_shell(password=deploy_info.pwd)
else: else:
log.info("Could not reach host via clearnet 'addrs'") log.info("Could not reach host via clearnet 'addrs'")
@@ -95,13 +105,14 @@ def ssh_command_parse(args: argparse.Namespace) -> DeployInfo | None:
def ssh_command(args: argparse.Namespace) -> None: def ssh_command(args: argparse.Namespace) -> None:
host_key_check = HostKeyCheck.from_str(args.host_key_check)
deploy_info = ssh_command_parse(args) deploy_info = ssh_command_parse(args)
if not deploy_info: if not deploy_info:
msg = "No --json or --png data provided" msg = "No --json or --png data provided"
raise ClanError(msg) raise ClanError(msg)
with AsyncRuntime() as runtime: with AsyncRuntime() as runtime:
ssh_shell_from_deploy(deploy_info, runtime) ssh_shell_from_deploy(deploy_info, runtime, host_key_check)
def register_parser(parser: argparse.ArgumentParser) -> None: def register_parser(parser: argparse.ArgumentParser) -> None:
@@ -119,4 +130,10 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"--ssh_args", nargs=argparse.REMAINDER, help="additional ssh arguments" "--ssh_args", nargs=argparse.REMAINDER, help="additional ssh arguments"
) )
parser.add_argument(
"--host-key-check",
choices=["strict", "ask", "tofu", "none"],
default="ask",
help="Host key (.ssh/known_hosts) check mode.",
)
parser.set_defaults(func=ssh_command) parser.set_defaults(func=ssh_command)

View File

@@ -207,9 +207,9 @@ class Host:
def is_ssh_reachable(host: Host) -> bool: def is_ssh_reachable(host: Host) -> bool:
sock = socket.socket( with socket.socket(
socket.AF_INET6 if ":" in host.host else socket.AF_INET, socket.SOCK_STREAM socket.AF_INET6 if ":" in host.host else socket.AF_INET, socket.SOCK_STREAM
) ) as sock:
sock.settimeout(2) sock.settimeout(2)
try: try:
sock.connect((host.host, host.port or 22)) sock.connect((host.host, host.port or 22))

View File

@@ -48,10 +48,12 @@ export const MachineListItem = (props: MachineListItemProps) => {
await toast.promise( await toast.promise(
callApi("install_machine", { callApi("install_machine", {
opts: { opts: {
machine: name, machine: {
name: name,
flake: { flake: {
loc: active_clan, loc: active_clan,
}, },
},
no_reboot: true, no_reboot: true,
target_host: info?.deploy.targetHost, target_host: info?.deploy.targetHost,
debug: true, debug: true,

View File

@@ -68,10 +68,12 @@ const InstallMachine = (props: InstallMachineProps) => {
); );
const r = await callApi("install_machine", { const r = await callApi("install_machine", {
opts: { opts: {
machine: {
name: props.name,
flake: { flake: {
loc: curr_uri, loc: curr_uri,
}, },
machine: props.name, },
target_host: props.targetHost, target_host: props.targetHost,
password: "", password: "",
}, },