---
plan: 02-02
phase: 02-core-vm-lifecycle
status: complete
started: 2026-03-22T21:18:00Z
completed: 2026-03-22T21:28:00Z
commits:
  - b61c6c1
  - 3af09c0
  - b5ac4b8
  - 180a0a9
requirements-completed: [CLUS-01, CLUS-07]
---

# Plan 02-02: XML builders, cloud-init ISO, cluster_define tool

## What Was Built

Three supporting modules and the first user-facing tool:

1. **`vmcluster_mcp/cluster/domain_builder.py`** — Programmatic KVM domain XML via ElementTree (D-06). Builds overlay disk (vda/virtio), cloud-init cdrom (sda/sata/readonly), bridge interface with deterministic MAC, serial console, and extra disks.

2. **`vmcluster_mcp/cluster/network_builder.py`** — NAT network XML via ElementTree. No DHCP block (D-03). `get_gateway_ip()` extracts first host from CIDR subnet for reuse.

3. **`vmcluster_mcp/cluster/cloud_init.py`** — NoCloud ISO generation with MAC-matched static IP network-config v2. Invokes `genisoimage` with `mkisofs` fallback.

4. **`vmcluster_mcp/cluster/__init__.py`** — `cluster_define` tool registered on FastMCP. Idempotent (CLUS-07), creates overlay directory, provisions network, generates ISOs, defines all domains. D-05 partial failure handling.

5. **`vmcluster_mcp/server.py`** — Added cluster import to trigger tool registration.

## Key Files

### Created
- `vmcluster_mcp/cluster/domain_builder.py`
- `vmcluster_mcp/cluster/network_builder.py`
- `vmcluster_mcp/cluster/cloud_init.py`
- `vmcluster_mcp/cluster/__init__.py` (was empty)
- `tests/test_builders.py` — 17 tests
- `tests/test_cloud_init.py` — 5 tests
- `tests/test_cluster_define.py` — 4 tests

### Modified
- `vmcluster_mcp/server.py` — cluster import added

## Task Results

| Task | Status | Commit |
|------|--------|--------|
| Task 1: domain_builder + network_builder (TDD) | ✓ Complete | 3af09c0 |
| Task 2: cloud_init + cluster_define (TDD) | ✓ Complete | 180a0a9 |

## Deviations from Plan

None — plan executed exactly as written.

## Self-Check
- [x] `domain_builder.py` uses `xml.etree.ElementTree` (no Jinja2)
- [x] `domain_builder.py` includes overlay disk (vda/virtio), cidata cdrom (sda/sata/readonly), bridge interface, serial console
- [x] `network_builder.py` forward mode='nat', no DHCP block
- [x] `cloud_init.py` calls `subprocess.run` with genisoimage / mkisofs fallback
- [x] `cluster/__init__.py` has `@mcp.tool()` + `@structured_tool_handler`
- [x] `cluster_define` is idempotent — lookupByName before define
- [x] `cluster_define` calls `qemu-img create` for overlay disks
- [x] `cluster_define` asserts base_image != overlay_path
- [x] `pytest tests/test_builders.py tests/test_cloud_init.py tests/test_cluster_define.py` → 26 passed
- [x] `ruff check vmcluster_mcp/cluster/ vmcluster_mcp/server.py` → All checks passed

## Self-Check: PASSED
