Skip to content

Commit 9287094

Browse files
claudePaul Miller
authored andcommitted
sudo: add mitogen_sudo_password_prompt for custom passprompt
Mitogen's PASSWORD_PROMPT_RE matches "password" in dozens of locales, covering any default sudo configuration. But if sudoers defines a custom passprompt (e.g. `[sudo] %u@%h:`) that omits the word "password", mitogen never recognizes the prompt, never sends the password, and the sudo bootstrap hangs until connect_timeout. Ansible's own sudo become plugin sidesteps this by always forcing -p, but mitogen matches the prompt on the wire instead. This commit adds a configurable regex pattern via the ansible variable `mitogen_sudo_password_prompt`. When set, the pattern is compiled and prepended to SetupProtocol.PARTIAL_PATTERNS, with the built-in patterns as fallback. Configuration (group_vars, host_vars, or play vars): mitogen_sudo_password_prompt: '\[sudo\] \w+@[\w.]+:'
1 parent 7c12aa3 commit 9287094

6 files changed

Lines changed: 113 additions & 21 deletions

File tree

ansible_mitogen/connection.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -336,18 +336,24 @@ def _connect_sudo(spec):
336336
"""
337337
Return ContextService arguments for sudo as a become method.
338338
"""
339+
kwargs = {
340+
'username': spec.become_user(),
341+
'password': spec.become_pass(),
342+
'python_path': spec.python_path(),
343+
'sudo_path': spec.become_exe(),
344+
'connect_timeout': spec.timeout(),
345+
'sudo_args': spec.sudo_args(),
346+
'remote_name': get_remote_name(spec),
347+
}
348+
349+
password_prompt = spec.mitogen_sudo_password_prompt()
350+
if password_prompt is not None:
351+
kwargs['password_prompt'] = password_prompt
352+
339353
return {
340354
'method': 'sudo',
341355
'enable_lru': True,
342-
'kwargs': {
343-
'username': spec.become_user(),
344-
'password': spec.become_pass(),
345-
'python_path': spec.python_path(),
346-
'sudo_path': spec.become_exe(),
347-
'connect_timeout': spec.timeout(),
348-
'sudo_args': spec.sudo_args(),
349-
'remote_name': get_remote_name(spec),
350-
}
356+
'kwargs': kwargs,
351357
}
352358

353359

@@ -390,17 +396,23 @@ def _connect_mitogen_sudo(spec):
390396
"""
391397
Return ContextService arguments for sudo as a first class connection.
392398
"""
399+
kwargs = {
400+
'username': spec.remote_user(),
401+
'password': spec.password(),
402+
'python_path': spec.python_path(),
403+
'sudo_path': spec.become_exe(),
404+
'connect_timeout': spec.timeout(),
405+
'sudo_args': spec.sudo_args(),
406+
'remote_name': get_remote_name(spec),
407+
}
408+
409+
password_prompt = spec.mitogen_sudo_password_prompt()
410+
if password_prompt is not None:
411+
kwargs['password_prompt'] = password_prompt
412+
393413
return {
394414
'method': 'sudo',
395-
'kwargs': {
396-
'username': spec.remote_user(),
397-
'password': spec.password(),
398-
'python_path': spec.python_path(),
399-
'sudo_path': spec.become_exe(),
400-
'connect_timeout': spec.timeout(),
401-
'sudo_args': spec.sudo_args(),
402-
'remote_name': get_remote_name(spec),
403-
}
415+
'kwargs': kwargs,
404416
}
405417

406418

ansible_mitogen/transport_config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,13 @@ def mitogen_ssh_compression(self):
414414
Whether SSH compression is enabled.
415415
"""
416416

417+
@abc.abstractmethod
418+
def mitogen_sudo_password_prompt(self):
419+
"""
420+
Optional regex pattern to match the sudo password prompt.
421+
Overrides the built-in PASSWORD_PROMPT_RE in mitogen.sudo.
422+
"""
423+
417424
@abc.abstractmethod
418425
def extra_args(self):
419426
"""
@@ -625,6 +632,9 @@ def mitogen_ssh_debug_level(self):
625632
def mitogen_ssh_compression(self):
626633
return self._connection.get_task_var('mitogen_ssh_compression')
627634

635+
def mitogen_sudo_password_prompt(self):
636+
return self._connection.get_task_var('mitogen_sudo_password_prompt')
637+
628638
def extra_args(self):
629639
return self._connection.get_extra_args()
630640

@@ -876,6 +886,9 @@ def mitogen_ssh_debug_level(self):
876886
def mitogen_ssh_compression(self):
877887
return self._host_vars.get('mitogen_ssh_compression')
878888

889+
def mitogen_sudo_password_prompt(self):
890+
return self._host_vars.get('mitogen_sudo_password_prompt')
891+
879892
def extra_args(self):
880893
return [] # TODO
881894

docs/ansible_detailed.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,11 @@ When used as a become method:
997997
Mitogen produces remote processes named like
998998
`"mitogen:user@controller.name:1234"`, however this may be a privacy issue in
999999
some circumstances.
1000+
* ``mitogen_sudo_password_prompt``: optional regular expression pattern to
1001+
match a custom sudo password prompt. Mitogen's built-in pattern matches
1002+
"password" in dozens of locales, but custom sudoers ``passprompt`` settings
1003+
that omit that word will cause the connection to hang. Set this to a pattern
1004+
matching your prompt, e.g. ``'\[sudo\] \w+@[\w.]+:'``.
10001005
* ansible.cfg: ``timeout``
10011006

10021007
When used as the ``mitogen_sudo`` connection method:
@@ -1005,6 +1010,7 @@ When used as the ``mitogen_sudo`` connection method:
10051010
* ``ansible_user``: username to sudo as.
10061011
* ``ansible_password``: password to sudo as.
10071012
* ``sudo_flags``, ``become_flags``
1013+
* ``mitogen_sudo_password_prompt``
10081014
* ``ansible_python_interpreter``
10091015

10101016

docs/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ In progress
3131
source modifier feature
3232
* :gh:issue:`1540` tests: Test :class:`mitogen.master.ModuleFinder` source
3333
override
34+
* :gh:issue:`1503` :mod:`ansible_mitogen`: Add ``mitogen_sudo_password_prompt``
35+
variable to allow matching custom sudo password prompts that omit the word
36+
"password"
3437

3538

3639
v0.3.51 (2026-07-18)

mitogen/sudo.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,23 +133,26 @@ class Options(mitogen.parent.Options):
133133
sudo_path = 'sudo'
134134
username = 'root'
135135
password = None
136+
password_prompt = None
136137
preserve_env = False
137138
set_home = False
138139
login = False
139-
140140
selinux_role = None
141141
selinux_type = None
142142

143143
def __init__(self, username=None, sudo_path=None, password=None,
144-
preserve_env=None, set_home=None, sudo_args=None,
145-
login=None, selinux_role=None, selinux_type=None, **kwargs):
144+
password_prompt=None, preserve_env=None, set_home=None,
145+
sudo_args=None, login=None, selinux_role=None,
146+
selinux_type=None, **kwargs):
146147
super(Options, self).__init__(**kwargs)
147148
opts = parse_sudo_flags(sudo_args or [])
148149

149150
self.username = option(self.username, username, opts.user)
150151
self.sudo_path = option(self.sudo_path, sudo_path)
151152
if password:
152153
self.password = mitogen.core.to_text(password)
154+
if password_prompt:
155+
self.password_prompt = password_prompt
153156
self.preserve_env = option(self.preserve_env,
154157
preserve_env, opts.preserve_env)
155158
self.set_home = option(self.set_home, set_home, opts.set_home)
@@ -196,6 +199,24 @@ class Connection(mitogen.parent.Connection):
196199
}
197200
child_is_immediate_subprocess = False
198201

202+
def stderr_stream_factory(self):
203+
"""
204+
If password_prompt is configured, build a stream using a subclass of
205+
SetupProtocol with the custom pattern prepended to PARTIAL_PATTERNS.
206+
"""
207+
prompt = self.options.password_prompt
208+
if not prompt:
209+
return self.diag_protocol_class.build_stream()
210+
211+
custom_re = re.compile(prompt.encode('utf-8'), re.I)
212+
213+
class CustomSetupProtocol(SetupProtocol):
214+
PARTIAL_PATTERNS = [
215+
(custom_re, SetupProtocol._on_password_prompt),
216+
] + SetupProtocol.PARTIAL_PATTERNS
217+
218+
return CustomSetupProtocol.build_stream()
219+
199220
def _get_name(self):
200221
return u'sudo.' + mitogen.core.to_text(self.options.username)
201222

tests/sudo_test.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,43 @@ def test_tty_preserved(self):
108108
del os.environ['PREHISTORIC_SUDO']
109109

110110

111+
class PasswordPromptPatternTest(testlib.TestCase):
112+
def _make_options(self, **kwargs):
113+
return mitogen.sudo.Options(
114+
max_message_size=mitogen.core.CHUNK_SIZE,
115+
**kwargs
116+
)
117+
118+
def test_default_has_no_custom_prompt(self):
119+
options = self._make_options()
120+
self.assertIsNone(options.password_prompt)
121+
122+
def test_custom_prompt_stored(self):
123+
options = self._make_options(
124+
password_prompt=r'\[sudo\] \w+@[\w.]+:',
125+
)
126+
self.assertEqual(options.password_prompt, r'\[sudo\] \w+@[\w.]+:')
127+
128+
def test_custom_protocol_prepends_pattern(self):
129+
options = self._make_options(
130+
password_prompt=r'\[sudo\] \w+@[\w.]+:',
131+
)
132+
conn = mitogen.sudo.Connection(options, router=None)
133+
stream = conn.stderr_stream_factory()
134+
patterns = stream.protocol.PARTIAL_PATTERNS
135+
# custom pattern first, built-in second
136+
self.assertEqual(len(patterns), 2)
137+
self.assertEqual(patterns[0][0].pattern, b'\\[sudo\\] \\w+@[\\w.]+:')
138+
self.assertIs(patterns[1][0], mitogen.sudo.PASSWORD_PROMPT_RE)
139+
140+
def test_no_custom_uses_default_protocol(self):
141+
options = self._make_options()
142+
conn = mitogen.sudo.Connection(options, router=None)
143+
stream = conn.stderr_stream_factory()
144+
self.assertIsInstance(stream.protocol, mitogen.sudo.SetupProtocol)
145+
self.assertEqual(len(stream.protocol.PARTIAL_PATTERNS), 1)
146+
147+
111148
class SudoMixin(testlib.DockerMixin):
112149
def test_password_required(self):
113150
ssh = self.docker_ssh(

0 commit comments

Comments
 (0)