From 9e7ece0c8e71edc21737c9bae0fe676b6b2c5eff Mon Sep 17 00:00:00 2001 From: swanandtambe Date: Fri, 17 Jul 2026 11:38:17 +0530 Subject: [PATCH 1/3] feat: Add support for multiple consecutive banners in configs. --- changes/859.fixed | 1 + netutils/config/parser.py | 13 +++++++---- .../ios_multiple_banners_actual.txt | 18 +++++++++++++++ .../ios_multiple_banners_feature.py | 2 ++ .../ios_multiple_banners_intended.txt | 18 +++++++++++++++ .../ios_multiple_banners_received.py | 10 +++++++++ .../cisco_ios/ios_dual_banner_received.py | 1 - .../ios_multiple_banners_received.py | 18 +++++++++++++++ .../cisco_ios/ios_multiple_banners_sent.txt | 22 +++++++++++++++++++ .../iosxr_multiple_banners_received.py | 14 ++++++++++++ .../iosxr_multiple_banners_sent.txt | 18 +++++++++++++++ 11 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 changes/859.fixed create mode 100644 tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_actual.txt create mode 100644 tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_feature.py create mode 100644 tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_intended.txt create mode 100644 tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_received.py create mode 100644 tests/unit/mock/config/parser/base/cisco_ios/ios_multiple_banners_received.py create mode 100644 tests/unit/mock/config/parser/base/cisco_ios/ios_multiple_banners_sent.txt create mode 100644 tests/unit/mock/config/parser/base/cisco_iosxr/iosxr_multiple_banners_received.py create mode 100644 tests/unit/mock/config/parser/base/cisco_iosxr/iosxr_multiple_banners_sent.txt diff --git a/changes/859.fixed b/changes/859.fixed new file mode 100644 index 00000000..32e0077b --- /dev/null +++ b/changes/859.fixed @@ -0,0 +1 @@ +Fixed config parser failing to parse more than three consecutive banners, which caused "Unable to parse banner end." errors on configs with many banners (e.g. Cisco IOS). diff --git a/netutils/config/parser.py b/netutils/config/parser.py index b38248f1..b4ab3bff 100644 --- a/netutils/config/parser.py +++ b/netutils/config/parser.py @@ -337,10 +337,11 @@ def build_config_relationship(self) -> t.List[ConfigLine]: if line is None: break elif self.is_banner_start(line): - line = self._build_banner(line) # type: ignore - # line can potentially be another banner start therefore we do a secondary check. - if self.is_banner_start(line): + # Handles multiple consecutive banners (e.g. Cisco IOS) + while line is not None and self.is_banner_start(line): line = self._build_banner(line) # type: ignore + if line is None: + break self._update_config_lines(line) return self.config_lines @@ -1413,7 +1414,11 @@ def build_config_relationship(self) -> t.List[ConfigLine]: if line is None: break elif self.is_banner_start(line): - line = self._build_banner(line) # type: ignore + # Handles multiple consecutive banners (e.g. Cisco IOS) + while line is not None and self.is_banner_start(line): + line = self._build_banner(line) # type: ignore + if line is None: + break self._update_config_lines(line) return self.config_lines diff --git a/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_actual.txt b/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_actual.txt new file mode 100644 index 00000000..6fa8f3a0 --- /dev/null +++ b/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_actual.txt @@ -0,0 +1,18 @@ +hostname multi-banner +! +banner exec ^C +* exec banner * +^C +banner incoming ^C +* incoming banner * +^C +banner login ^C +* login banner actual * +^C +banner motd ^C +* motd banner * +^C +banner prompt-timeout ^C +* prompt-timeout banner * +^C +! diff --git a/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_feature.py b/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_feature.py new file mode 100644 index 00000000..221beb45 --- /dev/null +++ b/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_feature.py @@ -0,0 +1,2 @@ +feature = {"name": "login banner", "ordered": False, "section": ["banner login"]} +network_os = "cisco_ios" diff --git a/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_intended.txt b/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_intended.txt new file mode 100644 index 00000000..944ec832 --- /dev/null +++ b/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_intended.txt @@ -0,0 +1,18 @@ +hostname multi-banner +! +banner exec ^C +* exec banner * +^C +banner incoming ^C +* incoming banner * +^C +banner login ^C +* login banner intended * +^C +banner motd ^C +* motd banner * +^C +banner prompt-timeout ^C +* prompt-timeout banner * +^C +! diff --git a/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_received.py b/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_received.py new file mode 100644 index 00000000..663442d6 --- /dev/null +++ b/tests/unit/mock/config/compliance/feature_compliance/ios_multiple_banners_received.py @@ -0,0 +1,10 @@ +data = { + "compliant": False, + "missing": "banner login ^C\n* login banner intended *\n^C", + "extra": "banner login ^C\n* login banner actual *\n^C", + "cannot_parse": True, + "unordered_compliant": False, + "ordered_compliant": False, + "actual": "banner login ^C\n* login banner actual *\n^C", + "intended": "banner login ^C\n* login banner intended *\n^C", +} diff --git a/tests/unit/mock/config/parser/base/cisco_ios/ios_dual_banner_received.py b/tests/unit/mock/config/parser/base/cisco_ios/ios_dual_banner_received.py index dca51426..07eb8661 100755 --- a/tests/unit/mock/config/parser/base/cisco_ios/ios_dual_banner_received.py +++ b/tests/unit/mock/config/parser/base/cisco_ios/ios_dual_banner_received.py @@ -8,5 +8,4 @@ ConfigLine( config_line="======\nintended config motd banner\n======\n || ($hostname) ||\n^C", parents=("banner motd ^C",) ), - ConfigLine(config_line=None, parents=()), ] diff --git a/tests/unit/mock/config/parser/base/cisco_ios/ios_multiple_banners_received.py b/tests/unit/mock/config/parser/base/cisco_ios/ios_multiple_banners_received.py new file mode 100644 index 00000000..ba63314c --- /dev/null +++ b/tests/unit/mock/config/parser/base/cisco_ios/ios_multiple_banners_received.py @@ -0,0 +1,18 @@ +from netutils.config.parser import ConfigLine + +data = [ + ConfigLine(config_line="hostname multi-banner", parents=()), + ConfigLine(config_line="banner exec ^C", parents=()), + ConfigLine(config_line="* exec banner line 1 *\n* exec banner line 2 *\n^C", parents=("banner exec ^C",)), + ConfigLine(config_line="banner incoming ^C", parents=()), + ConfigLine( + config_line="* incoming banner line 1 *\n* incoming banner line 2 *\n^C", parents=("banner incoming ^C",) + ), + ConfigLine(config_line="banner login ^C", parents=()), + ConfigLine(config_line="* login banner line 1 *\n* login banner line 2 *\n^C", parents=("banner login ^C",)), + ConfigLine(config_line="banner motd ^C", parents=()), + ConfigLine(config_line="* motd banner line 1 *\n* motd banner line 2 *\n^C", parents=("banner motd ^C",)), + ConfigLine(config_line="banner prompt-timeout ^C", parents=()), + ConfigLine(config_line="* prompt-timeout banner line 1 *\n^C", parents=("banner prompt-timeout ^C",)), + ConfigLine(config_line="ip route 0.0.0.0 0.0.0.0 192.168.1.1", parents=()), +] diff --git a/tests/unit/mock/config/parser/base/cisco_ios/ios_multiple_banners_sent.txt b/tests/unit/mock/config/parser/base/cisco_ios/ios_multiple_banners_sent.txt new file mode 100644 index 00000000..8c862ea4 --- /dev/null +++ b/tests/unit/mock/config/parser/base/cisco_ios/ios_multiple_banners_sent.txt @@ -0,0 +1,22 @@ +hostname multi-banner +! +banner exec ^C +* exec banner line 1 * +* exec banner line 2 * +^C +banner incoming ^C +* incoming banner line 1 * +* incoming banner line 2 * +^C +banner login ^C +* login banner line 1 * +* login banner line 2 * +^C +banner motd ^C +* motd banner line 1 * +* motd banner line 2 * +^C +banner prompt-timeout ^C +* prompt-timeout banner line 1 * +^C +ip route 0.0.0.0 0.0.0.0 192.168.1.1 diff --git a/tests/unit/mock/config/parser/base/cisco_iosxr/iosxr_multiple_banners_received.py b/tests/unit/mock/config/parser/base/cisco_iosxr/iosxr_multiple_banners_received.py new file mode 100644 index 00000000..0d6b2974 --- /dev/null +++ b/tests/unit/mock/config/parser/base/cisco_iosxr/iosxr_multiple_banners_received.py @@ -0,0 +1,14 @@ +from netutils.config.parser import ConfigLine + +data = [ + ConfigLine(config_line="hostname multi-banner-xr", parents=()), + ConfigLine(config_line="banner exec ~", parents=()), + ConfigLine(config_line="* exec banner line 1 *\n* exec banner line 2 *\n~", parents=("banner exec ~",)), + ConfigLine(config_line="banner login ~", parents=()), + ConfigLine(config_line="* login banner line 1 *\n* login banner line 2 *\n~", parents=("banner login ~",)), + ConfigLine(config_line="banner motd ~", parents=()), + ConfigLine(config_line="* motd banner line 1 *\n* motd banner line 2 *\n~", parents=("banner motd ~",)), + ConfigLine(config_line="banner prompt-timeout ~", parents=()), + ConfigLine(config_line="* prompt-timeout banner line 1 *\n~", parents=("banner prompt-timeout ~",)), + ConfigLine(config_line="logging trap informational", parents=()), +] diff --git a/tests/unit/mock/config/parser/base/cisco_iosxr/iosxr_multiple_banners_sent.txt b/tests/unit/mock/config/parser/base/cisco_iosxr/iosxr_multiple_banners_sent.txt new file mode 100644 index 00000000..e9544350 --- /dev/null +++ b/tests/unit/mock/config/parser/base/cisco_iosxr/iosxr_multiple_banners_sent.txt @@ -0,0 +1,18 @@ +hostname multi-banner-xr +! +banner exec ~ +* exec banner line 1 * +* exec banner line 2 * +~ +banner login ~ +* login banner line 1 * +* login banner line 2 * +~ +banner motd ~ +* motd banner line 1 * +* motd banner line 2 * +~ +banner prompt-timeout ~ +* prompt-timeout banner line 1 * +~ +logging trap informational From cf25c1d8bf0090f521d3524fe9afd195bfeae8ab Mon Sep 17 00:00:00 2001 From: swanandtambe Date: Fri, 17 Jul 2026 12:34:30 +0530 Subject: [PATCH 2/3] style: Typo. --- netutils/config/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netutils/config/parser.py b/netutils/config/parser.py index b4ab3bff..a29dc99b 100644 --- a/netutils/config/parser.py +++ b/netutils/config/parser.py @@ -291,7 +291,7 @@ def _update_config_lines(self, config_line: str) -> None: self.config_lines.append(entry) def build_config_relationship(self) -> t.List[ConfigLine]: - r"""Parse text tree of config lines and their parents. + """Parse text tree of config lines and their parents. Examples: >>> from netutils.config.parser import BaseSpaceConfigParser, ConfigLine From a13a1e8cb790db6d1589e053c6ba522bb829ae3d Mon Sep 17 00:00:00 2001 From: swanandtambe Date: Fri, 17 Jul 2026 12:47:18 +0530 Subject: [PATCH 3/3] test: --- netutils/config/parser.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netutils/config/parser.py b/netutils/config/parser.py index a29dc99b..09c9f6e0 100644 --- a/netutils/config/parser.py +++ b/netutils/config/parser.py @@ -290,8 +290,9 @@ def _update_config_lines(self, config_line: str) -> None: entry = ConfigLine(config_line, self._current_parents) self.config_lines.append(entry) + # pylint: disable=too-many-branches def build_config_relationship(self) -> t.List[ConfigLine]: - """Parse text tree of config lines and their parents. + r"""Parse text tree of config lines and their parents. Examples: >>> from netutils.config.parser import BaseSpaceConfigParser, ConfigLine @@ -1372,6 +1373,7 @@ def set_delimiter(self, config_line: str) -> None: return None raise ValueError("Unable to find banner delimiter.") + # pylint: disable=too-many-branches def build_config_relationship(self) -> t.List[ConfigLine]: r"""Parse text tree of config lines and their parents.