Repository: Juniper/py-junos-eznc

Issue:
Invalid host key error not caught
If the `~.ssh/known_hosts` file contains an invalid entry (in my case, caused by a missing newline) then PyEZ will only return the nondescript `ConnectError` message.

The failure comes from the **ncclient** `manager.connect()` function which, in turn, calls **paramiko**'s `load_known_hosts()`. The latter actually returns a clear error message:

```
InvalidHostKey: ('172.22.196.114 ssh-rsa AAAA...
    <snip />
    ...+SQ==', Error('Incorrect padding',))
```

I think there are 2 issues here:

1. **ncclient** `manager.connect()` takes several arguments, one of which is `hostkey_verify` which we set to `False` so I believe we should not even be loading the `known_hosts` file. I played with different parameters and we always seem to be performing this verification - this is a ncclient bug in my opinion.

2. PyEZ obfuscates the error by using the generic ConnectError message - this occurs in the following bit of code in the `device.py` file:

```python
        except Exception as err:
            # anything else, we will re-raise as a
            # generic ConnectError
            cnx_err = EzErrors.ConnectError(self)
            cnx_err._orig = err
            raise cnx_err
```
If we were to `raise cnx_err._orig` instead then we would have a clearer picture of what's going on.

Discussion:

Patch:
diff --git a/lib/jnpr/junos/exception.py b/lib/jnpr/junos/exception.py
index 6e3a89fa..390dc34e 100644
--- a/lib/jnpr/junos/exception.py
+++ b/lib/jnpr/junos/exception.py
@@ -1,4 +1,3 @@
-from lxml import etree
 from jnpr.junos import jxml
 
 
@@ -129,6 +128,9 @@ class ConnectError(Exception):
     """
     Parent class for all connection related exceptions
     """
+    def __init__(self, dev, msg=None):
+        self.dev = dev
+        self._orig = msg
 
     @property
     def user(self):
@@ -145,15 +147,18 @@ class ConnectError(Exception):
         """ login SSH port """
         return self.dev._port
 
-    def __init__(self, dev):
-        self.dev = dev
-        # @@@ need to attach attributes for each access
-        # @@@ to user-name, host, jump-host, etc.
+    @property
+    def msg(self):
+        """ login SSH port """
+        return self._orig
 
     def __repr__(self):
-        return "{0}({1})".format(
-            self.__class__.__name__,
-            self.dev.hostname)
+        if self._orig:
+            return "{0}(host: {1}, msg: {2})".format(self.__class__.__name__,
+                                                     self.dev.hostname, self._orig)
+        else:
+            return "{0}({1})".format(self.__class__.__name__,
+                                     self.dev.hostname)
 
     __str__ = __repr__
 


Test patch:
diff --git a/tests/unit/test_exception.py b/tests/unit/test_exception.py
index bf340cd9..99821e75 100644
--- a/tests/unit/test_exception.py
+++ b/tests/unit/test_exception.py
@@ -61,14 +61,19 @@ class Test_RpcError(unittest.TestCase):
         self.assertEqual(obj.rpc_error['bad_element'], 'unit 2')
 
     def test_ConnectError(self):
-        self.dev = Device(host='1.1.1.1', user='rick', password='password123',
-                          gather_facts=False)
+        self.dev = Device(host='1.1.1.1', user='rick')
         obj = ConnectError(self.dev)
         self.assertEqual(obj.user, 'rick')
         self.assertEqual(obj.host, '1.1.1.1')
         self.assertEqual(obj.port, 830)
         self.assertEqual(repr(obj), 'ConnectError(1.1.1.1)')
 
+    def test_ConnectError_msg(self):
+        self.dev = Device(host='1.1.1.1', user='rick')
+        obj = ConnectError(self.dev, msg='underlying exception info')
+        self.assertEqual(obj.msg, 'underlying exception info')
+        self.assertEqual(repr(obj), 'ConnectError(host: 1.1.1.1, msg: underlying exception info)')
+
     def test_CommitError_repr(self):
         rsp = etree.XML(commit_xml)
         obj = CommitError(rsp=rsp)