Repository: napalm-automation/napalm

Issue:
IOS: `get_vlans` does not return all interfaces if they wrap around in the config
### Description of Issue/Question

Running `get_vlans` on devices with many interfaces on the same vlan returns a subset (first line of config) of the configured interfaces.

### Did you follow the steps from https://github.com/napalm-automation/napalm#faq
(Place an ``x`` between the square brackets where applicable)

- [x] Yes
- [ ] No


### Setup

### napalm version
(Paste verbatim output from `pip freeze | grep napalm` between quotes below)

```
napalm==3.3.1
```

### Network operating system version
(Paste verbatim output from `show version` - or equivalent - between quotes below)

Irrelevant, but:
```
Cisco IOS Software, s72033_rp Software (s72033_rp-ADVIPSERVICESK9_WAN-M), Version 12.2(33)SXH4, RELEASE SOFTWARE (fc1)

```

### Steps to Reproduce the Issue

Run `get_vlans` where the output is too large, get only first set
```
VLAN Name                             Status    Ports
---- -------------------------------- --------- -------------------------------
1    default                          active    Te1/4, Gi2/7, Gi2/17, Gi2/28, Gi2/33, Gi2/40, Gi2/48, Gi3/24, Gi3/38, Gi3/40, Gi4/5, Gi4/8, Gi4/9, Gi4/10, Gi4/11, Gi4/12, Gi4/18, Gi4/20, Gi4/22, Gi4/23, Gi4/24, Gi5/1, Gi6/11, Gi6/26, Gi6/45
600  Vl600      active    Te1/2, Te1/3, Gi3/10, Gi4/35, Gi6/40
601  Vl601         active    Te1/2, Te1/3, Gi2/1, Gi2/2, Gi2/3, Gi2/4, Gi2/5, Gi2/6, Gi2/8, Gi2/9, Gi2/10, Gi2/11, Gi2/12, Gi2/13, Gi2/14, Gi2/15, Gi2/16, Gi2/19, Gi2/20, Gi2/21, Gi2/22, Gi2/23, Gi2/24, Gi2/26, Gi2/27, Gi2/29, Gi2/30, Gi2/31, Gi2/32, Gi2/34, Gi2/35, Gi2/36, Gi2/37, Gi2/39, Gi2/41, Gi2/42, Gi2/43, Gi2/44, Gi2/45, Gi2/46, Gi2/47, Gi3/2, Gi3/4, Gi3/5, Gi3/7, Gi3/8, Gi3/9, Gi3/11, Gi3/12, Gi3/13, Gi3/14, Gi3/15, Gi3/16, Gi3/17, Gi3/18, Gi3/19, Gi3/20, Gi3/21, Gi3/22, Gi3/23
                                                Gi3/29, Gi3/30, Gi3/32, Gi3/33, Gi3/34, Gi3/35, Gi3/37, Gi3/39, Gi3/41, Gi3/42, Gi3/43, Gi3/46, Gi3/47, Gi3/48, Gi4/1, Gi4/2, Gi4/3, Gi4/4, Gi4/6, Gi4/7, Gi4/17, Gi4/19, Gi4/21, Gi4/26, Gi4/27, Gi4/28, Gi4/30, Gi4/31, Gi4/32, Gi4/33, Gi4/34, Gi4/38, Gi4/39, Gi4/40, Gi4/41, Gi4/42, Gi4/43, Gi4/44, Gi6/2, Gi6/3, Gi6/4, Gi6/5, Gi6/7, Gi6/10, Gi6/13, Gi6/14, Gi6/18, Gi6/19, Gi6/22, Gi6/24, Gi6/29, Gi6/30, Gi6/31, Gi6/37, Gi6/38, Gi6/39, Gi6/41, Gi6/42, Gi6/43
603  Vl603             active    Te1/2, Te1/3
```

Here `Vl601` returns up to `Gi3/23`
IOS: `get_vlans` does not return all interfaces if they wrap around in the config
### Description of Issue/Question

Running `get_vlans` on devices with many interfaces on the same vlan returns a subset (first line of config) of the configured interfaces.

### Did you follow the steps from https://github.com/napalm-automation/napalm#faq
(Place an ``x`` between the square brackets where applicable)

- [x] Yes
- [ ] No


### Setup

### napalm version
(Paste verbatim output from `pip freeze | grep napalm` between quotes below)

```
napalm==3.3.1
```

### Network operating system version
(Paste verbatim output from `show version` - or equivalent - between quotes below)

Irrelevant, but:
```
Cisco IOS Software, s72033_rp Software (s72033_rp-ADVIPSERVICESK9_WAN-M), Version 12.2(33)SXH4, RELEASE SOFTWARE (fc1)

```

### Steps to Reproduce the Issue

Run `get_vlans` where the output is too large, get only first set
```
VLAN Name                             Status    Ports
---- -------------------------------- --------- -------------------------------
1    default                          active    Te1/4, Gi2/7, Gi2/17, Gi2/28, Gi2/33, Gi2/40, Gi2/48, Gi3/24, Gi3/38, Gi3/40, Gi4/5, Gi4/8, Gi4/9, Gi4/10, Gi4/11, Gi4/12, Gi4/18, Gi4/20, Gi4/22, Gi4/23, Gi4/24, Gi5/1, Gi6/11, Gi6/26, Gi6/45
600  Vl600      active    Te1/2, Te1/3, Gi3/10, Gi4/35, Gi6/40
601  Vl601         active    Te1/2, Te1/3, Gi2/1, Gi2/2, Gi2/3, Gi2/4, Gi2/5, Gi2/6, Gi2/8, Gi2/9, Gi2/10, Gi2/11, Gi2/12, Gi2/13, Gi2/14, Gi2/15, Gi2/16, Gi2/19, Gi2/20, Gi2/21, Gi2/22, Gi2/23, Gi2/24, Gi2/26, Gi2/27, Gi2/29, Gi2/30, Gi2/31, Gi2/32, Gi2/34, Gi2/35, Gi2/36, Gi2/37, Gi2/39, Gi2/41, Gi2/42, Gi2/43, Gi2/44, Gi2/45, Gi2/46, Gi2/47, Gi3/2, Gi3/4, Gi3/5, Gi3/7, Gi3/8, Gi3/9, Gi3/11, Gi3/12, Gi3/13, Gi3/14, Gi3/15, Gi3/16, Gi3/17, Gi3/18, Gi3/19, Gi3/20, Gi3/21, Gi3/22, Gi3/23
                                                Gi3/29, Gi3/30, Gi3/32, Gi3/33, Gi3/34, Gi3/35, Gi3/37, Gi3/39, Gi3/41, Gi3/42, Gi3/43, Gi3/46, Gi3/47, Gi3/48, Gi4/1, Gi4/2, Gi4/3, Gi4/4, Gi4/6, Gi4/7, Gi4/17, Gi4/19, Gi4/21, Gi4/26, Gi4/27, Gi4/28, Gi4/30, Gi4/31, Gi4/32, Gi4/33, Gi4/34, Gi4/38, Gi4/39, Gi4/40, Gi4/41, Gi4/42, Gi4/43, Gi4/44, Gi6/2, Gi6/3, Gi6/4, Gi6/5, Gi6/7, Gi6/10, Gi6/13, Gi6/14, Gi6/18, Gi6/19, Gi6/22, Gi6/24, Gi6/29, Gi6/30, Gi6/31, Gi6/37, Gi6/38, Gi6/39, Gi6/41, Gi6/42, Gi6/43
603  Vl603             active    Te1/2, Te1/3
```

Here `Vl601` returns up to `Gi3/23`


Discussion:

Resolving patch:
diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py
--- a/napalm/ios/ios.py
+++ b/napalm/ios/ios.py
@@ -3590,22 +3590,42 @@ def get_vlans(self):
             return self._get_vlan_all_ports(output)
 
     def _get_vlan_all_ports(self, output):
-        find_regexp = r"^(\d+)\s+(\S+)\s+\S+\s+([A-Z][a-z].*)$"
-        find = re.findall(find_regexp, output, re.MULTILINE)
+        find_regexp = re.compile(r"^(\d+)\s+(\S+)\s+\S+(\s+[A-Z][a-z].*)?$")
+        continuation_regexp = re.compile(r"^\s+([A-Z][a-z].*)$")
+        output = output.splitlines()
         vlans = {}
-        for vlan_id, vlan_name, interfaces in find:
-            vlans[vlan_id] = {
-                "name": vlan_name,
-                "interfaces": [
+
+        was_vlan_or_cont = False
+        vlan_id = None
+        vlan_name = None
+        interfaces = ""
+        for line in output:
+            vlan_m = find_regexp.match(line)
+            if vlan_m:
+                was_vlan_or_cont = True
+                vlan_id = vlan_m.group(1)
+                vlan_name = vlan_m.group(2)
+                interfaces = vlan_m.group(3) or ""
+                vlans[vlan_id] = {"name": vlan_name, "interfaces": []}
+
+            cont_m = None
+            if was_vlan_or_cont:
+                cont_m = continuation_regexp.match(line)
+                if cont_m:
+                    interfaces = cont_m.group(1)
+
+            if not cont_m and not vlan_m:
+                was_vlan_or_cont = False
+                continue
+
+            vlans[vlan_id]["interfaces"].extend(
+                [
                     canonical_interface_name(intf.strip())
                     for intf in interfaces.split(",")
-                ],
-            }
+                    if intf.strip()
+                ]
+            )
 
-        find_regexp = r"^(\d+)\s+(\S+)\s+\S+$"
-        find = re.findall(find_regexp, output, re.MULTILINE)
-        for vlan_id, vlan_name in find:
-            vlans[vlan_id] = {"name": vlan_name, "interfaces": []}
         return vlans
 
     def _get_vlan_from_id(self):
@@ -3616,22 +3636,12 @@ def _get_vlan_from_id(self):
         vlans = {}
         for vlan_id, vlan_name in find_vlan:
             output = self._send_command("show vlan id {}".format(vlan_id))
-            interface_regex = r"{}\s+{}\s+\S+\s+([A-Z][a-z].*)$".format(
-                vlan_id, re.escape(vlan_name)
-            )
-            interfaces = re.findall(interface_regex, output, re.MULTILINE)
-            if len(interfaces) == 1:
-                interfaces = interfaces[0]
-                vlans[vlan_id] = {
-                    "name": vlan_name,
-                    "interfaces": [
-                        canonical_interface_name(intf.strip())
-                        for intf in interfaces.split(",")
-                    ],
-                }
-            elif len(interfaces) == 0:
+            _vlans = self._get_vlan_all_ports(output)
+            if len(_vlans) == 0:
                 vlans[vlan_id] = {"name": vlan_name, "interfaces": []}
-            else:
+            elif len(_vlans) == 1:
+                vlans.update(_vlans)
+            elif len(_vlans.keys()) > 1:
                 raise ValueError(
                     "Error parsing vlan_id {}, "
                     "found more values than can be present.".format(vlan_id)


Test patch:
diff --git a/test/ios/mocked_data/test_get_vlans/no_show_vlan_all_ports/expected_result.json b/test/ios/mocked_data/test_get_vlans/no_show_vlan_all_ports/expected_result.json
--- a/test/ios/mocked_data/test_get_vlans/no_show_vlan_all_ports/expected_result.json
+++ b/test/ios/mocked_data/test_get_vlans/no_show_vlan_all_ports/expected_result.json
@@ -61,11 +61,136 @@
   "763": {
     "name": "Vlan763",
     "interfaces": [
-      "GigabitEthernet0/2",
-      "GigabitEthernet0/7",
-      "GigabitEthernet0/12",
-      "GigabitEthernet0/13",
-      "Port-channel1"
+      "GigabitEthernet1/1",
+      "GigabitEthernet1/2",
+      "GigabitEthernet1/3",
+      "GigabitEthernet1/4",
+      "GigabitEthernet1/5",
+      "GigabitEthernet1/6",
+      "GigabitEthernet1/7",
+      "GigabitEthernet1/8",
+      "GigabitEthernet1/9",
+      "GigabitEthernet1/10",
+      "GigabitEthernet1/11",
+      "GigabitEthernet1/12",
+      "GigabitEthernet1/37",
+      "GigabitEthernet1/38",
+      "GigabitEthernet1/39",
+      "GigabitEthernet1/40",
+      "GigabitEthernet1/41",
+      "GigabitEthernet1/42",
+      "GigabitEthernet1/43",
+      "GigabitEthernet1/44",
+      "GigabitEthernet1/47",
+      "GigabitEthernet1/48",
+      "GigabitEthernet2/1",
+      "GigabitEthernet2/2",
+      "GigabitEthernet2/3",
+      "GigabitEthernet2/4",
+      "GigabitEthernet2/7",
+      "GigabitEthernet2/8",
+      "GigabitEthernet2/9",
+      "GigabitEthernet2/10",
+      "GigabitEthernet2/11",
+      "GigabitEthernet2/12",
+      "GigabitEthernet2/15",
+      "GigabitEthernet2/16",
+      "GigabitEthernet2/41",
+      "GigabitEthernet2/42",
+      "GigabitEthernet2/43",
+      "GigabitEthernet2/44",
+      "GigabitEthernet3/3",
+      "GigabitEthernet3/4",
+      "GigabitEthernet3/5",
+      "GigabitEthernet3/6",
+      "GigabitEthernet3/7",
+      "GigabitEthernet3/8",
+      "GigabitEthernet3/9",
+      "GigabitEthernet3/10",
+      "GigabitEthernet3/37",
+      "GigabitEthernet3/38",
+      "GigabitEthernet3/39",
+      "GigabitEthernet3/40",
+      "GigabitEthernet3/41",
+      "GigabitEthernet3/42",
+      "GigabitEthernet3/43",
+      "GigabitEthernet3/44",
+      "GigabitEthernet3/45",
+      "GigabitEthernet3/46",
+      "GigabitEthernet3/47",
+      "GigabitEthernet3/48",
+      "GigabitEthernet4/1",
+      "GigabitEthernet4/2",
+      "GigabitEthernet4/3",
+      "GigabitEthernet4/4",
+      "GigabitEthernet4/5",
+      "GigabitEthernet4/6",
+      "GigabitEthernet4/7",
+      "GigabitEthernet4/8",
+      "GigabitEthernet4/41",
+      "GigabitEthernet4/42",
+      "GigabitEthernet4/43",
+      "GigabitEthernet4/44",
+      "GigabitEthernet4/45",
+      "GigabitEthernet4/46",
+      "GigabitEthernet4/47",
+      "GigabitEthernet4/48",
+      "GigabitEthernet7/1",
+      "GigabitEthernet7/2",
+      "GigabitEthernet7/3",
+      "GigabitEthernet7/4",
+      "GigabitEthernet7/5",
+      "GigabitEthernet7/6",
+      "GigabitEthernet7/7",
+      "GigabitEthernet7/27",
+      "GigabitEthernet7/29",
+      "GigabitEthernet7/32",
+      "GigabitEthernet7/37",
+      "GigabitEthernet7/38",
+      "GigabitEthernet7/39",
+      "GigabitEthernet7/40",
+      "GigabitEthernet7/41",
+      "GigabitEthernet7/42",
+      "GigabitEthernet7/43",
+      "GigabitEthernet7/44",
+      "GigabitEthernet7/45",
+      "GigabitEthernet7/46",
+      "GigabitEthernet8/1",
+      "GigabitEthernet8/2",
+      "GigabitEthernet8/3",
+      "GigabitEthernet8/4",
+      "GigabitEthernet8/5",
+      "GigabitEthernet8/6",
+      "GigabitEthernet8/9",
+      "GigabitEthernet8/10",
+      "GigabitEthernet8/13",
+      "GigabitEthernet8/14",
+      "GigabitEthernet8/39",
+      "GigabitEthernet8/40",
+      "GigabitEthernet8/41",
+      "GigabitEthernet8/42",
+      "GigabitEthernet8/43",
+      "GigabitEthernet8/44",
+      "GigabitEthernet8/45",
+      "GigabitEthernet8/46",
+      "GigabitEthernet8/47",
+      "GigabitEthernet8/48",
+      "GigabitEthernet9/7",
+      "GigabitEthernet9/8",
+      "GigabitEthernet9/9",
+      "GigabitEthernet9/10",
+      "GigabitEthernet9/11",
+      "GigabitEthernet9/12",
+      "GigabitEthernet9/41",
+      "GigabitEthernet9/42",
+      "GigabitEthernet9/43",
+      "GigabitEthernet9/44",
+      "GigabitEthernet9/45",
+      "GigabitEthernet10/29",
+      "GigabitEthernet10/30",
+      "GigabitEthernet10/31",
+      "GigabitEthernet10/33",
+      "GigabitEthernet10/34"
     ]
   },
   "795": {
diff --git a/test/ios/mocked_data/test_get_vlans/no_show_vlan_all_ports/show_vlan_id_763.txt b/test/ios/mocked_data/test_get_vlans/no_show_vlan_all_ports/show_vlan_id_763.txt
--- a/test/ios/mocked_data/test_get_vlans/no_show_vlan_all_ports/show_vlan_id_763.txt
+++ b/test/ios/mocked_data/test_get_vlans/no_show_vlan_all_ports/show_vlan_id_763.txt
@@ -1,6 +1,8 @@
 VLAN Name                             Status    Ports
 ---- -------------------------------- --------- -------------------------------
-763  Vlan763                          active    Gi0/2, Gi0/7, Gi0/12, Gi0/13, Po1
+763  Vlan763                          active    Gi1/1, Gi1/2, Gi1/3, Gi1/4, Gi1/5, Gi1/6, Gi1/7, Gi1/8, Gi1/9, Gi1/10, Gi1/11, Gi1/12, Gi1/37, Gi1/38, Gi1/39, Gi1/40, Gi1/41, Gi1/42, Gi1/43, Gi1/44, Gi1/47, Gi1/48, Gi2/1, Gi2/2, Gi2/3, Gi2/4, Gi2/7, Gi2/8, Gi2/9, Gi2/10, Gi2/11, Gi2/12, Gi2/15, Gi2/16, Gi2/41, Gi2/42, Gi2/43, Gi2/44, Gi3/3, Gi3/4, Gi3/5, Gi3/6, Gi3/7, Gi3/8, Gi3/9, Gi3/10, Gi3/37, Gi3/38, Gi3/39, Gi3/40, Gi3/41, Gi3/42, Gi3/43, Gi3/44, Gi3/45, Gi3/46, Gi3/47, Gi3/48, Gi4/1, Gi4/2, Gi4/3
+                                                Gi4/4, Gi4/5, Gi4/6, Gi4/7, Gi4/8, Gi4/41, Gi4/42, Gi4/43, Gi4/44, Gi4/45, Gi4/46, Gi4/47, Gi4/48, Gi7/1, Gi7/2, Gi7/3, Gi7/4, Gi7/5, Gi7/6, Gi7/7, Gi7/27, Gi7/29, Gi7/32, Gi7/37, Gi7/38, Gi7/39, Gi7/40, Gi7/41, Gi7/42, Gi7/43, Gi7/44, Gi7/45, Gi7/46, Gi8/1, Gi8/2, Gi8/3, Gi8/4, Gi8/5, Gi8/6, Gi8/9, Gi8/10, Gi8/13, Gi8/14, Gi8/39, Gi8/40, Gi8/41, Gi8/42, Gi8/43, Gi8/44, Gi8/45, Gi8/46, Gi8/47, Gi8/48, Gi9/7, Gi9/8, Gi9/9, Gi9/10, Gi9/11, Gi9/12, Gi9/41
+                                                Gi9/42, Gi9/43, Gi9/44, Gi9/45, Gi10/29, Gi10/30, Gi10/31, Gi10/33, Gi10/34
 
 VLAN Type  SAID       MTU   Parent RingNo BridgeNo Stp  BrdgMode Trans1 Trans2
 ---- ----- ---------- ----- ------ ------ -------- ---- -------- ------ ------
@@ -11,4 +13,4 @@ Remote SPAN VLAN
 Disabled
 
 Primary Secondary Type              Ports
-------- --------- ----------------- ------------------------------------------
\ No newline at end of file
+------- --------- ----------------- ------------------------------------------
diff --git a/test/ios/mocked_data/test_get_vlans/normal/expected_result.json b/test/ios/mocked_data/test_get_vlans/normal/expected_result.json
--- a/test/ios/mocked_data/test_get_vlans/normal/expected_result.json
+++ b/test/ios/mocked_data/test_get_vlans/normal/expected_result.json
@@ -681,7 +681,11 @@
             "Port-channel26",
             "Port-channel27",
             "Port-channel28",
-            "Port-channel30"
+            "Port-channel30",
+            "TenGigabitEthernet2/2/30",
+            "TenGigabitEthernet2/1/15",
+            "TenGigabitEthernet2/2/21",
+            "TenGigabitEthernet1/3/8"
         ]
     },
     "707": {
diff --git a/test/ios/mocked_data/test_get_vlans/normal/show_vlan_all_ports.txt b/test/ios/mocked_data/test_get_vlans/normal/show_vlan_all_ports.txt
--- a/test/ios/mocked_data/test_get_vlans/normal/show_vlan_all_ports.txt
+++ b/test/ios/mocked_data/test_get_vlans/normal/show_vlan_all_ports.txt
@@ -38,6 +38,8 @@ VLAN Name                             Status    Ports
 612  Vlan612                          active    Po1, Po3, Po4, Po5, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30
 613  Vlan613                          active    Po1, Po3, Po4, Po5, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30
 702  Vlan702                          active    Te2/2/32, Po1, Po5, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30
+                                                Te2/2/30, Te2/1/15
+                                                Te2/2/21, Te1/3/8
 707  Vlan707                          active    Po1, Po5, Po7, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30
 710  Vlan710                          active    Te1/1/9, Te1/1/11, Te1/3/4, Te2/2/31, Te2/3/4, Te2/3/6, Po1, Po3, Po4, Po5, Po6, Po7, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po29, Po30, Po100
 711  Vlan711                          active    Po1, Po5, Po21, Po22, Po23, Po24, Po25, Po26, Po27, Po28, Po30
@@ -141,4 +143,4 @@ VLAN Type  SAID       MTU   Parent RingNo BridgeNo Stp  BrdgMode Trans1 Trans2
 1275 enet  101275     1500  -      -      -        -    -        0      0
 
 Primary Secondary Type              Ports
-------- --------- ----------------- ------------------------------------------
\ No newline at end of file
+------- --------- ----------------- ------------------------------------------