---
phase: 04-snapshot-management
plan: "01"
subsystem: snapshot
tags: [snapshot, libvirt, tdd, tools]
dependency_graph:
  requires: [cluster/libvirt_client, topology/parser, models, server]
  provides: [snapshot_create tool, snapshot_list tool, SnapshotManager, LibvirtClient snapshot methods]
  affects: [server.py tool registry, 04-02-PLAN (builds on SnapshotManager)]
tech_stack:
  added: [xml.etree.ElementTree for snapshot XML parsing]
  patterns: [asyncio.gather concurrent per-node ops, @mcp.tool+@structured_tool_handler pattern, all-or-nothing failure mode D-06]
key_files:
  created:
    - vmcluster_mcp/snapshot/manager.py
    - tests/test_snapshot_create.py
    - tests/test_snapshot_list.py
  modified:
    - vmcluster_mcp/cluster/libvirt_client.py
    - vmcluster_mcp/snapshot/__init__.py
    - vmcluster_mcp/server.py
key_decisions:
  - "disk-only snapshots stored at {overlay_dir}/{cluster_name}/snapshots/{snapshot_name}/{node}.qcow2"
  - "idempotency guard via snapshot_lookup_by_name on first domain before creating"
  - "all-or-nothing failure: any node error returns success=False with SNAPSHOT_FAILED code"
metrics:
  duration: "272s"
  completed: "2026-03-23"
  tasks: 2
  files: 6
requirements-completed: [SNAP-01, SNAP-03]
---

# Phase 04 Plan 01: Snapshot Create + List Summary

Snapshot infrastructure: `SnapshotManager` with `create_snapshot`/`list_snapshots`, 5 new `LibvirtClient` snapshot wrapper methods, and `snapshot_create`/`snapshot_list` MCP tools registered via existing decorator pattern. 23 tests, all passing; ruff clean.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | LibvirtClient snapshot methods + SnapshotManager + snapshot_create | 31a6558 | libvirt_client.py, snapshot/manager.py, snapshot/__init__.py, server.py, test_snapshot_create.py |
| 2 | snapshot_list tool with XML parsing | 86d1192 | snapshot/manager.py, snapshot/__init__.py, server.py, test_snapshot_list.py |

## What Was Built

### LibvirtClient (5 new async methods — all via `_run` executor)
- `snapshot_create_xml(dom, xml, flags)` — `dom.snapshotCreateXML(xml, flags)`
- `list_all_snapshots(dom, flags=0)` — `dom.listAllSnapshots(flags)`
- `snapshot_lookup_by_name(dom, name)` — `dom.snapshotLookupByName(name, 0)`, returns `None` on error
- `snapshot_get_xml_desc(snap, flags=0)` — `snap.getXMLDesc(flags)`
- `get_domain_xml(dom, flags=0)` — `dom.XMLDesc(flags)`

### SnapshotManager (`vmcluster_mcp/snapshot/manager.py`)
- `create_snapshot(cluster_name, snapshot_name, nodes, overlay_dir)`:
  - Idempotency guard via `snapshot_lookup_by_name` before any writes
  - Creates snapshot dir: `{overlay_dir}/{cluster_name}/snapshots/{snapshot_name}/`
  - Concurrent per-node execution via `asyncio.gather(..., return_exceptions=True)`
  - All-or-nothing failure: any exception → `ToolResult(success=False, SNAPSHOT_FAILED, recoverable=False)`
  - Returns `ToolResult[SnapshotInfo]` with per-node disk paths and sizes
- `list_snapshots(cluster_name, nodes, overlay_dir)`:
  - Iterates nodes sequentially, calls `list_all_snapshots` per domain
  - Parses snapshot XML via `xml.etree.ElementTree` for `<creationTime>` (epoch→ISO) and disk path
  - `FileNotFoundError` on `os.path.getsize` → `size_bytes=0` (graceful degradation)
  - Groups by snapshot name across nodes; returns `ToolResult[list[SnapshotInfo]]`

### Tools registered on `mcp`
- `snapshot_create(cluster_name, snapshot_name, include_memory=False)`
  - `include_memory=True` logs warning, proceeds disk-only (v1 scope)
- `snapshot_list(cluster_name)` — returns all snapshots with timestamps and sizes

### Tests
- `tests/test_snapshot_create.py`: 15 tests (LibvirtClient methods + SnapshotManager unit + tool integration)
- `tests/test_snapshot_list.py`: 8 tests (empty, single, multi-snapshot, XML parsing, file not found, grouping)

## Deviations from Plan

None — plan executed exactly as written.

## Known Stubs

None — `snapshot_create` and `snapshot_list` are fully wired to `SnapshotManager` and `LibvirtClient`.

## Self-Check: PASSED

```
✓ vmcluster_mcp/snapshot/manager.py exists (175 lines)
✓ vmcluster_mcp/snapshot/__init__.py created with both tool registrations
✓ vmcluster_mcp/cluster/libvirt_client.py — 5 new snapshot methods added
✓ tests/test_snapshot_create.py — 15 tests pass
✓ tests/test_snapshot_list.py — 8 tests pass
✓ commits 31a6558 and 86d1192 exist in git log
✓ ruff clean on all modified files
✓ from vmcluster_mcp.snapshot import snapshot_create, snapshot_list → "Tools registered"
```
