Repository: ansible/ansible
Pull request: Add auth_timeout parameter when supported by paramiko
Paramiko 2.2 introduces the auth_timeout parameter. This will set the
parameter to the same value of the timeout parameter to prevent
"Authentication timeout" errors.

##### SUMMARY
If the paramiko version installed is at least 2.2 include the auth_timeout parameter to the connect method with the same value used for the timeout method. This will prevent "Authentication timeout" errors when a slow authentication step (>30s) happens with a host. This is common with some network devices.

Fixes #42596

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
- Bugfix Pull Request

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below -->
paramiko_ssh connection plugin 

##### ADDITIONAL INFORMATION
I noticed that problem when using the network_cli connection plugin, which in turn uses the paramiko_ssh connection plugin.


Problem statement:
Paramiko ignores timeout setting
##### SUMMARY
`timeout` in ansible.cfg appears to be ignored when using paramiko.


##### ISSUE TYPE
 - Bug Report


##### COMPONENT NAME
paramiko_ssh


##### ANSIBLE VERSION
```
ansible 2.5.5
  config file = /home/user/workspace/git/ansible/ansible.cfg
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.5/dist-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.5.2 (default, Nov 23 2017, 16:37:01) [GCC 5.4.0 20160609]
```

##### CONFIGURATION
<!--- If using Ansible 2.4 or above, paste, BELOW THIS COMMENT, the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).-->

    [defaults]
    timeout = 60
    transport = paramiko

##### OS / ENVIRONMENT

Control machine: Ubuntu 16.04 64-bit
Managed node: RHEL 7 64-bit


##### STEPS TO REPRODUCE

    ansible-playbook --connection=paramiko --timeout=60


##### EXPECTED RESULTS
Because `timeout` was set to 60, `ansible-playbook` shouldn't have timed out before then.

##### ACTUAL RESULTS

`ansible-playbook` returns after 34 seconds:

    $ time ansible-playbook --connection=paramiko --timeout=60 ... -vvvv
    ansible-playbook 2.5.5
    config file = ansible.cfg
    configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
    ansible python module location = /usr/local/lib/python3.5/dist-packages/ansible
    executable location = /usr/local/bin/ansible-playbook
    python version = 3.5.2 (default, Nov 23 2017, 16:37:01) [GCC 5.4.0 20160609]
    Using ansible.cfg as config file

    <server.example.org> ESTABLISH CONNECTION FOR USER: user on PORT 22 TO server.example.org
    fatal: [server.example.org]: UNREACHABLE! => {
        "changed": false,
        "msg": "Authentication timeout.",
        "unreachable": true
    }

    NO MORE HOSTS LEFT ************************************************************************************************************************************************************************************************

    PLAY RECAP ********************************************************************************************************************************************************************************************************
    server.example.org : ok=0    changed=0    unreachable=1    failed=0


    real	0m34.854s
    user	0m2.316s
    sys	0m0.398s

Hints from issue discussion:
Files identified in the description:
* [lib/ansible/plugins/connection/paramiko_ssh.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/connection/paramiko_ssh.py)

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 --->
This is due to not passing `auth_timeout` to `SSHClient.connect`.  The `timeout` argument for `SSHClient.connect` only handles the TCP connection.  In paramiko 2.2 `auth_timeout` was added (https://github.com/paramiko/paramiko/commit/0fd54bb9813c2a62dcea7351e05ce780127ccdbe), so older versions, such as those shipped with RHEL 6 and 7 do not support this option.

We would need to add logic to handle this.
> so older versions, such as those shipped with RHEL 6 and 7 do not support this option.

For what it's worth, even the latest Ubuntu (18.04) ships with paramiko 2.0:
https://packages.ubuntu.com/bionic/python-paramiko

Thanks!

Commit message: Add auth_timeout parameter when supported

Paramiko 2.2 introduces the auth_timeout parameter. This will set the
parameter to the same value of the timeout parameter to prevent
"Authentication timeout" errors.

Patch to review:
diff --git a/lib/ansible/plugins/connection/paramiko_ssh.py b/lib/ansible/plugins/connection/paramiko_ssh.py
index f6c8df219cb64c..47efcff8b8e96e 100644
--- a/lib/ansible/plugins/connection/paramiko_ssh.py
+++ b/lib/ansible/plugins/connection/paramiko_ssh.py
@@ -137,6 +137,7 @@
 import re
 
 from termios import tcflush, TCIFLUSH
+from distutils.version import LooseVersion
 from binascii import hexlify
 
 from ansible import constants as C
@@ -337,17 +338,32 @@ def _connect_uncached(self):
             if self._play_context.private_key_file:
                 key_filename = os.path.expanduser(self._play_context.private_key_file)
 
-            ssh.connect(
-                self._play_context.remote_addr.lower(),
-                username=self._play_context.remote_user,
-                allow_agent=allow_agent,
-                look_for_keys=self.get_option('look_for_keys'),
-                key_filename=key_filename,
-                password=self._play_context.password,
-                timeout=self._play_context.timeout,
-                port=port,
-                **sock_kwarg
-            )
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh.connect(
+                    self._play_context.remote_addr.lower(),
+                    username=self._play_context.remote_user,
+                    allow_agent=allow_agent,
+                    look_for_keys=self.get_option('look_for_keys'),
+                    key_filename=key_filename,
+                    password=self._play_context.password,
+                    timeout=self._play_context.timeout,
+                    auth_timeout=self._play_context.timeout,
+                    port=port,
+                    **sock_kwarg
+                )
+            else:
+                ssh.connect(
+                    self._play_context.remote_addr.lower(),
+                    username=self._play_context.remote_user,
+                    allow_agent=allow_agent,
+                    look_for_keys=self.get_option('look_for_keys'),
+                    key_filename=key_filename,
+                    password=self._play_context.password,
+                    timeout=self._play_context.timeout,
+                    port=port,
+                    **sock_kwarg
+                )
         except paramiko.ssh_exception.BadHostKeyException as e:
             raise AnsibleConnectionFailure('host key mismatch for %s' % e.hostname)
         except Exception as e:


Code review comments:
diff_hunk: @@ -337,17 +338,32 @@ def _connect_uncached(self):
             if self._play_context.private_key_file:
                 key_filename = os.path.expanduser(self._play_context.private_key_file)
 
-            ssh.connect(
-                self._play_context.remote_addr.lower(),
-                username=self._play_context.remote_user,
-                allow_agent=allow_agent,
-                look_for_keys=self.get_option('look_for_keys'),
-                key_filename=key_filename,
-                password=self._play_context.password,
-                timeout=self._play_context.timeout,
-                port=port,
-                **sock_kwarg
-            )
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh.connect(
+                    self._play_context.remote_addr.lower(),
+                    username=self._play_context.remote_user,
+                    allow_agent=allow_agent,
+                    look_for_keys=self.get_option('look_for_keys'),
+                    key_filename=key_filename,
+                    password=self._play_context.password,
+                    timeout=self._play_context.timeout,
+                    auth_timeout=self._play_context.timeout,
+                    port=port,
+                    **sock_kwarg
+                )
+            else:
path: lib/ansible/plugins/connection/paramiko_ssh.py
text: @user1:
I think instead of having `ssh.connect` in an if/else, you should instead rename `sock_kwarg` to something like `kwargs` and conditionally add `auth_timeout` to `kwargs`

@author:
Agree. Will do.

@author:
@user1 done.