Merge pull request 'bind ssh controlmaster to live time of CLI' (#3491) from ssh-refactoring into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3491
This commit is contained in:
Mic92
2025-05-04 14:45:13 +00:00
25 changed files with 184 additions and 159 deletions

View File

@@ -19,21 +19,23 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
if not backup_scripts["providers"]:
msg = "No providers specified"
raise ClanError(msg)
for provider in backup_scripts["providers"]:
proc = machine.target_host.run(
[backup_scripts["providers"][provider]["create"]],
)
if proc.returncode != 0:
msg = "failed to start backup"
raise ClanError(msg)
print("successfully started backup")
with machine.target_host() as host:
for provider in backup_scripts["providers"]:
proc = host.run(
[backup_scripts["providers"][provider]["create"]],
)
if proc.returncode != 0:
msg = "failed to start backup"
raise ClanError(msg)
print("successfully started backup")
else:
if provider not in backup_scripts["providers"]:
msg = f"provider {provider} not found"
raise ClanError(msg)
proc = machine.target_host.run(
[backup_scripts["providers"][provider]["create"]],
)
with machine.target_host() as host:
proc = host.run(
[backup_scripts["providers"][provider]["create"]],
)
if proc.returncode != 0:
msg = "failed to start backup"
raise ClanError(msg)

View File

@@ -10,6 +10,7 @@ from clan_cli.completions import (
)
from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
@dataclass
@@ -18,11 +19,11 @@ class Backup:
job_name: str | None = None
def list_provider(machine: Machine, provider: str) -> list[Backup]:
def list_provider(machine: Machine, host: Host, provider: str) -> list[Backup]:
results = []
backup_metadata = machine.eval_nix("config.clan.core.backups")
list_command = backup_metadata["providers"][provider]["list"]
proc = machine.target_host.run(
proc = host.run(
[list_command],
RunOpts(log=Log.NONE, check=False),
)
@@ -48,12 +49,13 @@ def list_provider(machine: Machine, provider: str) -> list[Backup]:
def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
backup_metadata = machine.eval_nix("config.clan.core.backups")
results = []
if provider is None:
for _provider in backup_metadata["providers"]:
results += list_provider(machine, _provider)
with machine.target_host() as host:
if provider is None:
for _provider in backup_metadata["providers"]:
results += list_provider(machine, host, _provider)
else:
results += list_provider(machine, provider)
else:
results += list_provider(machine, host, provider)
return results

View File

@@ -8,9 +8,12 @@ from clan_cli.completions import (
)
from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
def restore_service(machine: Machine, name: str, provider: str, service: str) -> None:
def restore_service(
machine: Machine, host: Host, name: str, provider: str, service: str
) -> None:
backup_metadata = machine.eval_nix("config.clan.core.backups")
backup_folders = machine.eval_nix("config.clan.core.state")
@@ -25,7 +28,7 @@ def restore_service(machine: Machine, name: str, provider: str, service: str) ->
env["FOLDERS"] = ":".join(set(folders))
if pre_restore := backup_folders[service]["preRestoreCommand"]:
proc = machine.target_host.run(
proc = host.run(
[pre_restore],
RunOpts(log=Log.STDERR),
extra_env=env,
@@ -34,7 +37,7 @@ def restore_service(machine: Machine, name: str, provider: str, service: str) ->
msg = f"failed to run preRestoreCommand: {pre_restore}, error was: {proc.stdout}"
raise ClanError(msg)
proc = machine.target_host.run(
proc = host.run(
[backup_metadata["providers"][provider]["restore"]],
RunOpts(log=Log.STDERR),
extra_env=env,
@@ -44,7 +47,7 @@ def restore_service(machine: Machine, name: str, provider: str, service: str) ->
raise ClanError(msg)
if post_restore := backup_folders[service]["postRestoreCommand"]:
proc = machine.target_host.run(
proc = host.run(
[post_restore],
RunOpts(log=Log.STDERR),
extra_env=env,
@@ -61,18 +64,19 @@ def restore_backup(
service: str | None = None,
) -> None:
errors = []
if service is None:
backup_folders = machine.eval_nix("config.clan.core.state")
for _service in backup_folders:
with machine.target_host() as host:
if service is None:
backup_folders = machine.eval_nix("config.clan.core.state")
for _service in backup_folders:
try:
restore_service(machine, host, name, provider, _service)
except ClanError as e:
errors.append(f"{_service}: {e}")
else:
try:
restore_service(machine, name, provider, _service)
restore_service(machine, host, name, provider, service)
except ClanError as e:
errors.append(f"{_service}: {e}")
else:
try:
restore_service(machine, name, provider, service)
except ClanError as e:
errors.append(f"{service}: {e}")
errors.append(f"{service}: {e}")
if errors:
raise ClanError(
"Restore failed for the following services:\n" + "\n".join(errors)

View File

@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
from pathlib import Path
import clan_cli.machines.machines as machines
from clan_cli.ssh.host import Host
class SecretStoreBase(ABC):
@@ -25,7 +26,7 @@ class SecretStoreBase(ABC):
def exists(self, service: str, name: str) -> bool:
pass
def needs_upload(self) -> bool:
def needs_upload(self, host: Host) -> bool:
return True
@abstractmethod

View File

@@ -6,6 +6,7 @@ from typing import override
from clan_cli.cmd import Log, RunOpts
from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell
from clan_cli.ssh.host import Host
from . import SecretStoreBase
@@ -93,9 +94,9 @@ class SecretStore(SecretStoreBase):
return b"\n".join(hashes)
@override
def needs_upload(self) -> bool:
def needs_upload(self, host: Host) -> bool:
local_hash = self.generate_hash()
remote_hash = self.machine.target_host.run(
remote_hash = host.run(
# TODO get the path to the secrets from the machine
["cat", f"{self.machine.secrets_upload_directory}/.pass_info"],
RunOpts(log=Log.STDERR, check=False),

View File

@@ -6,6 +6,7 @@ from clan_cli.secrets.folders import sops_secrets_folder
from clan_cli.secrets.machines import add_machine, has_machine
from clan_cli.secrets.secrets import decrypt_secret, encrypt_secret, has_secret
from clan_cli.secrets.sops import generate_private_key
from clan_cli.ssh.host import Host
from . import SecretStoreBase
@@ -60,7 +61,7 @@ class SecretStore(SecretStoreBase):
)
@override
def needs_upload(self) -> bool:
def needs_upload(self, host: Host) -> bool:
return False
# We rely now on the vars backend to upload the age key

View File

@@ -1,5 +1,6 @@
import shutil
from pathlib import Path
from typing import override
from clan_cli.dirs import vm_state_dir
from clan_cli.machines.machines import Machine
@@ -28,6 +29,7 @@ class SecretStore(SecretStoreBase):
def exists(self, service: str, name: str) -> bool:
return (self.dir / service / name).exists()
@override
def upload(self, output_dir: Path) -> None:
if output_dir.exists():
shutil.rmtree(output_dir)

View File

@@ -5,13 +5,14 @@ from tempfile import TemporaryDirectory
from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
from clan_cli.ssh.upload import upload
log = logging.getLogger(__name__)
def upload_secrets(machine: Machine) -> None:
if not machine.secret_facts_store.needs_upload():
def upload_secrets(machine: Machine, host: Host) -> None:
if not machine.secret_facts_store.needs_upload(host):
machine.info("Secrets already uploaded")
return
@@ -19,13 +20,13 @@ def upload_secrets(machine: Machine) -> None:
local_secret_dir = Path(_tempdir).resolve()
machine.secret_facts_store.upload(local_secret_dir)
remote_secret_dir = Path(machine.secrets_upload_directory)
upload(machine.target_host, local_secret_dir, remote_secret_dir)
upload(host, local_secret_dir, remote_secret_dir)
def upload_command(args: argparse.Namespace) -> None:
machine = Machine(name=args.machine, flake=args.flake)
upload_secrets(machine)
with machine.target_host() as host:
upload_secrets(machine, host)
def register_upload_parser(parser: argparse.ArgumentParser) -> None:

View File

@@ -14,7 +14,7 @@ from clan_cli.facts.generate import generate_facts
from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell
from clan_cli.vars.generate import generate_vars
from clan_cli.vars.upload import upload_secret_vars
from clan_cli.vars.upload import populate_secret_vars
from .automount import pause_automounting
from .list import list_possible_keymaps, list_possible_languages
@@ -107,7 +107,7 @@ def flash_machine(
local_dir.mkdir(parents=True)
machine.secret_facts_store.upload(local_dir)
upload_secret_vars(machine, local_dir)
populate_secret_vars(machine, local_dir)
disko_install = []
if os.geteuid() != 0:

View File

@@ -139,26 +139,26 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
"--show-hardware-config",
]
host = machine.target_host
host.ssh_options["StrictHostKeyChecking"] = "accept-new"
host.ssh_options["UserKnownHostsFile"] = "/dev/null"
if opts.password:
host.password = opts.password
with machine.target_host() as host:
host.ssh_options["StrictHostKeyChecking"] = "accept-new"
host.ssh_options["UserKnownHostsFile"] = "/dev/null"
if opts.password:
host.password = opts.password
out = host.run(config_command, become_root=True, opts=RunOpts(check=False))
if out.returncode != 0:
if "nixos-facter" in out.stderr and "not found" in out.stderr:
machine.error(str(out.stderr))
msg = (
"Please use our custom nixos install images from https://github.com/nix-community/nixos-images/releases/tag/nixos-unstable. "
"nixos-factor only works on nixos / clan systems currently."
)
out = host.run(config_command, become_root=True, opts=RunOpts(check=False))
if out.returncode != 0:
if "nixos-facter" in out.stderr and "not found" in out.stderr:
machine.error(str(out.stderr))
msg = (
"Please use our custom nixos install images from https://github.com/nix-community/nixos-images/releases/tag/nixos-unstable. "
"nixos-factor only works on nixos / clan systems currently."
)
raise ClanError(msg)
machine.error(str(out))
msg = f"Failed to inspect {opts.machine}. Address: {host.target}"
raise ClanError(msg)
machine.error(str(out))
msg = f"Failed to inspect {opts.machine}. Address: {host.target}"
raise ClanError(msg)
backup_file = None
if hw_file.exists():
backup_file = hw_file.with_suffix(".bak")

View File

@@ -36,7 +36,6 @@ class BuildOn(Enum):
@dataclass
class InstallOptions:
machine: Machine
target_host: str
kexec: str | None = None
debug: bool = False
no_reboot: bool = False
@@ -52,17 +51,16 @@ class InstallOptions:
@API.register
def install_machine(opts: InstallOptions) -> None:
machine = opts.machine
machine.override_target_host = opts.target_host
machine.info(f"installing {machine.name}")
h = machine.target_host
machine.info(f"target host: {h.target}")
machine.debug(f"installing {machine.name}")
generate_facts([machine])
generate_vars([machine])
with TemporaryDirectory(prefix="nixos-install-") as _base_directory:
with (
TemporaryDirectory(prefix="nixos-install-") as _base_directory,
machine.target_host() as host,
):
base_directory = Path(_base_directory).resolve()
activation_secrets = base_directory / "activation_secrets"
upload_dir = activation_secrets / machine.secrets_upload_directory.lstrip("/")
@@ -134,14 +132,14 @@ def install_machine(opts: InstallOptions) -> None:
if opts.build_on:
cmd += ["--build-on", opts.build_on.value]
if h.port:
cmd += ["--ssh-port", str(h.port)]
if host.port:
cmd += ["--ssh-port", str(host.port)]
if opts.kexec:
cmd += ["--kexec", opts.kexec]
if opts.debug:
cmd.append("--debug")
cmd.append(h.target)
cmd.append(host.target)
if opts.use_tor:
# nix copy does not support tor socks proxy
# cmd.append("--ssh-option")
@@ -178,17 +176,15 @@ def install_command(args: argparse.Namespace) -> None:
deploy_info: DeployInfo | None = ssh_command_parse(args)
if args.target_host:
target_host = args.target_host
machine.override_target_host = args.target_host
elif deploy_info:
host = find_reachable_host(deploy_info, host_key_check)
if host is None:
use_tor = True
target_host = f"root@{deploy_info.tor}"
machine.override_target_host = f"root@{deploy_info.tor}"
else:
target_host = host.target
machine.override_target_host = host.target
password = deploy_info.pwd
else:
target_host = machine.target_host.target
if args.password:
password = args.password
@@ -197,19 +193,16 @@ def install_command(args: argparse.Namespace) -> None:
else:
password = None
if not target_host:
msg = "No target host provided, please provide a target host."
raise ClanError(msg)
if not args.yes:
ask = input(f"Install {args.machine} to {target_host}? [y/N] ")
ask = input(
f"Install {args.machine} to {machine.target_host_address}? [y/N] "
)
if ask != "y":
return None
return install_machine(
InstallOptions(
machine=machine,
target_host=target_host,
kexec=args.kexec,
phases=args.phases,
debug=args.debug,

View File

@@ -2,6 +2,8 @@ import importlib
import json
import logging
import re
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
@@ -145,9 +147,9 @@ class Machine:
def flake_dir(self) -> Path:
return self.flake.path
@property
def target_host(self) -> Host:
return parse_deployment_address(
@contextmanager
def target_host(self) -> Iterator[Host]:
yield parse_deployment_address(
self.name,
self.target_host_address,
self.host_key_check,
@@ -155,23 +157,25 @@ class Machine:
meta={"machine": self},
)
@property
def build_host(self) -> Host:
@contextmanager
def build_host(self) -> Iterator[Host | None]:
"""
The host where the machine is built and deployed from.
Can be the same as the target host.
"""
build_host = self.override_build_host or self.deployment.get("buildHost")
if build_host is None:
return self.target_host
with self.target_host() as target_host:
yield target_host
return
# enable ssh agent forwarding to allow the build host to access the target host
return parse_deployment_address(
yield parse_deployment_address(
self.name,
build_host,
self.host_key_check,
forward_agent=True,
private_key=self.private_key,
meta={"machine": self, "target_host": self.target_host},
meta={"machine": self},
)
@cached_property

View File

@@ -5,6 +5,7 @@ import os
import re
import shlex
import sys
from contextlib import ExitStack
from clan_lib.api import API
@@ -43,8 +44,7 @@ def is_local_input(node: dict[str, dict[str, str]]) -> bool:
)
def upload_sources(machine: Machine) -> str:
host = machine.build_host
def upload_sources(machine: Machine, host: Host) -> str:
env = host.nix_ssh_env(os.environ.copy())
flake_url = (
@@ -126,22 +126,25 @@ def update_machines(base_path: str, machines: list[InventoryMachine]) -> None:
deploy_machines(group_machines)
def deploy_machines(machines: list[Machine]) -> None:
"""
Deploy to all hosts in parallel
"""
def deploy_machine(machine: Machine) -> None:
with ExitStack() as stack:
target_host = stack.enter_context(machine.target_host())
build_host = stack.enter_context(machine.build_host())
if machine._class_ == "darwin":
if not machine.deploy_as_root and target_host.user == "root":
msg = f"'targetHost' should be set to a non-root user for deploying to nix-darwin on machine '{machine.name}'"
raise ClanError(msg)
host = build_host or target_host
def deploy(machine: Machine) -> None:
host = machine.build_host
generate_facts([machine], service=None, regenerate=False)
generate_vars([machine], generator_name=None, regenerate=False)
upload_secrets(machine)
upload_secret_vars(machine)
upload_secrets(machine, target_host)
upload_secret_vars(machine, target_host)
path = upload_sources(
machine=machine,
)
path = upload_sources(machine, host)
nix_options = [
"--show-trace",
@@ -166,10 +169,9 @@ def deploy_machines(machines: list[Machine]) -> None:
"",
]
target_host: Host | None = host.meta.get("target_host")
if target_host:
if build_host:
become_root = False
nix_options += ["--target-host", target_host.target]
nix_options += ["--target-host", build_host.target]
if target_host.user != "root":
nix_options += ["--use-remote-sudo"]
@@ -211,19 +213,19 @@ def deploy_machines(machines: list[Machine]) -> None:
become_root=become_root,
)
def deploy_machines(machines: list[Machine]) -> None:
"""
Deploy to all hosts in parallel
"""
with AsyncRuntime() as runtime:
for machine in machines:
if machine._class_ == "darwin":
if not machine.deploy_as_root and machine.target_host.user == "root":
msg = f"'targetHost' should be set to a non-root user for deploying to nix-darwin on machine '{machine.name}'"
raise ClanError(msg)
machine.info(f"Updating {machine.name}")
runtime.async_run(
AsyncOpts(
tid=machine.name, async_ctx=AsyncContext(prefix=machine.name)
),
deploy,
deploy_machine,
machine,
)
runtime.join_all()

View File

@@ -1,15 +1,15 @@
# Adapted from https://github.com/numtide/deploykit
import errno
import logging
import os
import shlex
import socket
import stat
import subprocess
import types
from dataclasses import dataclass, field
from pathlib import Path
from shlex import quote
from tempfile import TemporaryDirectory
from typing import Any
from clan_cli.cmd import CmdOut, RunOpts, run
@@ -40,35 +40,34 @@ class Host:
ssh_options: dict[str, str] = field(default_factory=dict)
tor_socks: bool = False
def setup_control_master(self) -> None:
home = Path.home()
if not home.exists():
return
control_path = home / ".ssh"
try:
if not stat.S_ISDIR(control_path.stat().st_mode):
return
except OSError as e:
if e.errno == errno.ENOENT:
try:
control_path.mkdir(exist_ok=True)
except OSError:
return
else:
return
_temp_dir: TemporaryDirectory | None = None
def setup_control_master(self, control_path: Path) -> None:
self.ssh_options["ControlMaster"] = "auto"
# Can we make this a temporary directory?
self.ssh_options["ControlPath"] = str(control_path / "clan-%h-%p-%r")
# We use a short ttl because we want to mainly re-use the connection during the cli run
self.ssh_options["ControlPersist"] = "1m"
self.ssh_options["ControlPersist"] = "30m"
def __enter__(self) -> None:
self._temp_dir = TemporaryDirectory(prefix="clan-ssh-")
self.setup_control_master(Path(self._temp_dir.name))
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: types.TracebackType | None,
) -> None:
try:
if self._temp_dir:
self._temp_dir.cleanup()
except OSError:
pass
def __post_init__(self) -> None:
if not self.command_prefix:
self.command_prefix = self.host
if not self.user:
self.user = "root"
self.setup_control_master()
def __str__(self) -> str:
return self.target

View File

@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
from clan_cli.errors import ClanError
from clan_cli.machines import machines
from clan_cli.ssh.host import Host
if TYPE_CHECKING:
from .generate import Generator, Var
@@ -183,5 +184,5 @@ class StoreBase(ABC):
pass
@abstractmethod
def upload(self, phases: list[str]) -> None:
def upload(self, host: Host, phases: list[str]) -> None:
pass

View File

@@ -4,6 +4,7 @@ from pathlib import Path
from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var
@@ -72,6 +73,6 @@ class FactStore(StoreBase):
msg = "populate_dir is not implemented for public vars stores"
raise NotImplementedError(msg)
def upload(self, phases: list[str]) -> None:
def upload(self, host: Host, phases: list[str]) -> None:
msg = "upload is not implemented for public vars stores"
raise NotImplementedError(msg)

View File

@@ -6,6 +6,7 @@ from pathlib import Path
from clan_cli.dirs import vm_state_dir
from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var
@@ -69,6 +70,6 @@ class FactStore(StoreBase):
msg = "populate_dir is not implemented for public vars stores"
raise NotImplementedError(msg)
def upload(self, phases: list[str]) -> None:
def upload(self, host: Host, phases: list[str]) -> None:
msg = "upload is not implemented for public vars stores"
raise NotImplementedError(msg)

View File

@@ -3,6 +3,7 @@ import tempfile
from pathlib import Path
from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var
@@ -45,6 +46,6 @@ class SecretStore(StoreBase):
shutil.copytree(self.dir, output_dir)
shutil.rmtree(self.dir)
def upload(self, phases: list[str]) -> None:
def upload(self, host: Host, phases: list[str]) -> None:
msg = "Cannot upload secrets with FS backend"
raise NotImplementedError(msg)

View File

@@ -10,6 +10,7 @@ from tempfile import TemporaryDirectory
from clan_cli.cmd import CmdOut, Log, RunOpts, run
from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell
from clan_cli.ssh.host import Host
from clan_cli.ssh.upload import upload
from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var
@@ -146,9 +147,9 @@ class SecretStore(StoreBase):
manifest += hashes
return b"\n".join(manifest)
def needs_upload(self) -> bool:
def needs_upload(self, host: Host) -> bool:
local_hash = self.generate_hash()
remote_hash = self.machine.target_host.run(
remote_hash = host.run(
# TODO get the path to the secrets from the machine
[
"cat",
@@ -224,11 +225,11 @@ class SecretStore(StoreBase):
(output_dir / f".{self._store_backend}_info").write_bytes(self.generate_hash())
def upload(self, phases: list[str]) -> None:
def upload(self, host: Host, phases: list[str]) -> None:
if "partitioning" in phases:
msg = "Cannot upload partitioning secrets"
raise NotImplementedError(msg)
if not self.needs_upload():
if not self.needs_upload(host):
log.info("Secrets already uploaded")
return
with TemporaryDirectory(prefix="vars-upload-") as _tempdir:
@@ -237,4 +238,4 @@ class SecretStore(StoreBase):
upload_dir = Path(
self.machine.deployment["password-store"]["secretLocation"]
)
upload(self.machine.target_host, pass_dir, upload_dir)
upload(host, pass_dir, upload_dir)

View File

@@ -23,6 +23,7 @@ from clan_cli.secrets.secrets import (
groups_folder,
has_secret,
)
from clan_cli.ssh.host import Host
from clan_cli.ssh.upload import upload
from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator
@@ -220,14 +221,15 @@ class SecretStore(StoreBase):
target_path.write_bytes(self.get(generator, file.name))
target_path.chmod(file.mode)
def upload(self, phases: list[str]) -> None:
@override
def upload(self, host: Host, phases: list[str]) -> None:
if "partitioning" in phases:
msg = "Cannot upload partitioning secrets"
raise NotImplementedError(msg)
with TemporaryDirectory(prefix="sops-upload-") as _tempdir:
sops_upload_dir = Path(_tempdir).resolve()
self.populate_dir(sops_upload_dir, phases)
upload(self.machine.target_host, sops_upload_dir, Path("/var/lib/sops-nix"))
upload(host, sops_upload_dir, Path("/var/lib/sops-nix"))
def exists(self, generator: Generator, name: str) -> bool:
secret_folder = self.secret_path(generator, name)
@@ -260,7 +262,6 @@ class SecretStore(StoreBase):
return keys
# }
def needs_fix(self, generator: Generator, name: str) -> tuple[bool, str | None]:
secret_path = self.secret_path(generator, name)
current_recipients = sops.get_recipients(secret_path)

View File

@@ -4,6 +4,7 @@ from pathlib import Path
from clan_cli.dirs import vm_state_dir
from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
from clan_cli.vars._types import StoreBase
from clan_cli.vars.generate import Generator, Var
@@ -60,6 +61,6 @@ class SecretStore(StoreBase):
shutil.rmtree(output_dir)
shutil.copytree(self.dir, output_dir)
def upload(self, phases: list[str]) -> None:
def upload(self, host: Host, phases: list[str]) -> None:
msg = "Cannot upload secrets to VMs"
raise NotImplementedError(msg)

View File

@@ -4,17 +4,19 @@ from pathlib import Path
from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host
log = logging.getLogger(__name__)
def upload_secret_vars(machine: Machine, directory: Path | None = None) -> None:
if directory:
machine.secret_vars_store.populate_dir(
directory, phases=["activation", "users", "services"]
)
else:
machine.secret_vars_store.upload(phases=["activation", "users", "services"])
def upload_secret_vars(machine: Machine, host: Host) -> None:
machine.secret_vars_store.upload(host, phases=["activation", "users", "services"])
def populate_secret_vars(machine: Machine, directory: Path) -> None:
machine.secret_vars_store.populate_dir(
directory, phases=["activation", "users", "services"]
)
def upload_command(args: argparse.Namespace) -> None:
@@ -22,7 +24,11 @@ def upload_command(args: argparse.Namespace) -> None:
directory = None
if args.directory:
directory = Path(args.directory)
upload_secret_vars(machine, directory)
populate_secret_vars(machine, directory)
return
with machine.target_host() as host:
upload_secret_vars(machine, host)
def register_upload_parser(parser: argparse.ArgumentParser) -> None:

View File

@@ -22,7 +22,7 @@ from clan_cli.nix import nix_shell
from clan_cli.qemu.qga import QgaSession
from clan_cli.qemu.qmp import QEMUMonitorProtocol
from clan_cli.vars.generate import generate_vars
from clan_cli.vars.upload import upload_secret_vars
from clan_cli.vars.upload import populate_secret_vars
from .inspect import VmConfig, inspect_vm
from .qemu import qemu_command
@@ -84,7 +84,7 @@ def get_secrets(
generate_vars([machine])
machine.secret_facts_store.upload(secrets_dir)
upload_secret_vars(machine, secrets_dir)
populate_secret_vars(machine, secrets_dir)
return secrets_dir

View File

@@ -55,9 +55,9 @@ export const MachineListItem = (props: MachineListItemProps) => {
flake: {
identifier: active_clan,
},
override_target_host: info?.deploy.targetHost,
},
no_reboot: true,
target_host: info?.deploy.targetHost,
debug: true,
nix_options: [],
password: null,

View File

@@ -142,8 +142,8 @@ const InstallMachine = (props: InstallMachineProps) => {
flake: {
identifier: curr_uri,
},
override_target_host: target,
},
target_host: target,
password: "",
},
});