Repository: ncclient/ncclient

Issue:
connect_ssh timeout parameter used for contradicting purposes
The `timeout` kwd parameter of `manager.connect_ssh` ends up being used for two rather contradicting purposes:
1. It is passed as a kwd to `transport.SSHSession.connect` , where it serves as a connection timeout. This matches the documentation of the manager.connect_ssh function
1. It is also passed as kwd to `manager.Manager()`, where it serves as synchronous RPC timeout

If one wants to set the ssh connection timeout to a small value (e.g. 3 seconds, a sensible option for not waiting long for a host that is down), this results in having a RPC  timeout of 3 seconds too, which is not enough for retrieving large configurations.

I see two ways of fixing this:
* a) Discontinue the `timeout` parameter of `manager.Manager()` and rely solely on the `Manager.timeout` property for modifying its default value. This would be similar with how other Manager attributes are handled (e.g. `async_mode` or `raise_mode`).
* b) Change the `transport.SSHSession.connect` timeout parameter name to `connect_timeout`, and dispatch the parameters accordingly in `manager.connect_ssh`. This would however require extending the documentation of manager.connect, to explain that it receives arguments that are used by either `transport.SSHSession.connect` (e.g. connect_timeout, and all the others) or `Manager` (e.g. timeout).

IMHO option *(a)* is cleaner, as the `connect_ssh` method keeps its current behaviour of passing all non device_params to `transport.SSHSession.connect`.

I would be happy to contribute with a patch, once we agree on the approach.

Comments:

Patch:
diff --git a/ncclient/manager.py b/ncclient/manager.py
index 3618291..09a8aaf 100644
--- a/ncclient/manager.py
+++ b/ncclient/manager.py
@@ -89,32 +89,47 @@ def make_device_handler(device_params):
     return handler_obj
 
 
+def _extract_device_params(kwds):
+    if "device_params" in kwds:
+        device_params = kwds["device_params"]
+        del kwds["device_params"]
+    else:
+        device_params = None
+    return device_params
+
+def _extract_manager_params(kwds):
+    if "manager_params" in kwds:
+        manager_params = kwds["manager_params"]
+        del kwds["manager_params"]
+    else:
+        manager_params = {}
+    return manager_params
+
 def connect_ssh(*args, **kwds):
     """
     Initialize a :class:`Manager` over the SSH transport.
     For documentation of arguments see :meth:`ncclient.transport.SSHSession.connect`.
 
     The underlying :class:`ncclient.transport.SSHSession` is created with
-        :data:`CAPABILITIES`. It is first instructed to
-        :meth:`~ncclient.transport.SSHSession.load_known_hosts` and then
-        all the provided arguments are passed directly to its implementation
-        of :meth:`~ncclient.transport.SSHSession.connect`.
+    :data:`CAPABILITIES`. It is first instructed to
+    :meth:`~ncclient.transport.SSHSession.load_known_hosts` and then
+    all the provided arguments are passed directly to its implementation
+    of :meth:`~ncclient.transport.SSHSession.connect`.
 
-    To invoke advanced vendor related operation add device_params =
-        {'name':'<vendor_alias>'} in connection paramerers. For the time,
-        'junos' and 'nexus' are supported for Juniper and Cisco Nexus respectively.
+    To customize the :class:`Manager`, add a `manager_params` dictionnary in connection
+    parameters (e.g. `manager_params={'timeout': 60}` for a bigger RPC timeout paramater)
 
-    A custom device handler can be provided with device_params =
-        {'handler':<handler class>} in connection paramerers.
+    To invoke advanced vendor related operation add
+    `device_params={'name': '<vendor_alias>'}` in connection parameters. For the time,
+    'junos' and 'nexus' are supported for Juniper and Cisco Nexus respectively.
+
+    A custom device handler can be provided with
+    `device_params={'handler':<handler class>}` in connection parameters.
     """
-    # Extract device parameter dict, if it was passed into this function. Need to
-    # remove it from kwds, since the session.connect() doesn't like extra stuff in
-    # there.
-    if "device_params" in kwds:
-        device_params = kwds["device_params"]
-        del kwds["device_params"]
-    else:
-        device_params = None
+    # Extract device parameter and manager parameter dictionaries, if they were passed into this function.
+    # Remove them from kwds (which should keep only session.connect() parameters).
+    device_params = _extract_device_params(kwds)
+    manager_params = _extract_manager_params(kwds)
 
     device_handler = make_device_handler(device_params)
     device_handler.add_additional_ssh_connect_params(kwds)
@@ -130,17 +145,16 @@ def connect_ssh(*args, **kwds):
         if session.transport:
             session.close()
         raise
-    return Manager(session, device_handler, **kwds)
+    return Manager(session, device_handler, **manager_params)
 
 def connect_ioproc(*args, **kwds):
-    if "device_params" in kwds:
-        device_params = kwds["device_params"]
-        del kwds["device_params"]
+    device_params = _extract_device_params(kwds)
+    manager_params = _extract_manager_params(kwds)
+
+    if device_params:
         import_string = 'ncclient.transport.third_party.'
         import_string += device_params['name'] + '.ioproc'
         third_party_import = __import__(import_string, fromlist=['IOProc'])
-    else:
-        device_params = None
 
     device_handler = make_device_handler(device_params)
 
@@ -149,7 +163,7 @@ def connect_ioproc(*args, **kwds):
     session = third_party_import.IOProc(device_handler)
     session.connect()
 
-    return Manager(session, device_handler, **kwds)
+    return Manager(session, device_handler, **manager_params)
 
 
 def connect(*args, **kwds):
@@ -184,7 +198,7 @@ class Manager(object):
 
    # __metaclass__ = OpExecutor
 
-    def __init__(self, session, device_handler, timeout=30, *args, **kwargs):
+    def __init__(self, session, device_handler, timeout=30):
         self._session = session
         self._async_mode = False
         self._timeout = timeout
diff --git a/ncclient/transport/ssh.py b/ncclient/transport/ssh.py
index cf911ec..00a72a5 100644
--- a/ncclient/transport/ssh.py
+++ b/ncclient/transport/ssh.py
@@ -349,7 +349,8 @@ class SSHSession(Session):
             ssh_config = "~/.ssh/config" if sys.platform != "win32" else "~/ssh/config"
         if ssh_config is not None:
             config = paramiko.SSHConfig()
-            config.parse(open(os.path.expanduser(ssh_config)))
+            with open(os.path.expanduser(ssh_config)) as ssh_config_file_obj:
+                config.parse(ssh_config_file_obj)
             
             # Save default Paramiko SSH port so it can be reverted
             paramiko_default_ssh_port = paramiko.config.SSH_PORT


Test patch:
diff --git a/test/unit/test_manager.py b/test/unit/test_manager.py
index 8509379..55a9859 100644
--- a/test/unit/test_manager.py
+++ b/test/unit/test_manager.py
@@ -11,6 +11,12 @@ class TestManager(unittest.TestCase):
         m = MagicMock()
         mock_ssh.return_value = m
         conn = self._mock_manager()
+        m.connect.assert_called_once_with(host='10.10.10.10',
+                                          port=22,
+                                          username='user',
+                                          password='password',
+                                          hostkey_verify=False, allow_agent=False,
+                                          timeout=3)
         self.assertEqual(conn._session, m)
         self.assertEqual(conn._timeout, 10)
 
@@ -62,7 +68,7 @@ class TestManager(unittest.TestCase):
                                     username='user',
                                     password='password',
                                     timeout=10,
-                                    hostkey_verify=False, 
+                                    hostkey_verify=False,
                                     allow_agent=False,
                                     ssh_config=ssh_config_path)
         
@@ -90,9 +96,10 @@ class TestManager(unittest.TestCase):
                                     port=22,
                                     username='user',
                                     password='password',
-                                    timeout=10,
+                                    timeout=3,
+                                    hostkey_verify=False,
                                     device_params={'local': True, 'name': 'junos'},
-                                    hostkey_verify=False)
+                                    manager_params={'timeout': 10})
         self.assertEqual(mock_connect.called, 1)
         self.assertEqual(conn._timeout, 10)
         self.assertEqual(conn._device_handler.device_params, {'local': True, 'name': 'junos'}) 
@@ -182,9 +189,10 @@ class TestManager(unittest.TestCase):
                                     port=22,
                                     username='user',
                                     password='password',
-                                    timeout=10,
+                                    timeout=3,
+                                    hostkey_verify=False, allow_agent=False,
                                     device_params={'name': 'junos'},
-                                    hostkey_verify=False, allow_agent=False)
+                                    manager_params={'timeout': 10})
         return conn
 
     @patch('socket.fromfd')