diff --git a/pkgs/clan-cli/clan_cli/host_key_check.py b/pkgs/clan-cli/clan_cli/host_key_check.py deleted file mode 100644 index df1331574..000000000 --- a/pkgs/clan-cli/clan_cli/host_key_check.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Common argument types and utilities for host key checking in clan CLI commands.""" - -import argparse - -from clan_lib.ssh.host_key import HostKeyCheck - - -def host_key_check_type(value: str) -> HostKeyCheck: - """ - Argparse type converter for HostKeyCheck enum. - """ - try: - return HostKeyCheck(value) - except ValueError: - valid_values = [e.value for e in HostKeyCheck] - msg = f"Invalid host key check mode: {value}. Valid options: {', '.join(valid_values)}" - raise argparse.ArgumentTypeError(msg) from None - - -def add_host_key_check_arg( - parser: argparse.ArgumentParser, default: HostKeyCheck = HostKeyCheck.ASK -) -> None: - parser.add_argument( - "--host-key-check", - type=host_key_check_type, - default=default, - help=f"Host key (.ssh/known_hosts) check mode. Options: {', '.join([e.value for e in HostKeyCheck])}", - ) diff --git a/pkgs/clan-cli/clan_cli/machines/hardware.py b/pkgs/clan-cli/clan_cli/machines/hardware.py index cc36cdf0d..4db7c0e92 100644 --- a/pkgs/clan-cli/clan_cli/machines/hardware.py +++ b/pkgs/clan-cli/clan_cli/machines/hardware.py @@ -12,7 +12,6 @@ from clan_lib.machines.suggestions import validate_machine_names from clan_lib.ssh.remote import Remote from clan_cli.completions import add_dynamic_completer, complete_machines -from clan_cli.host_key_check import add_host_key_check_arg from .types import machine_name_type @@ -56,7 +55,12 @@ def register_update_hardware_config(parser: argparse.ArgumentParser) -> None: nargs="?", help="ssh address to install to in the form of user@host:2222", ) - add_host_key_check_arg(parser) + parser.add_argument( + "--host-key-check", + choices=["strict", "ask", "tofu", "none"], + default="ask", + help="Host key (.ssh/known_hosts) check mode.", + ) parser.add_argument( "--password", help="Pre-provided password the cli will prompt otherwise if needed.", diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index a6222e88f..b93a7331d 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -13,7 +13,6 @@ from clan_cli.completions import ( complete_machines, complete_target_host, ) -from clan_cli.host_key_check import add_host_key_check_arg from clan_cli.machines.hardware import HardwareConfig from clan_cli.ssh.deploy_info import DeployInfo, find_reachable_host, ssh_command_parse @@ -98,7 +97,12 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None: help="do not reboot after installation (deprecated)", default=False, ) - add_host_key_check_arg(parser) + 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", choices=[x.value for x in BuildOn], diff --git a/pkgs/clan-cli/clan_cli/ssh/deploy_info.py b/pkgs/clan-cli/clan_cli/ssh/deploy_info.py index 3734b47a7..9ba609c95 100644 --- a/pkgs/clan-cli/clan_cli/ssh/deploy_info.py +++ b/pkgs/clan-cli/clan_cli/ssh/deploy_info.py @@ -8,14 +8,12 @@ from typing import Any from clan_lib.cmd import run from clan_lib.errors import ClanError from clan_lib.nix import nix_shell -from clan_lib.ssh.host_key import HostKeyCheck -from clan_lib.ssh.remote import Remote +from clan_lib.ssh.remote import HostKeyCheck, Remote from clan_cli.completions import ( add_dynamic_completer, complete_machines, ) -from clan_cli.host_key_check import add_host_key_check_arg from clan_cli.ssh.tor import TorTarget, spawn_tor, ssh_tor_reachable log = logging.getLogger(__name__) @@ -183,5 +181,10 @@ def register_parser(parser: argparse.ArgumentParser) -> None: "--png", help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)", ) - add_host_key_check_arg(parser, default=HostKeyCheck.TOFU) + parser.add_argument( + "--host-key-check", + choices=["strict", "ask", "tofu", "none"], + default="tofu", + help="Host key (.ssh/known_hosts) check mode.", + ) parser.set_defaults(func=ssh_command) diff --git a/pkgs/clan-cli/clan_cli/ssh/test_deploy_info.py b/pkgs/clan-cli/clan_cli/ssh/test_deploy_info.py index 5efa7cbfb..3a4996f7d 100644 --- a/pkgs/clan-cli/clan_cli/ssh/test_deploy_info.py +++ b/pkgs/clan-cli/clan_cli/ssh/test_deploy_info.py @@ -4,7 +4,6 @@ from pathlib import Path import pytest from clan_lib.cmd import RunOpts, run from clan_lib.nix import nix_shell -from clan_lib.ssh.host_key import HostKeyCheck from clan_lib.ssh.remote import Remote from clan_cli.ssh.deploy_info import DeployInfo, find_reachable_host @@ -24,7 +23,7 @@ def test_qrcode_scan(temp_dir: Path) -> None: run(cmd, RunOpts(input=data.encode())) # Call the qrcode_scan function - deploy_info = DeployInfo.from_qr_code(img_path, HostKeyCheck.NONE) + deploy_info = DeployInfo.from_qr_code(img_path, "none") host = deploy_info.addrs[0] assert host.address == "192.168.122.86" @@ -47,7 +46,7 @@ def test_qrcode_scan(temp_dir: Path) -> None: def test_from_json() -> None: data = '{"pass":"scabbed-defender-headlock","tor":"qjeerm4r6t55hcfum4pinnvscn5njlw2g3k7ilqfuu7cdt3ahaxhsbid.onion","addrs":["192.168.122.86"]}' - deploy_info = DeployInfo.from_json(json.loads(data), HostKeyCheck.NONE) + deploy_info = DeployInfo.from_json(json.loads(data), "none") host = deploy_info.addrs[0] assert host.password == "scabbed-defender-headlock" @@ -70,9 +69,7 @@ def test_from_json() -> None: @pytest.mark.with_core def test_find_reachable_host(hosts: list[Remote]) -> None: host = hosts[0] - deploy_info = DeployInfo.from_hostnames( - ["172.19.1.2", host.ssh_url()], HostKeyCheck.NONE - ) + deploy_info = DeployInfo.from_hostnames(["172.19.1.2", host.ssh_url()], "none") assert deploy_info.addrs[0].address == "172.19.1.2" diff --git a/pkgs/clan-cli/clan_cli/tests/hosts.py b/pkgs/clan-cli/clan_cli/tests/hosts.py index f40c79e63..84c32e720 100644 --- a/pkgs/clan-cli/clan_cli/tests/hosts.py +++ b/pkgs/clan-cli/clan_cli/tests/hosts.py @@ -4,7 +4,6 @@ from pathlib import Path import pytest from clan_cli.tests.sshd import Sshd -from clan_lib.ssh.host_key import HostKeyCheck from clan_lib.ssh.remote import Remote @@ -17,7 +16,7 @@ def hosts(sshd: Sshd) -> list[Remote]: port=sshd.port, user=login, private_key=Path(sshd.key), - host_key_check=HostKeyCheck.NONE, + host_key_check="none", command_prefix="local_test", ) ] diff --git a/pkgs/clan-cli/clan_lib/ssh/host_key.py b/pkgs/clan-cli/clan_lib/ssh/host_key.py index e97932ef3..3f2e6968c 100644 --- a/pkgs/clan-cli/clan_lib/ssh/host_key.py +++ b/pkgs/clan-cli/clan_lib/ssh/host_key.py @@ -1,15 +1,15 @@ # Adapted from https://github.com/numtide/deploykit -from enum import Enum +from typing import Literal from clan_lib.errors import ClanError - -class HostKeyCheck(Enum): - STRICT = "strict" # Strictly check ssh host keys, prompt for unknown ones - ASK = "ask" # Ask for confirmation on first use - TOFU = "tofu" # Trust on ssh keys on first use - NONE = "none" # Do not check ssh host keys +HostKeyCheck = Literal[ + "strict", # Strictly check ssh host keys, prompt for unknown ones + "ask", # Ask for confirmation on first use + "tofu", # Trust on ssh keys on first use + "none", # Do not check ssh host keys +] def hostkey_to_ssh_opts(host_key_check: HostKeyCheck) -> list[str]: @@ -17,13 +17,13 @@ def hostkey_to_ssh_opts(host_key_check: HostKeyCheck) -> list[str]: Convert a HostKeyCheck value to SSH options. """ match host_key_check: - case HostKeyCheck.STRICT: + case "strict": return ["-o", "StrictHostKeyChecking=yes"] - case HostKeyCheck.ASK: + case "ask": return [] - case HostKeyCheck.TOFU: + case "tofu": return ["-o", "StrictHostKeyChecking=accept-new"] - case HostKeyCheck.NONE: + case "none": return [ "-o", "StrictHostKeyChecking=no", diff --git a/pkgs/clan-cli/clan_lib/ssh/remote.py b/pkgs/clan-cli/clan_lib/ssh/remote.py index 86f4aceff..8f4d4f8af 100644 --- a/pkgs/clan-cli/clan_lib/ssh/remote.py +++ b/pkgs/clan-cli/clan_lib/ssh/remote.py @@ -38,7 +38,7 @@ class Remote: private_key: Path | None = None password: str | None = None forward_agent: bool = True - host_key_check: HostKeyCheck = HostKeyCheck.ASK + host_key_check: HostKeyCheck = "ask" verbose_ssh: bool = False ssh_options: dict[str, str] = field(default_factory=dict) tor_socks: bool = False diff --git a/pkgs/clan-cli/clan_lib/ssh/remote_test.py b/pkgs/clan-cli/clan_lib/ssh/remote_test.py index 7eeb5feac..6d8a094b7 100644 --- a/pkgs/clan-cli/clan_lib/ssh/remote_test.py +++ b/pkgs/clan-cli/clan_lib/ssh/remote_test.py @@ -8,7 +8,6 @@ import pytest from clan_lib.async_run import AsyncRuntime from clan_lib.cmd import ClanCmdTimeoutError, Log, RunOpts from clan_lib.errors import ClanError, CmdOut -from clan_lib.ssh.host_key import HostKeyCheck from clan_lib.ssh.remote import Remote from clan_lib.ssh.sudo_askpass_proxy import SudoAskpassProxy @@ -114,7 +113,7 @@ def test_parse_deployment_address( result = Remote.from_ssh_uri( machine_name=machine_name, address=test_addr, - ).override(host_key_check=HostKeyCheck.STRICT) + ).override(host_key_check="strict") if expected_exception: return @@ -132,7 +131,7 @@ def test_parse_deployment_address( def test_parse_ssh_options() -> None: addr = "root@example.com:2222?IdentityFile=/path/to/private/key&StrictRemoteKeyChecking=yes" host = Remote.from_ssh_uri(machine_name="foo", address=addr).override( - host_key_check=HostKeyCheck.STRICT + host_key_check="strict" ) assert host.address == "example.com" assert host.port == 2222 diff --git a/pkgs/clan-cli/clan_lib/tests/test_create.py b/pkgs/clan-cli/clan_lib/tests/test_create.py index 16467d69f..29b460a19 100644 --- a/pkgs/clan-cli/clan_lib/tests/test_create.py +++ b/pkgs/clan-cli/clan_lib/tests/test_create.py @@ -33,7 +33,6 @@ from clan_lib.nix_models.clan import ( ) from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy from clan_lib.persist.util import set_value_by_path -from clan_lib.ssh.host_key import HostKeyCheck from clan_lib.ssh.remote import Remote, can_ssh_login log = logging.getLogger(__name__) @@ -189,7 +188,7 @@ def test_clan_create_api( clan_dir_flake.invalidate_cache() target_host = machine.target_host().override( - private_key=private_key, host_key_check=HostKeyCheck.NONE + private_key=private_key, host_key_check="none" ) result = can_ssh_login(target_host) assert result == "Online", f"Machine {machine.name} is not online"