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

# Plan 02-04 Summary — cluster_status / cluster_handle

## Status: COMPLETE

**Commits:**
- `8472680` — test(02-04): add failing tests for cluster_status and cluster_handle (RED)
- `a18bcbd` — feat(02-04): implement cluster_status and cluster_handle (GREEN)

## What was built

### `cluster_status` tool (`vmcluster_mcp/cluster/__init__.py`)
- `@mcp.tool()` + `@structured_tool_handler`
- Checks network via `lookup_network` + `is_network_active`
- Queries each node's `domain_state` via `get_domain_state`
- SSH checked **only for running nodes** — `asyncio.gather` runs checks in parallel
- Returns `ToolResult[ClusterStatus]` with `network_active` and per-node `NodeStatus`

### `cluster_handle` tool (`vmcluster_mcp/cluster/__init__.py`)
- `@mcp.tool()` + `@structured_tool_handler`
- Validates cluster is running (first node active) → `CLUSTER_NOT_RUNNING` error otherwise
- Fetches `kernel_version` via SSH `uname -r` on first node using `asyncssh.connect`
- Returns `ToolResult[ClusterHandle]` with `NodeDescriptor` list, `artifact_path="/opt/vmcluster/artifacts"`, and `kernel_version`

## Imports added to cluster/__init__.py
- `ClusterHandle`, `NodeDescriptor` from `vmcluster_mcp.models` (added to existing import line)

## Test coverage
13 new tests in `tests/test_cluster_status.py`:

### cluster_status (7 tests)
- `test_cluster_status_returns_cluster_name`
- `test_cluster_status_network_active_true`
- `test_cluster_status_per_node_domain_state`
- `test_cluster_status_ssh_checked_only_for_running_nodes`
- `test_cluster_status_not_running_no_ssh`
- `test_cluster_status_unknown_domain`
- `test_cluster_status_mixed_states`

### cluster_handle (6 tests)
- `test_cluster_handle_returns_cluster_name`
- `test_cluster_handle_node_descriptors`
- `test_cluster_handle_artifact_path`
- `test_cluster_handle_kernel_version_from_ssh`
- `test_cluster_handle_not_running_returns_error`
- `test_cluster_handle_ssh_key_path_from_topology`

All 124 suite tests passing; ruff clean.

## Design decisions applied
| Decision | Applied |
|----------|---------|
| D-08 ThreadPoolExecutor via LibvirtClient | ✅ All libvirt calls via `_client` |
| D-09 structured_tool_handler | ✅ Both tools decorated |
| D-11 asyncssh | ✅ `cluster_handle` uses `asyncssh.connect` for kernel version |
| CLUS-05 cluster_status | ✅ Returns per-node domain state + SSH reachability |
| CLUS-06 cluster_handle | ✅ Returns ClusterHandle with node descriptors and kernel_version |

## Phase 02 Completion

All 6 cluster tools now registered:
- `cluster_define` (02-02)
- `cluster_start` (02-03)
- `cluster_stop` (02-03)
- `cluster_destroy` (02-03)
- `cluster_status` (02-04)
- `cluster_handle` (02-04)

CLUS requirements covered: CLUS-01 through CLUS-07 ✅
