---
phase: 05-artifact-pipeline
plan: "03"
subsystem: artifact
tags: [artifact, install, modprobe, make_install, rpm, deb, asyncio, tdd, mcp-tools]
dependency_graph:
  requires: [artifact_sync, ArtifactSyncer, SSHConnectionManager, ArtifactRegistry]
  provides: [artifact_install, ArtifactInstaller]
  affects: [vmcluster_mcp.artifact.__init__]
tech_stack:
  added: [SSH multi-line command sequences, modprobe_path install, rpm/deb package install]
  patterns: [TDD, asyncio.gather fan-out, best-effort per-node reporting, SSH command fan-out]
key_files:
  created:
    - vmcluster_mcp/artifact/installer.py
    - tests/test_artifact_install.py
  modified:
    - vmcluster_mcp/artifact/__init__.py
decisions:
  - modprobe_path uses single SSH execute call with multi-line shell script — mkdir, find+cp, depmod -a, find — avoids multiple round-trips
  - installed_module_paths parsed from stdout of final find command (lines ending in .ko)
  - depmod_output taken from stderr of the combined command — captures depmod warnings
  - make_install stdout used as depmod_output field — repurposed for general install log
  - rpm/deb modes use shell variable PKGS with find, exit 1 if no packages found
  - asyncio.gather with return_exceptions=True for best-effort fan-out (D-09)
  - Exceptions in install_node reported as exit_code=-1 with str(exc) as depmod_output
metrics:
  duration: "~10m"
  completed: "2026-03-23"
  tasks_completed: 2
  files_modified: 3
requirements-completed: [ART-03]
---

# Phase 05 Plan 03: Artifact Installer Summary

SSH-based artifact installation with four modes (modprobe_path, make_install, rpm, deb), asyncio.gather concurrent fan-out, and best-effort per-node failure reporting — completing the full artifact pipeline: register → list → diff → sync → install.

## Tasks Completed

| Task | Description | Commit |
|------|-------------|--------|
| 1 (RED) | Failing tests for ArtifactInstaller four install modes | 81a84fe |
| 1 (GREEN) | ArtifactInstaller with four install modes and asyncio.gather fan-out | 8ae48dd |
| 2 | Wire artifact_install tool to MCP server | d57df83 |

## What Was Built

### ArtifactInstaller (`vmcluster_mcp/artifact/installer.py`)
- `install_node(ip, port, user, key_path, artifact, dest_base, install_mode) -> InstallNodeResult`:
  - **`modprobe_path`** (D-10): Single SSH call with multi-line shell script:
    - `mkdir -p /lib/modules/{kver}/extra/vmcluster`
    - `find {dest_path} -name '*.ko' -exec cp {} $MODDIR/ \;`
    - `depmod -a {kver}`
    - `find $MODDIR -name '*.ko' -type f` (stdout → installed_module_paths)
    - stderr → depmod_output
  - **`make_install`** (D-11): `cd {dest_path} && make install 2>&1` — stdout as depmod_output
  - **`rpm`** (D-12): `find {dest_path} -name '*.rpm'` → `rpm -ivh $PKGS 2>&1`; exit 1 if no packages
  - **`deb`** (D-12): `find {dest_path} -name '*.deb'` → `dpkg -i $PKGS 2>&1`; exit 1 if no packages
- `install_all(nodes, ssh_spec, artifact, dest_base, install_mode) -> InstallStatus`:
  - Fans out via `asyncio.gather(*tasks, return_exceptions=True)` (D-13)
  - Exceptions → `InstallNodeResult(exit_code=-1, depmod_output=str(exc))` (D-09 best-effort)
  - Returns `InstallStatus(per_node=dict[node_name → InstallNodeResult])`

### artifact_install MCP Tool (`vmcluster_mcp/artifact/__init__.py`)
- Validates `install_mode` against `{"modprobe_path", "make_install", "rpm", "deb"}` → INVALID_INSTALL_MODE
- Validates artifact exists in registry → ARTIFACT_NOT_FOUND
- Loads topology, resolves target nodes (all or named subset) → NODE_NOT_FOUND
- Delegates to `_installer.install_all(...)` and returns `ToolResult[InstallStatus]`
- 5 total MCP tools in artifact module: artifact_register, artifact_list, artifact_diff, artifact_sync, artifact_install

### Tests (`tests/test_artifact_install.py`)
- 13 unit tests covering all specified behaviors:
  - modprobe_path: correct SSH command (depmod -a, vmcluster dir, kernel version)
  - modprobe_path: installed_module_paths parsed from stdout
  - modprobe_path: depmod_output from stderr
  - make_install: correct SSH command (cd + make install, artifact_id in path)
  - make_install: stdout as depmod_output, empty module paths
  - rpm: correct command (rpm -ivh, .rpm glob)
  - rpm: non-zero exit when no packages
  - deb: correct command (dpkg -i, .deb glob)
  - deb: non-zero exit when no packages
  - install_all: fans out to all nodes
  - install_all: per-node failure does not abort others (best-effort)
  - install_node: non-zero exit_code reflected in result
  - install_all: per_node keys use node.name not IP

## Deviations from Plan

None — plan executed exactly as written.

## Self-Check

### Created files exist:
- [x] `vmcluster_mcp/artifact/installer.py` — FOUND
- [x] `tests/test_artifact_install.py` — FOUND
- [x] `vmcluster_mcp/artifact/__init__.py` (modified) — FOUND

### Commits exist:
- [x] 81a84fe — test(05-03): add failing tests for ArtifactInstaller four install modes
- [x] 8ae48dd — feat(05-03): implement ArtifactInstaller with four install modes
- [x] d57df83 — feat(05-03): wire artifact_install tool to MCP server

### Verification:
- [x] 228 tests pass (uv run pytest tests/ -x)
- [x] ruff check clean on artifact/installer.py and artifact/__init__.py
- [x] 5 @mcp.tool decorators in artifact/__init__.py
- [x] asyncio.gather present in installer.py
- [x] `from vmcluster_mcp.server import mcp` import clean

## Self-Check: PASSED
