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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @jtdub @aedwardstx
4 changes: 2 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: hier_config build and test

on:
push:
branches: [master]
branches: [master, next]
pull_request:
branches: [master]
branches: [master, next]

jobs:
build:
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `TextStyle` type alias (`Literal["without_comments", "merged", "with_comments"]`) for
the `style` parameter on `HConfigChild.cisco_style_text()` and
`RemediationReporter.to_text()`, replacing the unconstrained `str` type (#189).

- Performance benchmarks for parsing, remediation, and iteration (#202).
Skipped by default; run with `poetry run pytest -m benchmark -v -s`.

- Added support for Huawei VRP with a new driver and test suite (#238).
- Custom exception hierarchy: `HierConfigError` base, `DriverNotFoundError`,
`InvalidConfigError`, `IncompatibleDriverError` (#219). `DuplicateChildError`
reparented under `HierConfigError`.

### Changed

- Changed `style` parameter on `indented_text()` and `RemediationReporter.to_text()` from `str` to `Literal["without_comments", "merged", "with_comments"]` via new `TextStyle` type alias (#189).
- Renamed `load_hconfig_v2_options` to `load_driver_rules` (#221).
- Renamed `load_hconfig_v2_tags` to `load_tag_rules` (#221).
- Renamed `tags_add()`/`tags_remove()` to `add_tags()`/`remove_tags()` (#216).
- Renamed `cisco_style_text()` to `indented_text()` (#216).
- Renamed `dump_simple()` to `to_lines()` (#216).
- Renamed `config_to_get_to()` to `remediation()` (#216).
- Converted `depth()` method to `depth` property (#216).

### Removed

- Removed `HCONFIG_PLATFORM_V2_TO_V3_MAPPING` constant (#221).
- Removed `hconfig_v2_os_v3_platform_mapper()` function (#221).
- Removed `hconfig_v3_platform_v2_os_mapper()` function (#221).
- Removed `load_hconfig_v2_options_from_file()` function (#221).

### Fixed

Expand Down
34 changes: 30 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

hier_config is a Python library that compares network device configurations (running vs intended) and generates minimal remediation commands. It parses config text into hierarchical trees and computes diffs respecting vendor-specific syntax rules.

## Branching Strategy

- `master` — stable branch for v3.x releases and maintenance.
- `next` — long-lived development branch for v4 work. All v4 features and breaking changes target this branch. PRs for v4 work should be opened against `next`.

## Build & Test Commands

All commands use **poetry** (not pip):
Expand All @@ -21,10 +26,16 @@ poetry run ./scripts/build.py lint
poetry run ./scripts/build.py pytest --coverage

# Run a single test
poetry run pytest tests/test_driver_cisco_xr.py::test_multiple_groups_no_duplicate_child_error -v
poetry run pytest tests/unit/platforms/test_cisco_xr.py::test_multiple_groups_no_duplicate_child_error -v

# Run a single test file
poetry run pytest tests/test_driver_cisco_xr.py -v
poetry run pytest tests/unit/platforms/test_cisco_xr.py -v

# Run only unit tests
poetry run pytest tests/unit/ -v

# Run only integration tests
poetry run pytest tests/integration/ -v

# Auto-fix formatting
poetry run ruff format hier_config tests scripts
Expand Down Expand Up @@ -52,9 +63,9 @@ Three-layer design: **Tree** (parse/represent config), **Driver** (platform-spec

### Tree Layer

- `HConfig` (root.py) — root node, owns the driver reference. Key methods: `config_to_get_to()`, `future()`, `difference()`, `dump_simple()`.
- `HConfig` (root.py) — root node, owns the driver reference. Key methods: `remediation()`, `future()`, `difference()`, `to_lines()`.
- `HConfigChild` (child.py) — tree node with `text`, `parent`, `children`, `tags`, `comments`. Provides `is_lineage_match()` for rule evaluation and `negate()` for negation logic.
- `HConfigBase` (base.py) — abstract base shared by both. Provides `add_child()`, `get_children_deep()`, `_config_to_get_to()` (left pass = negate missing, right pass = add new).
- `HConfigBase` (base.py) — abstract base shared by both. Provides `add_child()`, `get_children_deep()`, `_remediation()` (left pass = negate missing, right pass = add new).
- `HConfigChildren` (children.py) — ordered collection with O(1) dict lookup by text.

### Driver Layer (`platforms/`)
Expand Down Expand Up @@ -90,6 +101,21 @@ This project follows **Test-Driven Development (TDD)**:

All new features and bug fixes must have corresponding tests. Write tests before or alongside implementation, not after.

### Test Organization

Tests mirror the source code structure and are split into categories:

- **`tests/unit/`** — Unit tests for individual classes and functions.
- `test_root.py`, `test_child.py`, `test_children.py` — Tree layer tests.
- `test_constructors.py`, `test_utils.py`, `test_workflows.py`, `test_reporting.py` — Core module tests.
- `platforms/` — Driver unit tests (post-load callbacks, swap_negation, etc.).
- `platforms/views/` — Config view tests.
- **`tests/integration/`** — Tests that exercise driver remediation scenarios (running config → generated config → remediation).
- Per-platform files (`test_cisco_ios.py`, `test_cisco_xr.py`, etc.).
- `test_remediation.py` — Cross-platform remediation, future, and difference tests.
- `test_circular_workflows.py` — Roundtrip workflow validation across all platforms.
- **`tests/benchmarks/`** — Performance benchmarks (skipped by default).

## Changelog

When creating a PR, always update `CHANGELOG.md` with a summary of the changes under the `## [Unreleased]` section. Use the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format with categories: `Added`, `Changed`, `Fixed`, `Removed`. Reference the GitHub issue number when applicable (e.g., `(#209)`). When a version is released, move unreleased entries under a new version heading with the release date.
Expand Down
9 changes: 4 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,11 @@ ruff format .
Example:

```
Add negation_negate_with support to load_hconfig_v2_options
Add negation_negate_with support to load_driver_rules

When migrating from v2 to v3, users may need to express custom negation
strings via the v2 option dict format. This change forwards that value
into the NegationDefaultWithRule model so that the behaviour is preserved
during migration.
When loading driver rules from a dict, users may need to express custom
negation strings. This change forwards that value into the
NegationDefaultWithRule model so that the behaviour is preserved.
```

---
Expand Down
10 changes: 5 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The tree layer lives in `hier_config/base.py`, `hier_config/root.py`, `hier_conf

- A reference to the **driver** for the platform.
- An `HConfigChildren` collection of top-level `HConfigChild` nodes.
- High-level operations: `future()`, `config_to_get_to()`, `merge()`, `difference()`, `dump()`.
- High-level operations: `future()`, `remediation()`, `merge()`, `difference()`, `dump()`.

Create an `HConfig` object via the constructor function:

Expand Down Expand Up @@ -62,7 +62,7 @@ Both `HConfig` and `HConfigChild` inherit from `HConfigBase`, which provides:

- Child manipulation: `add_child`, `add_children`, `add_deep_copy_of`, `add_shallow_copy_of`.
- Searching: `get_child`, `get_children`, `get_child_deep`, `get_children_deep`.
- Diffing: `unified_diff`, `_config_to_get_to`, `_difference`.
- Diffing: `unified_diff`, `_remediation`, `_difference`.
- Future prediction: `_future`, `_future_pre`.

---
Expand Down Expand Up @@ -129,9 +129,9 @@ remediation = workflow.remediation_config # what to apply
rollback = workflow.rollback_config # how to revert
```

Internally it calls `running_config.config_to_get_to(generated_config)` which traverses the tree and calls `_config_to_get_to_left` (what to negate) and `_config_to_get_to_right` (what to add).
Internally it calls `running_config.remediation(generated_config)` which traverses the tree and calls `_remediation_left` (what to negate) and `_remediation_right` (what to add).

### `config_to_get_to()`
### `remediation()`

This method computes the **minimal delta** between two configs:

Expand Down Expand Up @@ -204,7 +204,7 @@ HConfig tree (HConfigBase / HConfigChild nodes)
├──► HConfig.future() → predicted post-change HConfig
├──► HConfig.config_to_get_to()
├──► HConfig.remediation()
│ │
│ ▼
│ delta HConfig (remediation commands)
Expand Down
12 changes: 6 additions & 6 deletions docs/drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ intended = get_hconfig(Platform.HP_PROCURVE, intended_text)
workflow = WorkflowRemediation(running, intended)

for line in workflow.remediation_config.all_children_sorted():
print(line.cisco_style_text())
print(line.indented_text())
```

---
Expand Down Expand Up @@ -239,7 +239,7 @@ intended = get_hconfig(Platform.HP_COMWARE5, intended_text)
workflow = WorkflowRemediation(running, intended)

for line in workflow.remediation_config.all_children_sorted():
print(line.cisco_style_text())
print(line.indented_text())
```

---
Expand Down Expand Up @@ -605,7 +605,7 @@ Both approaches allow you to extend the functionality of the Cisco IOS driver:

Unused object detection is not enabled in any driver by default — it must be explicitly configured. This ensures no unintended side-effects for users who are not expecting it.

You can add unused object rules dynamically or via `load_hconfig_v2_options`:
You can add unused object rules dynamically or via `load_driver_rules`:

#### Dynamic Extension

Expand Down Expand Up @@ -634,11 +634,11 @@ for unused in config.unused_objects():
print(f"Unused: {unused.text}")
```

#### Via `load_hconfig_v2_options`
#### Via `load_driver_rules`

```python
from hier_config import get_hconfig, Platform
from hier_config.utils import load_hconfig_v2_options
from hier_config.utils import load_driver_rules

options = {
"unused_objects": [
Expand All @@ -654,7 +654,7 @@ options = {
},
],
}
driver = load_hconfig_v2_options(options, Platform.CISCO_XR)
driver = load_driver_rules(options, Platform.CISCO_XR)
config = get_hconfig(driver, running_config_text)

for unused in config.unused_objects():
Expand Down
14 changes: 7 additions & 7 deletions docs/future-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This feature is useful in scenarios where you need to determine the anticipated
- Verifying that a configuration change was successfully applied to a device
- For example, checking if the post-change configuration matches the predicted future configuration
- Generating a future-state configuration that can be analyzed by tools like Batfish to assess the potential impact of a change
- Building rollback configurations: once the future configuration state is known, a rollback configuration can be generated by simply creating the remediation in reverse `(rollback = future.config_to_get_to(running))`.
- Building rollback configurations: once the future configuration state is known, a rollback configuration can be generated by simply creating the remediation in reverse `(rollback = future.remediation(running))`.
- When building rollbacks for a series of configuration changes, you can use the future configuration from each change as input for the subsequent change. For example, use the future configuration after Change 1 as the input for determining the future configuration after Change 2, and so on.

## `merge()` versus `future()`
Expand All @@ -21,9 +21,9 @@ If you receive a `DuplicateChildError` while calling `merge()`, consider whether

```python
post_change_1_config = running_config.future(change_1_config)
change_1_rollback_config = post_change_1_config.config_to_get_to(running_config)
change_1_rollback_config = post_change_1_config.remediation(running_config)
post_change_2_config = post_change_1_config.future(change_2_config)
change_2_rollback_config = post_change_2_config.config_to_get_to(post_change_1_config)
change_2_rollback_config = post_change_2_config.remediation(post_change_1_config)
```


Expand Down Expand Up @@ -53,7 +53,7 @@ The `future()` algorithm is a best-effort simulation. The following cases are n

5. **ACL idempotency check.**
The extended ACL idempotency logic (sequence-number based matching) that drives
`config_to_get_to()` is not replicated in `future()`.
`remediation()` is not replicated in `future()`.

6. **And likely others.**
Complex platform-specific interactions (e.g. VRF-aware BGP sections, route-policy
Expand Down Expand Up @@ -93,7 +93,7 @@ configuration because their identities differ within the `neighbor` hierarchy.
>>> print("Running Config")
Running Config
>>> for line in running_config.all_children():
... print(line.cisco_style_text())
... print(line.indented_text())
...
hostname aggr-example.rtr
ip access-list extended TEST
Expand All @@ -116,7 +116,7 @@ interface Vlan3
>>> print("Remediation Config")
Remediation Config
>>> for line in remediation_config.all_children():
... print(line.cisco_style_text())
... print(line.indented_text())
...
vlan 3
name switch_mgmt_10.0.3.0/24
Expand All @@ -139,7 +139,7 @@ interface Vlan4
>>> print("Future Config")
Future Config
>>> for line in running_config.future(remediation_config).all_children():
... print(line.cisco_style_text())
... print(line.indented_text())
...
vlan 3
name switch_mgmt_10.0.3.0/24
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The `remediation_config` attribute generates the configuration needed to apply t
>>> print("Remediation configuration:")
Remediation configuration:
>>> for line in workflow.remediation_config.all_children_sorted():
... print(line.cisco_style_text())
... print(line.indented_text())
...
vlan 3
name switch_mgmt_10.0.3.0/24
Expand Down Expand Up @@ -79,7 +79,7 @@ Similarly, the `rollback_config` attribute generates a configuration that can re
>>> print("Rollback configuration:")
Rollback configuration:
>>> for line in workflow.rollback_config.all_children_sorted():
... print(line.cisco_style_text())
... print(line.indented_text())
...
no vlan 4
no interface Vlan4
Expand Down
4 changes: 2 additions & 2 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A Python class that encodes all operating-system-specific behaviour for one netw

A configuration command where only the *last* value applied takes effect — applying the same command twice with different values results in only the second value being active. Typical examples: `hostname`, `ip address`, `description`.

hier_config uses `IdempotentCommandsRule` to identify these commands. During `config_to_get_to()`, when both the running and intended configs contain a command that matches an idempotency rule, the running value is **not** negated before the new value is applied (the new value simply overwrites it).
hier_config uses `IdempotentCommandsRule` to identify these commands. During `remediation()`, when both the running and intended configs contain a command that matches an idempotency rule, the running value is **not** negated before the new value is applied (the new value simply overwrites it).

**Example rule:**

Expand Down Expand Up @@ -126,7 +126,7 @@ A `SectionalExitingRule` that instructs hier_config to emit a closing token at t

## Sectional overwrite

A `SectionalOverwriteRule` that tells `config_to_get_to()` to **negate the entire section** and then re-create it from the intended config rather than performing a line-by-line diff. Appropriate for configuration blocks where the order of entries matters globally or where partial changes are not supported by the OS.
A `SectionalOverwriteRule` that tells `remediation()` to **negate the entire section** and then re-create it from the intended config rather than performing a line-by-line diff. Appropriate for configuration blocks where the order of entries matters globally or where partial changes are not supported by the OS.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ intended = get_hconfig(Platform.CISCO_IOS, intended_config_text)
workflow = WorkflowRemediation(running, intended)

for line in workflow.remediation_config.all_children_sorted():
print(line.cisco_style_text())
print(line.indented_text())
```

---
Expand Down
Loading
Loading