From f8f44506451676900d4ef4626b1167cfc422d9df Mon Sep 17 00:00:00 2001 From: jtdub Date: Thu, 9 Apr 2026 22:27:47 -0500 Subject: [PATCH 1/3] Add Nokia SRL (Service Router Linux) platform driver (#245) Add a new platform driver for Nokia SR Linux devices that uses set/delete flat command syntax with a hierarchical config preprocessor, following the same pattern as the VyOS driver. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 4 + hier_config/constructors.py | 2 + hier_config/models.py | 1 + hier_config/platforms/nokia_srl/__init__.py | 0 hier_config/platforms/nokia_srl/driver.py | 38 +++ tests/test_driver_nokia_srl.py | 286 ++++++++++++++++++++ 6 files changed, 331 insertions(+) create mode 100644 hier_config/platforms/nokia_srl/__init__.py create mode 100644 hier_config/platforms/nokia_srl/driver.py create mode 100644 tests/test_driver_nokia_srl.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b89e1543..0b0582a5 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/hier_config/constructors.py b/hier_config/constructors.py index 5c178fc0..2e4704cf 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 cb5da59c..a8a87be3 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 00000000..e69de29b diff --git a/hier_config/platforms/nokia_srl/driver.py b/hier_config/platforms/nokia_srl/driver.py new file mode 100644 index 00000000..29380804 --- /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/tests/test_driver_nokia_srl.py b/tests/test_driver_nokia_srl.py new file mode 100644 index 00000000..01358a95 --- /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", + ) From 285cc769c28410a6b4e683902c89691582f7c469 Mon Sep 17 00:00:00 2001 From: jtdub Date: Thu, 9 Apr 2026 22:29:44 -0500 Subject: [PATCH 2/3] Add Nokia SRL documentation to README and docs Update supported platforms lists, driver documentation, architecture tables, and glossary to include the new Nokia SRL platform driver. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 1 + docs/architecture.md | 1 + docs/drivers.md | 33 +++++++++++++++++++++++++++++++++ docs/glossary.md | 2 +- docs/index.md | 1 + 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5840103c..3c4bf7df 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 bc46d3c4..f495c700 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 1f2b42f1..45d9f729 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 80e3822a..7d741e6b 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 9c5ab990..eb0638fe 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 | From dd6d961b889754af173027b9c7a719feb3648c6a Mon Sep 17 00:00:00 2001 From: jtdub Date: Fri, 10 Apr 2026 08:32:27 -0500 Subject: [PATCH 3/3] Add nokia_srl to HCONFIG_PLATFORM_V2_TO_V3_MAPPING Co-Authored-By: Claude Opus 4.6 (1M context) --- hier_config/utils.py | 1 + tests/test_utils.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/hier_config/utils.py b/hier_config/utils.py index e87bedff..ce6b7400 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_utils.py b/tests/test_utils.py index 63ee3bfe..7d779ae1 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"