- ARRAY_START_SCRIPTS: add api_cache_writer.sh so monitor/arrs cache is populated immediately on array start (/tmp is tmpfs — cleared on reboot, so first-boot load was hitting live API for up to 60 seconds until the cron fired) - api/monitor.php + arrs.php: raise cache TTL from 90s to 300s — stale-but-instant beats a 6-second live API wait if the writer is momentarily behind - include/unraid_api.php: replace last inline uptime formatter with vv_format_uptime()
246 lines
11 KiB
PHP
246 lines
11 KiB
PHP
<?php
|
|
// Unraid GraphQL API — single-request master fetch + per-function fallback tracking.
|
|
// All API-first functions call vv_api_data() then fall back to local reads on null.
|
|
//
|
|
// Confirmed schema (introspected 2026-05-31, Unraid 7.3):
|
|
// InfoOs: hostname, uptime (String), release, kernel (NEW 7.3), arch (NEW 7.3)
|
|
// — also new but unused: fqdn, build, codename, platform, distro, serial, uefi
|
|
// — no version/uptime-as-int
|
|
// InfoCpu: brand, threads, cores — also new but unused: manufacturer, model, speed,
|
|
// speedmin, speedmax, processors, socket, topology, packages
|
|
// InfoMemory: layout only — NO usage fields; memory usage via metrics.memory
|
|
// ArrayDisk: used for parities/disks/caches (ArrayParity/ArrayCache named types removed in 7.3)
|
|
// fields: name, device, type (ArrayDiskType enum), status (ArrayDiskStatus enum),
|
|
// size (BigInt), fsSize, fsFree, fsUsed (BigInt), temp, transport (String),
|
|
// rotational, isSpinning, fsType (String) — also new: numReads, numWrites,
|
|
// numErrors (BigInt), idx, warning, critical, color, exportable, comment, format
|
|
// MemoryUtilization: percentTotal, total, used, available, swapTotal, swapUsed —
|
|
// also new: free, active, buffcache, swapFree, percentSwapTotal
|
|
// VmDomain: name, state (VmState enum) — no memory/vcpus
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
// ── Fallback tracking ─────────────────────────────────────────────────────────
|
|
|
|
function &_vv_api_fallbacks(): array { static $f = []; return $f; }
|
|
function &_vv_api_key_missing(): bool { static $m = false; return $m; }
|
|
|
|
function vv_api_record_fallback(string $fn): void {
|
|
$f = &_vv_api_fallbacks();
|
|
$f[] = $fn;
|
|
}
|
|
|
|
function vv_api_get_status(): array {
|
|
$fallbacks = array_unique(_vv_api_fallbacks());
|
|
return [
|
|
'available' => empty($fallbacks),
|
|
'key_missing' => _vv_api_key_missing(),
|
|
'fallbacks' => $fallbacks,
|
|
];
|
|
}
|
|
|
|
// ── Master fetch ──────────────────────────────────────────────────────────────
|
|
|
|
// Single combined query — one HTTP round-trip, cached for the request lifetime.
|
|
// Returns null if API is unreachable, key missing, or any query error occurs.
|
|
function vv_api_data(): ?array {
|
|
static $cache = null, $fetched = false;
|
|
if ($fetched) return $cache;
|
|
$fetched = true;
|
|
|
|
$hostId = vv_detect_host();
|
|
$vars = vv_conf_vars();
|
|
$key = $vars[strtoupper($hostId) . '_UNRAID_API_KEY'] ?? '';
|
|
if (!$key) {
|
|
$m = &_vv_api_key_missing();
|
|
$m = true;
|
|
$cache = null;
|
|
return null;
|
|
}
|
|
|
|
// Fields verified against live schema introspection (2026-05-31, Unraid 7.3).
|
|
// array.parities / .disks / .caches are SEPARATE lists — .disks is DATA only.
|
|
// ArrayParity/ArrayCache are no longer named types in 7.3 but the query structure is unchanged.
|
|
// metrics.cpu.percentTotal and metrics.memory.* provide real-time utilisation.
|
|
$gql = <<<'GQL'
|
|
{
|
|
info {
|
|
os { hostname uptime release kernel }
|
|
cpu { brand threads cores }
|
|
}
|
|
metrics {
|
|
cpu { percentTotal }
|
|
memory { percentTotal total used available swapTotal swapUsed }
|
|
}
|
|
array {
|
|
state
|
|
parities { name device type status size fsSize fsFree fsUsed temp transport rotational isSpinning }
|
|
disks { name device type status size fsSize fsFree fsUsed temp transport rotational isSpinning }
|
|
caches { name device type status size fsSize fsFree fsUsed temp transport rotational isSpinning }
|
|
}
|
|
vms {
|
|
domains { name state }
|
|
}
|
|
}
|
|
GQL;
|
|
|
|
$cache = vv_unraid_api_query($hostId, $gql, 5, $key);
|
|
return $cache;
|
|
}
|
|
|
|
// ── Disk data helpers ─────────────────────────────────────────────────────────
|
|
|
|
// API size fields (BigInt) are in bytes on this schema.
|
|
// Heuristic: if raw > 100 billion → bytes; else → KB (covers both possible encodings).
|
|
function _vv_api_bytes_to_gb(float $raw): float {
|
|
return $raw > 100_000_000_000
|
|
? round($raw / (1024 ** 3), 1)
|
|
: round($raw / (1024 ** 2), 1);
|
|
}
|
|
|
|
// Map ArrayDiskType enum → role string used by the rest of the plugin.
|
|
// Unraid 6.9 used CACHE; 6.10+ renamed pools to POOL. FLASH is the USB boot drive (skip).
|
|
// Anything unrecognised (not DATA/PARITY*/FLASH) is treated as a pool.
|
|
function _vv_api_disk_role(string $type): string {
|
|
$t = strtoupper($type);
|
|
if (str_contains($t, 'PARITY')) return 'parity';
|
|
if ($t === 'DATA') return 'data';
|
|
if ($t === 'FLASH') return 'flash'; // USB boot — excluded from both views
|
|
return 'cache'; // CACHE, POOL, or future variants
|
|
}
|
|
|
|
// Build a normalised disk entry from API data, matching the shape vv_disk_entry() produces.
|
|
// $role may be overridden; if empty it is derived from the disk's type field.
|
|
function vv_api_disk_entry(array $d, string $role = ''): ?array {
|
|
if (!$role) $role = _vv_api_disk_role((string)($d['type'] ?? 'DATA'));
|
|
|
|
if ($role === 'parity') {
|
|
// Parity disks have no filesystem — use raw size only.
|
|
$sizeRaw = (float)($d['size'] ?? 0);
|
|
if ($sizeRaw <= 0) return null;
|
|
$sizeGb = _vv_api_bytes_to_gb($sizeRaw);
|
|
$usedGb = 0.0;
|
|
$pct = null;
|
|
} else {
|
|
// Data/cache disks: prefer fsSize/fsUsed; fall back to size if unmounted.
|
|
$sizeRaw = (float)($d['fsSize'] ?? $d['size'] ?? 0);
|
|
if ($sizeRaw <= 0) return null;
|
|
$usedRaw = (float)($d['fsUsed'] ?? 0);
|
|
$sizeGb = _vv_api_bytes_to_gb($sizeRaw);
|
|
$usedGb = _vv_api_bytes_to_gb($usedRaw);
|
|
$pct = $sizeGb > 0 ? round($usedGb / $sizeGb * 100, 1) : null;
|
|
}
|
|
|
|
$temp = isset($d['temp']) && is_numeric($d['temp']) ? (int)$d['temp'] : null;
|
|
$spinning = (bool)($d['isSpinning'] ?? true);
|
|
$transport = $d['transport'] ?? (str_contains(strtolower($d['device'] ?? ''), 'nvme') ? 'nvme' : 'ata');
|
|
|
|
return [
|
|
'name' => $d['name'] ?? '',
|
|
'device' => $d['device'] ?? '',
|
|
'role' => $role,
|
|
'size_gb' => $sizeGb,
|
|
'used_gb' => $usedGb,
|
|
'pct' => $pct,
|
|
'temp' => $temp,
|
|
'transport' => strtolower($transport),
|
|
'mounted' => $spinning,
|
|
'status' => $d['status'] ?? 'DISK_OK',
|
|
];
|
|
}
|
|
|
|
// ── Node metrics extractor ────────────────────────────────────────────────────
|
|
|
|
// Parse CPU%, RAM, array storage, disk temps, and VM count from a raw API response.
|
|
// Used by vv_remote_hosts_stats() and the local vv_api_data() path — one parser, no duplication.
|
|
// GQL must include: metrics.cpu.percentTotal, metrics.memory.{total,used},
|
|
// array.{disks,caches,parities}.{fsSize,fsUsed,temp}, vms.domains.
|
|
function vv_api_node_metrics(?array $d): array {
|
|
if (!$d) return [];
|
|
$cpu = (int)round((float)($d['metrics']['cpu']['percentTotal'] ?? 0));
|
|
$mem = $d['metrics']['memory'] ?? [];
|
|
$ramUsed = isset($mem['used']) ? _vv_api_bytes_to_gb((float)$mem['used']) : null;
|
|
$ramTot = isset($mem['total']) ? _vv_api_bytes_to_gb((float)$mem['total']) : null;
|
|
|
|
$disks = $d['array']['disks'] ?? [];
|
|
$caches = $d['array']['caches'] ?? [];
|
|
$pars = $d['array']['parities'] ?? [];
|
|
$usedGb = 0.0; $totGb = 0.0;
|
|
foreach (array_merge($disks, $caches) as $dk) {
|
|
$sz = (float)($dk['fsSize'] ?? 0);
|
|
if ($sz <= 0) continue;
|
|
$totGb += _vv_api_bytes_to_gb($sz);
|
|
$usedGb += _vv_api_bytes_to_gb((float)($dk['fsUsed'] ?? 0));
|
|
}
|
|
$temps = array_filter(
|
|
array_merge(array_column($disks,'temp'), array_column($caches,'temp'), array_column($pars,'temp')),
|
|
fn($t) => is_numeric($t) && $t > 0
|
|
);
|
|
return [
|
|
'cpu_pct' => $cpu,
|
|
'ram_used_gb' => $ramUsed !== null ? round($ramUsed, 1) : null,
|
|
'ram_total_gb' => $ramTot !== null ? round($ramTot, 1) : null,
|
|
'array_used_tb' => $totGb > 0 ? round($usedGb / 1000, 1) : null,
|
|
'array_total_tb' => $totGb > 0 ? round($totGb / 1000, 1) : null,
|
|
'max_disk_temp' => $temps ? (int)max($temps) : null,
|
|
'vm_count' => count($d['vms']['domains'] ?? []),
|
|
];
|
|
}
|
|
|
|
// ── Local host stats snapshot — same shape as one entry from vv_remote_hosts_stats() ─────────
|
|
// Called locally and via SSH by remote_arr_cache_writer.sh on remote hosts.
|
|
function vv_local_host_stats(): array {
|
|
$host = vv_detect_host();
|
|
$myId = strtoupper($host);
|
|
$vars = vv_conf_vars();
|
|
$name = $vars[$myId] ?? gethostname();
|
|
|
|
$api = vv_api_data();
|
|
$metrics = vv_api_node_metrics($api);
|
|
|
|
if (!$api) {
|
|
return ['available' => false, 'host_id' => $myId, 'hostname' => $name,
|
|
'no_api_key' => _vv_api_key_missing()];
|
|
}
|
|
|
|
$os = $api['info']['os'] ?? [];
|
|
$cpu = $api['info']['cpu'] ?? [];
|
|
$mem = $api['metrics']['memory'] ?? [];
|
|
|
|
$memPct = round((float)($mem['percentTotal'] ?? 0));
|
|
if ($memPct === 0) {
|
|
$tot = (float)($mem['total'] ?? 0); $avail = (float)($mem['available'] ?? 0);
|
|
$memPct = $tot > 0 ? (int)round(($tot - $avail) / $tot * 100) : 0;
|
|
}
|
|
$memTotalGb = isset($mem['total']) ? _vv_api_bytes_to_gb((float)$mem['total']) : 0;
|
|
|
|
$uptimeRaw = $os['uptime'] ?? '';
|
|
if (is_numeric($uptimeRaw)) {
|
|
$s = (int)$uptimeRaw;
|
|
$uptime = vv_format_uptime($s);
|
|
} else {
|
|
$s = 0; $uptime = $uptimeRaw ?: '—';
|
|
}
|
|
|
|
return array_merge([
|
|
'available' => true,
|
|
'host_id' => $myId,
|
|
'hostname' => $os['hostname'] ?? $name,
|
|
'version' => $os['release'] ?? '',
|
|
'uptime' => $uptime,
|
|
'uptime_sec' => $s,
|
|
'cpu_load' => $metrics['cpu_pct'] ?? 0,
|
|
'cpu_threads' => (int)($cpu['threads'] ?? 0),
|
|
'mem_total_gb' => $memTotalGb,
|
|
'mem_used_pct' => $memPct,
|
|
'array_state' => $api['array']['state'] ?? 'UNKNOWN',
|
|
], $metrics);
|
|
}
|
|
|
|
// ── Confirmed schema (Unraid 7.3, introspected 2026-05-31) ───────────────────
|
|
// Adding a new host: add HOSTn="hostname" to master.conf and HOSTn_UNRAID_API_KEY
|
|
// to hostn.conf, then run Deployment/deploy.sh. No schema work needed.
|
|
//
|
|
// If a future Unraid version renames a field, the affected function falls back
|
|
// to local reads and the api banner lists the fallback — fix by updating the GQL.
|