fix upload when sudo prompts are needed

This commit is contained in:
Jörg Thalheim
2025-05-02 13:38:29 +02:00
parent 3b5c22ebcf
commit f4d34b1326
2 changed files with 99 additions and 29 deletions

View File

@@ -2,13 +2,84 @@ 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,
@@ -89,33 +160,22 @@ 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:
run_local( if host.user == "root":
[ unpack_archive_as_root(
*host.ssh_cmd(), host,
"--", f,
f"{sudo}bash -c {quote(cmd)}", local_src,
str(remote_dest), remote_dest,
f"{dir_mode:o}", dir_mode=dir_mode,
], )
RunOpts( else:
input=f.read(), # For sudo we need to split the upload into two steps
log=Log.BOTH, unpack_archive_as_user(
prefix=host.command_prefix, host,
needs_user_terminal=True, f,
), local_src,
) remote_dest,
dir_mode=dir_mode,
)

View File

@@ -80,6 +80,16 @@ 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 "${{@}}"
""" """
) )