---
phase: 02-core-vm-lifecycle
plan: "03"
requirements-completed: [CLUS-02, CLUS-03, CLUS-04]
---

# Plan 02-03 Summary — cluster_start / cluster_stop / cluster_destroy

## Status: COMPLETE

**Commits:**
- `21a69b4` — test(02-03): add failing tests for cluster_start/stop/destroy (RED)
- `b909414` — feat(02-03): implement cluster_start, cluster_stop, cluster_destroy (GREEN)

## What was built

### `_wait_for_ssh` helper (`vmcluster_mcp/cluster/__init__.py`)
Module-level async helper (not a tool). Polls SSH via `asyncssh.connect` until
the node responds or `timeout_s` is exceeded. Retries every 2 seconds; never
raises — failures return `(False, elapsed)`. Implements D-11 (asyncssh).

### `cluster_start` tool
- Boots all stopped domains (`start_domain` only if not already active).
- If `wait_for_ssh=True`: fires `asyncio.gather` of `_wait_for_ssh` calls for
  all nodes concurrently; sets `ssh_reachable` per node.
- D-04 strict mode: if any node fails SSH → `success=False`, `SSH_TIMEOUT` error.
- Accepts `ssh_timeout_s` (default 120 s).

### `cluster_stop` tool
- Iterates nodes; calls `shutdown_domain` (mode=`"shutdown"`) or
  `destroy_domain` (mode=`"destroy"`) for active domains; skips inactive ones.

### `cluster_destroy` tool
- Force-stops any active domains → `undefine_domain` for all.
- Always destroys + undefines the network (D-01).
- `remove_overlays=True` → `shutil.rmtree(overlay_dir, ignore_errors=True)` (D-02).

## Imports added
- `asyncio` (already stdlib, no new dep)
- `shutil` (stdlib)
- `time` (stdlib)
- `asyncssh` (third-party, already in venv)

## Test coverage
13 new tests in `tests/test_cluster_lifecycle.py`:
- `test_cluster_start_boots_all_domains`
- `test_cluster_start_skips_already_active_domain`
- `test_cluster_start_no_wait_returns_immediately`
- `test_cluster_start_ssh_success_sets_reachable`
- `test_cluster_start_ssh_timeout_returns_failure`
- `test_cluster_stop_shutdown_mode`
- `test_cluster_stop_destroy_mode`
- `test_cluster_stop_skips_inactive_domains`
- `test_cluster_destroy_undefines_domains`
- `test_cluster_destroy_force_stops_active_domains`
- `test_cluster_destroy_always_removes_network`
- `test_cluster_destroy_remove_overlays_true`
- `test_cluster_destroy_remove_overlays_false`

All 111 suite tests passing; ruff clean.

## Design decisions applied
| Decision | Applied |
|----------|---------|
| D-01 Network always removed on destroy | ✅ `destroy_network` + `undefine_network` unconditionally |
| D-02 Overlays optional removal | ✅ `remove_overlays` flag |
| D-04 Strict SSH all-or-nothing | ✅ Any SSH timeout → `success=False` with `SSH_TIMEOUT` |
| D-08 ThreadPoolExecutor via LibvirtClient | ✅ All libvirt calls go through `_client` |
| D-09 structured_tool_handler | ✅ All three tools decorated |
| D-11 asyncssh for SSH polling | ✅ `_wait_for_ssh` uses `asyncssh.connect` |
