clan-cli: Fix clan install command and multiple other issues
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: "",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user