Ops Notes

HPE iLO 6 Automation with Python: A Practical Guide to Redfish API for ProLiant Gen11

Infrastructure Visualization

Stop Clicking Through the Web UI

Look, HPE iLO 6’s web interface is decent. But if you’re managing dozens or hundreds of ProLiant Gen11 servers, clicking through each one is a waste of time and a recipe for human error. Last week we racked 30 DL380 Gen11s. Every single one needed iLO credentials changed, NTP and SNMP configured, and firmware updated. Doing that through the web UI? That’s a three-day slog.

iLO 6 fully supports the Redfish API — an industry standard defined by the DMTF, not some HPE-proprietary protocol. That means you can use standard HTTP REST calls and Python to manage everything at scale. And iLO 6 has Redfish enabled by default, so there’s no extra configuration needed.

Here’s the gotcha: a lot of tutorials online still point you to the python-hpilo library. That thing talks the old RIBCL protocol, which has been deprecated since iLO 5. For iLO 6, you must use Redfish. Don’t waste time on the old path.

Redfish API Architecture at a Glance

iLO 6’s Redfish implementation follows the standard resource tree structure:

graph TD
    A["/redfish/v1/"] --> B["Systems"]
    A --> C["Chassis"]
    A --> D["Managers (BMC)"]
    A --> E["EventService"]
    A --> F["AccountService"]
    A --> G["UpdateService"]
    B --> H["/Systems/1/ (Server)"]
    H --> I["Processors"]
    H --> J["Memory"]
    H --> K["Storage"]
    H --> L["EthernetInterfaces"]
    D --> M["/Managers/1/ (iLO)"]
    M --> N["NetworkProtocol"]
    M --> O["LogServices"]
    M --> P["FirmwareInventory"]

Every operation is a plain HTTP GET/POST/PATCH/DELETE request returning JSON. For authentication, the recommended approach is Session-based: POST credentials to get a token, then pass it in the X-Auth-Token header for subsequent requests. Basic Auth works too, but it’s less secure.

Environment Setup

Python 3.8+ is all you need. No heavy frameworks required.

pip install requests urllib3

HPE provides the python-ilorest-library, but honestly, it’s bloated and the documentation is terrible. I recommend using bare requests — it’s more controllable and easier to debug when things go wrong.

Real-World Example: Bulk Password Change

Here’s the scenario: 30 new servers, all with the same default iLO password. We need to generate unique random passwords for each.

import requests
import json
import secrets
import string
import time

# Disable SSL warnings (iLO uses self-signed certs)
requests.packages.urllib3.disable_warnings()

def generate_password(length=20):
    chars = string.ascii_letters + string.digits + "!@#$%^&*"
    return ''.join(secrets.choice(chars) for _ in range(length))

def change_ilo_password(ilo_ip, current_user, current_pass, new_password):
    """
    Change iLO admin password via Redfish API
    """
    base_url = f"https://{ilo_ip}/redfish/v1"
    
    # Step 1: Create a session
    session_payload = {
        "UserName": current_user,
        "Password": current_pass
    }
    
    try:
        session_resp = requests.post(
            f"{base_url}/SessionService/Sessions",
            json=session_payload,
            verify=False,
            timeout=10
        )
        session_resp.raise_for_status()
        
        auth_token = session_resp.headers.get("X-Auth-Token")
        if not auth_token:
            print(f"[{ilo_ip}] Failed to get auth token")
            return False
            
        headers = {
            "X-Auth-Token": auth_token,
            "Content-Type": "application/json"
        }
        
        # Step 2: Find the admin account URI
        accounts_resp = requests.get(
            f"{base_url}/AccountService/Accounts",
            headers=headers,
            verify=False,
            timeout=10
        )
        accounts_resp.raise_for_status()
        accounts_data = accounts_resp.json()
        
        target_account_uri = None
        for member in accounts_data.get("Members", []):
            acct_resp = requests.get(
                f"{base_url}{member['@odata.id']}",
                headers=headers,
                verify=False,
                timeout=10
            )
            acct = acct_resp.json()
            if acct.get("UserName") == current_user:
                target_account_uri = f"{base_url}{member['@odata.id']}"
                break
        
        if not target_account_uri:
            print(f"[{ilo_ip}] User {current_user} not found")
            requests.delete(session_resp.headers.get("Location"), headers=headers, verify=False)
            return False
        
        # Step 3: Change the password
        update_payload = {
            "Password": new_password
        }
        patch_resp = requests.patch(
            target_account_uri,
            json=update_payload,
            headers=headers,
            verify=False,
            timeout=10
        )
        
        if patch_resp.status_code in [200, 204]:
            print(f"[{ilo_ip}] Password changed successfully")
            
            # Verify the new password
            session_location = session_resp.headers.get("Location")
            if session_location:
                requests.delete(session_location, headers=headers, verify=False)
            
            test_resp = requests.post(
                f"{base_url}/SessionService/Sessions",
                json={"UserName": current_user, "Password": new_password},
                verify=False,
                timeout=10
            )
            if test_resp.status_code == 201:
                print(f"[{ilo_ip}] New password verified")
                test_headers = {"X-Auth-Token": test_resp.headers.get("X-Auth-Token")}
                requests.delete(test_resp.headers.get("Location"), headers=test_headers, verify=False)
                return True
            else:
                print(f"[{ilo_ip}] WARNING: New password verification failed! {test_resp.status_code}")
                return False
        else:
            print(f"[{ilo_ip}] Password change failed: {patch_resp.status_code} {patch_resp.text}")
            return False
            
    except requests.exceptions.RequestException as e:
        print(f"[{ilo_ip}] Request exception: {str(e)}")
        return False
    finally:
        if 'headers' in locals() and 'session_resp' in locals():
            session_location = session_resp.headers.get("Location")
            if session_location:
                try:
                    requests.delete(session_location, headers=headers, verify=False, timeout=5)
                except:
                    pass

# Bulk execution
ilo_list = [
    {"ip": "192.168.1.10", "user": "admin", "pass": "defaultpass"},
    # ... more iLOs
]

for ilo in ilo_list:
    new_pass = generate_password()
    success = change_ilo_password(ilo["ip"], ilo["user"], ilo["pass"], new_pass)
    if success:
        print(f"{ilo['ip']} -> {new_pass}")
        # Log this to a password vault or Hashicorp Vault
    time.sleep(1)

Key points about this code:

  1. Session lifecycle: Redfish requires explicit session creation and deletion. If you don’t delete sessions, you’ll exhaust the iLO’s session limit (default is 8).
  2. PATCH, not PUT: Use PATCH to change the password. Only send the fields you’re modifying.
  3. Verification: After changing the password, I always create a new session with the new password to confirm it works before deleting the old session.

Bulk Hardware Inventory

Another common need: collecting serial numbers, firmware versions, and memory configurations across your fleet.

def get_server_inventory(ilo_ip, username, password):
    """
    Collect key server hardware information
    """
    base_url = f"https://{ilo_ip}/redfish/v1"
    
    session_resp = requests.post(
        f"{base_url}/SessionService/Sessions",
        json={"UserName": username, "Password": password},
        verify=False,
        timeout=10
    )
    session_resp.raise_for_status()
    token = session_resp.headers.get("X-Auth-Token")
    headers = {"X-Auth-Token": token}
    
    inventory = {}
    
    try:
        sys_resp = requests.get(
            f"{base_url}/Systems/1",
            headers=headers,
            verify=False,
            timeout=10
        )
        sys_data = sys_resp.json()
        
        inventory["serial"] = sys_data.get("SerialNumber", "N/A")
        inventory["model"] = sys_data.get("Model", "N/A")
        inventory["bios_version"] = sys_data.get("BiosVersion", "N/A")
        inventory["cpu_count"] = len(sys_data.get("Processors", {}).get("Members", []))
        inventory["total_memory_gb"] = sys_data.get("MemorySummary", {}).get("TotalSystemMemoryGiB", 0)
        
        mgr_resp = requests.get(
            f"{base_url}/Managers/1",
            headers=headers,
            verify=False,
            timeout=10
        )
        mgr_data = mgr_resp.json()
        inventory["ilo_firmware"] = mgr_data.get("FirmwareVersion", "N/A")
        
        fw_resp = requests.get(
            f"{base_url}/Managers/1/FirmwareInventory",
            headers=headers,
            verify=False,
            timeout=10
        )
        fw_data = fw_resp.json()
        
        inventory["firmware_versions"] = {}
        for member in fw_data.get("Members", []):
            item_resp = requests.get(
                f"{base_url}{member['@odata.id']}",
                headers=headers,
                verify=False,
                timeout=10
            )
            item = item_resp.json()
            inventory["firmware_versions"][item.get("Name", item.get("Id", "unknown"))] = item.get("Version", "N/A")
        
        return inventory
        
    finally:
        requests.delete(session_resp.headers.get("Location"), headers=headers, verify=False, timeout=5)

info = get_server_inventory("192.168.1.10", "admin", "newpass123")
print(json.dumps(info, indent=2))

Sample output:

{
  "serial": "SGH1234ABC",
  "model": "ProLiant DL380 Gen11",
  "bios_version": "U41 v2.10 (02/10/2026)",
  "cpu_count": 2,
  "total_memory_gb": 512,
  "ilo_firmware": "iLO 6 v2.45",
  "firmware_versions": {
    "System ROM": "U41 v2.10",
    "iLO 6": "2.45",
    "Power Management Controller": "1.20",
    "Intelligent Platform Management Interface": "2.0",
    "Smart Array P408i-a SR Gen11": "8.20"
  }
}

Firmware Update Automation

iLO 6 firmware updates go through the Redfish UpdateService, supporting both HTTP and SMB sources. HTTP is simpler.

def update_ilo_firmware(ilo_ip, username, password, firmware_url):
    """
    Upgrade iLO firmware via HTTP source
    firmware_url: HTTP URL to the firmware file
    """
    base_url = f"https://{ilo_ip}/redfish/v1"
    
    session_resp = requests.post(
        f"{base_url}/SessionService/Sessions",
        json={"UserName": username, "Password": password},
        verify=False,
        timeout=10
    )
    session_resp.raise_for_status()
    token = session_resp.headers.get("X-Auth-Token")
    headers = {"X-Auth-Token": token, "Content-Type": "application/json"}
    
    try:
        update_payload = {
            "ImageURI": firmware_url,
            "UpdateTarget": "Manager",
            "TransferProtocol": "HTTP",
            "ForceUpdate": False
        }
        
        resp = requests.post(
            f"{base_url}/UpdateService/Actions/UpdateService.SimpleUpdate",
            json=update_payload,
            headers=headers,
            verify=False,
            timeout=30
        )
        
        if resp.status_code == 202:
            task_uri = resp.headers.get("Location")
            print(f"[{ilo_ip}] Firmware update task submitted: {task_uri}")
            
            while True:
                task_resp = requests.get(
                    task_uri,
                    headers=headers,
                    verify=False,
                    timeout=10
                )
                task_data = task_resp.json()
                state = task_data.get("TaskState", "")
                status = task_data.get("TaskStatus", "")
                
                print(f"[{ilo_ip}] Task state: {state} / {status}")
                
                if state == "Completed":
                    print(f"[{ilo_ip}] Firmware update complete! iLO will reboot.")
                    return True
                elif state in ["Exception", "Killed"]:
                    print(f"[{ilo_ip}] Firmware update failed: {task_data.get('Messages', [])}")
                    return False
                
                time.sleep(30)
        else:
            print(f"[{ilo_ip}] Failed to submit update: {resp.status_code} {resp.text}")
            return False
            
    finally:
        requests.delete(session_resp.headers.get("Location"), headers=headers, verify=False, timeout=5)

Important note: After a firmware update, iLO reboots and is unavailable for 3-5 minutes. If you’re doing bulk updates, run them serially or at staggered intervals.

Performance Comparison: Redfish vs Legacy Protocol

| Dimension | Redfish API (iLO 6) | Legacy RIBCL (python

Elvin Hui

About the Author: Elvin Hui

Elvin is a Senior Infrastructure Engineer with 10+ years of experience spanning data centers, cloud-native architecture, and network security. Certified in CCNA and AWS Solutions Architecture, I focus on turning real-world production "war stories" into actionable, hardcore technical guides.