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— theRedfishClientclass (the whole library)pyredfish/cli.py— thepyredfishcommand line toolexamples/— 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 theX-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
withblock (or callinglogout()) 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. Calllogin()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 answers202 Acceptedwith aLocationheader, and polls it until it finishes (timeout=600,interval=5by default). A failed task raisesRedfishErrorcarrying 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
destis a directory (or ends with a path separator) the file name is taken fromContent-Disposition, otherwisedestis 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 on400/404/405/501; force it withmethod="POST"ormethod="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:
- 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.
- Vendor profile. What discovery cannot answer is expressed as data — id candidates, OEM namespaces, HTTP verbs, reset preferences.
- 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 carriesIML(Integrated Management Log) andSL(Security Log), the BMC carriesIEL(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:
1is the virtual floppy/USB,2is 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— includingCollectAllLog/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
RedfishErrorinstead 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
abd28e31cdd09f9d6e233f1916b95bf7036bca0c2af956bbfbb41289559de96d
|
|
| MD5 |
893895a14416d0fda0e02702248496ed
|
|
| BLAKE2b-256 |
b5c2e1eaf803040c5ef6c7f9888ac2f1ae494e42e1acfa9379359e548bac4df9
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bcaac63b2ab7b65f2331bb93277c7f53039566510f3eb5e3092836b618472768
|
|
| MD5 |
384bff24e4337f8ae64af2352c71c884
|
|
| BLAKE2b-256 |
a6bb0352509d094d8d94c93d028ee5919cddb071bfc6364927fae7d9ae1c9a6e
|