From a452cc1a02cd206a6e10af21ca501b0b57f9f8af Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 18 Dec 2024 15:29:08 +0100 Subject: [PATCH] clan-cli: Fix clan install command and multiple other issues --- pkgs/clan-cli/clan_cli/machines/install.py | 47 +++++++++---------- pkgs/clan-cli/clan_cli/ssh/deploy_info.py | 29 +++++++++--- pkgs/clan-cli/clan_cli/ssh/host.py | 20 ++++---- .../app/src/components/MachineListItem.tsx | 8 ++-- .../app/src/routes/machines/details.tsx | 8 ++-- 5 files changed, 65 insertions(+), 47 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index 16c98f7a0..0f839f7da 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -8,7 +8,6 @@ from pathlib import Path from tempfile import TemporaryDirectory from clan_cli.api import API -from clan_cli.clan_uri import FlakeId from clan_cli.cmd import Log, RunOpts, run from clan_cli.completions import ( add_dynamic_completer, @@ -21,6 +20,7 @@ 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.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 log = logging.getLogger(__name__) @@ -28,14 +28,11 @@ log = logging.getLogger(__name__) @dataclass class InstallOptions: - # flake to install - flake: FlakeId - machine: str + machine: Machine target_host: str kexec: str | None = None debug: bool = False no_reboot: bool = False - deploy_info: DeployInfo | None = None build_on_remote: bool = False nix_options: list[str] = field(default_factory=list) update_hardware_config: HardwareConfig = HardwareConfig.NONE @@ -44,13 +41,7 @@ class InstallOptions: @API.register def install_machine(opts: InstallOptions) -> None: - # TODO: Fixme, replace opts.machine and opts.flake with machine object - # 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 = opts.machine machine.override_target_host = opts.target_host 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.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: + host_key_check = HostKeyCheck.from_str(args.host_key_check) try: + machine = Machine(name=args.machine, flake=args.flake, nix_options=args.option) + if args.flake is None: msg = "Could not find clan flake toplevel directory" raise ClanError(msg) + deploy_info: DeployInfo | None = ssh_command_parse(args) - password = None if args.target_host: target_host = args.target_host elif deploy_info: - host = find_reachable_host(deploy_info) + host = find_reachable_host(deploy_info, host_key_check) if host is None: msg = f"Couldn't reach any host address: {deploy_info.addrs}" raise ClanError(msg) 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 + else: + target_host = machine.target_host.target if args.password: password = args.password + elif deploy_info and deploy_info.pwd: + password = deploy_info.pwd + else: + password = None if not 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( InstallOptions( - flake=args.flake, - machine=args.machine, + machine=machine, target_host=target_host, kexec=args.kexec, debug=args.debug, no_reboot=args.no_reboot, - deploy_info=deploy_info, nix_options=args.option, build_on_remote=args.build_on_remote, 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", 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( "--build-on-remote", action="store_true", diff --git a/pkgs/clan-cli/clan_cli/ssh/deploy_info.py b/pkgs/clan-cli/clan_cli/ssh/deploy_info.py index 84ea5f834..b8f19aea2 100644 --- a/pkgs/clan-cli/clan_cli/ssh/deploy_info.py +++ b/pkgs/clan-cli/clan_cli/ssh/deploy_info.py @@ -11,6 +11,8 @@ from clan_cli.cmd import run from clan_cli.errors import ClanError from clan_cli.nix import nix_shell 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 log = logging.getLogger(__name__) @@ -24,7 +26,9 @@ class DeployInfo: @staticmethod 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: @@ -34,11 +38,15 @@ def is_ipv6(ip: str) -> bool: 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 for addr in deploy_info.addrs: 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): break return host @@ -63,8 +71,10 @@ def parse_qr_code(picture_file: Path) -> DeployInfo: return DeployInfo.from_json(json.loads(data)) -def ssh_shell_from_deploy(deploy_info: DeployInfo, runtime: AsyncRuntime) -> None: - if host := find_reachable_host(deploy_info): +def ssh_shell_from_deploy( + 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) else: 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: + host_key_check = HostKeyCheck.from_str(args.host_key_check) deploy_info = ssh_command_parse(args) if not deploy_info: msg = "No --json or --png data provided" raise ClanError(msg) 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: @@ -119,4 +130,10 @@ def register_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--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) diff --git a/pkgs/clan-cli/clan_cli/ssh/host.py b/pkgs/clan-cli/clan_cli/ssh/host.py index ed7b6f778..3ea9f8711 100644 --- a/pkgs/clan-cli/clan_cli/ssh/host.py +++ b/pkgs/clan-cli/clan_cli/ssh/host.py @@ -207,14 +207,14 @@ class Host: 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 - ) - sock.settimeout(2) - try: - sock.connect((host.host, host.port or 22)) - sock.close() - except OSError: - return False - else: - return True + ) as sock: + sock.settimeout(2) + try: + sock.connect((host.host, host.port or 22)) + sock.close() + except OSError: + return False + else: + return True diff --git a/pkgs/webview-ui/app/src/components/MachineListItem.tsx b/pkgs/webview-ui/app/src/components/MachineListItem.tsx index 00871060d..00c209731 100644 --- a/pkgs/webview-ui/app/src/components/MachineListItem.tsx +++ b/pkgs/webview-ui/app/src/components/MachineListItem.tsx @@ -48,9 +48,11 @@ export const MachineListItem = (props: MachineListItemProps) => { await toast.promise( callApi("install_machine", { opts: { - machine: name, - flake: { - loc: active_clan, + machine: { + name: name, + flake: { + loc: active_clan, + }, }, no_reboot: true, target_host: info?.deploy.targetHost, diff --git a/pkgs/webview-ui/app/src/routes/machines/details.tsx b/pkgs/webview-ui/app/src/routes/machines/details.tsx index a3c5f6999..c9b897dce 100644 --- a/pkgs/webview-ui/app/src/routes/machines/details.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/details.tsx @@ -68,10 +68,12 @@ const InstallMachine = (props: InstallMachineProps) => { ); const r = await callApi("install_machine", { opts: { - flake: { - loc: curr_uri, + machine: { + name: props.name, + flake: { + loc: curr_uri, + }, }, - machine: props.name, target_host: props.targetHost, password: "", },