# Buggy code
def _ssh_retry(func):
    """
    Decorator to retry ssh/scp/sftp in the case of a connection failure

    Will retry if:
    * an exception is caught
    * ssh returns 255
    Will not retry if
    * sshpass returns 5 (invalid password, to prevent account lockouts)
    * remaining_tries is < 2
    * retries limit reached
    """

    @wraps(func)
    def wrapped(self, *args, **kwargs):
        remaining_tries = int(C.ANSIBLE_SSH_RETRIES) + 1
        cmd_summary = "%s..." % args[0]
        for attempt in range(remaining_tries):
            cmd = args[0]
            if attempt != 0 and self._play_context.password and isinstance(cmd, list):
                # If this is a retry, the fd/pipe for sshpass is closed, and we need a new one
                self.sshpass_pipe = os.pipe()
                cmd[1] = b"-d" + to_bytes(
                    self.sshpass_pipe[0],
                    nonstring="simplerepr",
                    errors="surrogate_or_strict",
                )

            try:
                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,
                        )
                    else:
                        display.vvv(return_tuple, host=self.host)
                    # 0 = success
                    # 1-254 = remote command return code
                    # 255 could be a failure from the ssh command itself
                except AnsibleControlPersistBrokenPipeError:
                    # Retry one more time because of the ControlPersist broken pipe (see #16731)
                    cmd = args[0]
                    if self._play_context.password and isinstance(cmd, list):
                        # This is a retry, so the fd/pipe for sshpass is closed, and we need a new one
                        self.sshpass_pipe = os.pipe()
                        cmd[1] = b"-d" + to_bytes(
                            self.sshpass_pipe[0],
                            nonstring="simplerepr",
                            errors="surrogate_or_strict",
                        )
                    display.vvv("RETRYING BECAUSE OF CONTROLPERSIST BROKEN PIPE")
                    return_tuple = func(self, *args, **kwargs)

                remaining_retries = remaining_tries - attempt - 1
                _handle_error(
                    remaining_retries,
                    cmd[0],
                    return_tuple,
                    self._play_context.no_log,
                    self.host,
                )

                break

            # 5 = Invalid/incorrect password from sshpass
            except AnsibleAuthenticationFailure:
                # Raising this exception, which is subclassed from AnsibleConnectionFailure, prevents further retries
                raise

            except (AnsibleConnectionFailure, Exception) as e:
                if attempt == remaining_tries - 1:
                    raise
                else:
                    pause = 2**attempt - 1
                    if pause > 30:
                        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)
                        )
                    else:
                        msg = (
                            "ssh_retry: attempt: %d, caught exception(%s) from cmd (%s), pausing for %d seconds"
                            % (attempt + 1, e, cmd_summary, pause)
                        )

                    display.vv(msg, host=self.host)

                    time.sleep(pause)
                    continue

        return return_tuple

    return wrapped


# Traceback (UnicodeDecodeError)
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 &amp;&amp; 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": ""
}

# Fixed code
def _ssh_retry(func):
    """
    Decorator to retry ssh/scp/sftp in the case of a connection failure

    Will retry if:
    * an exception is caught
    * ssh returns 255
    Will not retry if
    * sshpass returns 5 (invalid password, to prevent account lockouts)
    * remaining_tries is < 2
    * retries limit reached
    """

    @wraps(func)
    def wrapped(self, *args, **kwargs):
        remaining_tries = int(C.ANSIBLE_SSH_RETRIES) + 1
        cmd_summary = "%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):
                # If this is a retry, the fd/pipe for sshpass is closed, and we need a new one
                self.sshpass_pipe = os.pipe()
                cmd[1] = b"-d" + to_bytes(
                    self.sshpass_pipe[0],
                    nonstring="simplerepr",
                    errors="surrogate_or_strict",
                )

            try:
                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,
                        )
                    else:
                        display.vvv(return_tuple, host=self.host)
                    # 0 = success
                    # 1-254 = remote command return code
                    # 255 could be a failure from the ssh command itself
                except AnsibleControlPersistBrokenPipeError:
                    # Retry one more time because of the ControlPersist broken pipe (see #16731)
                    cmd = args[0]
                    if self._play_context.password and isinstance(cmd, list):
                        # This is a retry, so the fd/pipe for sshpass is closed, and we need a new one
                        self.sshpass_pipe = os.pipe()
                        cmd[1] = b"-d" + to_bytes(
                            self.sshpass_pipe[0],
                            nonstring="simplerepr",
                            errors="surrogate_or_strict",
                        )
                    display.vvv("RETRYING BECAUSE OF CONTROLPERSIST BROKEN PIPE")
                    return_tuple = func(self, *args, **kwargs)

                remaining_retries = remaining_tries - attempt - 1
                _handle_error(
                    remaining_retries,
                    cmd[0],
                    return_tuple,
                    self._play_context.no_log,
                    self.host,
                )

                break

            # 5 = Invalid/incorrect password from sshpass
            except AnsibleAuthenticationFailure:
                # Raising this exception, which is subclassed from AnsibleConnectionFailure, prevents further retries
                raise

            except (AnsibleConnectionFailure, Exception) as e:
                if attempt == remaining_tries - 1:
                    raise
                else:
                    pause = 2**attempt - 1
                    if pause > 30:
                        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)
                        )
                    else:
                        msg = (
                            "ssh_retry: attempt: %d, caught exception(%s) from cmd (%s), "
                            "pausing for %d seconds"
                            % (attempt + 1, to_text(e), cmd_summary, pause)
                        )

                    display.vv(msg, host=self.host)

                    time.sleep(pause)
                    continue

        return return_tuple

    return wrapped