Merge pull request 'clan-cli: Change rsync to ssh upload' (#2381) from Qubasa/clan-core:Qubasa-scp_upload into main
This commit is contained in:
@@ -44,6 +44,17 @@
|
|||||||
'';
|
'';
|
||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
|
deployment.nixosMobileWorkaround = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
description = ''
|
||||||
|
if true, the deployment will first do a nixos-rebuild switch
|
||||||
|
to register the boot profile the command will fail applying it to the running system
|
||||||
|
which is why afterwards we execute a nixos-rebuild test to apply
|
||||||
|
the new config without having to reboot.
|
||||||
|
This is a nixos-mobile deployment bug and will be removed in the future
|
||||||
|
'';
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
vm.create = lib.mkOption {
|
vm.create = lib.mkOption {
|
||||||
type = lib.types.path;
|
type = lib.types.path;
|
||||||
description = ''
|
description = ''
|
||||||
@@ -75,6 +86,7 @@
|
|||||||
};
|
};
|
||||||
sops.defaultGroups = config.clan.core.sops.defaultGroups;
|
sops.defaultGroups = config.clan.core.sops.defaultGroups;
|
||||||
inherit (config.clan.core.networking) targetHost buildHost;
|
inherit (config.clan.core.networking) targetHost buildHost;
|
||||||
|
inherit (config.system.clan.deployment) nixosMobileWorkaround;
|
||||||
inherit (config.clan.deployment) requireExplicitUpdate;
|
inherit (config.clan.deployment) requireExplicitUpdate;
|
||||||
};
|
};
|
||||||
system.clan.deployment.file = pkgs.writeText "deployment.json" (
|
system.clan.deployment.file = pkgs.writeText "deployment.json" (
|
||||||
|
|||||||
@@ -197,9 +197,11 @@ def run(
|
|||||||
logger.debug(f"{msg} \n{callers_str}")
|
logger.debug(f"{msg} \n{callers_str}")
|
||||||
|
|
||||||
if input:
|
if input:
|
||||||
print_trace(
|
if any(not ch.isprintable() for ch in input.decode("ascii", "replace")):
|
||||||
f"$: echo '{input.decode('utf-8', 'replace')}' | {indent_command(cmd)}"
|
filtered_input = "<<binary_blob>>"
|
||||||
)
|
else:
|
||||||
|
filtered_input = input.decode("ascii", "replace")
|
||||||
|
print_trace(f"$: echo '{filtered_input}' | {indent_command(cmd)}")
|
||||||
elif logger.isEnabledFor(logging.DEBUG):
|
elif logger.isEnabledFor(logging.DEBUG):
|
||||||
print_trace(f"$: {indent_command(cmd)}")
|
print_trace(f"$: {indent_command(cmd)}")
|
||||||
|
|
||||||
@@ -229,7 +231,7 @@ def run(
|
|||||||
|
|
||||||
global TIME_TABLE
|
global TIME_TABLE
|
||||||
if TIME_TABLE:
|
if TIME_TABLE:
|
||||||
TIME_TABLE.add(shlex.join(cmd), start - timeit.default_timer())
|
TIME_TABLE.add(shlex.join(cmd), timeit.default_timer() - start)
|
||||||
|
|
||||||
# Wait for the subprocess to finish
|
# Wait for the subprocess to finish
|
||||||
cmd_out = CmdOut(
|
cmd_out = CmdOut(
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from clan_cli.cmd import Log, run
|
|
||||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||||
from clan_cli.machines.machines import Machine
|
from clan_cli.machines.machines import Machine
|
||||||
from clan_cli.nix import nix_shell
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -19,30 +17,13 @@ def upload_secrets(machine: Machine) -> None:
|
|||||||
if not secret_facts_store.needs_upload():
|
if not secret_facts_store.needs_upload():
|
||||||
log.info("Secrets already uploaded")
|
log.info("Secrets already uploaded")
|
||||||
return
|
return
|
||||||
with TemporaryDirectory(prefix="facts-upload-") as tempdir:
|
|
||||||
secret_facts_store.upload(Path(tempdir))
|
|
||||||
host = machine.target_host
|
|
||||||
|
|
||||||
run(
|
with TemporaryDirectory(prefix="facts-upload-") as tempdir:
|
||||||
nix_shell(
|
local_secret_dir = Path(tempdir)
|
||||||
["nixpkgs#rsync"],
|
secret_facts_store.upload(local_secret_dir)
|
||||||
[
|
remote_secret_dir = Path(machine.secrets_upload_directory)
|
||||||
"rsync",
|
|
||||||
"-e",
|
machine.target_host.upload(local_secret_dir, remote_secret_dir)
|
||||||
" ".join(["ssh", *host.ssh_cmd_opts()]),
|
|
||||||
"--recursive",
|
|
||||||
"--links",
|
|
||||||
"--times",
|
|
||||||
"--compress",
|
|
||||||
"--delete",
|
|
||||||
"--chmod=D700,F600",
|
|
||||||
f"{tempdir!s}/",
|
|
||||||
f"{host.target_for_rsync}:{machine.secrets_upload_directory}/",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
log=Log.BOTH,
|
|
||||||
needs_user_terminal=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def upload_command(args: argparse.Namespace) -> None:
|
def upload_command(args: argparse.Namespace) -> None:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from clan_cli.cmd import run
|
|||||||
from clan_cli.completions import (
|
from clan_cli.completions import (
|
||||||
add_dynamic_completer,
|
add_dynamic_completer,
|
||||||
complete_machines,
|
complete_machines,
|
||||||
complete_target_host,
|
|
||||||
)
|
)
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
from clan_cli.facts.generate import generate_facts
|
from clan_cli.facts.generate import generate_facts
|
||||||
@@ -19,7 +18,7 @@ from clan_cli.facts.upload import upload_secrets
|
|||||||
from clan_cli.inventory import Machine as InventoryMachine
|
from clan_cli.inventory import Machine as InventoryMachine
|
||||||
from clan_cli.machines.machines import Machine
|
from clan_cli.machines.machines import Machine
|
||||||
from clan_cli.nix import nix_command, nix_metadata
|
from clan_cli.nix import nix_command, nix_metadata
|
||||||
from clan_cli.ssh import HostKeyCheck
|
from clan_cli.ssh import Host, HostKeyCheck
|
||||||
from clan_cli.vars.generate import generate_vars
|
from clan_cli.vars.generate import generate_vars
|
||||||
from clan_cli.vars.upload import upload_secret_vars
|
from clan_cli.vars.upload import upload_secret_vars
|
||||||
|
|
||||||
@@ -116,7 +115,6 @@ def deploy_machine(machines: MachineGroup) -> None:
|
|||||||
|
|
||||||
def deploy(machine: Machine) -> None:
|
def deploy(machine: Machine) -> None:
|
||||||
host = machine.build_host
|
host = machine.build_host
|
||||||
|
|
||||||
generate_facts([machine], None, False)
|
generate_facts([machine], None, False)
|
||||||
generate_vars([machine], None, False)
|
generate_vars([machine], None, False)
|
||||||
|
|
||||||
@@ -127,9 +125,7 @@ def deploy_machine(machines: MachineGroup) -> None:
|
|||||||
machine,
|
machine,
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd = [
|
nix_options = [
|
||||||
"nixos-rebuild",
|
|
||||||
"switch",
|
|
||||||
"--show-trace",
|
"--show-trace",
|
||||||
"--fast",
|
"--fast",
|
||||||
"--option",
|
"--option",
|
||||||
@@ -144,15 +140,26 @@ def deploy_machine(machines: MachineGroup) -> None:
|
|||||||
"--flake",
|
"--flake",
|
||||||
f"{path}#{machine.name}",
|
f"{path}#{machine.name}",
|
||||||
]
|
]
|
||||||
if target_host := host.meta.get("target_host"):
|
switch_cmd = ["nixos-rebuild", "switch", *nix_options]
|
||||||
target_host = f"{target_host.user or 'root'}@{target_host.host}"
|
test_cmd = ["nixos-rebuild", "test", *nix_options]
|
||||||
cmd.extend(["--target-host", target_host])
|
|
||||||
|
target_host: Host | None = host.meta.get("target_host")
|
||||||
|
if target_host:
|
||||||
|
switch_cmd.extend(["--target-host", target_host.target])
|
||||||
|
test_cmd.extend(["--target-host", target_host.target])
|
||||||
|
|
||||||
env = host.nix_ssh_env(None)
|
env = host.nix_ssh_env(None)
|
||||||
ret = host.run(cmd, extra_env=env, check=False)
|
ret = host.run(switch_cmd, extra_env=env, check=False)
|
||||||
# re-retry switch if the first time fails
|
|
||||||
if ret.returncode != 0:
|
# if the machine is mobile, we retry to deploy with the quirk method
|
||||||
ret = host.run(cmd, extra_env=env)
|
is_mobile = machine.deployment.get("nixosMobileWorkaround", False)
|
||||||
|
if is_mobile and ret.returncode != 0:
|
||||||
|
log.info("Mobile machine detected, applying quirk deployment method")
|
||||||
|
ret = host.run(test_cmd, extra_env=env)
|
||||||
|
|
||||||
|
# retry nixos-rebuild switch if the first attempt failed
|
||||||
|
elif ret.returncode != 0:
|
||||||
|
ret = host.run(switch_cmd, extra_env=env)
|
||||||
|
|
||||||
if len(machines.group.hosts) > 1:
|
if len(machines.group.hosts) > 1:
|
||||||
machines.run_function(deploy)
|
machines.run_function(deploy)
|
||||||
@@ -226,17 +233,9 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
default="ask",
|
default="ask",
|
||||||
help="Host key (.ssh/known_hosts) check mode.",
|
help="Host key (.ssh/known_hosts) check mode.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
target_host_parser = parser.add_argument(
|
|
||||||
"--target-host",
|
"--target-host",
|
||||||
type=str,
|
type=str,
|
||||||
help="Address of the machine to update, in the format of user@host:1234.",
|
help="Address of the machine to update, in the format of user@host:1234.",
|
||||||
)
|
)
|
||||||
add_dynamic_completer(target_host_parser, complete_target_host)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--darwin",
|
|
||||||
type=str,
|
|
||||||
help="Hack to deploy darwin machines. This will be removed in the future when we have full darwin integration.",
|
|
||||||
)
|
|
||||||
parser.set_defaults(func=update)
|
parser.set_defaults(func=update)
|
||||||
|
|||||||
@@ -113,6 +113,6 @@ def profile(func: Callable) -> Callable:
|
|||||||
raise
|
raise
|
||||||
return res
|
return res
|
||||||
|
|
||||||
if os.getenv("PERF", "0") == "1":
|
if os.getenv("CLAN_CLI_PERF", "0") == "1":
|
||||||
return wrapper
|
return wrapper
|
||||||
return func
|
return func
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import select
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tarfile
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from collections.abc import Callable, Iterator
|
from collections.abc import Callable, Iterator
|
||||||
@@ -15,10 +16,12 @@ from contextlib import ExitStack, contextmanager
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shlex import quote
|
from shlex import quote
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import IO, Any, Generic, TypeVar
|
from typing import IO, Any, Generic, TypeVar
|
||||||
|
|
||||||
from clan_cli.cmd import terminate_process_group
|
from clan_cli.cmd import Log, terminate_process_group
|
||||||
|
from clan_cli.cmd import run as local_run
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
|
|
||||||
# https://no-color.org
|
# https://no-color.org
|
||||||
@@ -207,7 +210,7 @@ class Host:
|
|||||||
self.host_key_check = host_key_check
|
self.host_key_check = host_key_check
|
||||||
self.meta = meta
|
self.meta = meta
|
||||||
self.verbose_ssh = verbose_ssh
|
self.verbose_ssh = verbose_ssh
|
||||||
self.ssh_options = ssh_options
|
self._ssh_options = ssh_options
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return str(self)
|
return str(self)
|
||||||
@@ -525,29 +528,89 @@ class Host:
|
|||||||
def nix_ssh_env(self, env: dict[str, str] | None) -> dict[str, str]:
|
def nix_ssh_env(self, env: dict[str, str] | None) -> dict[str, str]:
|
||||||
if env is None:
|
if env is None:
|
||||||
env = {}
|
env = {}
|
||||||
env["NIX_SSHOPTS"] = " ".join(self.ssh_cmd_opts())
|
env["NIX_SSHOPTS"] = " ".join(self.ssh_cmd_opts)
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
def upload(
|
||||||
|
self,
|
||||||
|
local_src: Path, # must be a directory
|
||||||
|
remote_dest: Path, # must be a directory
|
||||||
|
file_user: str = "root",
|
||||||
|
file_group: str = "root",
|
||||||
|
dir_mode: int = 0o700,
|
||||||
|
file_mode: int = 0o400,
|
||||||
|
) -> None:
|
||||||
|
# check if the remote destination is a directory (no suffix)
|
||||||
|
if remote_dest.suffix:
|
||||||
|
msg = "Only directories are allowed"
|
||||||
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
if not local_src.is_dir():
|
||||||
|
msg = "Only directories are allowed"
|
||||||
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
# Create the tarball from the temporary directory
|
||||||
|
with TemporaryDirectory(prefix="facts-upload-") as tardir:
|
||||||
|
tar_path = Path(tardir) / "upload.tar.gz"
|
||||||
|
# We set the permissions of the files and directories in the tarball to read only and owned by root
|
||||||
|
# As first uploading the tarball and then changing the permissions can lead an attacker to
|
||||||
|
# do a race condition attack
|
||||||
|
with tarfile.open(str(tar_path), "w:gz") as tar:
|
||||||
|
for root, dirs, files in local_src.walk():
|
||||||
|
for mdir in dirs:
|
||||||
|
dir_path = Path(root) / mdir
|
||||||
|
tarinfo = tar.gettarinfo(
|
||||||
|
dir_path, arcname=str(dir_path.relative_to(str(local_src)))
|
||||||
|
)
|
||||||
|
tarinfo.mode = dir_mode
|
||||||
|
tarinfo.uname = file_user
|
||||||
|
tarinfo.gname = file_group
|
||||||
|
tar.addfile(tarinfo)
|
||||||
|
for file in files:
|
||||||
|
file_path = Path(root) / file
|
||||||
|
tarinfo = tar.gettarinfo(
|
||||||
|
file_path,
|
||||||
|
arcname=str(file_path.relative_to(str(local_src))),
|
||||||
|
)
|
||||||
|
tarinfo.mode = file_mode
|
||||||
|
tarinfo.uname = file_user
|
||||||
|
tarinfo.gname = file_group
|
||||||
|
with file_path.open("rb") as f:
|
||||||
|
tar.addfile(tarinfo, f)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
*self.ssh_cmd(),
|
||||||
|
"rm",
|
||||||
|
"-r",
|
||||||
|
str(remote_dest),
|
||||||
|
";",
|
||||||
|
"mkdir",
|
||||||
|
f"--mode={dir_mode:o}",
|
||||||
|
"-p",
|
||||||
|
str(remote_dest),
|
||||||
|
"&&",
|
||||||
|
"tar",
|
||||||
|
"-C",
|
||||||
|
str(remote_dest),
|
||||||
|
"-xvzf",
|
||||||
|
"-",
|
||||||
|
]
|
||||||
|
|
||||||
|
# TODO accept `input` to be an IO object instead of bytes so that we don't have to read the tarfile into memory.
|
||||||
|
with tar_path.open("rb") as f:
|
||||||
|
local_run(cmd, input=f.read(), log=Log.BOTH, needs_user_terminal=True)
|
||||||
|
|
||||||
|
@property
|
||||||
def ssh_cmd_opts(
|
def ssh_cmd_opts(
|
||||||
self,
|
self,
|
||||||
verbose_ssh: bool = False,
|
|
||||||
tty: bool = False,
|
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
ssh_opts = ["-A"] if self.forward_agent else []
|
ssh_opts = ["-A"] if self.forward_agent else []
|
||||||
|
|
||||||
for k, v in self.ssh_options.items():
|
for k, v in self._ssh_options.items():
|
||||||
ssh_opts.extend(["-o", f"{k}={shlex.quote(v)}"])
|
ssh_opts.extend(["-o", f"{k}={shlex.quote(v)}"])
|
||||||
|
|
||||||
if self.port:
|
|
||||||
ssh_opts.extend(["-p", str(self.port)])
|
|
||||||
if self.key:
|
|
||||||
ssh_opts.extend(["-i", self.key])
|
|
||||||
|
|
||||||
ssh_opts.extend(self.host_key_check.to_ssh_opt())
|
ssh_opts.extend(self.host_key_check.to_ssh_opt())
|
||||||
if verbose_ssh or self.verbose_ssh:
|
|
||||||
ssh_opts.extend(["-v"])
|
|
||||||
if tty:
|
|
||||||
ssh_opts.extend(["-t"])
|
|
||||||
return ssh_opts
|
return ssh_opts
|
||||||
|
|
||||||
def ssh_cmd(
|
def ssh_cmd(
|
||||||
@@ -555,10 +618,21 @@ class Host:
|
|||||||
verbose_ssh: bool = False,
|
verbose_ssh: bool = False,
|
||||||
tty: bool = False,
|
tty: bool = False,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
|
ssh_opts = self.ssh_cmd_opts
|
||||||
|
if verbose_ssh or self.verbose_ssh:
|
||||||
|
ssh_opts.extend(["-v"])
|
||||||
|
if tty:
|
||||||
|
ssh_opts.extend(["-t"])
|
||||||
|
|
||||||
|
if self.port:
|
||||||
|
ssh_opts.extend(["-p", str(self.port)])
|
||||||
|
if self.key:
|
||||||
|
ssh_opts.extend(["-i", self.key])
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"ssh",
|
"ssh",
|
||||||
self.target,
|
self.target,
|
||||||
*self.ssh_cmd_opts(verbose_ssh=verbose_ssh, tty=tty),
|
*ssh_opts,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -658,6 +732,9 @@ class HostGroup:
|
|||||||
timeout: float = math.inf,
|
timeout: float = math.inf,
|
||||||
tty: bool = False,
|
tty: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if cwd is not None:
|
||||||
|
msg = "cwd is not supported for remote commands"
|
||||||
|
raise ClanError(msg)
|
||||||
if extra_env is None:
|
if extra_env is None:
|
||||||
extra_env = {}
|
extra_env = {}
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from clan_cli.cmd import Log, run
|
|
||||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||||
from clan_cli.machines.machines import Machine
|
from clan_cli.machines.machines import Machine
|
||||||
from clan_cli.nix import nix_shell
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,29 +18,10 @@ def upload_secret_vars(machine: Machine) -> None:
|
|||||||
log.info("Secrets already uploaded")
|
log.info("Secrets already uploaded")
|
||||||
return
|
return
|
||||||
with TemporaryDirectory(prefix="vars-upload-") as tempdir:
|
with TemporaryDirectory(prefix="vars-upload-") as tempdir:
|
||||||
secret_store.upload(Path(tempdir))
|
secret_dir = Path(tempdir)
|
||||||
host = machine.target_host
|
secret_store.upload(secret_dir)
|
||||||
|
machine.target_host.upload(
|
||||||
ssh_cmd = host.ssh_cmd()
|
secret_dir, Path(machine.secret_vars_upload_directory)
|
||||||
run(
|
|
||||||
nix_shell(
|
|
||||||
["nixpkgs#rsync"],
|
|
||||||
[
|
|
||||||
"rsync",
|
|
||||||
"-e",
|
|
||||||
" ".join(["ssh"] + ssh_cmd[2:]),
|
|
||||||
"--recursive",
|
|
||||||
"--links",
|
|
||||||
"--times",
|
|
||||||
"--compress",
|
|
||||||
"--delete",
|
|
||||||
"--chmod=D700,F600",
|
|
||||||
f"{tempdir!s}/",
|
|
||||||
f"{host.target_for_rsync}:{machine.secret_vars_upload_directory}/",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
log=Log.BOTH,
|
|
||||||
needs_user_terminal=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ MaxStartups 64:30:256
|
|||||||
AuthorizedKeysFile $host_key.pub
|
AuthorizedKeysFile $host_key.pub
|
||||||
AcceptEnv REALPATH
|
AcceptEnv REALPATH
|
||||||
PasswordAuthentication no
|
PasswordAuthentication no
|
||||||
|
Subsystem sftp $sftp_server
|
||||||
@@ -38,7 +38,7 @@ def substitute(
|
|||||||
str(clan_core_flake),
|
str(clan_core_flake),
|
||||||
)
|
)
|
||||||
line = line.replace("__CLAN_SOPS_KEY_PATH__", sops_key)
|
line = line.replace("__CLAN_SOPS_KEY_PATH__", sops_key)
|
||||||
line = line.replace("__CLAN_SOPS_KEY_DIR__", str(flake))
|
line = line.replace("__CLAN_SOPS_KEY_DIR__", str(flake / "facts"))
|
||||||
buf += line
|
buf += line
|
||||||
print(f"file: {file}")
|
print(f"file: {file}")
|
||||||
print(f"clan_core: {clan_core_flake}")
|
print(f"clan_core: {clan_core_flake}")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from sshd import Sshd
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def host_group(sshd: Sshd) -> HostGroup:
|
def host_group(sshd: Sshd) -> HostGroup:
|
||||||
login = pwd.getpwuid(os.getuid()).pw_name
|
login = pwd.getpwuid(os.getuid()).pw_name
|
||||||
return HostGroup(
|
group = HostGroup(
|
||||||
[
|
[
|
||||||
Host(
|
Host(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
@@ -20,3 +20,4 @@ def host_group(sshd: Sshd) -> HostGroup:
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
return group
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ class Sshd:
|
|||||||
|
|
||||||
class SshdConfig:
|
class SshdConfig:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, path: Path, login_shell: Path, key: str, preload_lib: Path
|
self, path: Path, login_shell: Path, key: str, preload_lib: Path, log_file: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.login_shell = login_shell
|
self.login_shell = login_shell
|
||||||
self.key = key
|
self.key = key
|
||||||
self.preload_lib = preload_lib
|
self.preload_lib = preload_lib
|
||||||
|
self.log_file = log_file
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -43,7 +44,14 @@ def sshd_config(test_root: Path) -> Iterator[SshdConfig]:
|
|||||||
host_key = test_root / "data" / "ssh_host_ed25519_key"
|
host_key = test_root / "data" / "ssh_host_ed25519_key"
|
||||||
host_key.chmod(0o600)
|
host_key.chmod(0o600)
|
||||||
template = (test_root / "data" / "sshd_config").read_text()
|
template = (test_root / "data" / "sshd_config").read_text()
|
||||||
content = string.Template(template).substitute({"host_key": host_key})
|
sshd = shutil.which("sshd")
|
||||||
|
assert sshd is not None
|
||||||
|
sshdp = Path(sshd)
|
||||||
|
sftp_server = sshdp.parent.parent / "libexec" / "sftp-server"
|
||||||
|
assert sftp_server is not None
|
||||||
|
content = string.Template(template).substitute(
|
||||||
|
{"host_key": host_key, "sftp_server": sftp_server}
|
||||||
|
)
|
||||||
config = tmpdir / "sshd_config"
|
config = tmpdir / "sshd_config"
|
||||||
config.write_text(content)
|
config.write_text(content)
|
||||||
login_shell = tmpdir / "shell"
|
login_shell = tmpdir / "shell"
|
||||||
@@ -84,8 +92,8 @@ exec {bash} -l "${{@}}"
|
|||||||
],
|
],
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
|
log_file = tmpdir / "sshd.log"
|
||||||
yield SshdConfig(config, login_shell, str(host_key), lib_path)
|
yield SshdConfig(config, login_shell, str(host_key), lib_path, log_file)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -106,7 +114,17 @@ def sshd(
|
|||||||
"LOGIN_SHELL": str(sshd_config.login_shell),
|
"LOGIN_SHELL": str(sshd_config.login_shell),
|
||||||
}
|
}
|
||||||
proc = command.run(
|
proc = command.run(
|
||||||
[sshd, "-f", str(sshd_config.path), "-D", "-p", str(port)], extra_env=env
|
[
|
||||||
|
sshd,
|
||||||
|
"-E",
|
||||||
|
str(sshd_config.log_file),
|
||||||
|
"-f",
|
||||||
|
str(sshd_config.path),
|
||||||
|
"-D",
|
||||||
|
"-p",
|
||||||
|
str(port),
|
||||||
|
],
|
||||||
|
extra_env=env,
|
||||||
)
|
)
|
||||||
monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
|
monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ def test_secrets_upload(
|
|||||||
new_text = flake.read_text().replace("__CLAN_TARGET_ADDRESS__", addr)
|
new_text = flake.read_text().replace("__CLAN_TARGET_ADDRESS__", addr)
|
||||||
|
|
||||||
flake.write_text(new_text)
|
flake.write_text(new_text)
|
||||||
|
|
||||||
cli.run(["facts", "upload", "--flake", str(test_flake_with_core.path), "vm1"])
|
cli.run(["facts", "upload", "--flake", str(test_flake_with_core.path), "vm1"])
|
||||||
|
|
||||||
# the flake defines this path as the location where the sops key should be installed
|
# the flake defines this path as the location where the sops key should be installed
|
||||||
sops_key = test_flake_with_core.path.joinpath("key.txt")
|
sops_key = test_flake_with_core.path / "facts" / "key.txt"
|
||||||
|
# breakpoint()
|
||||||
assert sops_key.exists()
|
assert sops_key.exists()
|
||||||
assert sops_key.read_text() == age_keys[0].privkey
|
assert sops_key.read_text() == age_keys[0].privkey
|
||||||
|
|||||||
Reference in New Issue
Block a user