Skip to main content

pyredfish

A small, dependency-light Python client for the Redfish server management API. Works against iDRAC, iLO, XClarity, OpenBMC and other BMCs, and includes support for the vendor-specific Oem/Public CollectAllLog and DownloadAllLog actions.

  • pyredfish/client.py — the RedfishClient class (the whole library)
  • pyredfish/cli.py — the pyredfish command line tool
  • examples/ — runnable example scripts

Requires Python 3.10+ and requests.

Install

pip install pyredfish

From a checkout:

python3 -m venv venv && source venv/bin/activate
pip install -e .

Installing puts a pyredfish command on your PATH; python -m pyredfish works too.

Quick start

from pyredfish import RedfishClient

with RedfishClient("https://10.0.0.5", "admin", "secret", verify=False) as rf:
    print(rf.system_info())
    print(rf.power_state())        # "On" / "Off"
    rf.power_on()

Or from the shell:

pyredfish -H 10.0.0.5 -u admin -p secret -k info

Connecting

RedfishClient(
    base_url=None,        # "https://10.0.0.5", or "10.0.0.5" (https:// is added)
    username=None,
    password=None,
    *,
    verify=True,          # True | False | "/path/ca.pem"
    timeout=30.0,         # seconds, per request
    use_session=True,     # False -> HTTP Basic Auth on every request
)

If base_url, username or password are omitted they are read from the REDFISH_URL, REDFISH_USER and REDFISH_PASSWORD environment variables:

import os
os.environ["REDFISH_URL"] = "10.0.0.5"
os.environ["REDFISH_USER"] = "admin"
os.environ["REDFISH_PASSWORD"] = "secret"

with RedfishClient(verify=False) as rf:
    print(rf.power_state())

TLS certificates

BMCs ship with self-signed certificates, so verify=False is the common case (urllib3's warning is silenced automatically). If you deployed your own CA, point at it instead and keep verification on:

RedfishClient("https://bmc.example.com", "admin", "secret", verify="/etc/ssl/bmc-ca.pem")

Sessions and tokens

Session handling lives entirely inside the class:

  • No explicit login needed. The first request opens a Redfish session (POST /redfish/v1/SessionService/Sessions) and stores the X-Auth-Token.
  • Automatic re-login. If the token expires and the BMC answers 401, the client logs in again and retries the request once. Bad credentials fail immediately instead of looping.
  • Clean logout. Leaving the with block (or calling logout()) deletes the session on the BMC. This matters: most BMCs allow only a handful of concurrent sessions and leaked ones lock you out until they time out.
  • After logout() the client refuses further calls rather than silently opening a new session. Call login() to reuse it.
rf = RedfishClient("10.0.0.5", "admin", "secret", verify=False)
print(rf.power_state())    # logs in on demand
rf.logout()                # session deleted on the BMC

rf.login()                 # explicit re-open
print(rf.power_state())
rf.logout()

Use use_session=False for BMCs whose SessionService is broken or disabled; every request then carries HTTP Basic Auth instead.


Usage examples

Inventory report

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    info = rf.system_info()
    print(f"{info['Manufacturer']} {info['Model']} (SN {info['SerialNumber']})")
    print(f"BIOS {info['BiosVersion']}, health {info['Health']}")
    print(f"{info['ProcessorCount']} x {info['ProcessorModel']}, "
          f"{info['MemoryGiB']} GiB RAM")

    for cpu in rf.processors():
        print(cpu["Id"], cpu.get("Model"), cpu.get("TotalCores"), "cores")

    for dimm in rf.memory():
        if dimm.get("CapacityMiB"):
            print(dimm["Id"], dimm["CapacityMiB"], "MiB",
                  dimm.get("Manufacturer"), dimm.get("PartNumber"))

    for nic in rf.ethernet_interfaces():
        print(nic["Id"], nic.get("MACAddress"), nic.get("IPv4Addresses"))

    for ctrl in rf.storage():
        for drive in ctrl.get("Drives", []):
            print("drive:", drive["@odata.id"])

Power control

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    if rf.power_state() == "Off":
        rf.power_on()

    rf.power_off()                 # GracefulShutdown — asks the OS
    rf.power_off(force=True)       # ForceOff — pulls the plug
    rf.restart()                   # GracefulRestart
    rf.restart(force=True)         # ForceRestart

    rf.reset("PowerCycle")         # any ResetType the BMC advertises

reset() checks the BMC's ResetType@Redfish.AllowableValues first and raises RedfishError for an unsupported type instead of sending a request that would fail. RedfishClient.RESET_TYPES lists the standard values.

Waiting for a power state

import time

def wait_for_power(rf, wanted, timeout=300, interval=5):
    deadline = time.monotonic() + timeout
    while rf.power_state() != wanted:
        if time.monotonic() > deadline:
            raise TimeoutError(f"still {rf.power_state()}, wanted {wanted}")
        time.sleep(interval)

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    rf.power_off(force=True)
    wait_for_power(rf, "Off")
    rf.power_on()
    wait_for_power(rf, "On")

One-shot PXE boot (reprovisioning)

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    print("supported targets:", rf.boot_options())

    rf.set_boot_override("Pxe", uefi=True)   # next boot only
    rf.restart(force=True)

persistent=True keeps the override for every boot (BootSourceOverrideEnabled = "Continuous"); omit uefi to leave the boot mode alone, or pass uefi=False for legacy BIOS mode. Other common targets are Hdd, Cd, Usb, BiosSetup and Utilities.

Mounting an ISO over virtual media

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    for device in rf.virtual_media():
        print(device["Id"], device.get("MediaTypes"), "inserted:",
              device.get("Inserted"))

    rf.insert_virtual_media("http://10.0.0.9/images/rescue.iso")
    rf.set_boot_override("Cd", uefi=True)
    rf.restart(force=True)

    # ... after the install ...
    rf.eject_virtual_media()

The CD device is found from the vendor profile and, failing that, from each device's MediaTypes — so the same call works on a BMC that calls it CD and on iLO, where it is device 2. Pass media_id= to force one, and manager_index= when the machine has more than one BMC.

Thermal and power draw

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    thermal = rf.thermal()
    for sensor in thermal.get("Temperatures", []):
        print(f"{sensor.get('Name'):<28} {sensor.get('ReadingCelsius')} C "
              f"(upper critical {sensor.get('UpperThresholdCritical')})")
    for fan in thermal.get("Fans", []):
        print(fan.get("Name"), fan.get("Reading"), fan.get("ReadingUnits"))

    for ctrl in rf.power().get("PowerControl", []):
        print("draw:", ctrl.get("PowerConsumedWatts"), "W",
              "| average:", ctrl.get("PowerMetrics", {}).get("AverageConsumedWatts"))
    for psu in rf.power().get("PowerSupplies", []):
        print(psu.get("Name"), psu.get("Status", {}).get("Health"),
              psu.get("LastPowerOutputWatts"), "W")

Firmware inventory

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    for item in rf.firmware_inventory():
        print(f"{item.get('Name'):<40} {item.get('Version')}")

Event log (SEL)

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    print("log services:", rf.log_service_ids())     # ["SEL"] / ["IML", "SL"]

    for entry in rf.log_entries(limit=20):           # vendor-aware default
        print(entry.get("Created"), entry.get("Severity"), entry.get("Message"))

    for entry in rf.log_entries(scope="manager", limit=20):
        print("BMC:", entry.get("Message"))

With no log_id, log_entries() tries the ids in the active vendor profile (SEL on most BMCs, IML/SL on HPE) and returns the first log it finds. Pass an explicit id to force one, scope="manager" for the BMC's own log, or limit=None for every entry.


Collecting all logs (OEM)

Many BMCs expose a bundle-everything action outside the Redfish standard:

POST /redfish/v1/Managers/1/LogServices/Actions/Oem/Public/CollectAllLog
POST /redfish/v1/Managers/1/LogServices/Actions/Oem/Public/DownloadAllLog

The client discovers those targets from the Actions.Oem block of the LogServices document and falls back to the paths above when the BMC does not advertise them, so it keeps working across firmware revisions.

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    # collect + download in one call; the file name comes from
    # the server's Content-Disposition header
    path = rf.download_all_log("./logs/")
    print("saved to", path)

Split into two steps when you want control over the wait:

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    task = rf.collect_all_log(timeout=1200)      # blocks until the BMC is done
    print("task state:", task.get("TaskState") if task else "no task returned")

    rf.download_all_log("./logs/bmc-10.0.0.5.tar.gz", collect_first=False)

Notes:

  • collect_all_log() returns a Task if the BMC answers 202 Accepted with a Location header, and polls it until it finishes (timeout=600, interval=5 by default). A failed task raises RedfishError carrying the BMC's own message.
  • The download is streamed in 1 MiB chunks, so a multi-hundred-megabyte bundle never has to fit in memory.
  • If dest is a directory (or ends with a path separator) the file name is taken from Content-Disposition, otherwise dest is used verbatim and its parent directories are created.
  • Firmware differs on the HTTP verb. method="auto" (the default) tries POST and falls back to GET on 400/404/405/501; force it with method="POST" or method="GET".
  • Some firmware wants a body ({"Type": "all"} and similar). Pass it through: rf.collect_all_log(payload={"Type": "all"}).

Bundling logs from many machines

import concurrent.futures, pathlib
from pyredfish import RedfishClient, RedfishError

HOSTS = ["10.0.0.5", "10.0.0.6", "10.0.0.7"]
OUT = pathlib.Path("./logs")
OUT.mkdir(exist_ok=True)

def grab(host):
    try:
        with RedfishClient(host, "admin", "secret", verify=False) as rf:
            target = OUT / host
            target.mkdir(exist_ok=True)
            return host, rf.download_all_log(str(target) + "/")
    except RedfishError as exc:
        return host, f"FAILED: {exc}"

with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
    for host, result in pool.map(grab, HOSTS):
        print(f"{host}: {result}")

Each thread gets its own client and therefore its own BMC session — do not share one RedfishClient across threads.



Vendor differences

Redfish is a standard, but every BMC bends it somewhere. pyredfish handles that in three layers, in this order:

  1. Discovery. Action targets, allowed reset types, log service ids and virtual media devices are read from the BMC's own documents. Most differences never reach the other two layers.
  2. Vendor profile. What discovery cannot answer is expressed as data — id candidates, OEM namespaces, HTTP verbs, reset preferences.
  3. Profile hooks. Only when a vendor does something structurally different (HPE's AHS dump) does the profile override behaviour with code.

An unrecognised BMC uses the generic profile and behaves exactly as before, so a new machine never fails just because it is unknown.

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    print(rf.profile.name)          # "hpe", "kaytus" or "generic"

Detection reads ServiceRoot.Vendor, the Oem keys and Manager.Manufacturer — never the hostname or IP. Pin it when detection guesses wrong:

RedfishClient("10.0.0.5", "admin", "secret", verify=False, vendor="hpe")
pyredfish -H 10.0.0.5 -u admin -k --vendor hpe info

Supported profiles

Profile Detected by What it changes
generic fallback Standard Redfish, Oem/Public OEM namespace
hpe Oem.Hpe / Oem.Hp, vendor string Log ids, virtual media numbering, AHS download
kaytus Oem.Public, KAYTUS/Inspur vendor string Oem/Public action namespace, log id order

HPE (iLO 4/5/6) — verified against HPE's Redfish documentation:

  • Event logs are not SEL: the system carries IML (Integrated Management Log) and SL (Security Log), the BMC carries IEL (iLO Event Log). rf.log_entries() picks the right one; rf.log_entries(scope="manager") reads the iLO log.
  • Virtual media devices are numbered, not named: 1 is the virtual floppy/USB, 2 is the virtual CD/DVD. rf.insert_virtual_media(url) finds the CD device without being told.
  • "Collect all logs" is the Active Health System dump, which is not a Redfish action but a query on the AHS resource. download_all_log() maps onto it, so the same call works on iLO and on everything else:
rf.download_all_log("./logs/")                       # whole AHS record
rf.download_all_log("./logs/", days=7)               # last 7 days
rf.download_all_log("./logs/", date_from="2026-08-01", date_to="2026-08-31")

KAYTUS (and the Inspur BMCs it descends from) — the toggle is the Oem/Public namespace, where the log bundle lives on the LogServices collection:

POST /redfish/v1/Managers/1/LogServices/Actions/Oem/Public/CollectAllLog
POST /redfish/v1/Managers/1/LogServices/Actions/Oem/Public/DownloadAllLog

collect_all_log() / download_all_log() use these, preferring the targets the BMC advertises in Actions.Oem and falling back to the paths above.

Reporting a new BMC

pyredfish probe prints exactly the structure a profile is written from — vendor strings, OEM keys, reset types, log service ids, virtual media ids and the OEM action block. Serial numbers, UUIDs, MACs, IPs and host names are redacted, so the output is safe to paste into an issue:

pyredfish -H 10.0.0.5 -u admin -k probe > my-bmc.json

Adding a profile

Subclass VendorProfile, set the fields that differ, and score matches() against the service root:

from pyredfish.vendors import VendorProfile, register

@register
class AcmeProfile(VendorProfile):
    name = "acme"
    system_log_ids = ("EventLog", "SEL")
    virtual_media_ids = ("Cd1",)
    oem_namespaces = ("Acme",)

    @classmethod
    def matches(cls, root, manager):
        return 10 if "Acme" in cls._oem_keys(root, manager) else 0

Third-party packages can ship profiles without touching this repository by declaring a pyredfish.vendors entry point:

[project.entry-points."pyredfish.vendors"]
acme = "pyredfish_acme:AcmeProfile"

Raw access

Every Redfish resource is reachable even when there is no helper for it:

with RedfishClient("10.0.0.5", "admin", "secret", verify=False) as rf:
    bios = rf.get("/redfish/v1/Systems/1/Bios")
    print(bios["Attributes"]["BootMode"])

    # staged BIOS change, applied on next reboot
    rf.patch("/redfish/v1/Systems/1/Bios/Settings",
             {"Attributes": {"BootMode": "Uefi", "ProcTurboMode": "Enabled"}})

    # any action
    rf.post("/redfish/v1/Managers/1/Actions/Manager.Reset",
            {"ResetType": "GracefulRestart"})

    # walk a collection
    for account in rf.members("/redfish/v1/AccountService/Accounts"):
        print(account.get("Id"), account.get("UserName"), account.get("RoleId"))

    rf.delete("/redfish/v1/SessionService/Sessions/12")

patch() sends If-Match: * by default; override it with headers={"If-Match": etag} when a BMC insists on a real ETag.

Long-running tasks

Actions that return 202 Accepted can be awaited with wait_for_task():

resp = rf.post("/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate",
               {"ImageURI": "http://10.0.0.9/fw/bios.bin",
                "TransferProtocol": "HTTP"})
task = rf.wait_for_task("/redfish/v1/TaskService/Tasks/3",
                        timeout=1800, interval=10)
print(task["TaskState"])

Error handling

from pyredfish import RedfishClient, RedfishError, RedfishAuthError

try:
    with RedfishClient("10.0.0.5", "admin", "wrong", verify=False) as rf:
        rf.power_on()
except RedfishAuthError as exc:
    print("check the credentials:", exc)
except RedfishError as exc:
    print("request failed:", exc)
    print("status:", exc.status_code)
    print("body:", exc.body)          # the BMC's parsed JSON error

RedfishError.status_code and .body carry the BMC's own response, and the message is built from Redfish's @Message.ExtendedInfo block so it reads like the vendor's own wording. RedfishAuthError is a subclass of RedfishError, raised on 401/403. Network-level problems surface as requests.exceptions.* (ConnectionError, Timeout) unchanged.


Command line

pyredfish -H 10.0.0.5 -u admin -p secret -k info
pyredfish -H 10.0.0.5 -u admin -k status              # prompts for the password
pyredfish -H 10.0.0.5 -u admin -k off --force
pyredfish -H 10.0.0.5 -u admin -k boot Pxe --uefi
pyredfish -H 10.0.0.5 -u admin -k restart -f
pyredfish -H 10.0.0.5 -u admin -k sel -n 50
pyredfish -H 10.0.0.5 -u admin -k download-log ./logs/
pyredfish -H 10.0.0.5 -u admin -k -j get /redfish/v1/Systems/1

Global options:

Option Meaning
-H, --host, --url BMC address or IP (env REDFISH_URL)
-u, --user user name (env REDFISH_USER)
-p, --password password (env REDFISH_PASSWORD); prompted if omitted
-k, --insecure skip TLS verification
--ca FILE verify against your own CA
--basic-auth use HTTP Basic Auth instead of a session
--timeout SEC per-request timeout (default 30)
-j, --json print raw JSON
-V, --version print the version and exit
--vendor NAME pin the vendor profile instead of detecting it

Commands: info, status, on, off, restart, boot, boot-options, nics, power-usage, firmware, sel, probe, collect-log, download-log, bmc-reset, get. Run pyredfish --help or pyredfish <command> --help for details. Exit codes: 0 success, 1 Redfish error, 2 bad arguments, 130 interrupted.

Credentials on the command line are visible in ps output — prefer the environment variables or the password prompt on shared machines:

export REDFISH_URL=10.0.0.5 REDFISH_USER=admin REDFISH_PASSWORD=secret
pyredfish -k info

API reference

RedfishClient

Area Methods
Session login(), logout(), context manager
Raw HTTP get(path), post(path, payload), patch(path, payload), delete(path)
Navigation service_root(), systems(), chassis(), managers(), system_uri(i), manager_uri(i), system(i), members(uri), member_uris(uri)
Power power_state(), power_on(), power_off(force=), restart(force=), reset(type)
Boot boot_options(), set_boot_override(target, persistent=, uefi=)
Inventory system_info(), processors(), memory(), ethernet_interfaces(), storage(), firmware_inventory()
Chassis thermal(i), power(i)
Logs log_entries(log_id, limit=), log_services_uri(i)
OEM logs collect_all_log(...), download_all_log(dest, ...)
Tasks wait_for_task(uri, timeout=, interval=)
Virtual media virtual_media(i), insert_virtual_media(url, ...), eject_virtual_media(...)
BMC reset_bmc(reset_type=)
Vendor profile, detect_vendor(), log_service_ids(scope=)

Every method that touches a system or chassis takes an index (or manager_index / chassis_index) argument, defaulting to 0 — the first resource in the collection. Multi-node chassis are addressed with rf.power_state(index=1) and friends.

system_info() returns a flat dict with the keys Id, Manufacturer, Model, SerialNumber, SKU, UUID, BiosVersion, PowerState, Health, State, ProcessorCount, ProcessorModel, MemoryGiB and HostName; missing fields come back as None rather than raising.

Examples directory

File What it shows
examples/inventory.py full hardware report for one host
examples/power_cycle.py power state machine with waiting
examples/pxe_reinstall.py one-shot PXE boot for reprovisioning
examples/virtual_media_install.py mount an ISO, boot it, eject
examples/collect_logs.py OEM CollectAllLog / DownloadAllLog
examples/fleet_report.py CSV inventory across many BMCs, in parallel
examples/health_monitor.py poll temperature, fans, power and SEL
examples/raw_bios.py raw GET/PATCH against BIOS attributes

Each script takes the BMC as its first argument, or falls back to REDFISH_URL; the credentials come from REDFISH_USER / REDFISH_PASSWORD and are prompted for when unset. They can be run from anywhere.

export REDFISH_USER=admin REDFISH_PASSWORD=secret

python3 examples/inventory.py 10.0.0.5
python3 examples/collect_logs.py 10.0.0.5 ./logs/
python3 examples/virtual_media_install.py 10.0.0.5 http://10.0.0.9/rescue.iso
python3 examples/raw_bios.py 10.0.0.5 BootMode Uefi
python3 examples/fleet_report.py hosts.txt > fleet.csv   # one host per line

examples/_common.py holds the shared connect-from-environment helper the other scripts import.

Compatibility notes

  • Redfish is a standard, but coverage varies. Anything under Oem — including CollectAllLog / DownloadAllLog — is vendor-specific and may not exist on your BMC.
  • The client always checks what the BMC advertises before sending an action (reset types, action targets, log service ids) and raises a clear RedfishError instead of failing obscurely.
  • BMC session limits are low (often 4–8). Always use the context manager or call logout() so sessions are not leaked.

License

GNU General Public License v3.0 or later — see LICENSE.

Releasing to PyPI

The project is packaged with pyproject.toml (setuptools backend); the version lives in pyredfish/__init__.py and everything else is derived from it.

pip install build twine

rm -rf dist/
python -m build          # builds dist/*.whl and dist/*.tar.gz
twine check dist/*       # validates the metadata PyPI will render

twine upload --repository testpypi dist/*   # rehearsal on TestPyPI
twine upload dist/*                         # the real thing

Publishing needs a PyPI account with 2FA enabled and an API token (PyPI no longer accepts passwords for uploads). Put it in ~/.pypirc, or export it:

export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-AgEIcHlwaS5vcmc...

A released version number can never be reused, so bump __version__ before every upload.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyredfish-0.1.0.tar.gz (51.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyredfish-0.1.0-py3-none-any.whl (41.8 kB view details)

Uploaded Python 3

File details

Details for the file pyredfish-0.1.0.tar.gz.

File metadata

  • Download URL: pyredfish-0.1.0.tar.gz
  • Upload date:
  • Size: 51.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for pyredfish-0.1.0.tar.gz
Algorithm Hash digest
SHA256 abd28e31cdd09f9d6e233f1916b95bf7036bca0c2af956bbfbb41289559de96d
MD5 893895a14416d0fda0e02702248496ed
BLAKE2b-256 b5c2e1eaf803040c5ef6c7f9888ac2f1ae494e42e1acfa9379359e548bac4df9

See more details on using hashes here.

File details

Details for the file pyredfish-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pyredfish-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 41.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for pyredfish-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bcaac63b2ab7b65f2331bb93277c7f53039566510f3eb5e3092836b618472768
MD5 384bff24e4337f8ae64af2352c71c884
BLAKE2b-256 a6bb0352509d094d8d94c93d028ee5919cddb071bfc6364927fae7d9ae1c9a6e

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page