Repository: napalm-automation/napalm

Issue:
EOSDriver._load_config() doesn't support HEREDOC syntax
If attempting to blindly copy a multiline command from a potential config to a file, conversion is required.
```
with EOSDriver(s, u, p) as d:
    load_merge_candidate(config="banner login\nTHIS IS MY BANNER\nEOF")
...
MergeConfigException: Error [1002]: CLI command 4 of 5 'THIS IS MY BANNER' failed: invalid command [Invalid input (at token 0: 'THIS')]
```


Discussion:

Resolving patch:
diff --git a/napalm/eos/eos.py b/napalm/eos/eos.py
--- a/napalm/eos/eos.py
+++ b/napalm/eos/eos.py
@@ -54,6 +54,12 @@ class EOSDriver(NetworkDriver):
 
     SUPPORTED_OC_MODELS = []
 
+    HEREDOC_COMMANDS = [
+        ("banner login", 1),
+        ("banner motd", 1),
+        ("protocol https certificate", 2)
+    ]
+
     _RE_BGP_INFO = re.compile('BGP neighbor is (?P<neighbor>.*?), remote AS (?P<as>.*?), .*') # noqa
     _RE_BGP_RID_INFO = re.compile('.*BGP version 4, remote router ID (?P<rid>.*?), VRF (?P<vrf>.*?)$') # noqa
     _RE_BGP_DESC = re.compile('\s+Description: (?P<description>.*?)')
@@ -135,6 +141,23 @@ def _lock(self):
         if [k for k, v in sess.items() if v['state'] == 'pending' and k != self.config_session]:
             raise SessionLockedException('Session is already in use')
 
+    @staticmethod
+    def _multiline_convert(config, start="banner login", end="EOF", depth=1):
+        """Converts running-config HEREDOC into EAPI JSON dict"""
+        ret = list(config)  # Don't modify list in-place
+        try:
+            s = ret.index(start)
+            e = s
+            while depth:
+                e = ret.index(end, e + 1)
+                depth = depth - 1
+        except ValueError:  # Couldn't find end, abort
+            return ret
+        ret[s] = {'cmd': ret[s], 'input': "\n".join(ret[s+1:e])}
+        del ret[s + 1:e + 1]
+
+        return ret
+
     def _load_config(self, filename=None, config=None, replace=True):
         commands = []
 
@@ -160,6 +183,9 @@ def _load_config(self, filename=None, config=None, replace=True):
                 continue
             commands.append(line)
 
+        for start, depth in [(s, d) for (s, d) in self.HEREDOC_COMMANDS if s in commands]:
+            commands = self._multiline_convert(commands, start=start, depth=depth)
+
         try:
             if self.eos_autoComplete is not None:
                 self.device.run_commands(commands, autoComplete=self.eos_autoComplete)


Test patch:
diff --git a/test/eos/test_heredoc.py b/test/eos/test_heredoc.py
new file mode 100644
--- /dev/null
+++ b/test/eos/test_heredoc.py
@@ -0,0 +1,56 @@
+import mock
+import pytest
+
+
+@pytest.mark.usefixtures("set_device_parameters")
+class TestConfigMangling(object):
+    def test_heredoc(self):
+
+        raw_config = """hostname vEOS
+        ip name-server 192.0.2.1
+        !
+        banner login
+        This is a banner that spans
+        multiple lines in order to test
+        HEREDOC conversion
+        EOF
+        !
+        management api http-commands
+          protocol https certificate
+          ---BEGIN CERTIFICATE---
+          FAKE-CERTIFICATE-DATA
+          ---END CERTIFICATE---
+          EOF
+          ---BEGIN PRIVATE KEY---
+          FAKE-KEY-DATA
+          ---END PRIVATE KEY---
+          EOF
+        !
+        management ssh
+          idle-timeout 15
+        !
+        """
+
+        self.device.device.run_commands = mock.MagicMock()
+
+        self.device._load_config(config=raw_config)
+
+        expected_result = [
+            "configure session {}".format(self.device.config_session),
+            "rollback clean-config",
+            "hostname vEOS",
+            "ip name-server 192.0.2.1",
+            {
+                "cmd": "banner login",
+                "input": "This is a banner that spans\nmultiple lines in order to test\nHEREDOC conversion" #  noqa
+            },
+            "management api http-commands",
+            {
+                "cmd": "protocol https certificate",
+                "input": "---BEGIN CERTIFICATE---\nFAKE-CERTIFICATE-DATA\n---END CERTIFICATE---\nEOF\n---BEGIN PRIVATE KEY---\nFAKE-KEY-DATA\n---END PRIVATE KEY---"  # noqa
+            },
+            "management ssh",
+            "idle-timeout 15"
+        ]
+
+        self.device.device.run_commands.assert_called_with(expected_result)