banshee is under active development: collectors will keep changing, and a version you publish will likely need adjustments. For stable use, wait for version 1.0.0.

Hardware

banshee/hardware-collection/hardware/redfish/https/

storage

The health of each storage controller, drive and volume of a server, failure predictions and SSD wear, by Redfish.

  • hardware
  • server
  • redfish
  • bmc
  • raid
  • drive
  • volume
  • ssd
  • https

by banshee1 installation1 version

v1Latest

Published on October 27, 2026. First version.

Keen 1.0.0 · 141 lines
// The storage of a server, by the Redfish service of its management card (DMTF Redfish): the health of each
// storage controller, of each drive and of each volume, whether a drive predicts its own failure, and the
// remaining life of an SSD. Any card that serves Redfish: Dell iDRAC, HPE iLO, Supermicro, Lenovo XCC,
// OpenBMC.
need('protocol:http');
$address = need('host:address');
$user = need('protocol:http:user');
$password = need('protocol:http:password');

per('component', 'Components');
output('health', 'text', array('label' => 'Health', 'per' => 'component'));
output('kind', 'text', array('label' => 'Kind', 'per' => 'component'));
output('failure-predicted', 'text', array('label' => 'Failure predicted', 'per' => 'component'));
output('remaining-life', 'number', array('label' => 'Remaining life', 'unit' => '%', 'per' => 'component'));

watch.state('unhealthy', 'Health degraded', 'health', array('warning' => array('warning'), 'critical' => array('critical')));
watch.state('failure-predicted', 'Drive failure predicted', 'failure-predicted', array('warning' => array('yes')));
watch.below('worn', 'Drive worn out', 'remaining-life', array('warning' => 10, 'critical' => 3));
watch.missing('missing', 'Component missing', 'health', array('enabled' => false));

// Returns one Redfish resource, read by its path (/redfish/v1/...), the account in Basic. The password only
// goes in the Basic header: the answer is not secret.
function redfish_get($address, $path, $user, $password) {
    $answer = http.get('https://' . $address . $path, headers=array('Accept' => 'application/json'), user=$user, password=$password);
    if ($answer['status'] == 401 || $answer['status'] == 403) {
        fail('misconfigured', 'The Redfish service refuses the account, code ' . $answer['status'], $path);
    }
    if ($answer['status'] != 200) {
        fail('unreachable', 'The Redfish service answers code ' . $answer['status'], $path);
    }
    return json.read($answer['body']);
}

// Returns the own health of a resource, in lower case (ok, warning, critical), unknown when it gives none.
// Its own, not the one it rolls up: a controller would otherwise raise again for each of its drives.
function health_of($resource) {
    $health = list.get(list.get($resource, 'Status', array()), 'Health', '');
    if ($health == '') {
        return 'unknown';
    }
    return text.lower($health);
}

// Returns true when the card says the resource absent: an empty bay.
function is_absent($resource) {
    return list.get(list.get($resource, 'Status', array()), 'State', '') == 'Absent';
}

// Returns the instance name of each item: its name, or, when another item bears the same name, its name
// followed by its id.
function instance_names($items) {
    $counts = array();
    foreach ($items as $item) {
        $counts[$item['name']] = list.get($counts, $item['name'], 0) + 1;
    }
    $names = array();
    foreach ($items as $key => $item) {
        $names[$key] = $item['name'];
        if ($item['name'] == '' || $counts[$item['name']] > 1) {
            $names[$key] = text.trim($item['name'] . ' ' . $item['id']);
        }
        // A card that gives neither a name nor an id (both null, read as '').
        if ($names[$key] == '') {
            $names[$key] = 'unnamed ' . ($key + 1);
        }
    }
    return $names;
}

// Each controller, drive and volume, as array('name', 'id', 'kind', 'health'), and for a drive what it says
// of its failure and of its life.
$items = array();
$systems = redfish_get($address, '/redfish/v1/Systems', $user, $password);
foreach (list.get($systems, 'Members', array()) as $member) {
    $system = redfish_get($address, $member['@odata.id'], $user, $password);
    if (!list.has($system, 'Storage')) {
        continue;
    }
    // A storage resource is a storage subsystem, most often one controller: {"Id": "RAID.Integrated.1-1",
    // "Name": "PERC H755 Front", "Status": {"Health": "OK", "HealthRollup": "Warning"}, "Drives":
    // [{"@odata.id": "..."}], "Volumes": {"@odata.id": "..."}}.
    $storages = redfish_get($address, $system['Storage']['@odata.id'], $user, $password);
    foreach (list.get($storages, 'Members', array()) as $link) {
        $storage = redfish_get($address, $link['@odata.id'], $user, $password);
        $storage_id = list.get($storage, 'Id', $link['@odata.id']);
        $items = list.add($items, array('name' => list.get($storage, 'Name', $storage_id), 'id' => $storage_id,
            'kind' => 'controller', 'health' => health_of($storage)));

        // A drive: {"Id": "0", "Name": "Disk 0", "MediaType": "SSD", "FailurePredicted": false,
        // "PredictedMediaLifeLeftPercent": 97, "Status": {"State": "Enabled", "Health": "OK"}}.
        foreach (list.get($storage, 'Drives', array()) as $drive_link) {
            $drive = redfish_get($address, $drive_link['@odata.id'], $user, $password);
            if (is_absent($drive)) {
                continue;
            }
            $item = array('name' => list.get($drive, 'Name', 'Drive'), 'id' => list.get($drive, 'Id', ''),
                'kind' => 'drive', 'health' => health_of($drive));
            // true or false; null, read as '', when the drive does not say: then not returned.
            $predicted = json.write(list.get($drive, 'FailurePredicted', ''));
            if ($predicted == 'true') {
                $item['failure'] = 'yes';
            } elseif ($predicted == 'false') {
                $item['failure'] = 'no';
            }
            if (list.get($drive, 'PredictedMediaLifeLeftPercent', '') != '') {
                $item['life'] = $drive['PredictedMediaLifeLeftPercent'];
            }
            $items = list.add($items, $item);
        }

        // A volume: {"Id": "Disk.Virtual.0", "Name": "System", "RAIDType": "RAID1", "Status": {...}}.
        if (list.has($storage, 'Volumes')) {
            $volumes = redfish_get($address, $storage['Volumes']['@odata.id'], $user, $password);
            foreach (list.get($volumes, 'Members', array()) as $volume_link) {
                $volume = redfish_get($address, $volume_link['@odata.id'], $user, $password);
                if (is_absent($volume)) {
                    continue;
                }
                $items = list.add($items, array('name' => list.get($volume, 'Name', 'Volume'),
                    'id' => list.get($volume, 'Id', ''), 'kind' => 'volume', 'health' => health_of($volume)));
            }
        }
    }
}

if (list.count($items) == 0) {
    fail('misconfigured', 'The Redfish service serves no storage',
        'No system links to a Storage collection, or the collection is empty.');
}
$names = instance_names($items);
foreach ($items as $key => $item) {
    $name = $names[$key];
    value('health', $item['health'], $name);
    value('kind', $item['kind'], $name);
    if (list.has($item, 'failure')) {
        value('failure-predicted', $item['failure'], $name);
    }
    if (list.has($item, 'life')) {
        value('remaining-life', $item['life'], $name);
    }
}