Repository: napalm-automation/napalm

Issue:
Unable to load banner MOTD on Arista
Firstly, I am not entirely sure what's the right format expected. I tried loading:

```text
banner motd
!!aaaaa
EOF
```

With this, it fails:

```python
>>> config = '''banner motd
... !!aaaa
... EOF
... '''
>>> e.load_merge_candidate(config=config)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/napalm/eos/eos.py", line 245, in load_merge_candidate
    self._load_config(filename, config, False)
  File "/usr/local/lib/python2.7/dist-packages/napalm/eos/eos.py", line 225, in _load_config
    commands = self._multiline_convert(commands, start=start, depth=depth)
  File "/usr/local/lib/python2.7/dist-packages/napalm/eos/eos.py", line 157, in _multiline_convert
    ret[s] = {'cmd': ret[s], 'input': "\n".join(ret[s+1:e])}
TypeError: sequence item 0: expected string or Unicode, dict found
```

Testing against a vEOS running:

```
Software image version: 4.18.1F
Architecture:           i386
Internal build version: 4.18.1F-4591672.4181F
Internal build ID:      6fcb426e-70a9-48b8-8958-54bb72ee28ed
```

Changing the line 157 to:

```diff
diff --git a/napalm/eos/eos.py b/napalm/eos/eos.py
index 4216ae5..bcb07e1 100644
--- a/napalm/eos/eos.py
+++ b/napalm/eos/eos.py
@@ -154,7 +154,7 @@ class EOSDriver(NetworkDriver):
                 depth = depth - 1
         except ValueError:  # Couldn't find end, abort
             return ret
-        ret[s] = {'cmd': ret[s], 'input': "\n".join(ret[s+1:e])}
+        ret[s] = {'cmd': ret[s], 'input': "\n".join([ele['input'] for ele in ret[s+1:e]])}
         del ret[s + 1:e + 1]

         return ret
```

As it looks like `ret` is a list looking like: `[u'configure session napalm_61252', 'banner motd', {u'input': u'aaaa', u'cmd': u'comment'}, 'EOF']`, and the object referenced by `ret[s+1:e]` is a list of dictionaries (not a list of strings).

@bewing I recall you've been working on this, would you recall any details and wondering if on other EOS versions pyeapi returns a list of strings as the code would suggest? I am running pyeapi `0.8.1`.


Discussion:
It appears that there's the same behaviour on a real device running `4.17.4M`, which makes me wonder whether there's a pyeapi versioning issue whatsoever.
Per slack discussion, I believe this is occurring because `napalm.eos.EOSDriver::_mode_comment_covert()` is being called prior to `napalm.eos.EOSDriver::_multiline_convert()` inside `napalm.eos.EOSDriver::_load_config()`

The former reformats block-mode comments (which appear in configs as multi-line comments, all starting with `!!`), while the latter reformats multi-line commands (banners, https certificates, etc) which do not have prelimiters.

If we re-order these two operations (so the banner is converted, and then we check for mode-level comments), it should work as intended, I hope.
Confirmed that the issue was caused only by the `!!` at the beginning of the lines. @bewing suggestion to reorder the operations might help.
Either way, I've added the `documentation` label, as the documentation can be enhanced a little.

Resolving patch:
diff --git a/napalm/eos/eos.py b/napalm/eos/eos.py
--- a/napalm/eos/eos.py
+++ b/napalm/eos/eos.py
@@ -219,11 +219,11 @@ def _load_config(self, filename=None, config=None, replace=True):
                 continue
             commands.append(line)
 
-        commands = self._mode_comment_convert(commands)
-
         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)
 
+        commands = self._mode_comment_convert(commands)
+
         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
--- a/test/eos/test_heredoc.py
+++ b/test/eos/test_heredoc.py
@@ -105,3 +105,38 @@ def test_mode_comment(self):
         ]
 
         self.device.device.run_commands.assert_called_with(expected_result)
+
+    def test_heredoc_with_bangs(self):
+
+        raw_config = dedent("""\
+        hostname vEOS
+        ip name-server 192.0.2.1
+        !
+        banner login
+        !! This is a banner that contains
+        !!!bangs!
+        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 contains\n!!!bangs!" #  noqa
+            },
+            "management ssh",
+            "idle-timeout 15"
+        ]
+
+        self.device.device.run_commands.assert_called_with(expected_result)