Repository: napalm-automation/napalm

Issue:
Device connection not closed upon exception in __enter__() method
### Description of Issue

Observed while connecting to a Juniper device over SSH with the Cisco IOS driver. (so wrong driver for the target platform)

When using Napalm as a context manager (or by manually calling `open()` and `close()`) an exception might be raised during the `__enter__()` method. Like for example when enable mode can't be reached.

When this happens, 2 issues arise:

1) When called as a context manager, the code in `__exit__()` is never called and the connection to the device remains open for the remainder of the program.

2) When called with `open()` and `close()`, the connection cannot be closed anymore in `close()` because `self.device` isn't set yet.

Open:
https://github.com/napalm-automation/napalm/blob/e0cc9af966230bcc069cd3ab7e397e906efc04c5/napalm/base/base.py#L81
Close:
https://github.com/napalm-automation/napalm/blob/e0cc9af966230bcc069cd3ab7e397e906efc04c5/napalm/base/base.py#L98

When running inside something like Celery or Django, where the program never closes, the connections remain open indefinitely and eventually occupy all SSH sessions on the target device if it's not configured to close the connection after an idle timeout.

### napalm version
2.4.0

### Steps to Reproduce the Issue

Use the IOS driver to connect to a Juniper device


Discussion:
I investigated this a bit deeper and it seems it doesn't matter whether the target device is configured with idle timeouts or not.

Looking at the Paramiko layer, this i what happens:
1. idle timeout passes
1. Network device sends a EOF message in the SSH channel
1. Paramiko sends a EOF back, but does NOT close the TCP connection

Compared to Putty, Putty closes the TCP connection right after the EOF message is received.

So, this issue is two fold.

- When an exception occurs during session setup in Napalm, the underlaying Netmiko connection is never closed and becomes a "floating" connection (e.g. there's no variable referencing it anymore. The `as` part in a `with ... as ...` never becomes available).

- Paramiko should (I think) close the TCP connection upon receiving an EOF in the SSH channel.

Resolving patch:
diff --git a/napalm/base/base.py b/napalm/base/base.py
--- a/napalm/base/base.py
+++ b/napalm/base/base.py
@@ -16,14 +16,16 @@
 from __future__ import print_function
 from __future__ import unicode_literals
 
+import sys
+
+from netmiko import ConnectHandler, NetMikoTimeoutException
+
 # local modules
 import napalm.base.exceptions
-from napalm.base.exceptions import ConnectionException
 import napalm.base.helpers
 from napalm.base import constants as c
 from napalm.base import validate
-
-from netmiko import ConnectHandler, NetMikoTimeoutException
+from napalm.base.exceptions import ConnectionException
 
 
 class NetworkDriver(object):
@@ -44,8 +46,15 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None)
         raise NotImplementedError
 
     def __enter__(self):
-        self.open()
-        return self
+        try:
+            self.open()
+            return self
+        except:  # noqa: E722
+            # Swallow exception if __exit__ returns a True value
+            if self.__exit__(*sys.exc_info()):
+                pass
+            else:
+                raise
 
     def __exit__(self, exc_type, exc_value, exc_traceback):
         self.close()
@@ -95,8 +104,9 @@ def _netmiko_open(self, device_type, netmiko_optional_args=None):
 
     def _netmiko_close(self):
         """Standardized method of closing a Netmiko connection."""
-        self.device.disconnect()
-        self._netmiko_device = None
+        if getattr(self, "_netmiko_device", None):
+            self._netmiko_device.disconnect()
+            self._netmiko_device = None
         self.device = None
 
     def open(self):


Test patch: