From 390b08b673e6228286013a7e89ad7e9eb03fa4f2 Mon Sep 17 00:00:00 2001 From: yanpla Date: Wed, 25 Mar 2026 11:17:56 +0100 Subject: [PATCH 1/6] add huawei_vpv8 platform --- hier_config/child.py | 11 +- hier_config/constructors.py | 2 + hier_config/models.py | 1 + hier_config/platforms/driver_base.py | 8 ++ .../platforms/huawei_vrpv8/__init__.py | 0 hier_config/platforms/huawei_vrpv8/driver.py | 53 +++++++++ hier_config/utils.py | 1 + tests/test_driver_huawei_vrpv8.py | 105 ++++++++++++++++++ 8 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 hier_config/platforms/huawei_vrpv8/__init__.py create mode 100644 hier_config/platforms/huawei_vrpv8/driver.py create mode 100644 tests/test_driver_huawei_vrpv8.py diff --git a/hier_config/child.py b/hier_config/child.py index 2945da86..33080fdd 100644 --- a/hier_config/child.py +++ b/hier_config/child.py @@ -128,16 +128,7 @@ def lines(self, *, sectional_exiting: bool = False) -> Iterable[str]: @property def sectional_exit(self) -> str | None: - for rule in self.driver.rules.sectional_exiting: - if self.is_lineage_match(rule.match_rules): - if exit_text := rule.exit_text: - return exit_text - return None - - if not self.children: - return None - - return "exit" + return self.driver.sectional_exit(self) @property def sectional_exit_text_parent_level(self) -> bool: diff --git a/hier_config/constructors.py b/hier_config/constructors.py index 3755a537..5328abe9 100644 --- a/hier_config/constructors.py +++ b/hier_config/constructors.py @@ -21,6 +21,7 @@ from .platforms.hp_comware5.driver import HConfigDriverHPComware5 from .platforms.hp_procurve.driver import HConfigDriverHPProcurve from .platforms.hp_procurve.view import HConfigViewHPProcurve +from .platforms.huawei_vrpv8.driver import HConfigDriverHuaweiVrpv8 from .platforms.juniper_junos.driver import HConfigDriverJuniperJUNOS from .platforms.view_base import HConfigViewBase from .platforms.vyos.driver import HConfigDriverVYOS @@ -40,6 +41,7 @@ def get_hconfig_driver(platform: Platform) -> HConfigDriverBase: Platform.GENERIC: HConfigDriverGeneric, Platform.HP_PROCURVE: HConfigDriverHPProcurve, Platform.HP_COMWARE5: HConfigDriverHPComware5, + Platform.HUAWEI_VRPV8: HConfigDriverHuaweiVrpv8, Platform.JUNIPER_JUNOS: HConfigDriverJuniperJUNOS, Platform.VYOS: HConfigDriverVYOS, } diff --git a/hier_config/models.py b/hier_config/models.py index 23d4a939..1b25602a 100644 --- a/hier_config/models.py +++ b/hier_config/models.py @@ -192,6 +192,7 @@ class Platform(str, Enum): GENERIC = auto() # used in cases where the specific platform is unimportant/unknown HP_COMWARE5 = auto() HP_PROCURVE = auto() + HUAWEI_VRPV8 = auto() JUNIPER_JUNOS = auto() VYOS = auto() diff --git a/hier_config/platforms/driver_base.py b/hier_config/platforms/driver_base.py index 1824856e..e7b94e93 100644 --- a/hier_config/platforms/driver_base.py +++ b/hier_config/platforms/driver_base.py @@ -178,6 +178,14 @@ def negate_with(self, config: HConfigChild) -> str | None: return with_rule.use return None + def sectional_exit(self, config: HConfigChild) -> str | None: + for exit_rule in self.rules.sectional_exiting: + if config.is_lineage_match(exit_rule.match_rules): + return exit_rule.exit_text + if config.children: + return "exit" + return None + def swap_negation(self, child: HConfigChild) -> HConfigChild: """Swap negation of a `child.text`.""" if child.text.startswith(self.negation_prefix): diff --git a/hier_config/platforms/huawei_vrpv8/__init__.py b/hier_config/platforms/huawei_vrpv8/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hier_config/platforms/huawei_vrpv8/driver.py b/hier_config/platforms/huawei_vrpv8/driver.py new file mode 100644 index 00000000..d4c5366a --- /dev/null +++ b/hier_config/platforms/huawei_vrpv8/driver.py @@ -0,0 +1,53 @@ +import re +from typing import override + +from hier_config.child import HConfigChild +from hier_config.models import PerLineSubRule +from hier_config.platforms.driver_base import HConfigDriverBase, HConfigDriverRules + + +class HConfigDriverHuaweiVrpv8(HConfigDriverBase): + """Driver for Huawei VRPv8 operating system. + + Platform enum: ``Platform.HUAWEI_VRPV8``. + """ + + @property + def negation_prefix(self) -> str: + return "undo " + + def swap_negation(self, child: HConfigChild) -> HConfigChild: + if child.text.startswith(self.negation_prefix): + child.text = child.text.removeprefix(self.negation_prefix) + return child + + text = child.text + if text.startswith("description "): + text = "description" + elif text.startswith("alias "): + text = "alias" + elif " remark " in text or text.startswith("remark "): + text = re.sub(r"^(.*?remark) .*", r"\1", text) + elif text.startswith("snmp-agent community "): + text = re.sub( + r"^(snmp-agent community (?:read |write )?(?:cipher )?\S+).*", + r"\1", + text, + ) + + child.text = f"{self.negation_prefix}{text}" + return child + + @override + def sectional_exit(self, config: HConfigChild) -> str | None: + if config.children: + return "quit" + return None + + @staticmethod + def _instantiate_rules() -> HConfigDriverRules: + return HConfigDriverRules( + per_line_sub=[ + PerLineSubRule(search="^\\s*[#!].*", replace=""), + ], + ) diff --git a/hier_config/utils.py b/hier_config/utils.py index a8ef0c6f..cce46d10 100644 --- a/hier_config/utils.py +++ b/hier_config/utils.py @@ -35,6 +35,7 @@ "eos": Platform.ARISTA_EOS, "junos": Platform.JUNIPER_JUNOS, "vyos": Platform.VYOS, + "huawei_vrpv8": Platform.HUAWEI_VRPV8, } diff --git a/tests/test_driver_huawei_vrpv8.py b/tests/test_driver_huawei_vrpv8.py new file mode 100644 index 00000000..06d7bf6c --- /dev/null +++ b/tests/test_driver_huawei_vrpv8.py @@ -0,0 +1,105 @@ +from hier_config import get_hconfig_fast_load +from hier_config.constructors import get_hconfig +from hier_config.models import Platform + + +def test_merge_with_undo() -> None: + platform = Platform.HUAWEI_VRPV8 + running_config = get_hconfig_fast_load( + platform, ("test_for_undo", "undo test_for_redo") + ) + generated_config = get_hconfig_fast_load( + platform, ("undo test_for_undo", "test_for_redo") + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ("undo test_for_undo", "test_for_redo") + + +def test_negate_description() -> None: + platform = Platform.HUAWEI_VRPV8 + running_config = get_hconfig_fast_load( + platform, ("interface GigabitEthernet0/0/0", " description some old blabla") + ) + generated_config = get_hconfig_fast_load( + platform, ("interface GigabitEthernet0/0/0",) + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "interface GigabitEthernet0/0/0", + " undo description", + ) + + +def test_negate_remark() -> None: + platform = Platform.HUAWEI_VRPV8 + running_config = get_hconfig_fast_load( + platform, ("acl number 2000", " rule 5 remark some old remark") + ) + generated_config = get_hconfig_fast_load(platform, ("acl number 2000",)) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "acl number 2000", + " undo rule 5 remark", + ) + + +def test_negate_alias() -> None: + platform = Platform.HUAWEI_VRPV8 + running_config = get_hconfig_fast_load( + platform, ("interface GigabitEthernet0/0/0", " alias some old alias") + ) + generated_config = get_hconfig_fast_load( + platform, ("interface GigabitEthernet0/0/0",) + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "interface GigabitEthernet0/0/0", + " undo alias", + ) + + +def test_negate_snmp_agent_community() -> None: + platform = Platform.HUAWEI_VRPV8 + running_config = get_hconfig_fast_load( + platform, ("snmp-agent community read cipher %^%#blabla%^%# acl 2000",) + ) + generated_config = get_hconfig(platform) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "undo snmp-agent community read cipher %^%#blabla%^%#", + ) + + +def test_comments_stripped() -> None: + platform = Platform.HUAWEI_VRPV8 + config = get_hconfig_fast_load( + platform, + ( + "#", + "! this is a comment", + "interface GigabitEthernet0/0/0", + " # another comment", + " description test", + "! yet another comment", + ), + ) + assert config.dump_simple() == ( + "interface GigabitEthernet0/0/0", + " description test", + ) + + +def test_sectional_exit_is_quit() -> None: + platform = Platform.HUAWEI_VRPV8 + config = get_hconfig_fast_load( + platform, + ( + "interface GigabitEthernet0/0/0", + " description test", + ), + ) + assert config.dump_simple(sectional_exiting=True) == ( + "interface GigabitEthernet0/0/0", + " description test", + " quit", + ) From 2366b6e9069674ca63ac918b4e56233529ffb405 Mon Sep 17 00:00:00 2001 From: yanpla <42470642+yanpla@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:12:18 +0100 Subject: [PATCH 2/6] Update hier_config/platforms/huawei_vrpv8/driver.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- hier_config/platforms/huawei_vrpv8/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hier_config/platforms/huawei_vrpv8/driver.py b/hier_config/platforms/huawei_vrpv8/driver.py index d4c5366a..649abce4 100644 --- a/hier_config/platforms/huawei_vrpv8/driver.py +++ b/hier_config/platforms/huawei_vrpv8/driver.py @@ -1,5 +1,5 @@ import re -from typing import override +from typing_extensions import override from hier_config.child import HConfigChild from hier_config.models import PerLineSubRule From d1ac90661d7575adf8cad4bf44a4bdff9b9b90ab Mon Sep 17 00:00:00 2001 From: yanpla Date: Wed, 25 Mar 2026 15:12:56 +0100 Subject: [PATCH 3/6] restore walrus --- hier_config/platforms/driver_base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hier_config/platforms/driver_base.py b/hier_config/platforms/driver_base.py index e7b94e93..6bab8262 100644 --- a/hier_config/platforms/driver_base.py +++ b/hier_config/platforms/driver_base.py @@ -181,7 +181,9 @@ def negate_with(self, config: HConfigChild) -> str | None: def sectional_exit(self, config: HConfigChild) -> str | None: for exit_rule in self.rules.sectional_exiting: if config.is_lineage_match(exit_rule.match_rules): - return exit_rule.exit_text + if exit_text := exit_rule.exit_text: + return exit_text + return None if config.children: return "exit" return None From 59a74a49ef4cc1444b7f492ec4bae488c83820b0 Mon Sep 17 00:00:00 2001 From: yanpla Date: Wed, 25 Mar 2026 15:17:12 +0100 Subject: [PATCH 4/6] rename to huawei_vrp --- hier_config/constructors.py | 4 ++-- hier_config/models.py | 2 +- .../{huawei_vrpv8 => huawei_vrp}/__init__.py | 0 .../{huawei_vrpv8 => huawei_vrp}/driver.py | 7 ++++--- hier_config/utils.py | 2 +- ...r_huawei_vrpv8.py => test_driver_huawei_vrp.py} | 14 +++++++------- 6 files changed, 15 insertions(+), 14 deletions(-) rename hier_config/platforms/{huawei_vrpv8 => huawei_vrp}/__init__.py (100%) rename hier_config/platforms/{huawei_vrpv8 => huawei_vrp}/driver.py (91%) rename tests/{test_driver_huawei_vrpv8.py => test_driver_huawei_vrp.py} (92%) diff --git a/hier_config/constructors.py b/hier_config/constructors.py index 5328abe9..5c178fc0 100644 --- a/hier_config/constructors.py +++ b/hier_config/constructors.py @@ -21,7 +21,7 @@ from .platforms.hp_comware5.driver import HConfigDriverHPComware5 from .platforms.hp_procurve.driver import HConfigDriverHPProcurve from .platforms.hp_procurve.view import HConfigViewHPProcurve -from .platforms.huawei_vrpv8.driver import HConfigDriverHuaweiVrpv8 +from .platforms.huawei_vrp.driver import HConfigDriverHuaweiVrp from .platforms.juniper_junos.driver import HConfigDriverJuniperJUNOS from .platforms.view_base import HConfigViewBase from .platforms.vyos.driver import HConfigDriverVYOS @@ -41,7 +41,7 @@ def get_hconfig_driver(platform: Platform) -> HConfigDriverBase: Platform.GENERIC: HConfigDriverGeneric, Platform.HP_PROCURVE: HConfigDriverHPProcurve, Platform.HP_COMWARE5: HConfigDriverHPComware5, - Platform.HUAWEI_VRPV8: HConfigDriverHuaweiVrpv8, + Platform.HUAWEI_VRP: HConfigDriverHuaweiVrp, Platform.JUNIPER_JUNOS: HConfigDriverJuniperJUNOS, Platform.VYOS: HConfigDriverVYOS, } diff --git a/hier_config/models.py b/hier_config/models.py index 1b25602a..c69f08a7 100644 --- a/hier_config/models.py +++ b/hier_config/models.py @@ -192,7 +192,7 @@ class Platform(str, Enum): GENERIC = auto() # used in cases where the specific platform is unimportant/unknown HP_COMWARE5 = auto() HP_PROCURVE = auto() - HUAWEI_VRPV8 = auto() + HUAWEI_VRP = auto() JUNIPER_JUNOS = auto() VYOS = auto() diff --git a/hier_config/platforms/huawei_vrpv8/__init__.py b/hier_config/platforms/huawei_vrp/__init__.py similarity index 100% rename from hier_config/platforms/huawei_vrpv8/__init__.py rename to hier_config/platforms/huawei_vrp/__init__.py diff --git a/hier_config/platforms/huawei_vrpv8/driver.py b/hier_config/platforms/huawei_vrp/driver.py similarity index 91% rename from hier_config/platforms/huawei_vrpv8/driver.py rename to hier_config/platforms/huawei_vrp/driver.py index 649abce4..f21d50da 100644 --- a/hier_config/platforms/huawei_vrpv8/driver.py +++ b/hier_config/platforms/huawei_vrp/driver.py @@ -1,4 +1,5 @@ import re + from typing_extensions import override from hier_config.child import HConfigChild @@ -6,10 +7,10 @@ from hier_config.platforms.driver_base import HConfigDriverBase, HConfigDriverRules -class HConfigDriverHuaweiVrpv8(HConfigDriverBase): - """Driver for Huawei VRPv8 operating system. +class HConfigDriverHuaweiVrp(HConfigDriverBase): + """Driver for Huawei VRP operating system. - Platform enum: ``Platform.HUAWEI_VRPV8``. + Platform enum: ``Platform.HUAWEI_VRP``. """ @property diff --git a/hier_config/utils.py b/hier_config/utils.py index cce46d10..e87bedff 100644 --- a/hier_config/utils.py +++ b/hier_config/utils.py @@ -35,7 +35,7 @@ "eos": Platform.ARISTA_EOS, "junos": Platform.JUNIPER_JUNOS, "vyos": Platform.VYOS, - "huawei_vrpv8": Platform.HUAWEI_VRPV8, + "huawei_vrp": Platform.HUAWEI_VRP, } diff --git a/tests/test_driver_huawei_vrpv8.py b/tests/test_driver_huawei_vrp.py similarity index 92% rename from tests/test_driver_huawei_vrpv8.py rename to tests/test_driver_huawei_vrp.py index 06d7bf6c..7f74541c 100644 --- a/tests/test_driver_huawei_vrpv8.py +++ b/tests/test_driver_huawei_vrp.py @@ -4,7 +4,7 @@ def test_merge_with_undo() -> None: - platform = Platform.HUAWEI_VRPV8 + platform = Platform.HUAWEI_VRP running_config = get_hconfig_fast_load( platform, ("test_for_undo", "undo test_for_redo") ) @@ -16,7 +16,7 @@ def test_merge_with_undo() -> None: def test_negate_description() -> None: - platform = Platform.HUAWEI_VRPV8 + platform = Platform.HUAWEI_VRP running_config = get_hconfig_fast_load( platform, ("interface GigabitEthernet0/0/0", " description some old blabla") ) @@ -31,7 +31,7 @@ def test_negate_description() -> None: def test_negate_remark() -> None: - platform = Platform.HUAWEI_VRPV8 + platform = Platform.HUAWEI_VRP running_config = get_hconfig_fast_load( platform, ("acl number 2000", " rule 5 remark some old remark") ) @@ -44,7 +44,7 @@ def test_negate_remark() -> None: def test_negate_alias() -> None: - platform = Platform.HUAWEI_VRPV8 + platform = Platform.HUAWEI_VRP running_config = get_hconfig_fast_load( platform, ("interface GigabitEthernet0/0/0", " alias some old alias") ) @@ -59,7 +59,7 @@ def test_negate_alias() -> None: def test_negate_snmp_agent_community() -> None: - platform = Platform.HUAWEI_VRPV8 + platform = Platform.HUAWEI_VRP running_config = get_hconfig_fast_load( platform, ("snmp-agent community read cipher %^%#blabla%^%# acl 2000",) ) @@ -71,7 +71,7 @@ def test_negate_snmp_agent_community() -> None: def test_comments_stripped() -> None: - platform = Platform.HUAWEI_VRPV8 + platform = Platform.HUAWEI_VRP config = get_hconfig_fast_load( platform, ( @@ -90,7 +90,7 @@ def test_comments_stripped() -> None: def test_sectional_exit_is_quit() -> None: - platform = Platform.HUAWEI_VRPV8 + platform = Platform.HUAWEI_VRP config = get_hconfig_fast_load( platform, ( From 7ea9d79dbf254cba01248eac3fc121044ec86479 Mon Sep 17 00:00:00 2001 From: yanpla Date: Wed, 25 Mar 2026 15:27:21 +0100 Subject: [PATCH 5/6] remove override --- hier_config/platforms/huawei_vrp/driver.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/hier_config/platforms/huawei_vrp/driver.py b/hier_config/platforms/huawei_vrp/driver.py index f21d50da..d61151c3 100644 --- a/hier_config/platforms/huawei_vrp/driver.py +++ b/hier_config/platforms/huawei_vrp/driver.py @@ -1,7 +1,5 @@ import re -from typing_extensions import override - from hier_config.child import HConfigChild from hier_config.models import PerLineSubRule from hier_config.platforms.driver_base import HConfigDriverBase, HConfigDriverRules @@ -39,11 +37,11 @@ def swap_negation(self, child: HConfigChild) -> HConfigChild: child.text = f"{self.negation_prefix}{text}" return child - @override def sectional_exit(self, config: HConfigChild) -> str | None: - if config.children: + result = super().sectional_exit(config) + if result == "exit": return "quit" - return None + return result @staticmethod def _instantiate_rules() -> HConfigDriverRules: From 9f92355b5f32a095ad2a145e67cb09395a93f087 Mon Sep 17 00:00:00 2001 From: yanpla Date: Wed, 25 Mar 2026 15:29:59 +0100 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afb32f76..901d62ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Performance benchmarks for parsing, remediation, and iteration (#202). Skipped by default; run with `poetry run pytest -m benchmark -v -s`. +- Added support for Huawei VRP with a new driver and test suite (#238). + ### Fixed - `DuplicateChildError` raised when parsing IOS-XR configs with indented `!` section