kvmfleet-bmc-adapters
Async Python library for out-of-band server management — Redfish across Dell / HPE / Supermicro / Lenovo / OpenBMC, IPMI for pre-Redfish hardware, smart PDUs (APC / Eaton / Raritan), and Wake-on-LAN. Vendor quirks absorbed so they don't bleed into your code.
Used in production by the hosted access-governance platform at kvmfleet.io. Apache 2.0.
Quick install
pip install kvmfleet-bmc-adapters # Redfish + WoL only
pip install 'kvmfleet-bmc-adapters[ipmi]' # + IPMI (pyghmi)
pip install 'kvmfleet-bmc-adapters[pdu]' # + PDU SNMP (pysnmp)
pip install 'kvmfleet-bmc-adapters[schema]' # + JSON-Schema validation (jsonschema)
pip install 'kvmfleet-bmc-adapters[all]' # everything
Command-line interface (v0.7.0+)
Installing the package puts a kvmfleet-bmc command on your PATH — an
operator CLI over the Redfish client for quick one-off checks and scripts:
# Inventory: make / model / serial / firmware
kvmfleet-bmc --host https://idrac.example.com -u root info
# Power state, then a graceful reboot (password from env, not the cmdline)
export BMC_PASSWORD=…
kvmfleet-bmc --host https://idrac.example.com -u root power status
kvmfleet-bmc --host https://idrac.example.com -u root power reboot
# Thermals as JSON, or a scriptable health check (exit 3 if not OK)
kvmfleet-bmc --host https://ilo.example.com -u admin --json thermal
kvmfleet-bmc --host https://ilo.example.com -u admin health || page-oncall
Power verbs: status, on, off, off-hard, cycle, reboot. TLS
verification is off by default (BMCs ship self-signed certs); pass
--verify-tls to enforce it. The CLI is a thin wrapper over RedfishClient
— anything it does, the library does too.
What this is
An async Python library that covers the operator-facing surface of out-of-band server management across four protocols:
| Protocol | Vendors / scope | Module |
|---|---|---|
| Redfish | Dell iDRAC, HPE iLO, Supermicro, Lenovo XCC, OpenBMC | bmc_adapters.RedfishClient |
| IPMI | pre-Redfish hardware (iDRAC6/7/8, iLO3/4, SMC X9/X10/X11, OEM Aspeed BMCs) | bmc_adapters.ipmi.IPMIClient |
| PDU | APC AP86xx/88xx/89xx, Eaton ePDU G4, Raritan PX2/3/4 (Legrand) | bmc_adapters.pdu.* |
| Wake-on-LAN | any NIC with magic-packet support enabled in BIOS | bmc_adapters.wake_on_lan |
Vendor quirks (cipher 17 vs cipher 3 negotiation, SDR cache bugs on SMC X9/X10, RAKP timing on iLO3, outlet 0-vs-1-indexing on Raritan, ATX power-LED state polling on PiKVM, ...) live inside the clients, not in your code.
Redfish — the original surface (v0.1.0+)
Covered operations across Dell iDRAC, HPE iLO, Supermicro, Lenovo XCC, and OpenBMC via the DMTF Redfish standard:
Heartbeat + identity
heartbeat()— power state + first temp + health rollupdetect_vendor()— vendor from Oem keys / Manager@odata.idsystem_info()— manufacturer, model, serial, asset tag, host name, UUID, BIOS + BMC firmware versions
Sensors
temperatures()— full thermal-sensor list with thresholdsfans()— RPM / PWM% per fanpower_supplies()— PSU model, capacity, input/output, statuspower_metrics()— chassis-aggregated consumed / avg / min / max
Hardware inventory
processor_inventory()— CPU socketsmemory_inventory()— DIMMsdrive_inventory(max_drives=None)— physical drives across all storage controllersvolume_inventory()— RAID / logical volumes (read-only)network_adapter_inventory()— host NICsfirmware_inventory()— firmware versions per component
Boot management
boot_config()— current one-time + persistent boot setupset_next_boot(target, mode="UEFI")— one-time overrideset_boot_order(devices)— persistent boot order
Power + virtual media + serial
power_action("on" / "off" / "off_hard" / "cycle" / "reboot")insert_virtual_media(url)/eject_virtual_media()nmi_trigger()— kernel-panic dump trigger
Logs
sel_entries(limit=100)— System Event Log / Lifecycle Log / IML — tries the standard SEL path first, falls back per vendorclear_sel()— clear the log
Network
network_info()— BMC NIC config + NTP + DNS
Chassis control + recovery
chassis_health()— per-subsystem health rollupindicator_led(state)— chassis locator LEDreset_bmc()— soft-reset the management controller
Read-only inventory
bmc_users()— list BMC accountslicense_info()— best-effort vendor license detection (iDRAC Express/Enterprise/Datacenter, iLO Standard/Advanced)
Plus session lifecycle handling — SessionService + Basic-auth fallback, token refresh, retry-on-401 — so the auth quirks don't bleed into your code.
Event streaming over Server-Sent Events (v0.7.0+)
When the BMC's Redfish EventService advertises an SSE endpoint, you can stream alerts (power, thermal, lifecycle) as they happen instead of polling the System Event Log:
async with RedfishClient(base_url="https://idrac.example", username="root", password="…") as bmc:
if await bmc.event_service_sse_uri(): # None when SSE is unsupported
async for ev in bmc.stream_events(): # runs until the BMC closes the stream
print(ev.severity, ev.message_id, ev.message)
else:
sel = await bmc.sel_entries(limit=50) # fall back to SEL polling
stream_events() yields RedfishEventRecords (message_id, message,
severity, event_type, event_timestamp, origin_of_condition, raw).
SSE support is vendor- and firmware-dependent — common on recent iDRAC9 /
iLO5 / OpenBMC, absent on older boxes — so check event_service_sse_uri()
first and keep sel_entries() polling as the fallback.
Fleet-scale collection reads (v0.7.0+)
Every collection the client walks (processors, memory, drives, volumes, NICs,
firmware, SEL, BMC users) follows Members@odata.nextLink, so a dense chassis
or a long event log doesn't silently lose members past the first page. To read
a whole collection in one round-trip instead of N+1, use $expand:
# All firmware components in a single $expand'd, paginated sweep
fw = await bmc.expand_collection("/redfish/v1/UpdateService/FirmwareInventory")
expand_collection() returns inlined member bodies when the service supports
$expand, and transparently falls back to plain paginated reads when it
doesn't (older firmware).
Validating responses (v0.7.0+)
Opt-in validation flags non-compliant BMC firmware. The structural-contract check is dependency-free and always available:
issues = await bmc.validate_resource("/redfish/v1/Systems/1")
for i in issues:
print(i.severity, i.field, i.message) # e.g. error @odata.id missing required @odata.id
It checks the Redfish contract — required @odata.id, well-formed
@odata.type, collection consistency, and legal Status / PowerState
enum values — and never raises. For strict DSP2046 conformance, pass a JSON
Schema you supply (we don't vendor the multi-MB DMTF bundle):
# pip install 'kvmfleet-bmc-adapters[schema]'
from bmc_adapters import validate_against_schema
issues = validate_against_schema(resource_dict, my_schema) # uses jsonschema
IPMI — pre-Redfish hardware (v0.4.0+)
For the long tail of pre-2018 servers Redfish doesn't reach. Wraps
pyghmi (Apache 2.0, used by OpenStack
Ironic) behind the same async surface as RedfishClient.
from bmc_adapters.ipmi import IPMIClient, IPMIConfig
async with IPMIClient(IPMIConfig(
host="bmc.example.com",
username="ADMIN",
password="...",
)) as bmc:
await bmc.power_action("cycle")
sensors = await bmc.sensors()
sel = await bmc.sel_entries(limit=50)
for finding in bmc.findings:
audit.append(finding.to_dict())
Secure-leaning defaults:
- Refuses IPMI 1.5 (
allow_ipmi_1_5=Falseby default). - Rejects a config that asks for cipher suites 0, 1, 2, 6, 7, 8, 11, 12
(the ones pen-test reports flag as auth-bypass / no-encryption /
weak-hash). Caveat, stated plainly: the actual RMCP+ session cipher
is negotiated by the underlying
pyghmilibrary against what the BMC offers — this adapter guards the config you pass, it does not override pyghmi's on-wire negotiation. If you need a hard cipher floor, enforce it on the BMC. - Default-credential detection without probing — constant-time compare against a documented vendor/user/password table; we never try the credential against the BMC, we just flag when the configured one matches a known default.
- Emits structured
BMCFindingrecords for default-credential matches and Pantsdown firmware windows (CVE-2019-6260) on AST2400 / AST2500 BMCs.
Smart PDU control (v0.4.0+)
When the BMC is dead or the NIC has hung, the operator reaches for the rack PDU. Async clients for the dominant vendors:
from bmc_adapters.pdu import APCPDUClient, RaritanPDUClient
# APC over SNMP (v2c or v3)
async with APCPDUClient("10.0.5.20", community="...") as pdu:
outlets = await pdu.list_outlets()
await pdu.outlet_cycle(3)
# Raritan via JSON-RPC over HTTPS
async with RaritanPDUClient(
"https://pdu-2.example.com",
username="admin",
password="...",
) as pdu:
await pdu.outlet_off("server-rack-7") # by name
Vendor coverage in v0.4.0:
| Vendor | Models | Protocol | Module |
|---|---|---|---|
| APC (Schneider) | AP86xx, AP88xx, AP89xx (NMC2 / NMC3) | SNMPv2c, SNMPv3 (PowerNet-MIB) | APCPDUClient |
| Eaton | ePDU G4 (Network-M2 / M3) | SNMPv2c, SNMPv3 (EATON-EPDU-MIB) | EatonPDUClient |
| Raritan / Legrand | PX2, PX3, PX4 | JSON-RPC over HTTPS | RaritanPDUClient |
Refuses SNMPv2c by default unless allow_snmpv2c=True is passed
(plaintext community strings are not okay on a shared management
network). Vendor auto-detect via SNMP sysObjectID available through
vendor_from_sysobjectid().
Wake-on-LAN (v0.4.0+)
For systems without a BMC at all (edge boxes, homelab, consumer boards). One function, no dependency:
from bmc_adapters import wake_on_lan
await wake_on_lan("aa:bb:cc:dd:ee:ff")
# or directed broadcast:
await wake_on_lan("aa:bb:cc:dd:ee:ff", broadcast="10.0.5.255")
Magic packets are L2-pattern-matched by the NIC firmware before the IP stack, so the destination port is cosmetic; we default to UDP/9.
Cross-protocol orchestration
The BMC orchestrator composes per-protocol adapters and dispatches
power actions across them. Borrowed from bmclib's
registry pattern.
from bmc_adapters import BMC, RedfishClient
from bmc_adapters.ipmi import IPMIClient, IPMIConfig
from bmc_adapters.pdu import APCPDUClient
async with (
RedfishClient(...) as redfish,
IPMIClient(IPMIConfig(...)) as ipmi,
APCPDUClient("10.0.5.20", community="...") as pdu,
):
bmc = BMC(redfish=redfish, ipmi=ipmi, pdu=pdu, pdu_outlet=3)
transport = await bmc.power_action("cycle")
# transport == "redfish" | "ipmi" | "pdu"
BMC.power_action walks Redfish → IPMI → PDU outlet-cycle in order
and returns the name of the transport that handled the action.
Security findings
Adapters emit structured BMCFinding records (default credentials matched,
Pantsdown firmware window on AST2400/2500, SNMPv2c plaintext on PDUs) that
callers can route into audit chains, SIEM events, or dashboards.
for finding in client.findings:
audit.append(finding.to_dict())
# {
# "code": "BMC_DEFAULT_CREDENTIALS_LIKELY",
# "severity": "high",
# "detail": "...",
# "cve": [],
# "vendor": "supermicro"
# }
What this is NOT
- Not a full Redfish client. We map the operations BMC operators reach for. If you need something we don't expose (vendor-specific Oem actions on resources we don't enumerate), go write that PR — the library is small enough to extend.
- Not RIBCL. RIBCL (older HPE iLO) is intentionally dropped — iLO 4
firmware ≥ 2.30 has working Redfish since 2016, and iLO 5/6 are
Redfish-first. (RACADM, Dell's CLI, is covered as of v0.6.0 — see
bmc_adapters.racadm.) - Not certified Redfish-conformant. Real BMC firmware ships bugs; the library absorbs them rather than pretending the spec is the world.
- No firmware updates, no BIOS attribute CRUD, no RAID configuration, no BMC-user CRUD. Each is vendor-quirk-hell with a different per-vendor shape; lumping them in would change the library's shape. They are intentionally out of scope.
Install
pip install kvmfleet-bmc-adapters
Requires Python 3.11+.
Quick start
import asyncio
from bmc_adapters import RedfishClient
async def main():
async with RedfishClient(
base_url="https://idrac.example.com",
username="root",
password="calvin",
) as client:
snap = await client.heartbeat()
print(snap)
# HeartbeatSnapshot(online=True, power_state='On', cpu_temp_c=51.5, health='OK')
info = await client.system_info()
print(info.manufacturer, info.model, info.bios_version)
for t in await client.temperatures():
if t.status not in ("OK", None):
print(f"unhealthy sensor: {t.name} = {t.reading_c}C ({t.status})")
for d in await client.drive_inventory():
if d.failure_predicted:
print(f"drive failure predicted: {d.name} ({d.model})")
await client.power_action("cycle")
await client.insert_virtual_media("https://my-iso-host/ubuntu.iso")
asyncio.run(main())
Keeping secrets encrypted at rest
The password argument accepts a string OR a zero-arg callable
(sync or async) returning a string. If you keep BMC creds encrypted
in your own secret store, hand the library a getter:
async def get_pw():
return await my_vault.decrypt(blob)
async with RedfishClient(
base_url=..., username=..., password=get_pw,
) as client:
...
The library calls the getter exactly once per login (not per request) and never stores or logs the plaintext.
TLS
verify_tls defaults to false because roughly 98% of
factory-shipped BMCs serve self-signed certs. The connection is
still TLS-encrypted; we just don't validate the leaf chain in that
default. Flip to true once you've pinned a real cert on the BMC:
RedfishClient(..., verify_tls=True)
If you need certificate-pinning (SPKI-pin enforcement), the
underlying httpx.AsyncClient exposes the hooks; we may add a
first-class verify_pin argument if there's demand.
Supported vendors
See docs/supported-vendors.md for the honest list of vendor + firmware version pairs. Tested in production: iDRAC 9 (4.40+), iLO 5, iLO 4 (2.78), Supermicro X11, Lenovo XCC2, OpenBMC. iDRAC 8 partially works (a virtual-media slot quirk). Everything else — including the recognised-not-verified vendors below — is fixture/mock-tested only. PRs adding a new pair (with a representative response fixture) are welcome.
detect_vendor() additionally recognises Cisco (CIMC/UCS),
Fujitsu (iRMC), Huawei (iBMC), and the AMI MegaRAC whitebox family
(Gigabyte, ASRock Rack, Tyan, Quanta, Inspur, and the generic
ami-megarac OEM stack) from their documented vendor signatures. These
speak standard Redfish, so the client's operations should work — but
they are not yet in the tested fleet, so treat them as recognised,
not verified. A fixture PR (or a pilot against real hardware) promotes
them to tested.
Honest caveats
Nonemeans "no signal", not "zero". Vendors partially implement the Redfish schema; the dataclass types returnNonefor any field the firmware leaves blank. Don't compare against0for sensor readings.predicted_life_left_percentis unreliable on consumer SSDs. Vendor support varies wildly; enterprise drives are honest, consumer drives often lie or omit the field.firmware_inventory()shape differs by vendor. iDRAC enumerates 30+ components (BIOS, BMC, each NIC, each drive, PSU FW); iLO is similar; Supermicro tends to expose fewer. Don't assume a fixed component set.license_info()is best-effort. iDRAC and iLO surface this through different Oem trees; other vendors usually return mostly-empty. The absence of license info doesn't mean the system is unlicensed.
Why does this exist
We needed multi-vendor BMC access for KVM Fleet's hosted access-governance platform. The DMTF Redfish standard is the right shape for the protocol layer, but real BMC firmware ships with vendor quirks (SessionService returning 204 No Content, MediaTypes missing on single-slot configurations, basic-auth-only when the spec says otherwise) that no library we found absorbed cleanly. So we wrote our own.
Open-sourcing it because:
- The protocol layer isn't where our value lives. Our value is the hosted access governance, audit chain, EU-resident retention, and the operational SLA on top.
- Anyone building tooling against multi-vendor BMCs hits the same vendor-quirks pit we did. No need for everyone to re-hit it.
- Open code means hostile reviewers can verify the auth flow, the retry logic, the TLS defaults. That trust transfer matters more to us than gatekeeping.
This is one of a series of OSS extractions from the KVM Fleet platform. See BUSINESS.md §N for the doctrine.
Comparison
- Sushy (OpenStack /
openstack/sushy): the reference multi-vendor Redfish client in Python (~15k LoC). Shaped for OpenStack Ironic. If you're standing up Ironic, use Sushy. - HPE python-ilorest-library: vendor-specific (iLO). Use it if you only run iLO and you want the full iLO surface including features we don't cover (firmware updates, BIOS attributes).
- DMTF Redfish-Tacklebox: reference scripts + toolkit from DMTF itself. A reference, not a library to vendor.
- check_redfish (bb-Ricardo): monitoring plugin. Solves a different shape — output to a monitoring system, not a Python library you import.
bmc-adapters sits in the gap: smaller and operator-shaped, not
OpenStack-shaped; multi-vendor, not iLO-only; library, not
monitoring plugin or script collection.
Coming soon
Shipped since earlier drafts of this README: the CLI (kvmfleet-bmc),
the IPMI module (bmc_adapters.ipmi), smart-PDU clients, RACADM,
Wake-on-LAN, and Redfish EventService SSE streaming. Still on the
list:
- Vendor contribution template for adding new BMC firmware to the test matrix without a maintainer review-cycle bottleneck.
- Certificate pinning (
verify_pin) as a first-class Redfish argument.
If you'd find either useful right now, open an issue saying so — real demand is what we prioritise on.
Contributing
Bug reports and patches welcome. The library is small enough that a serious contribution can be reviewed in a day. See docs/contributing.md for the local dev setup.
License
Apache 2.0 — see LICENSE. Copyright 2026 KVM Fleet.
Links
- KVM Fleet — the hosted platform this came from
- audit-verify — the OSS audit-chain verifier (BSL 1.1)
- agent — the OSS device agent (Apache 2.0)
- mcp — the read-only MCP server for AI assistants (MIT)
Metadata
Release files for kvmfleet-bmc-adapters 0.7.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kvmfleet_bmc_adapters-0.7.1.tar.gz | 95.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kvmfleet_bmc_adapters-0.7.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 181.1 kB
Release files / kvmfleet_bmc_adapters-0.7.1.tar.gz
| Download URL | kvmfleet_bmc_adapters-0.7.1.tar.gz |
|---|---|
| Size | 95.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8475f03b14988ef6eb52169ee4f6e79db5c0f9bead1bbebf08635bbefcc39470
|
|
BLAKE2b-256 checksum How to use checksums |
a0246958dfb2573df5b508a9b4c65cb81de814cbb2884afc8a01b8e6898f4059
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 24, 2026.
Transparency logRelease files / kvmfleet_bmc_adapters-0.7.1-py3-none-any.whl
| Download URL | kvmfleet_bmc_adapters-0.7.1-py3-none-any.whl |
|---|---|
| Size | 86.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4fc7dbb796f7ce067896c46f8a668c8d68de6977095cf9e61ef337e1ef23a48e
|
|
BLAKE2b-256 checksum How to use checksums |
303d8f66a3261aa311915485fbb3feb52dc825ed49838ecc402e988263265ab7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 24, 2026.
Transparency log