Repository: napalm-automation/napalm

Issue:
EOSDriver._load_config deletes ACL doublebang comments
```python
test_doublebang = ["ip access-list standard doublebang", "    !! This is a doublebang comment", "10 remark this is a remark", "20 permit 192.0.1.2/32"]
with EOSDriver(s, u, p) as d:
    d.load_merge_candidate(config=test_doublebang)
    print(d.compare_config())

@@ -1280,6 +1279,10 @@
<omitted>
+!
+ip access-list standard doublebang
+   10 remark this is a remark
+   20 permit host 192.0.1.2
 !
<omitted>
@@ -1800,7 +1803,6 @@
<omitted>
 !
 ip access-list standard existing_acl_with_comment
-   !! Testing
 !
```


Discussion:
Hrm.  Removing the other doublebang on the untouched ACL kind of points at a vendor bug.
Using session config via the CLI does not remove doublebang in other ACL entries.
TODO:  Confirm vendor issue using JSONRPCLib
Removal of unrelated doublebangs is definitely a vendor bug (via jsonrpclib):

```python
In [9]: sw.runCmds(1, ["configure session brandontest", "ip access-list standard brandontest", "permit host 192.0.2.1"
   ...: ])
Out[9]: [{}, {}, {}]

In [12]: print(sw.runCmds(1, ["show session-config named brandontest diffs"], "text")[0]['output'])
--- system:/running-config
+++ session:/brandontest-session-config
@@ -886,6 +886,9 @@
    10 permit stuff
    20 permit other stuff
 !
+ip access-list standard brandontest
+   10 permit host 192.0.2.1
+!
 ip access-list standard stuff
    10 permit stuff
 !
@@ -1065,7 +1068,6 @@
    20 permit host stuff
 !
 ip access-list standard doublebang-test
-   !! Blah blah
    10 permit host 192.0.2.1
 !
 ip access-list standard stuff
```


At least it's consistent through all parsers:
```
vEOS#configure session brandontest
vEOS(config-s-brando)#ip access-list standard brandontest
vEOS(config-s-brando-std-acl-brandontest)#permit host 192.0.2.1
vEOS(config-s-brando-std-acl-brandontest)#exit
vEOS(config-s-brando)#exit
vEOS#show session-config named brandontest diffs
--- system:/running-config
+++ session:/brandontest-session-config
@@ -886,6 +886,9 @@
    10 permit stuff
    20 permit other stuff
 !
+ip access-list standard brandontest
+   10 permit host 192.0.2.1
+!
 ip access-list standard stuff
    10 permit stuff
 !
@@ -1065,7 +1068,6 @@
    20 permit host stuff
 !
 ip access-list standard doublebang-test
-   !! Stuff
    10 permit host 192.0.2.1
 !
 ip access-list standard stuff

```
And committing does not actually remove the mode comments, so it's just a bug in their diff function.
We do still need to update `napalm.eos.eos.EOSDriver._load_config()` to accept double-bangs, as we currently don't pass on comments (I guess to save network IO?):

https://github.com/napalm-automation/napalm/blob/584197ed4a8f368ccbc1cb0eb18f14f4ab663f33/napalm/eos/eos.py#L182-L183
No, that check is there because `eAPI` used to break when sending those so we had to make sure they were removed. Not sure about `!!` comments though, never seen them before actually.

Resolving patch:
diff --git a/napalm/eos/eos.py b/napalm/eos/eos.py
--- a/napalm/eos/eos.py
+++ b/napalm/eos/eos.py
@@ -57,6 +57,7 @@ class EOSDriver(NetworkDriver):
     HEREDOC_COMMANDS = [
         ("banner login", 1),
         ("banner motd", 1),
+        ("comment", 1),
         ("protocol https certificate", 2)
     ]
 
@@ -158,6 +159,41 @@ def _multiline_convert(config, start="banner login", end="EOF", depth=1):
 
         return ret
 
+    @staticmethod
+    def _mode_comment_convert(commands):
+        """
+        EOS has the concept of multi-line mode comments, shown in the running-config
+        as being inside a config stanza (router bgp, ACL definition, etc) and beginning
+        with the normal level of spaces and '!!', followed by comments.
+
+        Unfortunately, pyeapi does not accept mode comments in this format, and have to be
+        converted to a specific type of pyeapi call that accepts multi-line input
+
+        Copy the config list into a new return list, converting consecutive lines starting with
+        "!!" into a single multiline comment command
+
+        :param commands: List of commands to be sent to pyeapi
+        :return: Converted list of commands to be sent to pyeapi
+        """
+
+        ret = []
+        comment_count = 0
+        for idx, element in enumerate(commands):
+            # Check first for stringiness, as we may have dicts in the command list already
+            if isinstance(element, py23_compat.string_types) and element.startswith('!!'):
+                comment_count += 1
+                continue
+            else:
+                if comment_count > 0:
+                    # append the previous comment
+                    ret.append({"cmd": "comment",
+                                "input": "\n".join(map(lambda s: s.lstrip("! "),
+                                                       commands[idx - comment_count:idx]))})
+                    comment_count = 0
+                ret.append(element)
+
+        return ret
+
     def _load_config(self, filename=None, config=None, replace=True):
         commands = []
 
@@ -179,10 +215,12 @@ def _load_config(self, filename=None, config=None, replace=True):
             line = line.strip()
             if line == '':
                 continue
-            if line.startswith('!'):
+            if line.startswith('!') and not line.startswith('!!'):
                 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)
 


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
@@ -1,12 +1,14 @@
 import mock
 import pytest
+from textwrap import dedent
 
 
 @pytest.mark.usefixtures("set_device_parameters")
 class TestConfigMangling(object):
     def test_heredoc(self):
 
-        raw_config = """hostname vEOS
+        raw_config = dedent("""\
+        hostname vEOS
         ip name-server 192.0.2.1
         !
         banner login
@@ -29,7 +31,7 @@ def test_heredoc(self):
         management ssh
           idle-timeout 15
         !
-        """
+        """)
 
         self.device.device.run_commands = mock.MagicMock()
 
@@ -54,3 +56,52 @@ def test_heredoc(self):
         ]
 
         self.device.device.run_commands.assert_called_with(expected_result)
+
+    def test_mode_comment(self):
+        raw_config = dedent("""\
+        ip access-list standard test1
+            !! This is a
+            !! multiline mode comment
+            !! for standard ACL test1
+            permit host 192.0.2.1
+        !
+        ip access-list standard test2
+            !! This is a single-line mode comment for standard ACL test2
+            permit host 192.0.2.2
+        !
+        ip access-list standard test3
+            comment
+            This is a multi-line HEREDOC
+            comment for standard ACL test3
+            EOF
+            permit host 192.0.2.3
+        !
+        """)
+
+        self.device.device.run_commands = mock.MagicMock()
+
+        self.device._load_config(config=raw_config, replace=False)
+
+        expected_result = [
+            "configure session {}".format(self.device.config_session),
+            "ip access-list standard test1",
+            {
+                "cmd": "comment",
+                "input": "This is a\nmultiline mode comment\nfor standard ACL test1"
+            },
+            "permit host 192.0.2.1",
+            "ip access-list standard test2",
+            {
+                "cmd": "comment",
+                "input": "This is a single-line mode comment for standard ACL test2"
+            },
+            "permit host 192.0.2.2",
+            "ip access-list standard test3",
+            {
+                "cmd": "comment",
+                "input": "This is a multi-line HEREDOC\ncomment for standard ACL test3"
+            },
+            "permit host 192.0.2.3"
+        ]
+
+        self.device.device.run_commands.assert_called_with(expected_result)