Repository: napalm-automation/napalm

Issue:
Question about napalm License
### Description of Issue/Question

From the official napalm documentation, napalm has Apache 2.0 license. However, I do see in the napalm 3.3.1, there is one library, `ciscoconfparse` with GPL license. Does that mean napalm itself should come with GPL license as well?

### 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)

```
N/A
```

### Steps to Reproduce the Issue

### Error Traceback
(Paste the complete traceback of the exception between quotes below)

```
linux# pipdeptree --packages napalm | grep cisco
  - ciscoconfparse [required: Any, installed: 1.5.46]

```


Discussion:
IMO it means we have to rip out CiscoConfParse from NAPALM. CiscoConfParse is trivial to NAPALM library itself.

Adding the GPL requirements to NAPALM (after the fact) would be incompatible with how the contributions were made (i.e. would be adding additional licensing burdens after the fact that weren't consistent with the license under which the contributions were made).

I will do a PR to do this and we should incorporate this in the next release.

We should also add an automated process that checks for any GPL dependencies as it can be easy for them to leak in and thus have a totally unintended and undesired effect of trying to change the explicit license requirements.
Code to remove:

```
./ios/ios.py:        bgp_config_text = napalm.base.helpers.cisco_conf_parse_objects(
./ios/ios.py:            afi_list = napalm.base.helpers.cisco_conf_parse_parents(
./ios/ios.py:                neighbor_config = napalm.base.helpers.cisco_conf_parse_objects(
./ios/ios.py:            neighbor_config = napalm.base.helpers.cisco_conf_parse_objects(
./ios/ios.py:            afi_list = napalm.base.helpers.cisco_conf_parse_parents(
./ios/ios.py:                afi_config = napalm.base.helpers.cisco_conf_parse_objects(
./base/helpers.py:def cisco_conf_parse_parents(parent, child, config):
./base/helpers.py:def cisco_conf_parse_objects(cfg_section, config):
```
That would be super, thanks for your effort on this!
Would https://github.com/netdevops/hier_config be a possible replacement?
@jvanderaa Yeah, we only use it in one area of the code...so need to look at easiest option (i.e. just regex, textfsm, or doing something similar as confparse, but with non-GPL license).
Hi @ktbyers , just curious if you have any planned timeline for the change? Thanks.
Hopefully, I can get this done in a week or two.
Or possibly even using netutils here.

https://netutils.readthedocs.io/en/latest/netutils/configs/index.html#netutils.config.parser.IOSConfigParser



Resolving patch:
diff --git a/napalm/base/helpers.py b/napalm/base/helpers.py
--- a/napalm/base/helpers.py
+++ b/napalm/base/helpers.py
@@ -12,11 +12,11 @@
 # third party libs
 import jinja2
 import textfsm
-from ciscoconfparse import CiscoConfParse
 from lxml import etree
 from netaddr import EUI
 from netaddr import IPAddress
 from netaddr import mac_unix
+from netutils.config.parser import IOSConfigParser
 
 # local modules
 import napalm.base.exceptions
@@ -127,42 +127,84 @@ def load_template(
     return cls.load_merge_candidate(config=configuration)
 
 
-def cisco_conf_parse_parents(
+def netutils_parse_parents(
     parent: str, child: str, config: Union[str, List[str]]
 ) -> List[str]:
     """
-    Use CiscoConfParse to find parent lines that contain a specific child line.
+    Use Netutils to find parent lines that contain a specific child line.
 
     :param parent: The parent line to search for
     :param child:  The child line required under the given parent
     :param config: The device running/startup config
     """
-    if type(config) == str:
-        config = config.splitlines()  # type: ignore
-    parse = CiscoConfParse(config)
-    cfg_obj = parse.find_parents_w_child(parent, child)
-    return cfg_obj
+    # Check if the config is a list, if it is a list, then join it to make a string.
+    if isinstance(config, list):
+        config = "\n".join(config)
+        config = config + "\n"
+
+    # Config tree is the entire configuration in a tree format,
+    # followed by getting the individual lines that has the formats:
+    # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0',
+    # parents=('interface GigabitEthernet1',))
+    # ConfigLine(config_line='Current configuration : 1624 bytes', parents=())
+    config_tree = IOSConfigParser(str(config))
+    configuration_lines = config_tree.build_config_relationship()
+
+    # Return config is the list that will be returned
+    return_config = []
+
+    # Loop over each of the configuration lines
+    for line in configuration_lines:
+        # Loop over any line that has a parent line. If there are no parents for a line item then
+        # the parents is an empty tuple.
+        for parent_line in line.parents:
+            if (
+                child in line.config_line
+                and re.match(parent, parent_line) is not None
+                and parent_line not in return_config
+            ):
+                return_config.append(parent_line)
+
+    return return_config
 
 
-def cisco_conf_parse_objects(
+def netutils_parse_objects(
     cfg_section: str, config: Union[str, List[str]]
 ) -> List[str]:
     """
-    Use CiscoConfParse to find and return a section of Cisco IOS config.
+    Use Netutils to find and return a section of Cisco IOS config.
     Similar to "show run | section <cfg_section>"
 
     :param cfg_section: The section of the config to return eg. "router bgp"
     :param config: The running/startup config of the device to parse
     """
+    # Check if the config is a list, if it is a list, then join it to make a string.
+    if isinstance(config, list):
+        config = "\n".join(config)
+        config = config + "\n"
+
+    # Config tree is the entire configuration in a tree format,
+    # followed by getting the individual lines that has the formats:
+    # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0',
+    # parents=('interface GigabitEthernet1',))
+    # ConfigLine(config_line='Current configuration : 1624 bytes', parents=())
+    config_tree = IOSConfigParser(str(config))
+    lines = config_tree.build_config_relationship()
+
+    # Return config is the list that will be returned
     return_config = []
-    if type(config) is str:
-        config = config.splitlines()  # type: ignore
-    parse = CiscoConfParse(config)
-    cfg_obj = parse.find_objects(cfg_section)
-    for parent in cfg_obj:
-        return_config.append(parent.text)
-        for child in parent.all_children:
-            return_config.append(child.text)
+    for line in lines:
+        # The parent configuration is expected on the function that this is replacing,
+        # add the parent line to the base of the return_config
+        if cfg_section in line.config_line:
+            return_config.append(line.config_line)
+        # Check if the tuple is greater than 0
+        if len(line.parents) > 0:
+            # Check the eldest parent, if that is part of the config section, then append
+            # the current line being checked to it.
+            if cfg_section in line.parents[0]:
+                return_config.append(line.config_line)
+
     return return_config
 
 
diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py
--- a/napalm/ios/ios.py
+++ b/napalm/ios/ios.py
@@ -1333,21 +1333,20 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout):
                 }
             return prefix_limit
 
-        # Get BGP config using ciscoconfparse because some old devices dont support "| sec bgp"
+        # Get BGP config using netutils because some old devices dont support "| sec bgp"
         cfg = self.get_config(retrieve="running")
-        cfg = cfg["running"].splitlines()
-        bgp_config_text = napalm.base.helpers.cisco_conf_parse_objects(
-            "router bgp", cfg
+        bgp_config_list = napalm.base.helpers.netutils_parse_objects(
+            "router bgp", cfg["running"]
         )
         bgp_asn = napalm.base.helpers.regex_find_txt(
-            r"router bgp (\d+)", bgp_config_text, default=0
+            r"router bgp (\d+)", bgp_config_list, default=0
         )
         # Get a list of all neighbors and groups in the config
         all_neighbors = set()
         all_groups = set()
         bgp_group_neighbors = {}
         all_groups.add("_")
-        for line in bgp_config_text:
+        for line in bgp_config_list:
             if " neighbor " in line:
                 if re.search(IP_ADDR_REGEX, line) is not None:
                     all_neighbors.add(re.search(IP_ADDR_REGEX, line).group())
@@ -1364,8 +1363,8 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout):
             if neighbor:
                 if bgp_neighbor != neighbor:
                     continue
-            afi_list = napalm.base.helpers.cisco_conf_parse_parents(
-                r"\s+address-family.*", bgp_neighbor, bgp_config_text
+            afi_list = napalm.base.helpers.netutils_parse_parents(
+                r"\s+address-family.*", bgp_neighbor, bgp_config_list
             )
             try:
                 afi = afi_list[0]
@@ -1376,8 +1375,8 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout):
             if "vrf" in str(afi_list):
                 continue
             else:
-                neighbor_config = napalm.base.helpers.cisco_conf_parse_objects(
-                    bgp_neighbor, bgp_config_text
+                neighbor_config = napalm.base.helpers.netutils_parse_objects(
+                    bgp_neighbor, bgp_config_list
                 )
             # For group_name- use peer-group name, else VRF name, else "_" for no group
             group_name = napalm.base.helpers.regex_find_txt(
@@ -1467,16 +1466,16 @@ def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout):
                     "neighbors": bgp_group_neighbors.get("_", {}),
                 }
                 continue
-            neighbor_config = napalm.base.helpers.cisco_conf_parse_objects(
-                group_name, bgp_config_text
+            neighbor_config = napalm.base.helpers.netutils_parse_objects(
+                group_name, bgp_config_list
             )
             multipath = False
-            afi_list = napalm.base.helpers.cisco_conf_parse_parents(
-                r"\s+address-family.*", group_name, neighbor_config
+            afi_list = napalm.base.helpers.netutils_parse_parents(
+                r"\s+address-family.*", group_name, bgp_config_list
             )
             for afi in afi_list:
-                afi_config = napalm.base.helpers.cisco_conf_parse_objects(
-                    afi, bgp_config_text
+                afi_config = napalm.base.helpers.netutils_parse_objects(
+                    afi, bgp_config_list
                 )
                 multipath = bool(
                     napalm.base.helpers.regex_find_txt(r" multipath", str(afi_config))


Test patch: