netops.change — Configuration Change Management¶
Semantic diff, change planning, safe push, and automated rollback.
netops.change.diff¶
Semantic-aware configuration diff engine.
Understands network device config structure rather than treating configs as plain text. Supports three input formats:
- cisco — IOS/IOS-XE/IOS-XR indented hierarchical style
- junos — JunOS set-format or bracketed hierarchical style
- flat — one directive per line (Nokia SR-OS, simple key/value)
Three output formats are available:
- unified — classic unified diff (compatible with
patch(1)) - semantic — human-readable tree view with parent context and highlights
- json — machine-readable dict suitable for programmatic consumption
CLI usage:
python -m netops.change.diff --before before.txt --after after.txt
python -m netops.change.diff --before b.txt --after a.txt --format semantic
python -m netops.change.diff --before b.txt --after a.txt --format json
diff ¶
Semantic-aware configuration diff engine.
Understands network device config structure rather than treating configs as plain text. Supports three input formats:
- cisco – IOS/IOS-XE/IOS-XR indented hierarchical style
- junos – JunOS set-format or bracketed hierarchical style
- flat – one directive per line (Nokia SR-OS, simple key/value)
Three output formats are available:
- unified – classic unified diff (compatible with
patch(1)) - semantic – human-readable tree view with parent context and highlights
- json – machine-readable dict suitable for programmatic consumption
Usage::
# From files:
python -m netops.change.diff --before before.txt --after after.txt
# With format selection:
python -m netops.change.diff --before b.txt --after a.txt --format semantic
# JSON output (suitable for CI pipelines):
python -m netops.change.diff --before b.txt --after a.txt --format json
Public API::
from netops.change.diff import diff_configs, format_unified, format_semantic, format_json
result = diff_configs(before_text, after_text, style="cisco")
print(format_semantic(result))
Classes¶
ConfigStyle ¶
ChangeKind ¶
Bases: str, Enum
Type of diff change.
ConfigNode
dataclass
¶
ConfigNode(key: str, raw: str, children: list[ConfigNode] = list(), depth: int = 0, is_security: bool = False)
One node in the hierarchical config tree.
For cisco style each node corresponds to a block header (e.g.
interface GigabitEthernet0/0) or a leaf line inside that block.
For junos set-format each set … directive is stored as a flat
node with its full path as key.
For flat style each non-blank, non-comment line is a leaf node.
instance-attribute
¶Original line as it appeared in the config (may include whitespace).
class-attribute
instance-attribute
¶Child nodes (sub-stanzas in Cisco hierarchical config).
class-attribute
instance-attribute
¶True when the line matches a security-sensitive pattern.
Return a string that uniquely identifies this node's content.
For leaf nodes this is the stripped line itself. For block headers
it is header + sorted(child signatures) so that reordering
children (where order does not matter) does not produce a diff.
DiffEntry
dataclass
¶
DiffEntry(kind: ChangeKind, path: list[str], before_lines: list[str], after_lines: list[str], is_security: bool = False)
A single semantic diff entry.
instance-attribute
¶Breadcrumb path from the root to this node (list of key strings).
instance-attribute
¶Lines from the before config (for REMOVED / CHANGED).
instance-attribute
¶Lines from the after config (for ADDED / CHANGED).
class-attribute
instance-attribute
¶True when any involved line is security-sensitive.
DiffResult
dataclass
¶
DiffResult(style: ConfigStyle, entries: list[DiffEntry] = list(), before_text: str = '', after_text: str = '')
Container for the full diff between two configs.
class-attribute
instance-attribute
¶All detected diff entries.
class-attribute
instance-attribute
¶Original before config text (used by unified formatter).
class-attribute
instance-attribute
¶Original after config text (used by unified formatter).
property
¶Return only entries that touch security-sensitive config.
Functions:¶
parse_config ¶
Parse text according to style and return a list of top-level nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Raw configuration text. |
required |
style
|
ConfigStyle
|
One of :class: |
CISCO
|
diff_configs ¶
Compare two config strings and return a :class:DiffResult.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
before
|
str
|
The before (original / running) configuration text. |
required |
after
|
str
|
The after (new / candidate) configuration text. |
required |
style
|
ConfigStyle | None
|
Parsing style. When None (default) the style is auto-detected from before. |
None
|
format_unified ¶
Return a classic unified diff string.
Uses Python's :mod:difflib on the original text lines so the output is
compatible with patch(1).
format_semantic ¶
Return a human-readable semantic diff.
Each change is prefixed with its parent breadcrumb so the operator sees
full context. Security-sensitive changes are marked with [SECURITY].
format_json ¶
Return a JSON string representing the diff.
The structure is::
{
"style": "cisco",
"summary": {"added": 1, "removed": 0, "changed": 2, "security": 1},
"entries": [
{
"kind": "added",
"section": "interface GigabitEthernet0/0",
"path": ["interface GigabitEthernet0/0"],
"is_security": false,
"before_lines": [],
"after_lines": [" description WAN uplink"]
},
...
]
}
netops.change.plan¶
Change planning — risk assessment, step ordering, and dry-run simulation.
CLI usage:
python -m netops.change.plan plan --host 10.0.0.1 --desired desired.cfg --export plan.json
python -m netops.change.plan apply --plan plan.json --approve
plan ¶
Change approval workflow: plan → dry-run → review → approve → execute.
Workflow::
1. Call :func:`generate_plan` with the *desired* config text and the
*current* (running) config text for one or more devices.
2. Inspect the returned :class:`ChangePlan`. The plan includes a
human-readable preview (semantic diff), risk score, and per-device
:class:`ChangeStep` list.
3. Export the plan to JSON/YAML for offline review with
:func:`export_plan`.
4. When approved, call :func:`apply_plan` (requires ``approved=True``).
Dry-run mode never modifies any device.
Usage::
# Generate and preview a plan (dry-run, no device changes):
python -m netops.change.plan plan \\
--host router1 --desired new_config.txt
# Export plan to file for offline review:
python -m netops.change.plan plan \\
--host router1 --desired new_config.txt --export plan.json
# Apply a previously exported + approved plan:
python -m netops.change.plan apply --plan plan.json --approve
Public API::
from netops.change.plan import (
generate_plan, apply_plan, export_plan, load_plan,
ChangePlan, ChangeStep, RiskLevel, DeviceRole,
)
Classes¶
RiskLevel ¶
Bases: str, Enum
Overall risk classification for a change plan.
DeviceRole ¶
Bases: str, Enum
Criticality classification of a network device.
Roles are ordered from lowest (ACCESS) to highest (CORE) criticality. The role influences the risk score of any change on that device.
ChangeStep
dataclass
¶
ChangeStep(host: str, device_type: str, device_role: DeviceRole, commands: list[str], current_config: str = '', desired_config: str = '', diff_preview: str = '', unified_diff: str = '', has_security_changes: bool = False, applied: bool = False, error: str | None = None)
A single per-device step inside a :class:ChangePlan.
class-attribute
instance-attribute
¶Running config captured from the device (or provided as input).
class-attribute
instance-attribute
¶Target config (full desired state — used when commands is empty).
class-attribute
instance-attribute
¶Human-readable semantic diff preview (populated by :func:generate_plan).
class-attribute
instance-attribute
¶Classic unified diff string (populated by :func:generate_plan).
class-attribute
instance-attribute
¶True when the diff contains security-sensitive configuration lines.
class-attribute
instance-attribute
¶True after this step has been successfully applied.
class-attribute
instance-attribute
¶Error message if this step failed during :func:apply_plan.
ChangePlan
dataclass
¶
ChangePlan(plan_id: str, created_at: str, operator: str, description: str, steps: list[ChangeStep] = list(), risk_level: RiskLevel = RiskLevel.LOW, risk_score: float = 0.0, dry_run: bool = True, approved: bool = False, applied_at: str | None = None, changelog_path: str | None = None)
Full change plan: metadata + one :class:ChangeStep per device.
instance-attribute
¶UUID for cross-system correlation. A fresh UUID is generated for each
:func:generate_plan call. The structure (steps, diff, risk score) is
fully reproducible given the same input — only the plan_id will differ
between two calls with identical arguments.
class-attribute
instance-attribute
¶Ordered list of per-device change steps.
class-attribute
instance-attribute
¶Overall risk level derived from scope and device criticality.
class-attribute
instance-attribute
¶Numeric risk score (used to derive :attr:risk_level).
Score components:
- device_weight — based on :class:
DeviceRole(1–4) - change_scope — number of diff entries (adds + removes + changes)
- security_bonus — +3 for each step that touches security config
- multi_device_bonus — +2 when more than one device is in the plan
class-attribute
instance-attribute
¶When True the plan was generated without connecting to any device.
class-attribute
instance-attribute
¶Set to True before calling :func:apply_plan to authorise execution.
class-attribute
instance-attribute
¶ISO-8601 UTC timestamp when :func:apply_plan was called.
class-attribute
instance-attribute
¶Optional path to a JSON-lines changelog to append results to.
Functions:¶
generate_plan ¶
generate_plan(steps_input: list[dict], *, operator: str = '', description: str = '', config_style: ConfigStyle | None = None) -> ChangePlan
Generate a :class:ChangePlan from desired-vs-current state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
steps_input
|
list[dict]
|
A list of dicts, one per device, with keys:
|
required |
operator
|
str
|
Human-readable name of the person or system generating the plan. |
''
|
description
|
str
|
Free-text plan description or ticket reference. |
''
|
config_style
|
ConfigStyle | None
|
Force a specific :class: |
None
|
Returns:
| Type | Description |
|---|---|
ChangePlan
|
A fully populated plan ready for export or review. The plan is
never applied here — call :func: |
apply_plan ¶
apply_plan(plan: ChangePlan, *, connection_params: list[ConnectionParams] | None = None, approved: bool = False, changelog_path: Path | None = None) -> ChangePlan
Apply an approved :class:ChangePlan to the target devices.
Dry-run guarantee: if approved is False (the default) this
function immediately returns the plan unchanged — no device is ever
modified.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plan
|
ChangePlan
|
The plan to apply. Must have been generated by :func: |
required |
connection_params
|
list[ConnectionParams] | None
|
A list of :class: |
None
|
approved
|
bool
|
Must be explicitly set to True to allow device modifications. |
False
|
changelog_path
|
Path | None
|
Optional path to a JSON-lines file. The applied plan dict is appended as a single line after successful completion. |
None
|
Returns:
| Type | Description |
|---|---|
ChangePlan
|
The same plan object with each step's |
export_plan ¶
Write plan to path in JSON or YAML format.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plan
|
ChangePlan
|
The plan to serialise. |
required |
path
|
Path
|
Destination file path. Parent directories are created if needed. |
required |
fmt
|
str
|
|
'json'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
When fmt is not |
load_plan ¶
Load a :class:ChangePlan from a JSON or YAML file.
The format is auto-detected from the file extension:
.yaml / .yml → YAML; everything else → JSON.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
Path to the exported plan file. |
required |
Returns:
| Type | Description |
|---|---|
ChangePlan
|
Deserialised plan. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
When path does not exist. |
netops.change.push¶
Safe configuration push — pre/post health validation with automatic rollback.
CLI usage:
python -m netops.change.push --host 10.0.0.1 --vendor cisco_ios --commands changes.txt
python -m netops.change.push --host 10.0.0.1 --vendor cisco_ios --commands changes.txt --commit
python -m netops.change.rollback --host 10.0.0.1 --vendor cisco_ios --commands changes.txt \
--commit --rollback-on-failure --validate-health
push ¶
Safe configuration push with pre/post diff and auto-rollback confirm timer.
Workflow:
- Connect to the device and snapshot the running config (pre-change).
- Optionally push the given commands (requires
--commitflag; dry-run by default). - Snapshot the config again (post-change) and compute a unified diff.
- If
--confirm-timer Nis set, start a countdown. The operator must typeconfirmwithin N minutes or the pre-change config is restored (rollback). - Append a structured entry to a JSON-lines change log.
Usage::
# Dry-run (default — no changes pushed):
python -m netops.change.push --host router1 --commands changes.txt
# Commit with 5-minute confirm timer:
python -m netops.change.push --host router1 --commands changes.txt \\
--commit --confirm-timer 5
Classes¶
ChangeRecord
dataclass
¶
ChangeRecord(host: str, operator: str, started_at: str, commands: list[str], pre_config: str, post_config: str | None = None, diff: str | None = None, committed: bool = False, confirmed: bool = False, rolled_back: bool = False, error: str | None = None)
Captures every meaningful attribute of a single config-push event.
Functions:¶
run_push ¶
run_push(params: ConnectionParams, commands: list[str], *, commit: bool = False, confirm_timer_minutes: int = 0, operator: str = '', changelog_path: Path | None = None) -> ChangeRecord
Execute the full safe-push workflow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
ConnectionParams
|
Connection parameters for the target device. |
required |
commands
|
list[str]
|
Ordered list of configuration commands to apply. |
required |
commit
|
bool
|
When False (default) snapshot + diff are generated but nothing is pushed to the device. |
False
|
confirm_timer_minutes
|
int
|
If > 0 the operator must confirm within this many minutes after a successful push or the pre-change config is restored. |
0
|
operator
|
str
|
Human-readable identifier of the person or system executing the change. |
''
|
changelog_path
|
Path | None
|
Optional path to a JSON-lines changelog file. Each call appends one record. |
None
|
append_changelog ¶
Append record as a JSON object to a newline-delimited log file.
load_changelog ¶
Return all change records from path as a list of dicts.
Returns:
| Type | Description |
|---|---|
list
|
All change records from path as a list of dicts. |
main ¶
CLI entry point for safe config-push with pre/post diff and optional auto-rollback.
netops.change.rollback¶
Automated rollback — pre-snapshot + health monitoring with rollback on degradation.
CLI usage:
python -m netops.change.rollback --host 10.0.0.1 --vendor cisco_ios --commands changes.txt \
--commit --rollback-on-failure --validate-health
rollback ¶
Automated rollback with pre/post health validation.
Workflow:
- Connect to the device and capture:
- Running configuration (pre-change snapshot)
- Health-check baseline (CPU, memory, interface errors, logs)
- Optionally save the snapshot via backup integration.
- Apply the configuration change (requires
--commit; dry-run by default). - Re-run health checks and capture a post-change snapshot + unified diff.
- Compare pre/post health:
- Any alert that was not firing before the change → validation FAILED.
- Device unreachable after the change → validation FAILED.
- On failure (when
--rollback-on-failureis set): - Restore the pre-change configuration automatically.
- Write a structured entry to a JSON-lines audit log (who/what/when/why).
Usage::
# Dry-run (default — no changes pushed):
python -m netops.change.rollback --host router1 --commands changes.txt
# Commit with health validation and auto-rollback on failure:
python -m netops.change.rollback --host router1 --commands changes.txt \\
--commit --rollback-on-failure --validate-health
# Include snapshot backup and custom thresholds:
python -m netops.change.rollback --host router1 --commands changes.txt \\
--commit --rollback-on-failure --validate-health \\
--snapshot-dir /var/backups/network --cpu-threshold 70
Classes¶
RollbackRecord
dataclass
¶
RollbackRecord(change_id: str, host: str, operator: str, reason: str, started_at: str, commands: list[str], pre_config: str = '', post_config: str | None = None, diff: str | None = None, pre_health: dict | None = None, post_health: dict | None = None, committed: bool = False, validation_passed: bool | None = None, rolled_back: bool = False, rollback_reason: str | None = None, snapshot_path: str | None = None, completed_at: str | None = None, error: str | None = None)
Full audit record for a single change-with-rollback event.
Functions:¶
run_rollback_push ¶
run_rollback_push(params: ConnectionParams, commands: list[str], *, commit: bool = False, validate_health: bool = True, rollback_on_failure: bool = True, cpu_threshold: float = DEFAULT_CPU_THRESHOLD, mem_threshold: float = DEFAULT_MEM_THRESHOLD, operator: str = '', reason: str = '', audit_log_path: Path | None = None, snapshot_dir: Path | None = None) -> RollbackRecord
Execute a configuration change with pre/post health validation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
ConnectionParams
|
Connection parameters for the target device. |
required |
commands
|
list[str]
|
Ordered list of configuration commands to apply. |
required |
commit
|
bool
|
When False (default) only a pre-change snapshot (and health check when validate_health is True) are collected — nothing is pushed. |
False
|
validate_health
|
bool
|
When True run health checks before and after the change and compare them. Any alert that was not present before the change causes validation to fail. |
True
|
rollback_on_failure
|
bool
|
When True automatically restore the pre-change config if validation fails. |
True
|
cpu_threshold
|
float
|
CPU alert threshold percentage forwarded to :func: |
DEFAULT_CPU_THRESHOLD
|
mem_threshold
|
float
|
Memory alert threshold percentage forwarded to :func: |
DEFAULT_MEM_THRESHOLD
|
operator
|
str
|
Human-readable identifier of the person or system executing the change. |
''
|
reason
|
str
|
Change rationale or ticket reference written to the audit log. |
''
|
audit_log_path
|
Path | None
|
Optional path to a JSON-lines audit log. One record is appended per call, even when the change or rollback fails. |
None
|
snapshot_dir
|
Path | None
|
Optional directory where pre-change snapshots are saved via the backup integration. |
None
|
append_audit_log ¶
Append record as a JSON object to a newline-delimited audit log file.
load_audit_log ¶
Return all audit records from path as a list of dicts.
Returns:
| Type | Description |
|---|---|
list
|
All audit records from path as a list of dicts. |