Skip to content

Commit d465f6c

Browse files
authored
fix(operations): handle password prompt from sudo-rs (eg. recent Ubuntu) (#1734)
1 parent 9499a71 commit d465f6c

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

src/pyinfra/connectors/util.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@
2424
SUDO_ASKPASS_ENV_VAR = "PYINFRA_SUDO_PASSWORD"
2525
SU_ASKPASS_ENV_VAR = "PYINFRA_SU_PASSWORD"
2626

27+
# Output lines that indicate sudo could not prompt for a password and we should retry with one.
28+
# - sudo (Todd C. Miller's): "sudo: a password is required"
29+
# - sudo-rs (Trifecta Tech): "sudo-rs: interactive authentication is required"
30+
# https://github.com/trifectatechfoundation/sudo-rs (default sudo on Ubuntu 25.10+)
31+
SUDO_PASSWORD_REQUIRED_LINES = (
32+
"sudo: a password is required",
33+
"sudo-rs: interactive authentication is required",
34+
)
35+
2736

2837
ASKPASS_COMMAND = r"""
2938
temp=$(mktemp "${{TMPDIR:={0}}}/pyinfra-sudo-askpass-XXXXXXXXXXXX")
@@ -204,7 +213,7 @@ def execute_command_with_sudo_retry(
204213
# https://github.com/pyinfra-dev/pyinfra/issues/1292
205214
if return_code != 0 and output and output.combined_lines:
206215
for line in reversed(output.combined_lines):
207-
if line.line.strip() == "sudo: a password is required":
216+
if line.line.strip() in SUDO_PASSWORD_REQUIRED_LINES:
208217
# If we need a password, ask the user for it and attach to the host
209218
# internal connector data for use when executing future commands.
210219
sudo_password = getpass(f"{host.print_prefix}sudo password: ")

tests/test_connectors/test_ssh.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,45 @@ def test_run_shell_command_retry_for_sudo_password(
636636
get_pty=False,
637637
)
638638

639+
@mock.patch("pyinfra.connectors.ssh.SSHClient")
640+
@mock.patch("pyinfra.connectors.util.getpass")
641+
def test_run_shell_command_retry_for_sudo_rs_password(
642+
self,
643+
fake_getpass,
644+
fake_ssh_client,
645+
):
646+
# sudo-rs (the Rust replacement, default in Ubuntu 25.10+) prints a different message
647+
# when it cannot prompt non-interactively; the retry path should recognize it too.
648+
fake_getpass.return_value = "PASSWORD"
649+
650+
fake_ssh = mock.MagicMock()
651+
fake_stdin = mock.MagicMock()
652+
fake_stdout = mock.MagicMock()
653+
fake_stderr = ["sudo-rs: interactive authentication is required"]
654+
fake_ssh.exec_command.return_value = fake_stdin, fake_stdout, fake_stderr
655+
656+
fake_ssh_client.return_value = fake_ssh
657+
658+
inventory = make_inventory(hosts=("somehost",))
659+
state = State(inventory, Config())
660+
host = inventory.get_host("somehost")
661+
host.connect(state)
662+
host.connector_data["sudo_askpass_path"] = "/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX"
663+
664+
command = "echo hi"
665+
return_values = [1, 0] # return 0 on the second call
666+
fake_stdout.channel.recv_exit_status.side_effect = lambda: return_values.pop(0)
667+
668+
out = host.run_shell_command(command, _sudo=True)
669+
assert len(out) == 2
670+
assert out[0] is True
671+
assert fake_getpass.called
672+
fake_ssh.exec_command.assert_called_with(
673+
"env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX "
674+
"PYINFRA_SUDO_PASSWORD=PASSWORD sudo -H -A -k sh -c 'echo hi'",
675+
get_pty=False,
676+
)
677+
639678
# SSH file put/get tests
640679
#
641680

0 commit comments

Comments
 (0)