Repository: ansible/ansible
Pull request: ssh: Ensure debug messages are properly converted to text
##### SUMMARY
ssh: Ensure debug messages are properly converted to text. Fixes #57843

##### ISSUE TYPE
- Bugfix Pull Request

##### COMPONENT NAME
lib/ansible/plugins/connection/ssh.py

##### ADDITIONAL INFORMATION
<!--- Include additional information to help people understand the change here -->
<!--- A step-by-step reproduction of the problem is helpful if there is no related issue -->

<!--- Paste verbatim command output below, e.g. before and after your change -->
```paste below

```

Problem statement:
ssh: UnicodeDecodeError: 'ascii' codec can't decode byte
##### SUMMARY
Executing a playbook in verbose 3 (-vvv) leads to the following error:

> UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 63: ordinal not in range(128)
fatal: [server]: FAILED! => {
    "msg": "Unexpected failure during module execution.",
    "stdout": ""
}

when following conditions are met:
- task in error: permission denied for example
- characters with an accent in connection messages: pre login message (file /etc/issue) for example

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
ssh.py / display.py

##### ANSIBLE VERSION
```
ansible 2.6.13
  config file = /jenkins/test/ansible/ansible.cfg
  configured module search path = [u'/jenkins/test/ansible/lib/python2.6/site-packages/ansible/modules', u'/jenkins/test/ansible/odyssey/modules']
  ansible python module location = /jenkins/test/ansible/lib/python2.6/site-packages/ansible
  executable location = /jenkins/test/ansible/bin/ansible
  python version = 2.6.6 (r266:84292, May 22 2015, 08:34:51) [GCC 4.4.7 20120313 (Red Hat 4.4.7-15)]
```

##### CONFIGURATION
```
ANSIBLE_FORCE_COLOR(/jenkins/test/ansible/ansible.cfg) = True
ANSIBLE_PIPELINING(/jenkins/test/ansible/ansible.cfg) = True
ANSIBLE_SSH_ARGS(/jenkins/test/ansible/ansible.cfg) = -o PreferredAuthentications=publickey
ANSIBLE_SSH_CONTROL_PATH(/jenkins/test/ansible/ansible.cfg) = %(directory)s/sge-ssh-%%h-%%p-%%r
ANSIBLE_SSH_RETRIES(/jenkins/test/ansible/ansible.cfg) = 0
ANY_ERRORS_FATAL(/jenkins/test/ansible/ansible.cfg) = True
CACHE_PLUGIN(/jenkins/test/ansible/ansible.cfg) = memory
DEFAULT_ACTION_PLUGIN_PATH(/jenkins/test/ansible/ansible.cfg) = [u'/jenkins/test/ansible/project/modules/plugin_config_ssh/action_plugins']
DEFAULT_CALLBACK_PLUGIN_PATH(/jenkins/test/ansible/ansible.cfg) = [u'/jenkins/test/ansible/project/roles/project_callback_plugins']
DEFAULT_CALLBACK_WHITELIST(/jenkins/test/ansible/ansible.cfg) = ['profile_tasks']
DEFAULT_FILTER_PLUGIN_PATH(/jenkins/test/ansible/ansible.cfg) = [u'/jenkins/test/ansible/project/roles/project_filters_plugin']
DEFAULT_FORKS(/jenkins/test/ansible/ansible.cfg) = 50
DEFAULT_GATHERING(/jenkins/test/ansible/ansible.cfg) = explicit
DEFAULT_HASH_BEHAVIOUR(/jenkins/test/ansible/ansible.cfg) = merge
DEFAULT_JINJA2_EXTENSIONS(/jenkins/test/ansible/ansible.cfg) = jinja2.ext.do,jinja2.ext.i18n
DEFAULT_LOCAL_TMP(/jenkins/test/ansible/ansible.cfg) = /home/jenkins/.ansible/tmp/ansible-local-21303iN97VZ
DEFAULT_MANAGED_STR(/jenkins/test/ansible/ansible.cfg) = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
DEFAULT_MODULE_PATH(/jenkins/test/ansible/ansible.cfg) = [u'/jenkins/test/ansible/lib/python2.6/site-packages/ansible/modules', u'/jenkins/test/ansible/project/modules']
DEFAULT_MODULE_UTILS_PATH(/jenkins/test/ansible/ansible.cfg) = [u'/jenkins/test/ansible']
DEFAULT_POLL_INTERVAL(/jenkins/test/ansible/ansible.cfg) = 15
DEFAULT_REMOTE_PORT(/jenkins/test/ansible/ansible.cfg) = 22
DEFAULT_REMOTE_USER(/jenkins/test/ansible/ansible.cfg) = deploy
DEFAULT_ROLES_PATH(/jenkins/test/ansible/ansible.cfg) = [u'/jenkins/test/ansible/project/roles']
DEFAULT_SCP_IF_SSH(/jenkins/test/ansible/ansible.cfg) = True
DEFAULT_TIMEOUT(/jenkins/test/ansible/ansible.cfg) = 20
DEFAULT_TRANSPORT(/jenkins/test/ansible/ansible.cfg) = ssh
DISPLAY_SKIPPED_HOSTS(/jenkins/test/ansible/ansible.cfg) = False
HOST_KEY_CHECKING(/jenkins/test/ansible/ansible.cfg) = False
INVENTORY_UNPARSED_IS_FAILED(/jenkins/test/ansible/ansible.cfg) = True
SYSTEM_WARNINGS(/jenkins/test/ansible/ansible.cfg) = True
```

##### OS / ENVIRONMENT
target OS version: Red Hat Enterprise Linux Server release 7.3 (Maipo)

##### STEPS TO REPRODUCE
Playbook:
```yaml
- hosts: server
  tasks:
  - stat:
      path: "/home/anotheruser/lvi"
```
Set file /etc/issue on target machine with content:
```
ATTENTION:
Quiconque accéderait ou tenterait d'accéder à ce système.
```
Execute with ansible-playbook -vvv -i playbook.yml

Notes:
- path /home/anotheruser is not accessible to the current user
- running without -vvv (ie -vv or -v or none) does not lead to the error

##### EXPECTED RESULTS
Permission denied as /home/anotheruser is not accessible to the current user and as result is with ansible version < 2.6.13:
```
TASK [stat] *************************************************************************************************************************************************************
task path: /home/jenkins/playbook.yml:3
Friday 14 June 2019  10:26:01 +0200 (0:00:00.170)       0:00:00.170 ***********
Using module file /jenkins/test/ansible/lib/python2.6/site-packages/ansible/modules/files/stat.py
<server> ESTABLISH SSH CONNECTION FOR USER: myuser
<server> SSH: EXEC ssh -o PreferredAuthentications=publickey -o StrictHostKeyChecking=no -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=myuser -o ConnectTimeout=20 server '/bin/sh -c '"'"'/usr/bin/python && sleep 0'"'"''
<server> (1, '\n{"msg": "Permission denied", "failed": true, "exception": "  File \\"/tmp/ansible_1CvBt6/ansible_module_stat.py\\", line 476, in main\\n    st = os.lstat(b_path)\\n", "invocation": {"module_args": {"checksum_algorithm": "sha1", "get_checksum": true, "follow": false, "path": "/home/anotheruser/lvi", "get_md5": null, "get_mime": true, "get_attributes": true}}}\n', "ATTENTION:\nQuiconque acc\xc3\xa9derait ou tenterait d'acc\xc3\xa9der \xc3\xa0 ce syst\xc3\xa8me.\n\n")
The full traceback is:
  File "/tmp/ansible_1CvBt6/ansible_module_stat.py", line 476, in main
    st = os.lstat(b_path)

fatal: [server]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "checksum_algorithm": "sha1",
            "follow": false,
            "get_attributes": true,
            "get_checksum": true,
            "get_md5": null,
            "get_mime": true,
            "path": "/home/anotheruser/lvi"
        }
    },
    "msg": "Permission denied"
}
```

##### ACTUAL RESULTS
The execution failed with error "Unexpected failure during module execution":
```
TASK [stat] *************************************************************************************************************************************************************
task path: /home/jenkins/playbook.yml:3
Friday 14 June 2019  10:36:17 +0200 (0:00:00.170)       0:00:00.170 ***********
Using module file /jenkins/test/ansible/lib/python2.6/site-packages/ansible/modules/files/stat.py
<server> ESTABLISH SSH CONNECTION FOR USER: myuser
<server> SSH: EXEC ssh -o PreferredAuthentications=publickey -o StrictHostKeyChecking=no -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=myuser -o ConnectTimeout=20 server '/bin/sh -c '"'"'/usr/bin/python && sleep 0'"'"''
<server> (1, '\n{"msg": "Permission denied", "failed": true, "exception": "  File \\"/tmp/ansible_Vvkdbf/ansible_module_stat.py\\", line 476, in main\\n    st = os.lstat(b_path)\\n", "invocation": {"module_args": {"checksum_algorithm": "sha1", "get_checksum": true, "follow": false, "path": "/home/anotheruser/lvi", "get_md5": null, "get_mime": true, "get_attributes": true}}}\n', "ATTENTION:\nQuiconque acc\xc3\xa9derait ou tenterait d'acc\xc3\xa9der \xc3\xa0 ce syst\xc3\xa8me.\n\n")
The full traceback is:
Traceback (most recent call last):
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/executor/task_executor.py", line 139, in run
    res = self._execute()
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/executor/task_executor.py", line 584, in _execute
    result = self._handler.run(task_vars=variables)
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/plugins/action/normal.py", line 46, in run
    result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=wrap_async))
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/plugins/action/__init__.py", line 853, in _execute_module
    res = self._low_level_execute_command(cmd, sudoable=sudoable, in_data=in_data)
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/plugins/action/__init__.py", line 960, in _low_level_execute_command
    rc, stdout, stderr = self._connection.exec_command(cmd, in_data=in_data, sudoable=sudoable)
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/plugins/connection/ssh.py", line 1080, in exec_command
    (returncode, stdout, stderr) = self._run(cmd, in_data, sudoable=sudoable)
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/plugins/connection/ssh.py", line 354, in wrapped
    _handle_error(remaining_retries, cmd[0], return_tuple, self._play_context.no_log, self.host)
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/plugins/connection/ssh.py", line 307, in _handle_error
    display.vvv(msg, host=host)
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/utils/display.py", line 183, in vvv
    return self.verbose(msg, host=host, caplevel=2)
  File "/jenkins/test/ansible/lib/python2.6/site-packages/ansible/utils/display.py", line 206, in verbose
    self.display("<%s> %s" % (host, msg), color=C.COLOR_VERBOSE, screen_only=True)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 63: ordinal not in range(128)

fatal: [server]: FAILED! => {
    "msg": "Unexpected failure during module execution.",
    "stdout": ""
}
```

Hints from issue discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->

Commit message: surrogate_then_replace is default, be less explicit

Patch to review:
diff --git a/changelogs/fragments/ssh-fix-text-conv.yml b/changelogs/fragments/ssh-fix-text-conv.yml
new file mode 100644
index 00000000000000..64eed8f4c17c26
--- /dev/null
+++ b/changelogs/fragments/ssh-fix-text-conv.yml
@@ -0,0 +1,2 @@
+bugfixes:
+- ssh connection plugin - Ensure that debug messages are properly encoded as text
diff --git a/lib/ansible/plugins/connection/ssh.py b/lib/ansible/plugins/connection/ssh.py
index 61b0ba9eccca88..5d1bbbfacb4ff8 100644
--- a/lib/ansible/plugins/connection/ssh.py
+++ b/lib/ansible/plugins/connection/ssh.py
@@ -321,23 +321,23 @@ def _handle_error(remaining_retries, command, return_tuple, no_log, host, displa
     if command == b'sshpass':
         # Error 5 is invalid/incorrect password. Raise an exception to prevent retries from locking the account.
         if return_tuple[0] == 5:
-            msg = 'Invalid/incorrect username/password. Skipping remaining {0} retries to prevent account lockout:'.format(remaining_retries)
+            msg = u'Invalid/incorrect username/password. Skipping remaining {0} retries to prevent account lockout:'.format(remaining_retries)
             if remaining_retries <= 0:
-                msg = 'Invalid/incorrect password:'
+                msg = u'Invalid/incorrect password:'
             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:
-                msg = '{0} {1}'.format(msg, to_native(return_tuple[2].rstrip()))
+                msg = u'{0} {1}'.format(msg, to_text(return_tuple[2]).rstrip())
             raise AnsibleAuthenticationFailure(msg)
 
         # sshpass returns codes are 1-6. We handle 5 previously, so this catches other scenarios.
         # No exception is raised, so the connection is retried.
         elif return_tuple[0] in [1, 2, 3, 4, 6]:
-            msg = 'sshpass error:'
+            msg = u'sshpass error:'
             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:
-                msg = '{0} {1}'.format(msg, to_native(return_tuple[2].rstrip()))
+                msg = u'{0} {1}'.format(msg, to_text(return_tuple[2]).rstrip())
 
     if return_tuple[0] == 255:
         SSH_ERROR = True
@@ -347,20 +347,20 @@ def _handle_error(remaining_retries, command, return_tuple, no_log, host, displa
                 break
 
         if SSH_ERROR:
-            msg = "Failed to connect to the host via ssh:"
+            msg = u"Failed to connect to the host via ssh:"
             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:
-                msg = '{0} {1}'.format(msg, to_native(return_tuple[2]).rstrip())
+                msg = u'{0} {1}'.format(msg, to_text(return_tuple[2]).rstrip())
             raise AnsibleConnectionFailure(msg)
 
     # For other errors, no execption is raised so the connection is retried and we only log the messages
     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:
-            msg = '{0} <error censored due to no log>'.format(msg)
+            msg = u'{0} <error censored due to no log>'.format(msg)
         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)
 
 
@@ -379,7 +379,7 @@ def _ssh_retry(func):
     @wraps(func)
     def wrapped(self, *args, **kwargs):
         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):
             cmd = args[0]
             if attempt != 0 and self._play_context.password and isinstance(cmd, list):
@@ -391,7 +391,7 @@ def wrapped(self, *args, **kwargs):
                 try:
                     return_tuple = func(self, *args, **kwargs)
                     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:
                         display.vvv(return_tuple, host=self.host)
                     # 0 = success
@@ -427,9 +427,10 @@ def wrapped(self, *args, **kwargs):
                         pause = 30
 
                     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:
-                        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)
 
@@ -681,7 +682,7 @@ def _send_initial_data(self, fh, in_data, ssh_process):
         just hang forever waiting for more commands.)
         '''
 
-        display.debug('Sending initial data')
+        display.debug(u'Sending initial data')
 
         try:
             fh.write(to_bytes(in_data))
@@ -697,7 +698,7 @@ def _send_initial_data(self, fh, in_data, ssh_process):
                     '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
     @staticmethod
@@ -727,18 +728,18 @@ def _examine_output(self, source, state, b_chunk, sudoable):
 
             # 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):
-                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
                 suppress_output = True
             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
                 suppress_output = True
             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
             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
 
             if not suppress_output:
@@ -762,8 +763,8 @@ def _bare_run(self, cmd, in_data, sudoable=True, checkrc=True):
         '''
 
         # 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.vvv(u'SSH: EXEC {0}'.format(u' '.join(display_cmd)), host=self.host)
+        display_cmd = u' '.join(shlex_quote(to_text(c)) for c in cmd)
+        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
         # to use a pseudo-tty (ssh will have been invoked with -tt). If we are
@@ -835,12 +836,12 @@ def _bare_run(self, cmd, in_data, sudoable=True, checkrc=True):
                 # We're requesting escalation with a password, so we have to
                 # wait for a password prompt.
                 state = states.index('awaiting_prompt')
-                display.debug(u'Initial state: %s: %s' % (states[state], prompt))
-            elif self._play_context.become and self._play_context.success_key:
+                display.debug(u'Initial state: %s: %s' % (states[state], to_text(prompt)))
+            elif self._play_context.become and self.become.success:
                 # We're requesting escalation without a password, so we have to
                 # detect success/failure before sending any initial data.
                 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,
         # but strip any privilege escalation prompt/confirmation lines first.
@@ -911,7 +912,7 @@ def _bare_run(self, cmd, in_data, sudoable=True, checkrc=True):
                             # not going to arrive until the persisted connection closes.
                             timeout = 1
                         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:
                         b_chunk = p.stderr.read()
                         if b_chunk == b'':
@@ -945,7 +946,7 @@ def _bare_run(self, cmd, in_data, sudoable=True, checkrc=True):
 
                 if states[state] == 'awaiting_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')
                         # On python3 stdin is a BufferedWriter, and we don't have a guarantee
                         # that the write will happen without a flush
@@ -960,23 +961,23 @@ def _bare_run(self, cmd, in_data, sudoable=True, checkrc=True):
 
                 if states[state] == 'awaiting_escalation':
                     if self._flags['become_success']:
-                        display.vvv('Escalation succeeded')
+                        display.vvv(u'Escalation succeeded')
                         self._flags['become_success'] = False
                         state += 1
                     elif self._flags['become_error']:
-                        display.vvv('Escalation failed')
+                        display.vvv(u'Escalation failed')
                         self._terminate_process(p)
                         self._flags['become_error'] = False
                         raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
                     elif self._flags['become_nopasswd_error']:
-                        display.vvv('Escalation requires password')
+                        display.vvv(u'Escalation requires password')
                         self._terminate_process(p)
                         self._flags['become_nopasswd_error'] = False
                         raise AnsibleError('Missing %s password' % self._play_context.become_method)
                     elif self._flags['become_prompt']:
                         # This shouldn't happen, because we should see the "Sorry,
                         # try again" message first.
-                        display.vvv('Escalation prompt repeated')
+                        display.vvv(u'Escalation prompt repeated')
                         self._terminate_process(p)
                         self._flags['become_prompt'] = False
                         raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
@@ -1126,9 +1127,9 @@ def _file_transport_command(self, in_path, out_path, sftp_action):
             else:
                 # If not in smart mode, the data will be printed by the raise below
                 if len(methods) > 1:
-                    display.warning('%s transfer mechanism failed on %s. Use ANSIBLE_DEBUG=1 to see detailed information' % (method, host))
-                    display.debug('%s' % to_text(stdout))
-                    display.debug('%s' % to_text(stderr))
+                    display.warning(u'%s transfer mechanism failed on %s. Use ANSIBLE_DEBUG=1 to see detailed information' % (method, host))
+                    display.debug(u'%s' % to_text(stdout))
+                    display.debug(u'%s' % to_text(stderr))
 
         if returncode == 255:
             raise AnsibleConnectionFailure("Failed to connect to the host via %s: %s" % (method, to_native(stderr)))
@@ -1237,12 +1238,12 @@ def reset(self):
             run_reset = True
 
         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)
             stdout, stderr = p.communicate()
             status_code = p.wait()
             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()
 


Code review comments:
diff_hunk: @@ -321,23 +321,23 @@ def _handle_error(remaining_retries, command, return_tuple, no_log, host, displa
     if command == b'sshpass':
         # Error 5 is invalid/incorrect password. Raise an exception to prevent retries from locking the account.
         if return_tuple[0] == 5:
-            msg = 'Invalid/incorrect username/password. Skipping remaining {0} retries to prevent account lockout:'.format(remaining_retries)
+            msg = u'Invalid/incorrect username/password. Skipping remaining {0} retries to prevent account lockout:'.format(remaining_retries)
             if remaining_retries <= 0:
-                msg = 'Invalid/incorrect password:'
+                msg = u'Invalid/incorrect password:'
             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:
-                msg = '{0} {1}'.format(msg, to_native(return_tuple[2].rstrip()))
+                msg = u'{0} {1}'.format(msg, to_text(return_tuple[2]).rstrip())
path: lib/ansible/plugins/connection/ssh.py
text: @author:
Hrm, I might have been overzealous.  This is wrong.  This is used in an exception, not in display.