Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Old readthedocs.io URLs (both the original flat layout and the 3.7 `user/`
layout) keep working via the mkdocs-redirects plugin; CLAUDE.md was slimmed
to an overlay that imports `AGENTS.md` (#290).
- Built-in driver post-load callbacks are now public functions exported from
their driver modules (e.g. `remove_ipv4_acl_remarks` in
`hier_config.platforms.cisco_ios.driver`), so a built-in callback can be
removed by identity with `rules.post_load_callbacks.remove(...)` (#286).

### Fixed

Expand Down
12 changes: 3 additions & 9 deletions docs/admin/customizing-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,13 @@ driver.rules.negation.append(

Post-load callbacks are Python functions that a driver runs against the tree after parsing (`driver.rules.post_load_callbacks`). Sometimes you want to *remove* one of a built-in driver's callbacks — for example, Cisco IOS strips IPv4 ACL `remark` lines by default, and you may want to keep them so remarks participate in remediation.

Because `HConfigDriverRules` is frozen, filter the callback list *in place* (slice assignment) rather than reassigning the attribute:
Built-in callbacks are public functions exported from their driver modules, so a callback can be removed by identity. Because `HConfigDriverRules` is frozen, mutate the callback list *in place* with `list.remove()` — which raises `ValueError` if the callback was already removed — rather than reassigning the attribute:

```python
from hier_config import Platform, register_driver
from hier_config.platforms.cisco_ios.driver import (
HConfigDriverCiscoIOS,
_remove_ipv4_acl_remarks,
remove_ipv4_acl_remarks,
)


Expand All @@ -274,13 +274,7 @@ class HConfigDriverCiscoIOSKeepRemarks(HConfigDriverCiscoIOS):
@staticmethod
def _instantiate_rules():
rules = HConfigDriverCiscoIOS._instantiate_rules()
# Frozen model: attribute reassignment fails, but the list is
# mutable — filter it in place with slice assignment.
rules.post_load_callbacks[:] = [
callback
for callback in rules.post_load_callbacks
if callback is not _remove_ipv4_acl_remarks
]
rules.post_load_callbacks.remove(remove_ipv4_acl_remarks)
return rules


Expand Down
6 changes: 3 additions & 3 deletions docs/dev/creating-drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,17 @@ The preprocessor runs inside `HConfig.from_text()` after full-text substitutions

## Step 4: Add imperative callbacks (if needed)

For transformations that declarative rules cannot express, add plain functions to the rules model:
For transformations that declarative rules cannot express, add plain functions to the rules model. Give them public (non-underscore) names — built-in callbacks are public API so users can remove them from the list by identity:

```python
def _split_collapsed_vlans(config: HConfig) -> None:
def split_collapsed_vlans(config: HConfig) -> None:
"""Example post-load normalization."""
...

# inside _instantiate_rules():
return HConfigDriverRules(
...,
post_load_callbacks=[_split_collapsed_vlans],
post_load_callbacks=[split_collapsed_vlans],
remediation_transform_callbacks=[],
)
```
Expand Down
2 changes: 2 additions & 0 deletions docs/dev/rule-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ NegationRule(
- `post_load_callbacks` — run against the tree immediately after parsing (e.g. IOS VLAN-list splitting, ProCurve VLAN membership normalization).
- `remediation_transform_callbacks` — run against each computed remediation, before user plugins (see [Remediation Workflows](../user/remediation-workflows.md#the-remediation-transform-pipeline)).

Built-in driver callbacks are public functions exported from their driver modules (e.g. `hier_config.platforms.cisco_ios.driver.remove_ipv4_acl_remarks`), so they can be removed from the list by identity — see [Customizing Driver Rules](../admin/customizing-rules.md#customizing-post-load-callbacks).

---

## Rendering
Expand Down
2 changes: 2 additions & 0 deletions docs/user/migrating-from-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ Not required for migration, but these are the headline additions:
root.
- `HConfigDriverBase` and `HConfigDriverRules` are public API for
[custom drivers](../dev/creating-drivers.md).
- Built-in post-load callbacks are public functions removable by identity —
see [Customizing Driver Rules](../admin/customizing-rules.md#customizing-post-load-callbacks).

## Next steps

Expand Down
4 changes: 2 additions & 2 deletions hier_config/platforms/aruba_aoscx/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from hier_config.root import HConfig


def _split_interface_vlan_trunk_allowed(config: HConfig) -> None:
def split_interface_vlan_trunk_allowed(config: HConfig) -> None:
"""Split AOS-CX additive trunk VLAN lists into one VLAN per line.

``vlan trunk allowed`` is additive on AOS-CX rather than declarative, so
Expand Down Expand Up @@ -182,6 +182,6 @@ def _instantiate_rules() -> HConfigDriverRules:
],
post_load_callbacks=[
split_vlan_id_lists,
_split_interface_vlan_trunk_allowed,
split_interface_vlan_trunk_allowed,
],
)
13 changes: 7 additions & 6 deletions hier_config/platforms/cisco_ios/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,23 @@
logger = getLogger(__name__)


def _rm_ipv6_acl_sequence_numbers(config: HConfig) -> None:
def remove_ipv6_acl_sequence_numbers(config: HConfig) -> None:
"""If there are sequence numbers in the IPv6 ACL, remove them."""
for acl in config.get_children(startswith="ipv6 access-list "):
for entry in acl.children:
if entry.text.startswith("sequence"):
entry.text = " ".join(entry.text.split()[2:])


def _remove_ipv4_acl_remarks(config: HConfig) -> None:
def remove_ipv4_acl_remarks(config: HConfig) -> None:
"""Remove remark lines from IPv4 ACLs so they do not participate in diffs."""
for acl in config.get_children(startswith="ip access-list "):
for entry in tuple(acl.children):
if entry.text.startswith("remark"):
entry.delete()


def _add_acl_sequence_numbers(config: HConfig) -> None:
def add_acl_sequence_numbers(config: HConfig) -> None:
"""Add ACL sequence numbers."""
ipv4_acl_sw = "ip access-list"
acl_line_sw: tuple[str, ...] = ("permit", "deny")
Expand Down Expand Up @@ -200,9 +201,9 @@ def _instantiate_rules() -> HConfigDriverRules:
),
],
post_load_callbacks=[
_rm_ipv6_acl_sequence_numbers,
_remove_ipv4_acl_remarks,
_add_acl_sequence_numbers,
remove_ipv6_acl_sequence_numbers,
remove_ipv4_acl_remarks,
add_acl_sequence_numbers,
split_vlan_id_lists,
],
)
4 changes: 2 additions & 2 deletions hier_config/platforms/cisco_xr/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from hier_config.root import HConfig


def _fixup_xr_comments(config: HConfig) -> None:
def fixup_xr_comments(config: HConfig) -> None:
"""Move ``!`` comment lines into the next sibling's comments set."""
for parent in (config, *config.all_children()):
siblings = list(parent.children)
Expand Down Expand Up @@ -187,7 +187,7 @@ def _instantiate_rules() -> HConfigDriverRules:
PerLineSubRule(search="^\\s*#.*", replace=""),
PerLineSubRule(search="^\\s*!\\s*$", replace=""),
],
post_load_callbacks=[_fixup_xr_comments],
post_load_callbacks=[fixup_xr_comments],
idempotent_commands=[
IdempotentCommandsRule(
match_rules=(
Expand Down
12 changes: 6 additions & 6 deletions hier_config/platforms/hp_procurve/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from hier_config.root import HConfig


def _fixup_hp_procurve_aaa_port_access_fixup(config: HConfig) -> None:
def fixup_hp_procurve_aaa_port_access(config: HConfig) -> None:
"""Expands the interface ranges present in aaa port-access commands.

aaa port-access authenticator 1/15-1/20,1/26-1/40,2/14-2/20,2/25-2/28,2/30-2/44,3/8-3/44,4/1-4/2,4/8-4/44,5/1-5/2,5/8-5/15,5/17-5/28,5/30-5/44
Expand All @@ -40,7 +40,7 @@ def _fixup_hp_procurve_aaa_port_access_fixup(config: HConfig) -> None:
aaa_port_access.delete()


def _fixup_hp_procurve_vlan(config: HConfig) -> None:
def fixup_hp_procurve_vlan(config: HConfig) -> None:
"""Move native/tagged vlan config to the interface config for easier modeling and remediation.

vlan 1
Expand Down Expand Up @@ -90,7 +90,7 @@ def _fixup_hp_procurve_vlan(config: HConfig) -> None:
no_untagged_interfaces.delete()


def _fixup_hp_procurve_device_profile(config: HConfig) -> None:
def fixup_hp_procurve_device_profile(config: HConfig) -> None:
"""Separates the device-profile tagged-vlans onto individual lines.

device-profile name "phone"
Expand Down Expand Up @@ -335,8 +335,8 @@ def _instantiate_rules() -> HConfigDriverRules:
),
],
post_load_callbacks=[
_fixup_hp_procurve_aaa_port_access_fixup,
_fixup_hp_procurve_device_profile,
_fixup_hp_procurve_vlan,
fixup_hp_procurve_aaa_port_access,
fixup_hp_procurve_device_profile,
fixup_hp_procurve_vlan,
],
)
13 changes: 13 additions & 0 deletions tests/unit/platforms/test_aruba_aoscx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from hier_config.platforms.aruba_aoscx.driver import (
HConfigDriverArubaAOSCX,
split_interface_vlan_trunk_allowed,
)
from hier_config.platforms.utils import split_vlan_id_lists


def test_default_post_load_callbacks_are_public() -> None:
"""Built-in AOS-CX post-load callbacks are public, pinned by identity (#286)."""
callbacks = HConfigDriverArubaAOSCX().rules.post_load_callbacks

assert split_vlan_id_lists in callbacks
assert split_interface_vlan_trunk_allowed in callbacks
36 changes: 35 additions & 1 deletion tests/unit/platforms/test_cisco_ios.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from hier_config import HConfig
from hier_config.models import Platform
from hier_config.platforms.cisco_ios.driver import (
HConfigDriverCiscoIOS,
add_acl_sequence_numbers,
remove_ipv4_acl_remarks,
remove_ipv6_acl_sequence_numbers,
)
from hier_config.platforms.utils import split_vlan_id_lists


def test_rm_ipv6_acl_sequence_numbers() -> None:
def test_remove_ipv6_acl_sequence_numbers() -> None:
"""Test post-load callback that removes IPv6 ACL sequence numbers."""
platform = Platform.CISCO_IOS
config_text = "ipv6 access-list TEST_IPV6_ACL\n sequence 10 permit tcp any any eq 443\n sequence 20 deny ipv6 any any"
Expand Down Expand Up @@ -39,3 +46,30 @@ def test_add_acl_sequence_numbers() -> None:
assert acl.get_child(equals="10 permit tcp any any eq 443") is not None
assert acl.get_child(equals="20 permit tcp any any eq 80") is not None
assert acl.get_child(equals="30 deny ip any any") is not None


def test_default_post_load_callbacks_are_public() -> None:
"""Built-in IOS post-load callbacks are public and pinned by identity (#286)."""
callbacks = HConfigDriverCiscoIOS().rules.post_load_callbacks

assert remove_ipv6_acl_sequence_numbers in callbacks
assert remove_ipv4_acl_remarks in callbacks
assert add_acl_sequence_numbers in callbacks
assert split_vlan_id_lists in callbacks


def test_remove_ipv4_acl_remarks_callback_removable_by_identity() -> None:
"""The docs recipe: removing the public callback keeps ACL remarks (#286)."""
driver = HConfigDriverCiscoIOS()
driver.rules.post_load_callbacks.remove(remove_ipv4_acl_remarks)
config_text = (
"ip access-list extended TEST_ACL\n"
" remark Allow HTTPS traffic\n"
" permit tcp any any eq 443\n"
)
config = HConfig.from_text(driver, config_text)
acl = config.get_child(equals="ip access-list extended TEST_ACL")

assert acl is not None
assert acl.get_child(equals="remark Allow HTTPS traffic") is not None
assert acl.get_child(equals="10 permit tcp any any eq 443") is not None
11 changes: 11 additions & 0 deletions tests/unit/platforms/test_cisco_xr.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from hier_config import HConfig
from hier_config.models import Platform
from hier_config.platforms.cisco_xr.driver import (
HConfigDriverCiscoIOSXR,
fixup_xr_comments,
)


def test_multiple_groups_no_duplicate_child_error() -> None:
Expand Down Expand Up @@ -466,3 +470,10 @@ def test_xr_trailing_comment_with_no_following_sibling_is_dropped() -> None:
assert len(net_child.comments) == 0
for child in router_isis.all_children():
assert not child.text.startswith("!")


def test_default_post_load_callbacks_are_public() -> None:
"""Built-in XR post-load callbacks are public and pinned by identity (#286)."""
callbacks = HConfigDriverCiscoIOSXR().rules.post_load_callbacks

assert fixup_xr_comments in callbacks
15 changes: 15 additions & 0 deletions tests/unit/platforms/test_hp_procurve.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from hier_config import HConfig
from hier_config.models import Platform
from hier_config.platforms.hp_procurve.driver import (
HConfigDriverHPProcurve,
fixup_hp_procurve_aaa_port_access,
fixup_hp_procurve_device_profile,
fixup_hp_procurve_vlan,
)


def test_fixup_aaa_port_access_ranges() -> None:
Expand Down Expand Up @@ -96,3 +102,12 @@ def test_fixup_device_profile_tagged_vlans() -> None:

assert device_profile_printer is not None
assert device_profile_printer.get_child(equals="tagged-vlan 40") is not None


def test_default_post_load_callbacks_are_public() -> None:
"""Built-in ProCurve post-load callbacks are public, pinned by identity (#286)."""
callbacks = HConfigDriverHPProcurve().rules.post_load_callbacks

assert fixup_hp_procurve_aaa_port_access in callbacks
assert fixup_hp_procurve_device_profile in callbacks
assert fixup_hp_procurve_vlan in callbacks