Metadata-Version: 2.4
Name: kvmfleet-bmc-adapters
Version: 0.7.0
Summary: Async Python library for out-of-band server management — Redfish, IPMI, smart PDUs (APC / Eaton / Raritan), and Wake-on-LAN. Vendor quirks absorbed.
Project-URL: Homepage, https://github.com/KVMFleet/bmc-adapters
Project-URL: Repository, https://github.com/KVMFleet/bmc-adapters
Project-URL: Issues, https://github.com/KVMFleet/bmc-adapters/issues
Project-URL: KVM Fleet, https://kvmfleet.io
Author-email: KVM Fleet <support@kvmfleet.io>
License:                                  Apache License
                                   Version 2.0, January 2004
                                http://www.apache.org/licenses/
        
           TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
        
           1. Definitions.
        
              "License" shall mean the terms and conditions for use, reproduction,
              and distribution as defined by Sections 1 through 9 of this document.
        
              "Licensor" shall mean the copyright owner or entity authorized by
              the copyright owner that is granting the License.
        
              "Legal Entity" shall mean the union of the acting entity and all
              other entities that control, are controlled by, or are under common
              control with that entity. For the purposes of this definition,
              "control" means (i) the power, direct or indirect, to cause the
              direction or management of such entity, whether by contract or
              otherwise, or (ii) ownership of fifty percent (50%) or more of the
              outstanding shares, or (iii) beneficial ownership of such entity.
        
              "You" (or "Your") shall mean an individual or Legal Entity
              exercising permissions granted by this License.
        
              "Source" form shall mean the preferred form for making modifications,
              including but not limited to software source code, documentation
              source, and configuration files.
        
              "Object" form shall mean any form resulting from mechanical
              transformation or translation of a Source form, including but
              not limited to compiled object code, generated documentation,
              and conversions to other media types.
        
              "Work" shall mean the work of authorship, whether in Source or
              Object form, made available under the License, as indicated by a
              copyright notice that is included in or attached to the work
              (an example is provided in the Appendix below).
        
              "Derivative Works" shall mean any work, whether in Source or Object
              form, that is based on (or derived from) the Work and for which the
              editorial revisions, annotations, elaborations, or other modifications
              represent, as a whole, an original work of authorship. For the purposes
              of this License, Derivative Works shall not include works that remain
              separable from, or merely link (or bind by name) to the interfaces of,
              the Work and Derivative Works thereof.
        
              "Contribution" shall mean any work of authorship, including
              the original version of the Work and any modifications or additions
              to that Work or Derivative Works thereof, that is intentionally
              submitted to Licensor for inclusion in the Work by the copyright owner
              or by an individual or Legal Entity authorized to submit on behalf of
              the copyright owner. For the purposes of this definition, "submitted"
              means any form of electronic, verbal, or written communication sent
              to the Licensor or its representatives, including but not limited to
              communication on electronic mailing lists, source code control systems,
              and issue tracking systems that are managed by, or on behalf of, the
              Licensor for the purpose of discussing and improving the Work, but
              excluding communication that is conspicuously marked or otherwise
              designated in writing by the copyright owner as "Not a Contribution."
        
              "Contributor" shall mean Licensor and any individual or Legal Entity
              on behalf of whom a Contribution has been received by Licensor and
              subsequently incorporated within the Work.
        
           2. Grant of Copyright License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              copyright license to reproduce, prepare Derivative Works of,
              publicly display, publicly perform, sublicense, and distribute the
              Work and such Derivative Works in Source or Object form.
        
           3. Grant of Patent License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              (except as stated in this section) patent license to make, have made,
              use, offer to sell, sell, import, and otherwise transfer the Work,
              where such license applies only to those patent claims licensable
              by such Contributor that are necessarily infringed by their
              Contribution(s) alone or by combination of their Contribution(s)
              with the Work to which such Contribution(s) was submitted. If You
              institute patent litigation against any entity (including a
              cross-claim or counterclaim in a lawsuit) alleging that the Work
              or a Contribution incorporated within the Work constitutes direct
              or contributory patent infringement, then any patent licenses
              granted to You under this License for that Work shall terminate
              as of the date such litigation is filed.
        
           4. Redistribution. You may reproduce and distribute copies of the
              Work or Derivative Works thereof in any medium, with or without
              modifications, and in Source or Object form, provided that You
              meet the following conditions:
        
              (a) You must give any other recipients of the Work or
                  Derivative Works a copy of this License; and
        
              (b) You must cause any modified files to carry prominent notices
                  stating that You changed the files; and
        
              (c) You must retain, in the Source form of any Derivative Works
                  that You distribute, all copyright, patent, trademark, and
                  attribution notices from the Source form of the Work,
                  excluding those notices that do not pertain to any part of
                  the Derivative Works; and
        
              (d) If the Work includes a "NOTICE" text file as part of its
                  distribution, then any Derivative Works that You distribute must
                  include a readable copy of the attribution notices contained
                  within such NOTICE file, excluding those notices that do not
                  pertain to any part of the Derivative Works, in at least one
                  of the following places: within a NOTICE text file distributed
                  as part of the Derivative Works; within the Source form or
                  documentation, if provided along with the Derivative Works; or,
                  within a display generated by the Derivative Works, if and
                  wherever such third-party notices normally appear. The contents
                  of the NOTICE file are for informational purposes only and
                  do not modify the License. You may add Your own attribution
                  notices within Derivative Works that You distribute, alongside
                  or as an addendum to the NOTICE text from the Work, provided
                  that such additional attribution notices cannot be construed
                  as modifying the License.
        
              You may add Your own copyright statement to Your modifications and
              may provide additional or different license terms and conditions
              for use, reproduction, or distribution of Your modifications, or
              for any such Derivative Works as a whole, provided Your use,
              reproduction, and distribution of the Work otherwise complies with
              the conditions stated in this License.
        
           5. Submission of Contributions. Unless You explicitly state otherwise,
              any Contribution intentionally submitted for inclusion in the Work
              by You to the Licensor shall be under the terms and conditions of
              this License, without any additional terms or conditions.
              Notwithstanding the above, nothing herein shall supersede or modify
              the terms of any separate license agreement you may have executed
              with Licensor regarding such Contributions.
        
           6. Trademarks. This License does not grant permission to use the trade
              names, trademarks, service marks, or product names of the Licensor,
              except as required for describing the origin of the Work and
              reproducing the content of the NOTICE file.
        
           7. Disclaimer of Warranty. Unless required by applicable law or
              agreed to in writing, Licensor provides the Work (and each
              Contributor provides its Contributions) on an "AS IS" BASIS,
              WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
              implied, including, without limitation, any warranties or conditions
              of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
              PARTICULAR PURPOSE. You are solely responsible for determining the
              appropriateness of using or redistributing the Work and assume any
              risks associated with Your exercise of permissions under this License.
        
           8. Limitation of Liability. In no event and under no legal theory,
              whether in tort (including negligence), contract, or otherwise,
              unless required by applicable law (such as deliberate and grossly
              negligent acts) or agreed to in writing, shall any Contributor be
              liable to You for damages, including any direct, indirect, special,
              incidental, or consequential damages of any character arising as a
              result of this License or out of the use or inability to use the
              Work (including but not limited to damages for loss of goodwill,
              work stoppage, computer failure or malfunction, or any and all
              other commercial damages or losses), even if such Contributor
              has been advised of the possibility of such damages.
        
           9. Accepting Warranty or Support. While redistributing the Work or
              Derivative Works thereof, You may choose to offer, and charge a
              fee for, acceptance of support, warranty, indemnity, or other
              liability obligations and/or rights consistent with this License.
              However, in accepting such obligations, You may act only on Your
              own behalf and on Your sole responsibility, not on behalf of any
              other Contributor, and only if You agree to indemnify, defend,
              and hold each Contributor harmless for any liability incurred by,
              or claims asserted against, such Contributor by reason of your
              accepting any such warranty or support.
        
           END OF TERMS AND CONDITIONS
        
           APPENDIX: How to apply the Apache License to your work.
        
              To apply the Apache License to your work, attach the following
              boilerplate notice, with the fields enclosed by brackets "[]"
              replaced with your own identifying information. (Don't include
              the brackets!)  The text should be enclosed in the appropriate
              comment syntax for the file format. We also recommend that a
              file or class name and description of purpose be included on the
              same "printed page" as the copyright notice for easier
              identification within third-party archives.
        
           Copyright 2026 KVM Fleet
        
           Licensed under the Apache License, Version 2.0 (the "License");
           you may not use this file except in compliance with the License.
           You may obtain a copy of the License at
        
               http://www.apache.org/licenses/LICENSE-2.0
        
           Unless required by applicable law or agreed to in writing, software
           distributed under the License is distributed on an "AS IS" BASIS,
           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
           See the License for the specific language governing permissions and
           limitations under the License.
License-File: LICENSE
Keywords: apc,bmc,eaton,idrac,ilo,ipmi,lenovo-xcc,openbmc,out-of-band,pdu,raritan,redfish,snmp,supermicro,wake-on-lan,wol
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Hardware
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.11
Requires-Dist: httpx<1.0,>=0.27
Provides-Extra: all
Requires-Dist: asyncssh>=2.14; extra == 'all'
Requires-Dist: jsonschema>=4; extra == 'all'
Requires-Dist: pyghmi>=1.6.0; extra == 'all'
Requires-Dist: pysnmp<7,>=6.2; extra == 'all'
Provides-Extra: dev
Requires-Dist: mypy==2.1.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff==0.15.17; extra == 'dev'
Provides-Extra: ipmi
Requires-Dist: pyghmi>=1.6.0; extra == 'ipmi'
Provides-Extra: pdu
Requires-Dist: pysnmp<7,>=6.2; extra == 'pdu'
Provides-Extra: schema
Requires-Dist: jsonschema>=4; extra == 'schema'
Provides-Extra: ssh
Requires-Dist: asyncssh>=2.14; extra == 'ssh'
Description-Content-Type: text/markdown

# 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](https://kvmfleet.io). Apache 2.0.

## Quick install

```bash
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:

```bash
# 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 rollup
- `detect_vendor()` — vendor from Oem keys / Manager `@odata.id`
- `system_info()` — manufacturer, model, serial, asset tag, host
  name, UUID, BIOS + BMC firmware versions

**Sensors**
- `temperatures()` — full thermal-sensor list with thresholds
- `fans()` — RPM / PWM% per fan
- `power_supplies()` — PSU model, capacity, input/output, status
- `power_metrics()` — chassis-aggregated consumed / avg / min / max

**Hardware inventory**
- `processor_inventory()` — CPU sockets
- `memory_inventory()` — DIMMs
- `drive_inventory(max_drives=None)` — physical drives across all
  storage controllers
- `volume_inventory()` — RAID / logical volumes (read-only)
- `network_adapter_inventory()` — host NICs
- `firmware_inventory()` — firmware versions per component

**Boot management**
- `boot_config()` — current one-time + persistent boot setup
- `set_next_boot(target, mode="UEFI")` — one-time override
- `set_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 vendor
- `clear_sel()` — clear the log

**Network**
- `network_info()` — BMC NIC config + NTP + DNS

**Chassis control + recovery**
- `chassis_health()` — per-subsystem health rollup
- `indicator_led(state)` — chassis locator LED
- `reset_bmc()` — soft-reset the management controller

**Read-only inventory**
- `bmc_users()` — list BMC accounts
- `license_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:

```python
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 `RedfishEventRecord`s (`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`:

```python
# 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:

```python
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):

```python
# 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](https://opendev.org/x/pyghmi) (Apache 2.0, used by OpenStack
Ironic) behind the same async surface as `RedfishClient`.

```python
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 defaults:

- Refuses IPMI 1.5 (`allow_ipmi_1_5=False` by default).
- Refuses cipher suites 0, 1, 2, 6, 7, 8, 11, 12 — period.
- Prefers cipher 17 (SHA-256), falls back to cipher 3 (SHA-1).
- Default-credential detection without probing — constant-time
  compare against the documented vendor/user/password table.
- Emits structured `BMCFinding` records for cipher-0 acceptance,
  default-cred matches, 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:

```python
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:

```python
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](https://github.com/bmc-toolbox/bmclib)'s
registry pattern.

```python
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

All adapters emit structured `BMCFinding` records (cipher 0 acceptance,
default credentials matched, Pantsdown firmware window, SNMPv2c
plaintext on the wire, HTTP-without-TLS endpoints) that callers can
route into audit chains, SIEM events, or dashboards.

```python
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 / RACADM / SUM.** Vendor-specific pre-Redfish CLIs
  are a separate concern. RACADM (Dell) is on the v0.6.0 roadmap;
  RIBCL is intentionally dropped (iLO 4 firmware ≥ 2.30 has
  working Redfish since 2016, iLO 5/6 are Redfish-first).
- **Not a CLI.** (Maybe soon — see "Coming soon" below.)
- **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

```bash
pip install kvmfleet-bmc-adapters
```

Requires Python 3.11+.

## Quick start

```python
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:

```python
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:

```python
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](docs/supported-vendors.md) for the
honest list of vendor + firmware version pairs we've tested
against. The bulk of the test fleet is iDRAC 7/8/9; iLO 4/5 and
Supermicro are tested less heavily; Lenovo XCC and OpenBMC are
fixture-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

- **`None` means "no signal", not "zero".** Vendors partially
  implement the Redfish schema; the dataclass types return `None`
  for any field the firmware leaves blank. Don't compare against
  `0` for sensor readings.
- **`predicted_life_left_percent` is 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](https://kvmfleet.io)'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](https://github.com/KVMFleet/kvmfleet/blob/main/kvmfleet/BUSINESS.md)
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

These are real plans, not roadmap theatre:

- **CLI tool** (`kvmfleet-bmc`) wrapping the library for
  one-shot ops. Useful by itself; doubles as a usability test
  of the library.
- **Vendor contribution template** for adding new BMC firmware
  to the test matrix without a maintainer review-cycle
  bottleneck.
- **IPMI module** (`bmc_adapters.ipmi`) if there's demand —
  Supermicro power-control on older firmware still benefits
  from IPMI.
- **Event subscription API** — Redfish supports RedfishEvent
  (HTTP push of BMC alerts). Bigger ergonomic shape (callback
  URL, signed verification) so it sits in a follow-up.

If you'd find any of these 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](docs/contributing.md) for the local dev setup.

## License

Apache 2.0 — see [LICENSE](LICENSE). Copyright 2026 KVM Fleet.

## Links

- [KVM Fleet](https://kvmfleet.io) — the hosted platform this came from
- [audit-verify](https://github.com/KVMFleet/audit-verify) — the OSS
  audit-chain verifier (BSL 1.1)
- [agent](https://github.com/KVMFleet/agent) — the OSS device agent
  (Apache 2.0)
- [mcp](https://github.com/KVMFleet/mcp) — the read-only MCP server
  for AI assistants (MIT)
