Skip to content
749 changes: 749 additions & 0 deletions docs/unused-object-remediation.md

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions example_custom_unused_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Example demonstrating extensible unused object detection.

This example shows how to use the new extensible unused object system
to detect custom configuration objects on any platform.
"""

from hier_config import (
UnusedObjectRuleBuilder,
create_simple_rule,
get_hconfig,
)
from hier_config.models import Platform

# Example configuration for a hypothetical custom platform
CONFIG = """
# Custom ACL definitions
access-list WEB_TRAFFIC
permit tcp any any eq 80
permit tcp any any eq 443

access-list UNUSED_ACL
permit ip any any

# NAT pool definitions
nat-pool PUBLIC_POOL
range 203.0.113.10 203.0.113.20

nat-pool UNUSED_POOL
range 203.0.113.30 203.0.113.40

# Interface configuration
interface eth0
description Uplink interface
apply-acl WEB_TRAFFIC inbound

# NAT configuration
nat source interface eth0 pool PUBLIC_POOL
"""


def main() -> None:
"""Demonstrate custom unused object detection."""
print("=" * 70)
print("Extensible Unused Object Detection Example")
print("=" * 70)

# Create config using generic platform (works with ANY platform!)
config = get_hconfig(Platform.GENERIC, CONFIG)

print("\n1. Using UnusedObjectRuleBuilder (fluent API):")
print("-" * 70)

# Define rule for access-lists using the builder
acl_rule = (
UnusedObjectRuleBuilder("custom-access-list")
.define_with(startswith="access-list ")
.referenced_in(
context_match=[
{"startswith": "interface "},
{"startswith": "apply-acl "},
],
extract_regex=r"apply-acl\s+(\S+)",
reference_type="interface-applied",
)
.remove_with("no access-list {name}")
.with_weight(150)
.build()
)

print(f"Created rule: {acl_rule.object_type}")
print(" - Definition pattern: startswith='access-list '")
print(" - Reference extraction: apply-acl\\s+(\\S+)")
print(f" - Removal template: {acl_rule.removal_template}")

print("\n2. Using create_simple_rule (simplified API):")
print("-" * 70)

# Define rule for NAT pools using the simple helper
nat_pool_rule = create_simple_rule(
object_type="custom-nat-pool",
definition_pattern="nat-pool ",
reference_pattern=r"pool\s+(\S+)",
reference_context="nat source ",
removal_template="no nat-pool {name}",
removal_weight=140,
)

print(f"Created rule: {nat_pool_rule.object_type}")
print(" - Definition pattern: startswith='nat-pool '")
print(" - Reference extraction: pool\\s+(\\S+)")
print(f" - Removal template: {nat_pool_rule.removal_template}")

print("\n3. Adding rules to driver:")
print("-" * 70)

# Add both custom rules to the driver
config.driver.add_unused_object_rules([acl_rule, nat_pool_rule])
print(f"Added {len(config.driver.get_unused_object_rules())} rules to driver")

print("\n4. Analyzing configuration:")
print("-" * 70)

# Analyze and generate cleanup
analysis = config.driver.find_unused_objects(config)

print(f"Total defined objects: {analysis.total_defined}")
print(f"Total unused objects: {analysis.total_unused}")

print("\n5. Results by object type:")
print("-" * 70)

for object_type, definitions in analysis.defined_objects.items():
unused = analysis.unused_objects.get(object_type, ())
print(f"\n{object_type}:")
print(f" Defined: {len(definitions)}")
print(f" Unused: {len(unused)}")

if unused:
print(" Unused objects:")
for obj in unused:
print(f" - {obj.name}")

print("\n6. Removal commands:")
print("-" * 70)

if analysis.removal_commands:
for cmd in analysis.removal_commands:
print(f" {cmd}")
else:
print(" (no unused objects to remove)")

print(f"\n{'=' * 70}")
print("Example completed successfully!")
print("=" * 70)


if __name__ == "__main__":
main()
12 changes: 12 additions & 0 deletions hier_config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,28 @@
)
from .models import Platform
from .root import HConfig
from .unused_object_helpers import (
UnusedObjectRuleBuilder,
create_simple_rule,
load_unused_object_rules_from_dict,
load_unused_object_rules_from_json,
load_unused_object_rules_from_yaml,
)
from .workflows import WorkflowRemediation

__all__ = (
"HConfig",
"HConfigChild",
"Platform",
"UnusedObjectRuleBuilder",
"WorkflowRemediation",
"create_simple_rule",
"get_hconfig",
"get_hconfig_driver",
"get_hconfig_fast_load",
"get_hconfig_from_dump",
"get_hconfig_view",
"load_unused_object_rules_from_dict",
"load_unused_object_rules_from_json",
"load_unused_object_rules_from_yaml",
)
132 changes: 132 additions & 0 deletions hier_config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,135 @@ class Platform(str, Enum):

class Dump(BaseModel):
lines: tuple[DumpLine, ...]


class ReferencePattern(BaseModel):
"""Defines where an object might be referenced in the configuration.

Attributes:
match_rules: Sequence of MatchRules to locate reference contexts.
extract_regex: Regular expression to extract the object name from the reference.
capture_group: Which regex capture group contains the object name.
reference_type: Descriptive type of the reference (e.g., "interface-applied").
ignore_patterns: Patterns to exclude from consideration as references.

"""

match_rules: tuple[MatchRule, ...]
extract_regex: str
capture_group: PositiveInt = 1
reference_type: str
ignore_patterns: tuple[str, ...] = ()


class UnusedObjectRule(BaseModel):
r"""Defines how to identify and remove an unused object type.

This rule is completely user-definable and can be applied to ANY configuration
object type on ANY platform. Users can create custom rules for their specific
use cases without code changes.

Attributes:
object_type: User-defined identifier for organizing results. Can be any
descriptive string (e.g., "ipv4-acl", "my-custom-object", "firewall-policy").
This is purely for categorization and has no functional restrictions.
definition_match: MatchRules to locate object definitions in the configuration.
These patterns identify where objects of this type are defined.
reference_patterns: Patterns describing where objects can be referenced.
Each pattern includes match rules to find the reference context and a
regex to extract the referenced object name.
removal_template: Template string for generating removal commands.
Use {name} for the object name and any metadata keys from extract_metadata.
Example: "no ip access-list {acl_type} {name}"
removal_order_weight: Controls the order in which unused objects are removed.
Lower weights are removed first. Use this to respect dependencies
(e.g., remove policy-maps before class-maps they reference).
allow_in_comment: Whether to consider references in comments as valid usage.
case_sensitive: Whether object name matching is case-sensitive.
Set to False for platforms with case-insensitive names (e.g., Cisco IOS).
require_exact_match: Whether to require exact name matches.

Example:
>>> rule = UnusedObjectRule(
... object_type="custom-firewall-policy",
... definition_match=(MatchRule(startswith="firewall policy "),),
... reference_patterns=(
... ReferencePattern(
... match_rules=(MatchRule(startswith="interface "),),
... extract_regex=r"apply-policy\s+(\S+)",
... reference_type="interface-applied",
... ),
... ),
... removal_template="no firewall policy {name}",
... removal_order_weight=100,
... )

"""

object_type: str
definition_match: tuple[MatchRule, ...]
reference_patterns: tuple[ReferencePattern, ...]
removal_template: str
removal_order_weight: int = 100
allow_in_comment: bool = False
case_sensitive: bool = True
require_exact_match: bool = True


class UnusedObjectDefinition(BaseModel):
"""Represents a discovered object definition in the configuration.

Note: This model uses model_config to allow arbitrary types for config_section.

Attributes:
object_type: Type of the object (e.g., "ipv4-acl").
name: Name of the object.
definition_location: Hierarchical path to the definition.
metadata: Additional information extracted from the definition.

"""

model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)

object_type: str
name: str
definition_location: tuple[str, ...]
metadata: dict[str, str] = {}


class UnusedObjectReference(BaseModel):
"""Represents a reference to an object in the configuration.

Attributes:
object_type: Type of the object being referenced.
name: Name of the referenced object.
reference_location: Hierarchical path to the reference.
reference_type: Type of reference (from ReferencePattern).

"""

object_type: str
name: str
reference_location: tuple[str, ...]
reference_type: str


class UnusedObjectAnalysis(BaseModel):
"""Results of unused object analysis.

Attributes:
defined_objects: Mapping of object types to their definitions.
referenced_objects: Mapping of object types to their references.
unused_objects: Mapping of object types to unused definitions.
total_defined: Total count of defined objects across all types.
total_unused: Total count of unused objects across all types.
removal_commands: List of commands to remove unused objects.

"""

defined_objects: dict[str, tuple[UnusedObjectDefinition, ...]]
referenced_objects: dict[str, tuple[UnusedObjectReference, ...]]
unused_objects: dict[str, tuple[UnusedObjectDefinition, ...]]
total_defined: NonNegativeInt
total_unused: NonNegativeInt
removal_commands: tuple[str, ...]
Loading
Loading