Issue 1: finaler Agent-Job-Status

Management pollt jetzt offene Agent-Jobs und aktualisiert job_history mit:

  - finalem success / failed
  - Fehlertext
  - aktuellem/finalem Step
  - finishedAt
  - durationMs
  - Snapshot-ID aus den Agent-Logs, wenn ein Backup erfolgreich war

  Die Operations-Seite zeigt jetzt Dauer und Snapshot-ID.

  Issue 2: erweiterte Agent-Healthchecks
  /api/health am Agent prüft jetzt strukturiert:

  - fehlende Env-Konfiguration
  - Commands: incus, zfs, zpool, restic, udevadm, dd
  - ZFS Pool Existenz
  - ZFS Pool Capacity/Freespace via zpool list
  - /dev/zvol Zugriff
  - Restic Repository Zugriff
This commit is contained in:
Philipp
2026-05-21 10:27:04 +02:00
parent dc4989acfb
commit 87682a777f
8 changed files with 168 additions and 26 deletions
+62 -22
View File
@@ -1,5 +1,7 @@
import { Router } from 'express';
import { missingEnvVars } from '../config.js';
import { access } from 'node:fs/promises';
import { constants } from 'node:fs';
import { config, missingEnvVars } from '../config.js';
import { spawnCommand, runRestic } from '../executor.js';
export const healthRouter = Router();
@@ -7,35 +9,73 @@ export const healthRouter = Router();
healthRouter.get('/', async (_req, res) => {
const missing = missingEnvVars();
const checks = {
config: missing.length ? `missing: ${missing.join(', ')}` : 'ok',
incus: 'pending',
zfs: 'pending',
restic: 'pending',
config: checkValue(!missing.length, missing.length ? `missing: ${missing.join(', ')}` : 'ok'),
commands: await checkCommands(['incus', 'zfs', 'zpool', 'restic', 'udevadm', 'dd']),
zfsPool: await checkZfsPool(),
zvol: await checkZvolAccess(),
resticRepository: await checkResticRepository(),
};
await Promise.all([
checkCommand('incus', ['version']).then((value) => {
checks.incus = value;
}),
checkCommand('zfs', ['version']).then((value) => {
checks.zfs = value;
}),
runRestic(['snapshots', '--json'], { ignoreExitCode: true }).then((result) => {
checks.restic = result.exitCode === 0 ? 'ok' : result.stderr.trim() || 'failed';
}).catch((error) => {
checks.restic = error.message;
}),
]);
const ok = Object.values(checks).every((value) => value === 'ok');
const ok = Object.values(checks).every((value) => value.ok);
res.status(ok ? 200 : 503).json({ ok, checks });
});
async function checkCommands(commands) {
const results = {};
await Promise.all(commands.map(async (command) => {
results[command] = await checkCommand(command, ['--version']);
}));
const failed = Object.entries(results).filter(([, result]) => !result.ok);
return {
ok: failed.length === 0,
message: failed.length ? `failed: ${failed.map(([command]) => command).join(', ')}` : 'ok',
details: results,
};
}
async function checkCommand(command, args) {
try {
const result = await spawnCommand(command, args, { ignoreExitCode: true });
return result.exitCode === 0 ? 'ok' : result.stderr.trim() || 'failed';
return checkValue(result.exitCode === 0, result.exitCode === 0 ? 'ok' : result.stderr.trim() || result.stdout.trim() || 'failed');
} catch (error) {
return error.message;
return checkValue(false, error.message);
}
}
async function checkZfsPool() {
if (!config.zfsPoolName) return checkValue(false, 'ZFS_POOL_NAME is not configured');
try {
const result = await spawnCommand('zpool', ['list', '-Hp', '-o', 'name,size,alloc,free,cap', config.zfsPoolName], { ignoreExitCode: true });
if (result.exitCode !== 0) return checkValue(false, result.stderr.trim() || 'zpool list failed');
const [name, size, allocated, free, capacity] = result.stdout.trim().split('\t');
return {
ok: true,
message: 'ok',
details: { name, size: Number(size), allocated: Number(allocated), free: Number(free), capacity },
};
} catch (error) {
return checkValue(false, error.message);
}
}
async function checkZvolAccess() {
try {
await access('/dev/zvol', constants.R_OK);
return checkValue(true, 'ok');
} catch (error) {
return checkValue(false, error.message);
}
}
async function checkResticRepository() {
try {
const result = await runRestic(['snapshots', '--json'], { ignoreExitCode: true });
return checkValue(result.exitCode === 0, result.exitCode === 0 ? 'ok' : result.stderr.trim() || 'restic repository check failed');
} catch (error) {
return checkValue(false, error.message);
}
}
function checkValue(ok, message, details = null) {
return { ok, message, ...(details ? { details } : {}) };
}