ssh: Ensure debug messages are properly converted to text (#57850)
* ssh: Ensure debug messages are properly converted to text. Fixes #57843 * surrogate_then_replace is default, be less explicit * We only needed to_text for display, not exceptions
This commit is contained in:
parent
3a3465c47d
commit
375ac76e58
2 changed files with 34 additions and 31 deletions
2
changelogs/fragments/ssh-fix-text-conv.yml
Normal file
2
changelogs/fragments/ssh-fix-text-conv.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
bugfixes:
|
||||||
|
- ssh connection plugin - Ensure that debug messages are properly encoded as text
|
|
@ -327,7 +327,7 @@ def _handle_error(remaining_retries, command, return_tuple, no_log, host, displa
|
||||||
if no_log:
|
if no_log:
|
||||||
msg = '{0} <error censored due to no log>'.format(msg)
|
msg = '{0} <error censored due to no log>'.format(msg)
|
||||||
else:
|
else:
|
||||||
msg = '{0} {1}'.format(msg, to_native(return_tuple[2].rstrip()))
|
msg = '{0} {1}'.format(msg, to_native(return_tuple[2]).rstrip())
|
||||||
raise AnsibleAuthenticationFailure(msg)
|
raise AnsibleAuthenticationFailure(msg)
|
||||||
|
|
||||||
# sshpass returns codes are 1-6. We handle 5 previously, so this catches other scenarios.
|
# sshpass returns codes are 1-6. We handle 5 previously, so this catches other scenarios.
|
||||||
|
@ -337,7 +337,7 @@ def _handle_error(remaining_retries, command, return_tuple, no_log, host, displa
|
||||||
if no_log:
|
if no_log:
|
||||||
msg = '{0} <error censored due to no log>'.format(msg)
|
msg = '{0} <error censored due to no log>'.format(msg)
|
||||||
else:
|
else:
|
||||||
msg = '{0} {1}'.format(msg, to_native(return_tuple[2].rstrip()))
|
msg = '{0} {1}'.format(msg, to_native(return_tuple[2]).rstrip())
|
||||||
|
|
||||||
if return_tuple[0] == 255:
|
if return_tuple[0] == 255:
|
||||||
SSH_ERROR = True
|
SSH_ERROR = True
|
||||||
|
@ -356,11 +356,11 @@ def _handle_error(remaining_retries, command, return_tuple, no_log, host, displa
|
||||||
|
|
||||||
# For other errors, no execption is raised so the connection is retried and we only log the messages
|
# For other errors, no execption is raised so the connection is retried and we only log the messages
|
||||||
if 1 <= return_tuple[0] <= 254:
|
if 1 <= return_tuple[0] <= 254:
|
||||||
msg = "Failed to connect to the host via ssh:"
|
msg = u"Failed to connect to the host via ssh:"
|
||||||
if no_log:
|
if no_log:
|
||||||
msg = '{0} <error censored due to no log>'.format(msg)
|
msg = u'{0} <error censored due to no log>'.format(msg)
|
||||||
else:
|
else:
|
||||||
msg = '{0} {1}'.format(msg, to_native(return_tuple[2]).rstrip())
|
msg = u'{0} {1}'.format(msg, to_text(return_tuple[2]).rstrip())
|
||||||
display.vvv(msg, host=host)
|
display.vvv(msg, host=host)
|
||||||
|
|
||||||
|
|
||||||
|
@ -379,7 +379,7 @@ def _ssh_retry(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapped(self, *args, **kwargs):
|
def wrapped(self, *args, **kwargs):
|
||||||
remaining_tries = int(C.ANSIBLE_SSH_RETRIES) + 1
|
remaining_tries = int(C.ANSIBLE_SSH_RETRIES) + 1
|
||||||
cmd_summary = "%s..." % args[0]
|
cmd_summary = u"%s..." % to_text(args[0])
|
||||||
for attempt in range(remaining_tries):
|
for attempt in range(remaining_tries):
|
||||||
cmd = args[0]
|
cmd = args[0]
|
||||||
if attempt != 0 and self._play_context.password and isinstance(cmd, list):
|
if attempt != 0 and self._play_context.password and isinstance(cmd, list):
|
||||||
|
@ -391,7 +391,7 @@ def _ssh_retry(func):
|
||||||
try:
|
try:
|
||||||
return_tuple = func(self, *args, **kwargs)
|
return_tuple = func(self, *args, **kwargs)
|
||||||
if self._play_context.no_log:
|
if self._play_context.no_log:
|
||||||
display.vvv('rc=%s, stdout and stderr censored due to no log' % return_tuple[0], host=self.host)
|
display.vvv(u'rc=%s, stdout and stderr censored due to no log' % return_tuple[0], host=self.host)
|
||||||
else:
|
else:
|
||||||
display.vvv(return_tuple, host=self.host)
|
display.vvv(return_tuple, host=self.host)
|
||||||
# 0 = success
|
# 0 = success
|
||||||
|
@ -427,9 +427,10 @@ def _ssh_retry(func):
|
||||||
pause = 30
|
pause = 30
|
||||||
|
|
||||||
if isinstance(e, AnsibleConnectionFailure):
|
if isinstance(e, AnsibleConnectionFailure):
|
||||||
msg = "ssh_retry: attempt: %d, ssh return code is 255. cmd (%s), pausing for %d seconds" % (attempt + 1, cmd_summary, pause)
|
msg = u"ssh_retry: attempt: %d, ssh return code is 255. cmd (%s), pausing for %d seconds" % (attempt + 1, cmd_summary, pause)
|
||||||
else:
|
else:
|
||||||
msg = "ssh_retry: attempt: %d, caught exception(%s) from cmd (%s), pausing for %d seconds" % (attempt + 1, e, cmd_summary, pause)
|
msg = (u"ssh_retry: attempt: %d, caught exception(%s) from cmd (%s), "
|
||||||
|
u"pausing for %d seconds" % (attempt + 1, to_text(e), cmd_summary, pause))
|
||||||
|
|
||||||
display.vv(msg, host=self.host)
|
display.vv(msg, host=self.host)
|
||||||
|
|
||||||
|
@ -681,7 +682,7 @@ class Connection(ConnectionBase):
|
||||||
just hang forever waiting for more commands.)
|
just hang forever waiting for more commands.)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
display.debug('Sending initial data')
|
display.debug(u'Sending initial data')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fh.write(to_bytes(in_data))
|
fh.write(to_bytes(in_data))
|
||||||
|
@ -697,7 +698,7 @@ class Connection(ConnectionBase):
|
||||||
'over ssh: %s' % (self.host, to_native(e)), orig_exc=e
|
'over ssh: %s' % (self.host, to_native(e)), orig_exc=e
|
||||||
)
|
)
|
||||||
|
|
||||||
display.debug('Sent initial data (%d bytes)' % len(in_data))
|
display.debug(u'Sent initial data (%d bytes)' % len(in_data))
|
||||||
|
|
||||||
# Used by _run() to kill processes on failures
|
# Used by _run() to kill processes on failures
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -727,18 +728,18 @@ class Connection(ConnectionBase):
|
||||||
|
|
||||||
# display.debug("Examining line (source=%s, state=%s): '%s'" % (source, state, display_line))
|
# display.debug("Examining line (source=%s, state=%s): '%s'" % (source, state, display_line))
|
||||||
if self.become.expect_prompt() and self.become.check_password_prompt(b_line):
|
if self.become.expect_prompt() and self.become.check_password_prompt(b_line):
|
||||||
display.debug("become_prompt: (source=%s, state=%s): '%s'" % (source, state, display_line))
|
display.debug(u"become_prompt: (source=%s, state=%s): '%s'" % (source, state, display_line))
|
||||||
self._flags['become_prompt'] = True
|
self._flags['become_prompt'] = True
|
||||||
suppress_output = True
|
suppress_output = True
|
||||||
elif self.become.success and self.become.check_success(b_line):
|
elif self.become.success and self.become.check_success(b_line):
|
||||||
display.debug("become_success: (source=%s, state=%s): '%s'" % (source, state, display_line))
|
display.debug(u"become_success: (source=%s, state=%s): '%s'" % (source, state, display_line))
|
||||||
self._flags['become_success'] = True
|
self._flags['become_success'] = True
|
||||||
suppress_output = True
|
suppress_output = True
|
||||||
elif sudoable and self.become.check_incorrect_password(b_line):
|
elif sudoable and self.become.check_incorrect_password(b_line):
|
||||||
display.debug("become_error: (source=%s, state=%s): '%s'" % (source, state, display_line))
|
display.debug(u"become_error: (source=%s, state=%s): '%s'" % (source, state, display_line))
|
||||||
self._flags['become_error'] = True
|
self._flags['become_error'] = True
|
||||||
elif sudoable and self.become.check_missing_password(b_line):
|
elif sudoable and self.become.check_missing_password(b_line):
|
||||||
display.debug("become_nopasswd_error: (source=%s, state=%s): '%s'" % (source, state, display_line))
|
display.debug(u"become_nopasswd_error: (source=%s, state=%s): '%s'" % (source, state, display_line))
|
||||||
self._flags['become_nopasswd_error'] = True
|
self._flags['become_nopasswd_error'] = True
|
||||||
|
|
||||||
if not suppress_output:
|
if not suppress_output:
|
||||||
|
@ -762,8 +763,8 @@ class Connection(ConnectionBase):
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# We don't use _shell.quote as this is run on the controller and independent from the shell plugin chosen
|
# We don't use _shell.quote as this is run on the controller and independent from the shell plugin chosen
|
||||||
display_cmd = list(map(shlex_quote, map(to_text, cmd)))
|
display_cmd = u' '.join(shlex_quote(to_text(c)) for c in cmd)
|
||||||
display.vvv(u'SSH: EXEC {0}'.format(u' '.join(display_cmd)), host=self.host)
|
display.vvv(u'SSH: EXEC {0}'.format(display_cmd), host=self.host)
|
||||||
|
|
||||||
# Start the given command. If we don't need to pipeline data, we can try
|
# Start the given command. If we don't need to pipeline data, we can try
|
||||||
# to use a pseudo-tty (ssh will have been invoked with -tt). If we are
|
# to use a pseudo-tty (ssh will have been invoked with -tt). If we are
|
||||||
|
@ -835,12 +836,12 @@ class Connection(ConnectionBase):
|
||||||
# We're requesting escalation with a password, so we have to
|
# We're requesting escalation with a password, so we have to
|
||||||
# wait for a password prompt.
|
# wait for a password prompt.
|
||||||
state = states.index('awaiting_prompt')
|
state = states.index('awaiting_prompt')
|
||||||
display.debug(u'Initial state: %s: %s' % (states[state], prompt))
|
display.debug(u'Initial state: %s: %s' % (states[state], to_text(prompt)))
|
||||||
elif self._play_context.become and self._play_context.success_key:
|
elif self._play_context.become and self.become.success:
|
||||||
# We're requesting escalation without a password, so we have to
|
# We're requesting escalation without a password, so we have to
|
||||||
# detect success/failure before sending any initial data.
|
# detect success/failure before sending any initial data.
|
||||||
state = states.index('awaiting_escalation')
|
state = states.index('awaiting_escalation')
|
||||||
display.debug(u'Initial state: %s: %s' % (states[state], self._play_context.success_key))
|
display.debug(u'Initial state: %s: %s' % (states[state], to_text(self.become.success)))
|
||||||
|
|
||||||
# We store accumulated stdout and stderr output from the process here,
|
# We store accumulated stdout and stderr output from the process here,
|
||||||
# but strip any privilege escalation prompt/confirmation lines first.
|
# but strip any privilege escalation prompt/confirmation lines first.
|
||||||
|
@ -911,7 +912,7 @@ class Connection(ConnectionBase):
|
||||||
# not going to arrive until the persisted connection closes.
|
# not going to arrive until the persisted connection closes.
|
||||||
timeout = 1
|
timeout = 1
|
||||||
b_tmp_stdout += b_chunk
|
b_tmp_stdout += b_chunk
|
||||||
display.debug("stdout chunk (state=%s):\n>>>%s<<<\n" % (state, to_text(b_chunk)))
|
display.debug(u"stdout chunk (state=%s):\n>>>%s<<<\n" % (state, to_text(b_chunk)))
|
||||||
elif key.fileobj == p.stderr:
|
elif key.fileobj == p.stderr:
|
||||||
b_chunk = p.stderr.read()
|
b_chunk = p.stderr.read()
|
||||||
if b_chunk == b'':
|
if b_chunk == b'':
|
||||||
|
@ -945,7 +946,7 @@ class Connection(ConnectionBase):
|
||||||
|
|
||||||
if states[state] == 'awaiting_prompt':
|
if states[state] == 'awaiting_prompt':
|
||||||
if self._flags['become_prompt']:
|
if self._flags['become_prompt']:
|
||||||
display.debug('Sending become_password in response to prompt')
|
display.debug(u'Sending become_password in response to prompt')
|
||||||
stdin.write(to_bytes(self._play_context.become_pass) + b'\n')
|
stdin.write(to_bytes(self._play_context.become_pass) + b'\n')
|
||||||
# On python3 stdin is a BufferedWriter, and we don't have a guarantee
|
# On python3 stdin is a BufferedWriter, and we don't have a guarantee
|
||||||
# that the write will happen without a flush
|
# that the write will happen without a flush
|
||||||
|
@ -960,23 +961,23 @@ class Connection(ConnectionBase):
|
||||||
|
|
||||||
if states[state] == 'awaiting_escalation':
|
if states[state] == 'awaiting_escalation':
|
||||||
if self._flags['become_success']:
|
if self._flags['become_success']:
|
||||||
display.vvv('Escalation succeeded')
|
display.vvv(u'Escalation succeeded')
|
||||||
self._flags['become_success'] = False
|
self._flags['become_success'] = False
|
||||||
state += 1
|
state += 1
|
||||||
elif self._flags['become_error']:
|
elif self._flags['become_error']:
|
||||||
display.vvv('Escalation failed')
|
display.vvv(u'Escalation failed')
|
||||||
self._terminate_process(p)
|
self._terminate_process(p)
|
||||||
self._flags['become_error'] = False
|
self._flags['become_error'] = False
|
||||||
raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
|
raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
|
||||||
elif self._flags['become_nopasswd_error']:
|
elif self._flags['become_nopasswd_error']:
|
||||||
display.vvv('Escalation requires password')
|
display.vvv(u'Escalation requires password')
|
||||||
self._terminate_process(p)
|
self._terminate_process(p)
|
||||||
self._flags['become_nopasswd_error'] = False
|
self._flags['become_nopasswd_error'] = False
|
||||||
raise AnsibleError('Missing %s password' % self._play_context.become_method)
|
raise AnsibleError('Missing %s password' % self._play_context.become_method)
|
||||||
elif self._flags['become_prompt']:
|
elif self._flags['become_prompt']:
|
||||||
# This shouldn't happen, because we should see the "Sorry,
|
# This shouldn't happen, because we should see the "Sorry,
|
||||||
# try again" message first.
|
# try again" message first.
|
||||||
display.vvv('Escalation prompt repeated')
|
display.vvv(u'Escalation prompt repeated')
|
||||||
self._terminate_process(p)
|
self._terminate_process(p)
|
||||||
self._flags['become_prompt'] = False
|
self._flags['become_prompt'] = False
|
||||||
raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
|
raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
|
||||||
|
@ -1126,9 +1127,9 @@ class Connection(ConnectionBase):
|
||||||
else:
|
else:
|
||||||
# If not in smart mode, the data will be printed by the raise below
|
# If not in smart mode, the data will be printed by the raise below
|
||||||
if len(methods) > 1:
|
if len(methods) > 1:
|
||||||
display.warning('%s transfer mechanism failed on %s. Use ANSIBLE_DEBUG=1 to see detailed information' % (method, host))
|
display.warning(u'%s transfer mechanism failed on %s. Use ANSIBLE_DEBUG=1 to see detailed information' % (method, host))
|
||||||
display.debug('%s' % to_text(stdout))
|
display.debug(u'%s' % to_text(stdout))
|
||||||
display.debug('%s' % to_text(stderr))
|
display.debug(u'%s' % to_text(stderr))
|
||||||
|
|
||||||
if returncode == 255:
|
if returncode == 255:
|
||||||
raise AnsibleConnectionFailure("Failed to connect to the host via %s: %s" % (method, to_native(stderr)))
|
raise AnsibleConnectionFailure("Failed to connect to the host via %s: %s" % (method, to_native(stderr)))
|
||||||
|
@ -1237,12 +1238,12 @@ class Connection(ConnectionBase):
|
||||||
run_reset = True
|
run_reset = True
|
||||||
|
|
||||||
if run_reset:
|
if run_reset:
|
||||||
display.vvv(u'sending stop: %s' % cmd)
|
display.vvv(u'sending stop: %s' % to_text(cmd))
|
||||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
status_code = p.wait()
|
status_code = p.wait()
|
||||||
if status_code != 0:
|
if status_code != 0:
|
||||||
display.warning("Failed to reset connection:%s" % stderr)
|
display.warning(u"Failed to reset connection:%s" % to_text(stderr))
|
||||||
|
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue