Revert "Merge pull request 'Fix deploying with sudo + password' (#3470) from target-host into main"

This reverts commit 8a849eb90f, reversing
changes made to 3b5c22ebcf.
This commit is contained in:
Jörg Thalheim
2025-05-04 13:37:09 +02:00
parent 8a849eb90f
commit 6539a6a24f
5 changed files with 33 additions and 121 deletions

View File

@@ -2,7 +2,6 @@ import inspect
import logging import logging
import os import os
import sys import sys
import termios
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -75,16 +74,7 @@ class PrefixFormatter(logging.Formatter):
if self.trace_prints: if self.trace_prints:
format_str += f"\nSource: {filepath}:%(lineno)d::%(funcName)s\n" format_str += f"\nSource: {filepath}:%(lineno)d::%(funcName)s\n"
line = logging.Formatter(format_str).format(record) return logging.Formatter(format_str).format(record)
try:
# Programs like sudo can set the terminal into raw mode.
# This means newlines are no longer translated to include a carriage return to the end.
# https://unix.stackexchange.com/questions/151916/why-is-this-binary-file-transferred-over-ssh-t-being-changed/151963#15196
if not termios.tcgetattr(sys.stdout.fileno())[3] & termios.ECHO:
line += "\r"
except Exception: # not a tty or mocked sys.stdout
pass
return line
def hostname_colorcode(self, hostname: str) -> tuple[int, int, int]: def hostname_colorcode(self, hostname: str) -> tuple[int, int, int]:
colorcodes = RgbColor.list_values() colorcodes = RgbColor.list_values()

View File

@@ -9,7 +9,7 @@ import sys
from clan_lib.api import API from clan_lib.api import API
from clan_cli.async_run import AsyncContext, AsyncOpts, AsyncRuntime, is_async_cancelled from clan_cli.async_run import AsyncContext, AsyncOpts, AsyncRuntime, is_async_cancelled
from clan_cli.cmd import Log, MsgColor, RunOpts, run from clan_cli.cmd import MsgColor, RunOpts, run
from clan_cli.colors import AnsiColor from clan_cli.colors import AnsiColor
from clan_cli.completions import ( from clan_cli.completions import (
add_dynamic_completer, add_dynamic_completer,
@@ -158,7 +158,6 @@ def deploy_machines(machines: list[Machine]) -> None:
] ]
become_root = machine.deploy_as_root become_root = machine.deploy_as_root
needs_tty_for_sudo = become_root or machine._class_ == "darwin"
if machine._class_ == "nixos": if machine._class_ == "nixos":
nix_options += [ nix_options += [
@@ -174,7 +173,6 @@ def deploy_machines(machines: list[Machine]) -> None:
if target_host.user != "root": if target_host.user != "root":
nix_options += ["--use-remote-sudo"] nix_options += ["--use-remote-sudo"]
needs_tty_for_sudo = become_root
switch_cmd = [f"{machine._class_}-rebuild", "switch", *nix_options] switch_cmd = [f"{machine._class_}-rebuild", "switch", *nix_options]
test_cmd = [f"{machine._class_}-rebuild", "test", *nix_options] test_cmd = [f"{machine._class_}-rebuild", "test", *nix_options]
@@ -182,10 +180,7 @@ def deploy_machines(machines: list[Machine]) -> None:
env = host.nix_ssh_env(None) env = host.nix_ssh_env(None)
ret = host.run( ret = host.run(
switch_cmd, switch_cmd,
RunOpts( RunOpts(check=False, msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
check=False, msg_color=MsgColor(stderr=AnsiColor.DEFAULT), log=Log.BOTH
),
tty=needs_tty_for_sudo,
extra_env=env, extra_env=env,
become_root=become_root, become_root=become_root,
) )

View File

@@ -2,84 +2,13 @@ import tarfile
from pathlib import Path from pathlib import Path
from shlex import quote from shlex import quote
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import IO
from clan_cli.cmd import Log, RunOpts from clan_cli.cmd import Log, RunOpts
from clan_cli.cmd import run as run_local
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.ssh.host import Host from clan_cli.ssh.host import Host
def unpack_archive_as_root(
host: Host, f: IO[bytes], local_src: Path, remote_dest: Path, dir_mode: int = 0o700
) -> None:
if local_src.is_dir():
cmd = 'rm -rf "$0" && mkdir -m "$1" -p "$0" && tar -C "$0" -xzf -'
elif local_src.is_file():
cmd = 'rm -f "$0" && tar -C "$(dirname "$0")" -xzf -'
else:
msg = f"Unsupported source file type: {local_src}"
raise ClanError(msg)
host.run(
[
"sudo",
"-p",
f"Enter sudo password for {quote(host.host)}: ",
"--",
"bash",
"-c",
cmd,
str(remote_dest),
f"{dir_mode:o}",
],
RunOpts(
input=f,
log=Log.BOTH,
),
)
def unpack_archive_as_user(
host: Host, f: IO[bytes], local_src: Path, remote_dest: Path, dir_mode: int = 0o700
) -> None:
archive = host.run(
["bash", "-c", "f=$(mktemp); echo $f; cat > $f"],
RunOpts(
input=f,
log=Log.BOTH,
),
).stdout.strip()
if local_src.is_dir():
cmd = 'trap "rm -f $0" EXIT; rm -rf "$1" && mkdir -m "$2" -p "$1" && tar -C "$1" -xzf "$0"'
elif local_src.is_file():
cmd = 'trap "rm -f $0" EXIT; rm -f "$1" && tar -C "$(dirname "$1")" -xzf "$0"'
else:
msg = f"Unsupported source type: {local_src}"
raise ClanError(msg)
# We also need some sort of locks in case we have multiple prompts
host.run(
[
"sudo",
"-p",
f"Enter sudo password for {host.host}:\n",
"--",
"bash",
"-c",
cmd,
archive,
str(remote_dest),
f"{dir_mode:o}",
],
tty=True,
opts=RunOpts(
log=Log.BOTH,
prefix="",
),
)
def upload( def upload(
host: Host, host: Host,
local_src: Path, local_src: Path,
@@ -160,22 +89,33 @@ def upload(
with local_src.open("rb") as f: with local_src.open("rb") as f:
tar.addfile(tarinfo, f) tar.addfile(tarinfo, f)
sudo = ""
if host.user != "root":
sudo = "sudo -- "
cmd = None
if local_src.is_dir():
cmd = 'rm -rf "$0" && mkdir -m "$1" -p "$0" && tar -C "$0" -xzf -'
elif local_src.is_file():
cmd = 'rm -f "$0" && tar -C "$(dirname "$0")" -xzf -'
else:
msg = f"Unsupported source type: {local_src}"
raise ClanError(msg)
# TODO accept `input` to be an IO object instead of bytes so that we don't have to read the tarfile into memory. # 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: with tar_path.open("rb") as f:
if host.user == "root": run_local(
unpack_archive_as_root( [
host, *host.ssh_cmd(),
f, "--",
local_src, f"{sudo}bash -c {quote(cmd)}",
remote_dest, str(remote_dest),
dir_mode=dir_mode, f"{dir_mode:o}",
) ],
else: RunOpts(
# For sudo we need to split the upload into two steps input=f.read(),
unpack_archive_as_user( log=Log.BOTH,
host, prefix=host.command_prefix,
f, needs_user_terminal=True,
local_src, ),
remote_dest,
dir_mode=dir_mode,
) )

View File

@@ -80,16 +80,6 @@ exec {bash} -l "${{@}}"
fake_sudo.write_text( fake_sudo.write_text(
f"""#!{bash} f"""#!{bash}
# skip over every sudo option
for arg in "${{@}}"; do
if [[ "$arg" == "-p" ]]; then
shift
shift
continue
fi
break
done
exec "${{@}}" exec "${{@}}"
""" """
) )

View File

@@ -982,10 +982,7 @@ def test_secrets_key_generate_gpg(
] ]
) )
assert "age private key" not in output.out assert "age private key" not in output.out
assert re.match(r"PGP key.+is already set", output.err) is not None
assert re.match(r"PGP key.+is already set", output.err), (
f"expected /PGP key.+is already set/ =~ {output.err}"
)
with capture_output as output: with capture_output as output:
cli.run( cli.run(