Skip to content
Open
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
15 changes: 14 additions & 1 deletion hier_config/child.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,12 @@ def lines(self, *, sectional_exiting: bool = False) -> Iterable[str]:
yield from child.lines(sectional_exiting=sectional_exiting)

if sectional_exiting and (exit_text := self.sectional_exit):
yield " " * self.driver.rules.indentation * self.depth() + exit_text
depth = (
self.depth() - 1
if self.sectional_exit_text_parent_level
else self.depth()
)
yield " " * self.driver.rules.indentation * depth + exit_text

@property
def sectional_exit(self) -> str | None:
Expand All @@ -129,6 +134,14 @@ def sectional_exit(self) -> str | None:

return "exit"

@property
def sectional_exit_text_parent_level(self) -> bool:
for rule in self.driver.rules.sectional_exiting:
if self.is_lineage_match(rule.match_rules):
return rule.exit_text_parent_level

return False

def delete_sectional_exit(self) -> None:
try:
potential_exit = self.children[-1]
Expand Down
1 change: 1 addition & 0 deletions hier_config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class TagRule(BaseModel):
class SectionalExitingRule(BaseModel):
match_rules: tuple[MatchRule, ...]
exit_text: str
exit_text_parent_level: bool = False


class SectionalOverwriteRule(BaseModel):
Expand Down
7 changes: 7 additions & 0 deletions hier_config/platforms/cisco_xr/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,37 @@ def _instantiate_rules() -> HConfigDriverRules:
SectionalExitingRule(
match_rules=(MatchRule(startswith="route-policy"),),
exit_text="end-policy",
exit_text_parent_level=True,
),
SectionalExitingRule(
match_rules=(MatchRule(startswith="prefix-set"),),
exit_text="end-set",
exit_text_parent_level=True,
),
SectionalExitingRule(
match_rules=(MatchRule(startswith="policy-map"),),
exit_text="end-policy-map",
exit_text_parent_level=True,
),
SectionalExitingRule(
match_rules=(MatchRule(startswith="class-map"),),
exit_text="end-class-map",
exit_text_parent_level=True,
),
SectionalExitingRule(
match_rules=(MatchRule(startswith="community-set"),),
exit_text="end-set",
exit_text_parent_level=True,
),
SectionalExitingRule(
match_rules=(MatchRule(startswith="extcommunity-set"),),
exit_text="end-set",
exit_text_parent_level=True,
),
SectionalExitingRule(
match_rules=(MatchRule(startswith="template"),),
exit_text="end-template",
exit_text_parent_level=True,
),
SectionalExitingRule(
match_rules=(MatchRule(startswith="interface"),),
Expand Down
265 changes: 265 additions & 0 deletions tests/test_driver_cisco_xr.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,268 @@ def test_ipv4_acl_sequence_number_addition() -> None:
"ipv4 access-list TEST_ACL",
" 20 permit tcp any any eq 22",
)


def test_sectional_exit_text_parent_level_route_policy() -> None:
"""Test that route-policy exit text appears at parent level (no indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"route-policy TEST",
" set local-preference 200",
" pass",
),
)

route_policy = config.get_child(equals="route-policy TEST")
assert route_policy is not None
assert route_policy.sectional_exit_text_parent_level is True

output = config.dump_simple(sectional_exiting=True)
assert output == (
"route-policy TEST",
" set local-preference 200",
" pass",
"end-policy",
)


def test_sectional_exit_text_parent_level_prefix_set() -> None:
"""Test that prefix-set exit text appears at parent level (no indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"prefix-set TEST_PREFIX",
" 192.0.2.0/24",
" 198.51.100.0/24",
),
)

prefix_set = config.get_child(equals="prefix-set TEST_PREFIX")
assert prefix_set is not None
assert prefix_set.sectional_exit_text_parent_level is True

output = config.dump_simple(sectional_exiting=True)
assert output == (
"prefix-set TEST_PREFIX",
" 192.0.2.0/24",
" 198.51.100.0/24",
"end-set",
)


def test_sectional_exit_text_parent_level_policy_map() -> None:
"""Test that policy-map exit text appears at parent level (no indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"policy-map TEST_POLICY",
" class TEST_CLASS",
" set precedence 5",
),
)

policy_map = config.get_child(equals="policy-map TEST_POLICY")
assert policy_map is not None
assert policy_map.sectional_exit_text_parent_level is True

output = config.dump_simple(sectional_exiting=True)
assert output == (
"policy-map TEST_POLICY",
" class TEST_CLASS",
" set precedence 5",
" exit",
"end-policy-map",
)


def test_sectional_exit_text_parent_level_class_map() -> None:
"""Test that class-map exit text appears at parent level (no indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"class-map match-any TEST_CLASS",
" match access-group TEST_ACL",
),
)

class_map = config.get_child(equals="class-map match-any TEST_CLASS")
assert class_map is not None
assert class_map.sectional_exit_text_parent_level is True

output = config.dump_simple(sectional_exiting=True)
assert output == (
"class-map match-any TEST_CLASS",
" match access-group TEST_ACL",
"end-class-map",
)


def test_sectional_exit_text_parent_level_community_set() -> None:
"""Test that community-set exit text appears at parent level (no indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"community-set TEST_COMM",
" 65001:100",
" 65001:200",
),
)

community_set = config.get_child(equals="community-set TEST_COMM")
assert community_set is not None
assert community_set.sectional_exit_text_parent_level is True

output = config.dump_simple(sectional_exiting=True)
assert output == (
"community-set TEST_COMM",
" 65001:100",
" 65001:200",
"end-set",
)


def test_sectional_exit_text_parent_level_extcommunity_set() -> None:
"""Test that extcommunity-set exit text appears at parent level (no indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"extcommunity-set rt TEST_RT",
" 1:100",
" 2:200",
),
)

extcommunity_set = config.get_child(equals="extcommunity-set rt TEST_RT")
assert extcommunity_set is not None
assert extcommunity_set.sectional_exit_text_parent_level is True

output = config.dump_simple(sectional_exiting=True)
assert output == (
"extcommunity-set rt TEST_RT",
" 1:100",
" 2:200",
"end-set",
)


def test_sectional_exit_text_parent_level_template() -> None:
"""Test that template exit text appears at parent level (no indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"template TEST_TEMPLATE",
" description test template",
),
)

template = config.get_child(equals="template TEST_TEMPLATE")
assert template is not None
assert template.sectional_exit_text_parent_level is True

output = config.dump_simple(sectional_exiting=True)
assert output == (
"template TEST_TEMPLATE",
" description test template",
"end-template",
)


def test_sectional_exit_text_current_level_interface() -> None:
"""Test that interface exit text appears at current level (with indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"interface GigabitEthernet0/0/0/0",
" description test interface",
" ipv4 address 192.0.2.1 255.255.255.0",
),
)

interface = config.get_child(equals="interface GigabitEthernet0/0/0/0")
assert interface is not None
assert interface.sectional_exit_text_parent_level is False

output = config.dump_simple(sectional_exiting=True)
assert output == (
"interface GigabitEthernet0/0/0/0",
" description test interface",
" ipv4 address 192.0.2.1 255.255.255.0",
" root",
)


def test_sectional_exit_text_current_level_router_bgp() -> None:
"""Test that router bgp exit text appears at current level (with indentation)."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"router bgp 65000",
" bgp router-id 192.0.2.1",
" address-family ipv4 unicast",
),
)

router_bgp = config.get_child(equals="router bgp 65000")
assert router_bgp is not None
assert router_bgp.sectional_exit_text_parent_level is False

output = config.dump_simple(sectional_exiting=True)
assert output == (
"router bgp 65000",
" bgp router-id 192.0.2.1",
" address-family ipv4 unicast",
" root",
)


def test_sectional_exit_text_multiple_sections() -> None:
"""Test multiple sections with different exit text level behaviors."""
platform = Platform.CISCO_XR
config = get_hconfig_fast_load(
platform,
(
"route-policy TEST1",
" pass",
"!",
"interface GigabitEthernet0/0/0/0",
" description test",
"!",
"prefix-set TEST_PREFIX",
" 192.0.2.0/24",
),
)

route_policy = config.get_child(equals="route-policy TEST1")
assert route_policy is not None
assert route_policy.sectional_exit_text_parent_level is True

interface = config.get_child(equals="interface GigabitEthernet0/0/0/0")
assert interface is not None
assert interface.sectional_exit_text_parent_level is False

prefix_set = config.get_child(equals="prefix-set TEST_PREFIX")
assert prefix_set is not None
assert prefix_set.sectional_exit_text_parent_level is True

output = config.dump_simple(sectional_exiting=True)
assert output == (
"route-policy TEST1",
" pass",
"end-policy",
"interface GigabitEthernet0/0/0/0",
" description test",
" root",
"prefix-set TEST_PREFIX",
" 192.0.2.0/24",
"end-set",
)
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test case that verifies the specific remediation scenario described in issue #130. The test should:

  1. Create a running config with a prefix-set containing multiple entries
  2. Create a generated config with the same prefix-set but with some entries removed
  3. Generate the remediation config using config_to_get_to
  4. Verify that the remediation config outputs with 'end-set' at the parent level (no indentation)

This would directly test the fix for the reported issue where the diff output was incorrectly indenting 'end-set'.

Copilot uses AI. Check for mistakes.
Loading
Loading