---
phase: "07"
plan: "01"
subsystem: net
tags: [fault-registry, persistence, reconciliation, iptables, tc, atomic-writes]
dependency_graph:
  requires: [vmcluster_mcp/models.py, vmcluster_mcp/artifact/registry.py]
  provides: [vmcluster_mcp/net/fault_registry.py, vmcluster_mcp/config.py#fault_registry]
  affects: [all Phase 7 plans — 07-02 through 07-05 depend on FaultRegistry]
tech_stack:
  added: [asyncio.create_subprocess_exec for host iptables/tc commands]
  patterns: [atomic tmp+os.replace write (mirrors ArtifactRegistry), TDD red-green]
key_files:
  created:
    - vmcluster_mcp/net/fault_registry.py
    - tests/test_fault_registry.py
  modified:
    - vmcluster_mcp/config.py
decisions:
  - "fault_registry default path: /var/lib/vmcluster/faults/registry.json (consistent with artifact_registry layout)"
  - "reconcile() is async, uses asyncio.create_subprocess_exec + _run_host_cmd helper for testability"
  - "register() returns NetFaultHandle with only the fields defined in models.py (handle_id, fault_type, affected_nodes, created_at)"
metrics:
  duration_s: 141
  completed_date: "2026-03-24"
  tasks: 2
  files: 3
requirements-completed: [FAULT-07]
---

# Phase 07 Plan 01: Fault Registry Core Summary

**One-liner:** Disk-backed `FaultRegistry` with atomic JSON persistence and startup reconciliation against live iptables/tc state.

## What Was Built

The `FaultRegistry` class in `vmcluster_mcp/net/fault_registry.py` provides the persistence backbone for all Phase 7 fault injection tools. It stores active network fault entries on disk, keyed by `handle_id`, and reconciles against live kernel state at startup to prune entries whose iptables/tc rules no longer exist.

### Files Created / Modified

| File | Change |
|------|--------|
| `vmcluster_mcp/net/fault_registry.py` | New — FaultRegistry class (145 lines) |
| `vmcluster_mcp/config.py` | Modified — added `fault_registry` field + `VMCLUSTER_FAULT_REGISTRY` env var |
| `tests/test_fault_registry.py` | New — 22 tests covering CRUD, atomic writes, round-trip, reconciliation |

### Commit History

| Commit | Type | Description |
|--------|------|-------------|
| `1dc1101` | `test(07-01)` | RED — failing tests for FaultRegistry CRUD and reconciliation |
| `bb5c913` | `feat(07-01)` | GREEN — FaultRegistry with atomic persistence and startup reconciliation |

## Tasks Completed

### Task 1: Config update + FaultRegistry with atomic JSON persistence ✅

- `Config.fault_registry` field with default `/var/lib/vmcluster/faults/registry.json`
- `VMCLUSTER_FAULT_REGISTRY` env var added to `_ENV_MAP`
- `FaultRegistry.__init__`, `load()`, `save()` (atomic tmp+os.replace per D-03)
- `register()` → builds entry dict + returns `NetFaultHandle`, saves immediately
- `get()`, `remove()` (returns False if not found, saves on success)
- `list_faults(cluster_name)` → returns `list[NetFaultInfo]` filtered by cluster
- `all_handles()` → for reconciliation iteration
- Round-trip persistence test (register → new instance → load → verify)

### Task 2: Startup reconciliation against live iptables/tc state ✅

- `async def reconcile() -> int` — validates all registry entries against live kernel state
- Partition faults: runs `iptables -L FORWARD -n --line-numbers -v`, checks for `vmcluster:<cluster>:<handle_id>` tag (D-07)
- Impairment faults: runs `tc qdisc show dev <interface>`, checks for `netem` presence
- Pruned entries logged at WARNING; `save()` called only if stale entries found
- Empty registry is a no-op (no subprocess calls)
- `_run_host_cmd(cmd)` helper encapsulates `asyncio.create_subprocess_exec` for testability

## Verification Results

```
22 passed in 0.27s
296 total tests, 0 failures
ruff check: All checks passed!
```

## Deviations from Plan

None — plan executed exactly as written.

## Self-Check: PASSED

- `vmcluster_mcp/net/fault_registry.py` — EXISTS (145 lines, > 100 line minimum ✅)
- `vmcluster_mcp/config.py` — fault_registry field present ✅
- `tests/test_fault_registry.py` — EXISTS (334 lines, > 60 line minimum ✅)
- Commits `1dc1101` and `bb5c913` — FOUND in git log ✅
- `from vmcluster_mcp.models import.*NetFault` pattern — present ✅
- `os.replace` atomic write pattern — present ✅
- `fault_registry` in config.py — present ✅
