Repository navigation
python library
kvm-pilot is also a plain Python library: the CLI and the MCP server are thin
layers over the same objects. This page is the map of that public surface —
what to import, what each piece returns, and what it raises. It is assembled
from the module docstrings; when they disagree, the code wins and this page
has a bug. The agent-facing how to drive it safely playbook is the bundled
skill's Python library page;
how to add a driver is Writing a first-class driver.
What is public: everything in kvm_pilot.__all__ plus the subpackage
names below. Anything with a leading underscore is private and may change in
any release.
from kvm_pilot import (
KVMClient, make_driver, resolve_host, # build a driver
SafetyPolicy, deny_all, interactive_confirm, # gate destructive ops
run_healthcheck, preflight, HealthReport, HealthGateError, Severity,
KVMPilotError, SafetyError, CapabilityError, # (the full hierarchy below)
Capability, KVMDriver,
)
from kvm_pilot.vision import ScreenAnalyzer, make_backend, ScreenState, ALL_PHASESThe library core (client.py, drivers/, http.py, vision/) imports only the
standard library; third-party needs (mcp, pyotp, websocket-client,
Pillow) are imported lazily inside the feature that uses them.
Three ways, from most to least explicit:
from kvm_pilot import KVMClient, make_driver, resolve_host
from kvm_pilot.drivers import make_driver_from_config
kvm = KVMClient("192.168.8.1", "admin", "secret") # PiKVM family, direct
bmc = make_driver("redfish", host="idrac.lan", user="root", passwd="…") # by kind
cfg = resolve_host("homelab") # profile > env > file
drv = make_driver_from_config(cfg, dry_run=True) # honours cfg.driver, incl. "auto"resolve_host(profile=None, **overrides) merges explicit arguments over
KVM_PILOT_* environment variables over the [hosts.NAME] profile in the config
file (~/.config/kvm-pilot/config.toml, or KVM_PILOT_CONFIG) and returns a
HostConfig dataclass — every key is listed in Configuration.
Naming a profile that does not exist raises KeyError, never a silent fallback.
make_driver_from_config is what the CLI and MCP server use. When
cfg.driver == "auto" (the default) it probes the host and resolves a concrete
kind, memoised per process — see §9 — or raises
with the probe inventory when nothing matches.
| kind | class | constructor essentials | notes |
|---|---|---|---|
pikvm |
KVMClient (= PiKVMDriver) |
host, user="admin", passwd="admin", *, port=443, scheme="https", verify_ssl=False, ssl_ca_file=None, timeout=30.0, totp_secret=None, dry_run=False, confirm=None, max_retries=3 |
Stock PiKVM and any kvmd-API-compatible device. verify_ssl defaults off because these ship self-signed certs; pin with ssl_ca_file instead. |
glkvm |
GLKVMDriver |
same as pikvm
|
GL.iNet fork. Turns a 404 across /api/* into ApiDisabledError; carries the GL quirk list. |
blikvm |
BliKVMDriver |
same as pikvm
|
BliKVM fork. |
redfish |
RedfishDriver |
host, user="root", passwd="", *, port=443, scheme="https", verify_ssl=False, ssl_ca_file=None, timeout=30.0, auth="session", system_index=0, power_wait_timeout=60.0, async_timeout=120.0, dry_run=False, confirm=None, max_retries=3 |
Dell iDRAC, HPE iLO, Supermicro, Lenovo XCC, OpenBMC. auth="basic" for BMCs/emulators with no SessionService. system_index picks the ComputerSystem on multi-node gear (programmatic only). |
ipmi |
IpmiDriver |
host, user="ADMIN", passwd="", *, port=623, interface="lanplus", cipher=None, ipmitool="ipmitool", timeout=30.0, dry_run=False, confirm=None |
Pre-Redfish BMCs (iDRAC6, older iLO) through the system ipmitool binary. |
amt |
AmtDriver |
host, user="admin", passwd="", *, port=16992, tls=False, verify_ssl=False, ssl_ca_file=None, sol_port=16994, kvm_port=5900, kvm_password=None, amtterm="amtterm", timeout=30.0, dry_run=False, confirm=None |
Intel AMT / vPro: WS-Man power/boot, SOL, and RFB KVM redirection (firmware-level video). kvm_password is the separate 8-character RFB password. |
ssh |
SshDriver |
host, *, ssh_channel=None (from_config needs ssh_host) |
The OS plane: a host with no KVM/BMC at all (#248). Explicit only — auto never selects it. |
fake |
FakeDriver |
host="fake", *, powered=False, phase="power_off", video_signal=True, hid_connected=True, ocr_text="", logs="", events=None, image=None, dry_run=False, confirm=None |
In-memory, scriptable, no network. Destructive ops gate exactly as on hardware — the right target for tests and for a dry rehearsal. |
Every driver class also has from_config(cfg, *, confirm=None, dry_run=False)
(the shape-aware constructor the factory uses), and make_driver raises
ValueError naming the known kinds for an unknown one. A third-party driver
plugs in with kvm_pilot.drivers.register_driver(kind, factory).
Drivers are heterogeneous: a Redfish BMC has no screenshot, a GL unit has no
sensors. The Capability enum and the mixin every driver carries let you ask
instead of catching:
from kvm_pilot import Capability
drv.capabilities() # set[Capability] this driver implements
drv.supports(Capability.VIDEO) # or supports("video")
drv.video_scope() # VideoScope: FIRMWARE | CAPTURE | NONE
drv.close() # release sessions/sockets (idempotent)Capability |
protocol (kvm_pilot.drivers.base) |
methods |
|---|---|---|
system_info |
SystemInfo |
get_info(fields=None) -> dict |
power |
Power |
power_on(wait=True), power_off(wait=True), power_off_hard(wait=True), reset_hard(wait=True), is_powered_on() -> bool; plus hard_cycle(off_delay=None, on_delay=None) from PowerMixin
|
hid |
HID |
type_text(text), press_key(key), send_shortcut(keys), mouse_move(x, y), mouse_click(button="left")
|
video |
Video |
snapshot() -> bytes (JPEG), snapshot_base64() -> str
|
virtual_media |
VirtualMedia |
mount_iso(source, image_name=None, cdrom=True) -> str, msd_connect(), msd_disconnect(). Key off get_msd_state() for truth, not the return value (#243). |
boot_config |
BootConfig |
get_boot_options() -> dict, set_boot_device(device, *, once=True, uefi=True) -> dict
|
gpio |
GPIO |
gpio_switch(channel, state), gpio_pulse(channel)
|
events |
Events |
watch_events(on_event=None, stream=True, timeout=None) — a generator of event dicts |
logs |
Logs |
get_logs(seek=0, follow=False) -> str |
boot_progress |
BootProgress |
get_boot_progress() -> str | None (a phase token, from a BMC's structured BootProgress) |
sensors |
Sensors |
read_sensors() -> dict |
serial_console |
SerialConsole |
serial_read(timeout=1.0) -> str, serial_write(data)
|
watchdog |
Watchdog |
watchdog_status() -> dict, watchdog_arm(timeout_s), watchdog_pet()
|
firmware_update |
FirmwareUpdate |
get_upgrade_status() -> dict, apply_firmware_update(*, image=None, dry_run=True) -> dict
|
ssh |
RemoteShell |
ssh_reachable() -> bool, ssh_exec(command, *, timeout=None) -> dict — a per-profile channel to the managed host's OS, not something the KVM implements (§8) |
VideoScope answers the question VIDEO cannot: will BIOS/POST/GRUB be in
the screenshot? FIRMWARE (AMT KVM, a BMC's own KVM) renders the host's
framebuffer, so firmware screens are visible. CAPTURE (PiKVM/GLKVM/BliKVM
over HDMI/DP) sees only what the host routes to that output — on laptops that
is nothing until an OS framebuffer starts (#210). NONE (IPMI, a Redfish BMC
without KVM) has no video; SOL may still give text.
The protocols are runtime_checkable, so isinstance(drv, Power) works, but
supports() is the intended check: it reflects what the driver declares, and
a declared capability on a device whose combo the support matrix has never
exercised live is still unverified — see Driver features.
The richest driver, grouped. All drivers share the capability methods above;
these are the extras KVMClient adds. Destructive ones are marked ⚡ (gated
by §4).
-
Session:
login() -> str,check_auth() -> bool,logout(),check_api_enabled() -> dict(raisesApiDisabledErroron GL with the API off),get_firmware_info() -> dict(the normalisedvendor/product/versionthe firmware registry joins on). -
Read:
get_info(fields=None),get_logs(seek=0, follow=False),get_metrics() -> str,get_streamer_state() -> dict,video_signal_info() -> dict,has_video_signal() -> bool,get_hid_state(),get_atx_state(),get_msd_state(),get_gpio_state(). -
Video:
snapshot() -> bytes,snapshot_save(path) -> Path,snapshot_base64(),snapshot_ocr(lang="eng", region=None) -> str(kvmd's server-side OCR),display_awake()— a context manager that holds the target display awake. A snapshot can fail while video is fine (GL headless/H.264 cases); the typedSnapshotFormatErrorandUnavailableErrortell the two apart, and Troubleshooting has the map. -
HID ⚡:
type_text(text, keymap="en-us", slow=False, delay=0.0),press_key(key, hold_ms=50),key_event(key, state),send_shortcut(keys),ctrl_alt_delete(),send_password(passwd, keymap="en-us"),enter_bios(key="F2", wait_s=3.0); mouse:mouse_move(x, y)(absolute kvmd units),mouse_move_pixels(...),mouse_move_percent(x_pct, y_pct),mouse_move_rel(dx, dy),mouse_click(button="left", hold_ms=50, double=False),mouse_scroll(delta_x=0, delta_y=-3);set_hid_params(...). Recovery:reset_hid(),recover_hid(timeout=5.0) -> bool,set_jiggler(active) -> dict. -
Power ⚡:
power_on,power_off,power_off_hard,reset_hard(allwait=True),atx_click(button="power", wait=True),hard_cycle(),is_powered_on(),wait_for_power_state(target, timeout=60, poll=2.0). GL units report ATX state unreliably — verify with the OS or the screen. -
Virtual media ⚡:
mount_iso(source, image_name=None, cdrom=True) -> str(local path or URL; verifies the medium reports online, elseMediaOfflineError),msd_upload_file(...),msd_upload_url(...),msd_set_params(...),msd_connect(),msd_disconnect(),msd_remove_image(name),msd_reset(). -
GPIO ⚡:
gpio_switch(channel, state, wait=True),gpio_pulse(channel, delay=0.1, wait=True). -
Events:
watch_events(on_event=None, stream=True, timeout=None)over the kvmd WebSocket (websocket-clientis a base dependency).
GLKVMDriver and BliKVMDriver subclass it and override only the deltas; GL
specifics (quirks, the on-demand encoder wake-up before snapshot, the
firmware-update surface) live in kvm_pilot.drivers.glkvm.
Every driver carries a SafetyPolicy at drv.safety, built from the
dry_run / confirm constructor arguments. Any operation that can change the
target's running state is in the explicit, auditable set
kvm_pilot.safety.DESTRUCTIVE_OPS (atx.*, wol.wake, msd.*, gpio.*,
hid.*, redfish.*, ipmi.*, amt.*, ssh.exec, …) and routes through
self.safety.guard(op, description) before anything is sent. Two independent
guards, evaluated in this order:
-
dry_run=True— the call is logged and skipped; the confirm callback is never invoked, so dry runs never prompt or block. Read-only calls always run. -
confirm(op, description) -> bool— consulted only for a call that would really be sent.Falseblocks it withSafetyError.
from kvm_pilot import SafetyPolicy, deny_all, interactive_confirm, SafetyError
from kvm_pilot.safety import allow_all, DESTRUCTIVE_OPS, effect_of, EffectClass
drv = make_driver("fake", confirm=deny_all)
try:
drv.power_on()
except SafetyError:
... # blocked, nothing sent
drv.safety.dry_run = True # flip at runtime; dry-run now wins
drv.power_on() # logged, skipped, no prompt
effect_of("atx.reset_hard") is EffectClass.POWER_HARD # TrueThe library default is allow_all so plain scripts work; the CLI installs
interactive_confirm (a y/N prompt that fails closed without a TTY) and the
MCP server installs its own per-call approval flow. Omitting confirm therefore
means unattended; pass deny_all, interactive_confirm, or a callback that
relays the question to a human.
EffectClass / effect_of(op) are the additive layer the MCP server uses to
pick the operator enable-flag by effect rather than transport (Ctrl+Alt+Del
is POWER_SOFT even though it travels over the keyboard); shortcut_effect(keys)
classifies a chord. DESTRUCTIVE_OPS stays the single source of truth for
"is this gated at all".
from kvm_pilot import run_healthcheck, preflight, HealthGateError, Severity
report = run_healthcheck(drv) # every applicable check, never raises
report.worst # Severity.OK | INFO | WARNING | CRITICAL
report.criticals # the CRITICAL CheckResults
report.to_dict() # JSON-safe (what `healthcheck --json` prints)
for r in report.results:
r.id, r.pillar, r.severity, r.title, r.detail, r.remediation, r.auto_fix
preflight(drv, confirm=None, skip=False, cache=None, enforce=True)run_healthcheck audits readiness (API/recovery path/HID/video), security
posture (TLS, default credentials, exposed services) and firmware (currency
against the bundled registry, capability profile, live evidence). Checks that
raise are reported, not propagated. preflight is the gate the destructive CLI
commands run first: stable checks come from cache when fresh (a firmware
change invalidates them and the report carries a firmware-delta), volatile
checks always run live, and an unacknowledged CRITICAL raises
HealthGateError unless enforce=False or skip=True. preflight_once
memoises per process. A CheckResult.auto_fix, when present, is an opt-in
remediation (AutoFix.apply(driver); safe_reversible says whether it may
be applied with per-item consent) — the audit itself never applies one.
All library errors derive from KVMPilotError (.status_code carries the HTTP
status when there was one). Catch the base for "the device said no", the
subclasses to react differently:
| exception | raised when |
|---|---|
AuthError |
401/403 — credentials, or a 2FA device without totp_secret
|
ConnectionError |
could not reach the device at all (DNS, refused, TLS) — shadows the builtin inside the package namespace on purpose |
TimeoutError |
an operation or wait loop exceeded its deadline (also shadows the builtin) |
BusyError |
409 / result.busy — the subsystem is mid-operation; retry later |
UnavailableError |
503 — streamer down, ATX board absent, encoder asleep |
ApiDisabledError |
every /api/* 404s (GL firmware ships the REST API off) |
SnapshotFormatError |
the snapshot endpoint returned non-JPEG bytes (GL raw H.264 at native resolution, #107) |
MediaOfflineError |
virtual media attached but the device reports it offline — the host sees nothing (#77) |
ProtocolError |
a non-JSON body where JSON was expected (truncated response); deliberately not retried |
CapabilityError |
the driver does not implement what you asked for (also: a required system binary such as ipmitool/ssh is missing) |
SafetyError |
the confirm callback denied a destructive op |
VisionError |
the vision backend failed or returned nothing usable; .retry_after carries a 429's Retry-After
|
HealthGateError |
preflight blocked on an unacknowledged CRITICAL |
The transport retries transient failures (busy/unavailable/network) up to
max_retries with backoff, and redacts secrets from logged URLs and bodies.
from kvm_pilot.vision import ScreenAnalyzer, make_backend, ALL_PHASES
backend = make_backend("anthropic") # model auto-resolved at runtime
backend = make_backend("local", base_url="http://127.0.0.1:1234/v1", model="qwen2.5-vl-7b")
analyzer = ScreenAnalyzer(
drv, backend,
default_poll_interval=3.0, min_confidence=0.70, on_state_change=None,
gate_on_power_signal=True, gate_on_boot_progress=True,
skip_unchanged_frames=True, ocr_rules=None,
)
state = analyzer.classify(hint="") # -> ScreenState
state.phase, state.description, state.confidence, state.raw_text, state.timestamp
analyzer.wait_for_state("grub_menu", timeout=120) # -> ScreenState, or TimeoutError
analyzer.wait_for_any_state(["login_prompt", "desktop"], timeout=600,
poll_interval=None, hint="", min_confidence=None,
consensus=2, on_poll=None)
analyzer.vlm_calls, analyzer.cheap_resolves # how often the gates avoided a model call-
Backends —
make_backend("anthropic" | "claude")buildsAnthropicBackend(api_key/modeloptional; the newest vision-capable model is resolved at runtime, never hard-coded);make_backend("local" | "openai" | "lmstudio" | "ollama" | "vllm", base_url=…, model=…)buildsOpenAICompatBackendfor any OpenAI-compatible server, so frames never leave your network. A backend is anything implementingVisionBackend:classify(image_b64, hint="") -> ScreenStateand amodelproperty. -
Phases —
ALL_PHASESis the closed vocabulary (no_signal,power_off,post_screen,bios_menu,uefi_shell,grub_menu,booting,login_prompt,desktop,os_running,installer_welcome,installer_partitioning,installer_progress,installer_complete,crash_screen,unknown);parse_classificationturns a model reply into aScreenState. -
Sensing hierarchy —
classify()tries the cheap signals first and only then the model: the driver's power signal (is_powered_on/has_video_signal→power_off/no_signal), a BMC's structuredget_boot_progress(), thensnapshot_ocragainstocr_rules(DEFAULT_OCR_RULESmaps "gnu grub" →grub_menu, "kernel panic" →crash_screen, "uefi interactive shell" →uefi_shell). An unchanged frame reuses the last actionable result. Passimage_b64=to classify a specific frame verbatim with the gates off. -
Consensus —
wait_for_any_staterequires the target phase onconsensusconsecutive polls (default 2) so one transient black frame or model misread cannot end the wait (#166). AVisionError429 honoursretry_after.
A vision classification is a sensor reading. It must never trigger a destructive action on its own — the gate in §4 exists so that the decision stays with the caller.
Two different machines, two channels, both in kvm_pilot.ssh, both driven by
the system ssh binary (no library dependency):
from kvm_pilot.ssh import SSHChannel, ApplianceChannel
ch = SSHChannel.from_config(cfg, confirm=None, dry_run=False) # the MANAGED HOST (cfg.ssh_host)
ch.ssh_reachable() # bool, read-only
ch.ssh_exec("uptime", timeout=30) # {command, returncode, stdout, stderr, ok, dry_run}; gated as ssh.exec
ap = ApplianceChannel.from_config(cfg) # the KVM APPLIANCE's own OS (root@<kvm-ip>, opt-in)
ap.loadavg(); ap.d_state_video_threads(); ap.reboot() # reboot is gatedmake_driver_from_config attaches the host channel to the driver as
drv.ssh_channel when the profile has ssh_host, which is what makes
Capability.SSH appear. discover_ssh_hosts(cidr, ...) is the opt-in subnet
sweep behind kvm-pilot ssh-discover.
With no driver pinned anywhere, make_driver_from_config runs cheap read-only
probes in verdict-priority order (kvm_pilot.detect.resolve_auto); the first
marker wins and the rest are skipped:
| verdict | probe | marker |
|---|---|---|
pikvm |
GET /api/info on the API port |
kvmd-style JSON (ok key) |
glkvm |
GET /api/upgrade/version (authenticated) |
GL's proprietary endpoint (GL self-reports as a Raspberry Pi PiKVM, so this is the only tell) |
redfish |
GET /redfish/v1/ |
RedfishVersion / odata |
amt |
GET / on 16992 (16993 with TLS) |
Intel AMT Server banner |
ipmi |
RMCP/ASF presence ping (UDP 623) | ASF pong |
Only when nothing matches is the full inventory assembled — including an SSH
banner probe on :22 — so the failure can say "this host answers SSH only, not
a KVM/BMC" instead of a misleading CRITICAL (#235). SSH never wins the verdict.
Pin driver= in the profile to skip the probe entirely.
Read-only helpers over the data shipped in the wheel, for tools that want to reason about what has been verified rather than what is declared:
from kvm_pilot.firmware_registry import load_registry # bundled registry (+ cache/override precedence)
from kvm_pilot.support_matrix import load_ledger, rollup # live run ledger -> per-combo evidence rows
from kvm_pilot.maturity import compute_maturity # alpha/beta/rc/ga from a combo's records
rows = rollup(vendor="gl.inet", product="RM1PE")The model, the promotion ladder and the ingestion pipeline are in Firmware registry; the per-driver view is Driver features; the community page it all renders to is the Hardware-Compatibility wiki list.
from kvm_pilot import make_driver, run_healthcheck, Severity, interactive_confirm
from kvm_pilot.vision import ScreenAnalyzer, make_backend
kvm = make_driver("glkvm", host="192.168.8.1", user="admin", passwd="secret",
confirm=interactive_confirm) # ask before every destructive step
report = run_healthcheck(kvm) # intake gate first, always
if report.worst is Severity.CRITICAL:
raise SystemExit("\n".join(r.title for r in report.criticals))
analyzer = ScreenAnalyzer(kvm, make_backend("anthropic"))
kvm.mount_iso("https://example.com/distro.iso") # prompts
kvm.hard_cycle() # prompts
analyzer.wait_for_state("grub_menu", timeout=120)
kvm.press_key("Enter") # prompts
analyzer.wait_for_state("installer_complete", timeout=1800)
kvm.close()Rehearse it first with dry_run=True on the real profile, or run it unchanged
against make_driver("fake", powered=True, phase="grub_menu") — the fake reports
that phase as structured boot progress, so the analyzer resolves it without a model call.
- Intel AMT onboarding runbook
- GLKVM onboarding runbook
- IPMI onboarding runbook
- Unattended Linux installs
- Remote firmware update
- Troubleshooting & FAQ
- CLI reference
- Configuration
- Python library reference
- Driver features
- Architecture
- Redfish reference
- Intel AMT vPro reference
- Firmware registry
- Claude skill
- Skill playbook: interfaces
- Skill playbook: recovery
- Skill playbook: setup & gates
- Skill playbook: Linux installs
- Skill playbook: target context
- Skill playbook: Python library
- MCP server
- Hardware compatibility