diff --git a/CHANGELOG.md b/CHANGELOG.md index b89e154..0b0582a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Nokia SRL platform driver (`Platform.NOKIA_SRL`) (#245) + --- ## [3.6.0] - 2026-03-26 diff --git a/README.md b/README.md index 5840103..3c4bf7d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Hierarchical Configuration has been used extensively on: In addition to the Cisco-style syntax, hier_config offers experimental support for Juniper-style configurations using set and delete commands. This allows users to remediate Junos configurations in native syntax. However, please note that Juniper syntax support is still in an experimental phase and has not been tested extensively. Use with caution in production environments. - [x] Juniper JunOS +- [x] Nokia SRL (Service Router Linux) - [x] VyOS Hier Config is compatible with any NOS that utilizes a structured CLI syntax similar to Cisco IOS or Junos OS. diff --git a/docs/architecture.md b/docs/architecture.md index bc46d3c..f495c70 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -111,6 +111,7 @@ A frozen Pydantic model holding lists of typed rule objects: | `HP_COMWARE5` | `HConfigDriverHPComware5` | `platforms/hp_comware5/driver.py` | | `HP_PROCURVE` | `HConfigDriverHPProcurve` | `platforms/hp_procurve/driver.py` | | `JUNIPER_JUNOS` | `HConfigDriverJuniperJUNOS` | `platforms/juniper_junos/driver.py` | +| `NOKIA_SRL` | `HConfigDriverNokiaSRL` | `platforms/nokia_srl/driver.py` | | `VYOS` | `HConfigDriverVYOS` | `platforms/vyos/driver.py` | See [Drivers](drivers.md) for full documentation on customising or creating drivers. diff --git a/docs/drivers.md b/docs/drivers.md index 1f2b42f..45d9f72 100644 --- a/docs/drivers.md +++ b/docs/drivers.md @@ -40,6 +40,7 @@ The following drivers are included in Hier Config: - **HP_PROCURVE** - **HUAWEI_VRP** - **JUNIPER_JUNOS** +- **NOKIA_SRL** - **VYOS** To activate a driver, use the `get_hconfig_driver` utility provided by Hier Config: @@ -144,6 +145,38 @@ driver = get_hconfig_driver(Platform.VYOS) --- +### Nokia SRL (Service Router Linux) Driver + +Nokia SR Linux uses `set` and `delete` command syntax, similar to VyOS and JunOS. The driver converts hierarchical SRL configuration (from `info` output) into flat `set`/`delete` commands via a preprocessor. + +> **Experimental:** Nokia SRL support has not been tested extensively in production environments. Use with caution. + +- **[Declaration prefix](glossary.md#declaration-prefix)**: `set ` (prepended to each positive command). +- **[Negation prefix](glossary.md#negation-prefix)**: `delete ` (replaces `no `). + +Platform enum: `Platform.NOKIA_SRL` + +```python +from hier_config import Platform, get_hconfig_driver + +driver = get_hconfig_driver(Platform.NOKIA_SRL) +``` + +**Remediation example:** + +```python +from hier_config import WorkflowRemediation, get_hconfig, Platform + +running = get_hconfig(Platform.NOKIA_SRL, running_text) +intended = get_hconfig(Platform.NOKIA_SRL, intended_text) +workflow = WorkflowRemediation(running, intended) + +for line in workflow.remediation_config.all_children_sorted(): + print(line.cisco_style_text()) +``` + +--- + ### Generic Driver The `GENERIC` driver contains no platform-specific rules. It is useful as a starting point for custom drivers or for platforms that follow standard Cisco-style syntax with few special cases. diff --git a/docs/glossary.md b/docs/glossary.md index 80e3822..7d741e6 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -74,7 +74,7 @@ The string prepended to a command to negate (remove) it. `HConfigDriverBase.neg |----------|----------------| | Cisco IOS / EOS / NX-OS | `"no "` | | HP Comware5 / H3C | `"undo "` | -| JunOS / VyOS | `"delete "` | +| JunOS / VyOS / Nokia SRL | `"delete "` | --- diff --git a/docs/index.md b/docs/index.md index 9c5ab99..eb0638f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,6 +29,7 @@ | HP ProCurve (Aruba AOSS) | `Platform.HP_PROCURVE` | Fully supported | | HP Comware5 / H3C | `Platform.HP_COMWARE5` | Fully supported | | Juniper JunOS | `Platform.JUNIPER_JUNOS` | Experimental | +| Nokia SRL | `Platform.NOKIA_SRL` | Experimental | | VyOS | `Platform.VYOS` | Experimental | | Generic | `Platform.GENERIC` | Base for custom drivers | diff --git a/hier_config/constructors.py b/hier_config/constructors.py index 5c178fc..2e4704c 100644 --- a/hier_config/constructors.py +++ b/hier_config/constructors.py @@ -23,6 +23,7 @@ from .platforms.hp_procurve.view import HConfigViewHPProcurve from .platforms.huawei_vrp.driver import HConfigDriverHuaweiVrp from .platforms.juniper_junos.driver import HConfigDriverJuniperJUNOS +from .platforms.nokia_srl.driver import HConfigDriverNokiaSRL from .platforms.view_base import HConfigViewBase from .platforms.vyos.driver import HConfigDriverVYOS from .root import HConfig @@ -43,6 +44,7 @@ def get_hconfig_driver(platform: Platform) -> HConfigDriverBase: Platform.HP_COMWARE5: HConfigDriverHPComware5, Platform.HUAWEI_VRP: HConfigDriverHuaweiVrp, Platform.JUNIPER_JUNOS: HConfigDriverJuniperJUNOS, + Platform.NOKIA_SRL: HConfigDriverNokiaSRL, Platform.VYOS: HConfigDriverVYOS, } driver_cls = platform_drivers.get(platform) diff --git a/hier_config/models.py b/hier_config/models.py index cb5da59..a8a87be 100644 --- a/hier_config/models.py +++ b/hier_config/models.py @@ -197,6 +197,7 @@ class Platform(str, Enum): HP_PROCURVE = auto() HUAWEI_VRP = auto() JUNIPER_JUNOS = auto() + NOKIA_SRL = auto() VYOS = auto() diff --git a/hier_config/platforms/nokia_srl/__init__.py b/hier_config/platforms/nokia_srl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hier_config/platforms/nokia_srl/driver.py b/hier_config/platforms/nokia_srl/driver.py new file mode 100644 index 0000000..2938080 --- /dev/null +++ b/hier_config/platforms/nokia_srl/driver.py @@ -0,0 +1,38 @@ +from hier_config.child import HConfigChild +from hier_config.platforms.driver_base import HConfigDriverBase, HConfigDriverRules +from hier_config.platforms.functions import convert_to_set_commands + + +class HConfigDriverNokiaSRL(HConfigDriverBase): # pylint: disable=too-many-instance-attributes + """Driver for Nokia SR Linux. + + Converts hierarchical SRL configuration into flat ``set``/``delete`` + command syntax via a preprocessor. Overrides ``declaration_prefix`` to + ``"set "`` and ``negation_prefix`` to ``"delete "``. + Platform enum: ``Platform.NOKIA_SRL``. + """ + + def swap_negation(self, child: HConfigChild) -> HConfigChild: + """Swap negation of a `self.text`.""" + if child.text.startswith(self.negation_prefix): + child.text = f"{self.declaration_prefix}{child.text_without_negation}" + elif child.text.startswith(self.declaration_prefix): + child.text = f"{self.negation_prefix}{child.text.removeprefix(self.declaration_prefix)}" + + return child + + @property + def declaration_prefix(self) -> str: + return "set " + + @property + def negation_prefix(self) -> str: + return "delete " + + @staticmethod + def config_preprocessor(config_text: str) -> str: + return convert_to_set_commands(config_text) + + @staticmethod + def _instantiate_rules() -> HConfigDriverRules: + return HConfigDriverRules() diff --git a/hier_config/utils.py b/hier_config/utils.py index e87bedf..ce6b740 100644 --- a/hier_config/utils.py +++ b/hier_config/utils.py @@ -36,6 +36,7 @@ "junos": Platform.JUNIPER_JUNOS, "vyos": Platform.VYOS, "huawei_vrp": Platform.HUAWEI_VRP, + "nokia_srl": Platform.NOKIA_SRL, } diff --git a/tests/test_driver_nokia_srl.py b/tests/test_driver_nokia_srl.py new file mode 100644 index 0000000..01358a9 --- /dev/null +++ b/tests/test_driver_nokia_srl.py @@ -0,0 +1,286 @@ +from hier_config import WorkflowRemediation, get_hconfig, get_hconfig_fast_load +from hier_config.child import HConfigChild +from hier_config.models import Platform +from hier_config.platforms.nokia_srl.driver import HConfigDriverNokiaSRL + + +def test_nokia_srl_basic_remediation() -> None: + """Test basic Nokia SRL set/delete commands.""" + platform = Platform.NOKIA_SRL + running_config_str = "set interface ethernet-1/1 subinterface 0 ipv4 admin-state enable address 192.168.1.1/24" + generated_config_str = "set interface ethernet-1/1 subinterface 0 ipv4 admin-state enable address 192.168.2.1/24" + remediation_str = "delete interface ethernet-1/1 subinterface 0 ipv4 admin-state enable address 192.168.1.1/24\nset interface ethernet-1/1 subinterface 0 ipv4 admin-state enable address 192.168.2.1/24" + + workflow_remediation = WorkflowRemediation( + get_hconfig_fast_load(platform, running_config_str), + get_hconfig_fast_load(platform, generated_config_str), + ) + + assert workflow_remediation.remediation_config_filtered_text() == remediation_str + + +def test_swap_negation_delete_to_set() -> None: + """Test swapping from 'delete' to 'set' prefix.""" + platform = Platform.NOKIA_SRL + driver = HConfigDriverNokiaSRL() + root = get_hconfig(platform) + + child = HConfigChild( + root, "delete interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24" + ) + result = driver.swap_negation(child) + + assert ( + result.text + == "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24" + ) + assert result.text.startswith("set ") + + +def test_swap_negation_set_to_delete() -> None: + """Test swapping from 'set' to 'delete' prefix.""" + platform = Platform.NOKIA_SRL + driver = HConfigDriverNokiaSRL() + root = get_hconfig(platform) + + child = HConfigChild( + root, "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24" + ) + result = driver.swap_negation(child) + + assert ( + result.text + == "delete interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24" + ) + assert result.text.startswith("delete ") + + +def test_swap_negation_no_prefix() -> None: + """Test swap_negation when text has neither prefix.""" + driver = HConfigDriverNokiaSRL() + root = get_hconfig(Platform.NOKIA_SRL) + + child = HConfigChild( + root, "interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24" + ) + original_text = child.text + + result = driver.swap_negation(child) + assert result.text == original_text + + +def test_declaration_prefix() -> None: + """Test declaration_prefix property.""" + driver = HConfigDriverNokiaSRL() + assert driver.declaration_prefix == "set " + + +def test_negation_prefix() -> None: + """Test negation_prefix property.""" + driver = HConfigDriverNokiaSRL() + assert driver.negation_prefix == "delete " + + +def test_config_preprocessor() -> None: + """Test config_preprocessor with hierarchical SRL config.""" + hierarchical_config = """interface { + ethernet-1/1 { + subinterface 0 { + ipv4 { + admin-state enable + address 192.168.1.1/24 + } + } + } +} +system { + name { + host-name srl-router + } +}""" + + result = HConfigDriverNokiaSRL.config_preprocessor(hierarchical_config) + + assert "set interface ethernet-1/1 subinterface 0 ipv4 admin-state enable" in result + assert ( + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24" + in result + ) + assert "set system name host-name srl-router" in result + + +def test_interface_address_addition() -> None: + """Test adding an interface address.""" + platform = Platform.NOKIA_SRL + running_config = get_hconfig_fast_load( + platform, + ("set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24",), + ) + generated_config = get_hconfig_fast_load( + platform, + ( + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24", + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.2/24", + ), + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.2/24", + ) + + +def test_interface_description_modification() -> None: + """Test modifying interface description.""" + platform = Platform.NOKIA_SRL + running_config = get_hconfig_fast_load( + platform, + ( + "set interface ethernet-1/1 description Old Description", + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24", + ), + ) + generated_config = get_hconfig_fast_load( + platform, + ( + "set interface ethernet-1/1 description New Description", + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24", + ), + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "delete interface ethernet-1/1 description Old Description", + "set interface ethernet-1/1 description New Description", + ) + + +def test_interface_removal() -> None: + """Test removing an interface configuration.""" + platform = Platform.NOKIA_SRL + running_config = get_hconfig_fast_load( + platform, + ( + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24", + "set interface ethernet-1/2 subinterface 0 ipv4 address 10.0.0.1/24", + ), + ) + generated_config = get_hconfig_fast_load( + platform, + ("set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24",), + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "delete interface ethernet-1/2 subinterface 0 ipv4 address 10.0.0.1/24", + ) + + +def test_network_instance_remediation() -> None: + """Test network-instance (VRF) block handling.""" + platform = Platform.NOKIA_SRL + running_config = get_hconfig_fast_load( + platform, + ( + "set network-instance default router-id 10.0.0.1", + "set network-instance default interface ethernet-1/1.0", + ), + ) + generated_config = get_hconfig_fast_load( + platform, + ( + "set network-instance default router-id 10.0.0.2", + "set network-instance default interface ethernet-1/1.0", + "set network-instance mgmt interface mgmt0.0", + ), + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "delete network-instance default router-id 10.0.0.1", + "set network-instance default router-id 10.0.0.2", + "set network-instance mgmt interface mgmt0.0", + ) + + +def test_system_configuration() -> None: + """Test system configuration changes.""" + platform = Platform.NOKIA_SRL + running_config = get_hconfig_fast_load( + platform, + ( + "set system name host-name old-srl-router", + "set system dns network-instance mgmt", + ), + ) + generated_config = get_hconfig_fast_load( + platform, + ( + "set system name host-name new-srl-router", + "set system dns network-instance mgmt", + "set system ntp network-instance mgmt", + ), + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "delete system name host-name old-srl-router", + "set system name host-name new-srl-router", + "set system ntp network-instance mgmt", + ) + + +def test_empty_to_basic_config() -> None: + """Test building configuration from empty state.""" + platform = Platform.NOKIA_SRL + running_config = get_hconfig(platform) + generated_config = get_hconfig_fast_load( + platform, + ( + "set system name host-name srl-router", + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24", + ), + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "set system name host-name srl-router", + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24", + ) + future_config = running_config.future(remediation_config) + assert future_config.dump_simple() == ( + "set system name host-name srl-router", + "set interface ethernet-1/1 subinterface 0 ipv4 address 192.168.1.1/24", + ) + + +def test_routing_policy_configuration() -> None: + """Test routing-policy configuration changes.""" + platform = Platform.NOKIA_SRL + running_config = get_hconfig_fast_load( + platform, + ("set routing-policy policy accept-all default-action policy-result accept",), + ) + generated_config = get_hconfig_fast_load( + platform, + ( + "set routing-policy policy accept-all default-action policy-result accept", + "set routing-policy policy deny-all default-action policy-result reject", + ), + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "set routing-policy policy deny-all default-action policy-result reject", + ) + + +def test_ipv6_address_configuration() -> None: + """Test configuring IPv6 addresses on interfaces.""" + platform = Platform.NOKIA_SRL + running_config = get_hconfig_fast_load( + platform, + ("set interface ethernet-1/1 subinterface 0 ipv6 address 2001:db8:1::1/64",), + ) + generated_config = get_hconfig_fast_load( + platform, + ("set interface ethernet-1/1 subinterface 0 ipv6 address 2001:db8:2::1/64",), + ) + remediation_config = running_config.config_to_get_to(generated_config) + assert remediation_config.dump_simple() == ( + "delete interface ethernet-1/1 subinterface 0 ipv6 address 2001:db8:1::1/64", + "set interface ethernet-1/1 subinterface 0 ipv6 address 2001:db8:2::1/64", + ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 63ee3bf..7d779ae 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -96,6 +96,7 @@ def test_hconfig_v2_os_v3_platform_mapper() -> None: assert hconfig_v2_os_v3_platform_mapper("ios") == Platform.CISCO_IOS assert hconfig_v2_os_v3_platform_mapper("nxos") == Platform.CISCO_NXOS assert hconfig_v2_os_v3_platform_mapper("junos") == Platform.JUNIPER_JUNOS + assert hconfig_v2_os_v3_platform_mapper("nokia_srl") == Platform.NOKIA_SRL assert hconfig_v2_os_v3_platform_mapper("invalid") == Platform.GENERIC @@ -104,6 +105,7 @@ def test_hconfig_v3_platform_v2_os_mapper() -> None: assert hconfig_v3_platform_v2_os_mapper(Platform.CISCO_IOS) == "ios" assert hconfig_v3_platform_v2_os_mapper(Platform.CISCO_NXOS) == "nxos" assert hconfig_v3_platform_v2_os_mapper(Platform.JUNIPER_JUNOS) == "junos" + assert hconfig_v3_platform_v2_os_mapper(Platform.NOKIA_SRL) == "nokia_srl" assert hconfig_v3_platform_v2_os_mapper(Platform.GENERIC) == "generic"