---
phase: 04-snapshot-management
plan: "02"
subsystem: snapshot
tags: [snapshot, revert, delete, libvirt, tdd, tools, D-05, D-07, D-08]
dependency_graph:
  requires: [snapshot/manager (04-01), cluster/libvirt_client, cluster/_wait_for_ssh, models]
  provides: [snapshot_revert tool, snapshot_delete tool, SnapshotManager.revert_snapshot, SnapshotManager.delete_snapshot, LibvirtClient.revert_to_snapshot, LibvirtClient.start_domain_by_name, LibvirtClient.delete_snapshot]
  affects: [server.py tool registry, 04-02 closes snapshot phase]
tech_stack:
  added: []
  patterns: [destroy-all-before-revert D-07, best-effort per-node D-08, active-backing guard D-05, asyncio.gather phase barriers, lazy import to avoid circular deps]
key_files:
  created:
    - tests/test_snapshot_revert.py
    - tests/test_snapshot_delete.py
  modified:
    - vmcluster_mcp/snapshot/manager.py
    - vmcluster_mcp/snapshot/__init__.py
    - vmcluster_mcp/cluster/libvirt_client.py
    - vmcluster_mcp/models.py
key_decisions:
  - "lazy import of _wait_for_ssh inside revert_snapshot method body to avoid circular import (cluster/__init__ partially loaded when snapshot/manager imports)"
  - "patch target for tests: vmcluster_mcp.cluster._wait_for_ssh (definition site), not vmcluster_mcp.snapshot.manager._wait_for_ssh"
  - "active-backing guard parses both snapshot XML and domain XML to compare source file paths"
metrics:
  duration: "~480s"
  completed: "2026-03-23"
  tasks: 2
  files: 6
requirements-completed: [SNAP-02, SNAP-04]
---

# Phase 04 Plan 02: Snapshot Revert + Delete Summary

`snapshot_revert` and `snapshot_delete` MCP tools completing the snapshot management phase. Revert enforces destroy-all-before-revert via sequenced `asyncio.gather` phases; delete is best-effort per-node with an active-backing guard. 18 new tests, all 188 tests pass; ruff clean.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 RED | Failing tests for snapshot_revert | 9316020 | tests/test_snapshot_revert.py |
| 1 GREEN | snapshot_revert implementation | 18884ac | libvirt_client.py, manager.py, __init__.py, models.py |
| 1 FIX | Circular import fix + test patch update | 5aedeed | manager.py, test_snapshot_revert.py |
| 2 RED+GREEN | snapshot_delete tests (impl already in place) | a925ef3 | tests/test_snapshot_delete.py |

## What Was Built

### LibvirtClient (3 new async methods — all via `_run` executor)
- `revert_to_snapshot(dom, snap, flags=0)` — `dom.revertToSnapshot(snap, flags)`
- `start_domain_by_name(name)` — fresh `lookup_domain` + `dom.create` (handles stale domain refs post-revert)
- `delete_snapshot(snap, flags=0)` — `snap.delete(flags)`

### SnapshotManager — `revert_snapshot` (D-07 destroy-all-before-revert)
Four sequenced `asyncio.gather` phases:
1. **Destroy** all active domains (all must complete before Phase 2 begins)
2. **Revert** each domain to named snapshot via `revert_to_snapshot(..., VIR_DOMAIN_SNAPSHOT_REVERT_FORCE)`
3. **Start** all domains (if `restart_after=True`) via `start_domain_by_name` (fresh handle)
4. **SSH wait** (if `wait_for_ssh=True`) via lazy-imported `_wait_for_ssh` from `vmcluster_mcp.cluster`

Any failure in any phase returns `ToolResult(success=False)` — no silent partial reverts.
Returns `ToolResult[ClusterStatus]` with per-node domain state and SSH reachability.

### SnapshotManager — `delete_snapshot` (D-05/D-08 best-effort with active-backing guard)
Per-node via `asyncio.gather(return_exceptions=True)`:
- Lookup domain and snapshot; missing → per-node error, continue
- **Active-backing guard**: parse snapshot XML + domain XML; if snapshot overlay == active domain disk → error "Cannot delete active backing layer", skip
- Delete libvirt snapshot metadata via `client.delete_snapshot(snap)`
- Try `os.remove(overlay_path)`; `FileNotFoundError` silently ignored

Returns `ToolResult[SnapshotDeleteResult]`; `success=True` only if all nodes deleted.

### New Models in `models.py`
- `SnapshotDeleteNodeResult(node_name, deleted, error)`
- `SnapshotDeleteResult(snapshot_name, cluster_name, per_node)`

### Tools registered on `mcp`
- `snapshot_revert(cluster_name, snapshot_name, restart_after=True, wait_for_ssh=True, ssh_timeout_s=120)`
- `snapshot_delete(cluster_name, snapshot_name)`

### Test Files
- `tests/test_snapshot_revert.py`: 10 tests covering all TDD behaviors
- `tests/test_snapshot_delete.py`: 8 tests covering all TDD behaviors

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Circular import: `from vmcluster_mcp.cluster import _wait_for_ssh` at module level**
- **Found during:** Full test suite run after Task 1 GREEN
- **Issue:** `snapshot/manager.py` top-level import of `_wait_for_ssh` from `vmcluster_mcp.cluster` caused `ImportError: cannot import name '_wait_for_ssh' from partially initialized module` when `test_builders.py` triggered a different module load order
- **Fix:** Changed to lazy import inside `revert_snapshot` method body (`from vmcluster_mcp.cluster import _wait_for_ssh # noqa: PLC0415`). Updated tests to patch `vmcluster_mcp.cluster._wait_for_ssh` (the definition site) instead of `vmcluster_mcp.snapshot.manager._wait_for_ssh`
- **Files modified:** `vmcluster_mcp/snapshot/manager.py`, `tests/test_snapshot_revert.py`
- **Commit:** 5aedeed

**2. [Rule 2 - Pre-implementation] Task 2 implementation included in Task 1 GREEN commit**
- `delete_snapshot` (LibvirtClient method, SnapshotManager method, tool registration) and new models were implemented in the same GREEN pass as `revert_snapshot` for efficiency. Task 2 tests were written and committed separately to preserve the TDD record, but the RED phase was skipped since implementation was already present.

## Known Stubs

None — `snapshot_revert` and `snapshot_delete` are fully wired to `SnapshotManager` and `LibvirtClient`.

## Self-Check: PASSED

```
✓ vmcluster_mcp/snapshot/manager.py — revert_snapshot and delete_snapshot methods exist
✓ vmcluster_mcp/cluster/libvirt_client.py — revert_to_snapshot, start_domain_by_name, delete_snapshot added
✓ vmcluster_mcp/snapshot/__init__.py — snapshot_revert and snapshot_delete tools registered
✓ vmcluster_mcp/models.py — SnapshotDeleteNodeResult, SnapshotDeleteResult added
✓ tests/test_snapshot_revert.py — 10 tests, all pass
✓ tests/test_snapshot_delete.py — 8 tests, all pass
✓ Full suite: 188 passed, 0 failed
✓ ruff check: All checks passed
✓ All 4 snapshot tools importable: snapshot_create, snapshot_list, snapshot_revert, snapshot_delete
✓ Commits: 9316020, 18884ac, a925ef3, 5aedeed
```
