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.

Virtualization

banshee/virtualization-collection/virtualization/proxmox/https/

backups

The number of backups kept and the age of the newest for each guest of a Proxmox VE cluster by its API.

  • virtualization
  • proxmox
  • pve
  • backup
  • vzdump
  • pbs
  • https
  • api

by banshee1 installation1 version

v1Latest

Published on October 27, 2026. First version.

Keen 1.0.0 · 114 lines
// The backups of the guests of a Proxmox VE cluster, or a single Proxmox VE server, by its API behind an API
// token: for each virtual machine and container, the number of backups kept and the age of the newest, read
// in the storage that holds backups (a directory, NFS, CIFS, a Proxmox Backup Server). A backup that failed
// leaves no archive: the age of the newest grows, and says it.
// Reads GET /api2/json/cluster/resources?type=storage, then GET
// /api2/json/nodes/<node>/storage/<storage>/content?content=backup for each backup storage (once for a
// shared one), then GET /api2/json/cluster/resources?type=vm for the guests.
// A guest is named by its id (VMID), as in proxmox/https/guests.
// The address is any node of the cluster, the host's: https://<host:address>:<port>/api2/json/.
need('protocol:http');
$address = need('host:address');
$token = need('protocol:http:token');
$port = variable('proxmox:port', 'integer', array(
    'label'   => 'API port',
    'help'    => 'The port of the Proxmox VE API: 8006, unless a proxy serves it on another.',
    'default' => 8006,
));

per('guest', 'Guests');
output('backups', 'number', array('label' => 'Backups kept', 'per' => 'guest'));
output('age', 'number', array('label' => 'Age of the newest backup', 'unit' => 'h', 'per' => 'guest'));
output('unread-storage', 'number', array('label' => 'Backup storage unread'));

watch.below('none', 'No backup', 'backups', array('critical' => 1));
watch.above('old', 'Newest backup too old', 'age', array('warning' => 30, 'critical' => 54));
watch.above('unread', 'Backup storage unread', 'unread-storage', array('warning' => 0));

// One GET, the token in the header Authorization: PVEAPIToken=<user>@<realm>!<token id>=<secret>. Proxmox VE
// answers {"data": ...}. The answer is not secret, its body having carried none.
function api_get($base, $path, $token) {
    $answer = http.get($base . $path, headers=array('Authorization' => 'PVEAPIToken=' . $token));
    if ($answer['status'] == 401) {
        fail('misconfigured', 'Proxmox VE refuses the API token, code 401');
    }
    if ($answer['status'] == 403) {
        fail('misconfigured', 'The API token may not read ' . $path . ', code 403');
    }
    if ($answer['status'] != 200) {
        fail('unreachable', 'Proxmox VE answers ' . $path . ' with code ' . $answer['status']);
    }
    $page = json.read($answer['body']);
    return $page['data'];
}

$base = 'https://' . $address . ':' . $port . '/api2/json';

// Each storage as each node sees it: {"storage": "nfs-backup", "node": "pve1", "status": "available",
// "shared": 1, "content": "backup,iso"}. A shared storage holds the same backups seen from any node: it is
// read once, from the first node that reads it. A local one is read on its node.
$storages = api_get($base, '/cluster/resources?type=storage', $token);
$count = array();
$newest = array();
$read = array();
$failed = array();
foreach ($storages as $storage) {
    if (!text.contains(',' . list.get($storage, 'content', '') . ',', ',backup,')) {
        continue;
    }
    $key = $storage['node'] . '/' . $storage['storage'];
    if (list.get($storage, 'shared', 0) == 1) {
        $key = $storage['storage'];
    }
    if (list.has($read, $key)) {
        continue;
    }
    if ($storage['status'] != 'available') {
        $failed[$key] = true;
        continue;
    }
    // [{"volid": "nfs-backup:backup/vzdump-qemu-101-2026_10_26-01_00_03.vma.zst", "vmid": 101, "ctime":
    // 1793062803, "size": 2147483648, "format": "vma.zst"}], ctime in seconds since 1970. A storage the node
    // cannot read now (a backup server down) answers another code: it is counted unread.
    $answer = http.get($base . '/nodes/' . url.encode($storage['node']) . '/storage/' . url.encode($storage['storage'])
        . '/content?content=backup', headers=array('Authorization' => 'PVEAPIToken=' . $token));
    if ($answer['status'] == 401) {
        fail('misconfigured', 'Proxmox VE refuses the API token, code 401');
    }
    if ($answer['status'] != 200) {
        $failed[$key] = true;
        continue;
    }
    $read[$key] = true;
    $page = json.read($answer['body']);
    foreach ($page['data'] as $volume) {
        $id = '' . list.get($volume, 'vmid', '');
        if ($id == '') {
            continue;
        }
        $count[$id] = list.get($count, $id, 0) + 1;
        if ($volume['ctime'] > list.get($newest, $id, 0)) {
            $newest[$id] = $volume['ctime'];
        }
    }
}

// A storage counts unread when no node could read it.
$unread = 0;
foreach ($failed as $key => $flag) {
    if (!list.has($read, $key)) {
        $unread = $unread + 1;
    }
}
value('unread-storage', $unread);

// The guests that exist now; the backups of a guest removed since are not returned.
$now = time.now();
$guests = api_get($base, '/cluster/resources?type=vm', $token);
foreach ($guests as $guest) {
    $id = '' . $guest['vmid'];
    value('backups', list.get($count, $id, 0), $id);
    if (list.has($newest, $id)) {
        value('age', number.round(($now - $newest[$id]) / 3600, 1), $id);
    }
}