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.
by banshee1 installation1 version
Published on October 27, 2026. First version.
// 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);
}
}