diff --git a/CHANGELOG.md b/CHANGELOG.md index afb32f7..901d62a 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 diff --git a/hier_config/child.py b/hier_config/child.py index 2945da8..33080fd 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 3755a53..5c178fc 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_vrp.driver import HConfigDriverHuaweiVrp 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_VRP: HConfigDriverHuaweiVrp, Platform.JUNIPER_JUNOS: HConfigDriverJuniperJUNOS, Platform.VYOS: HConfigDriverVYOS, } diff --git a/hier_config/models.py b/hier_config/models.py index 23d4a93..c69f08a 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_VRP = auto() JUNIPER_JUNOS = auto() VYOS = auto() diff --git a/hier_config/platforms/driver_base.py b/hier_config/platforms/driver_base.py index 1824856..6bab826 100644 --- a/hier_config/platforms/driver_base.py +++ b/hier_config/platforms/driver_base.py @@ -178,6 +178,16 @@ 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): + if exit_text := exit_rule.exit_text: + return exit_text + return None + 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_vrp/__init__.py b/hier_config/platforms/huawei_vrp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hier_config/platforms/huawei_vrp/driver.py b/hier_config/platforms/huawei_vrp/driver.py new file mode 100644 index 0000000..d61151c --- /dev/null +++ b/hier_config/platforms/huawei_vrp/driver.py @@ -0,0 +1,52 @@ +import re + +from hier_config.child import HConfigChild +from hier_config.models import PerLineSubRule +from hier_config.platforms.driver_base import HConfigDriverBase, HConfigDriverRules + + +class HConfigDriverHuaweiVrp(HConfigDriverBase): + """Driver for Huawei VRP operating system. + + Platform enum: ``Platform.HUAWEI_VRP``. + """ + + @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 + + def sectional_exit(self, config: HConfigChild) -> str | None: + result = super().sectional_exit(config) + if result == "exit": + return "quit" + return result + + @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 a8ef0c6..e87bedf 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_vrp": Platform.HUAWEI_VRP, } diff --git a/tests/test_driver_huawei_vrp.py b/tests/test_driver_huawei_vrp.py new file mode 100644 index 0000000..7f74541 --- /dev/null +++ b/tests/test_driver_huawei_vrp.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_VRP + 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_VRP + 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_VRP + 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_VRP + 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_VRP + 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_VRP + 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_VRP + 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", + )