Hot-reload sshHosts/knownDevices from hosts.yaml, no restart needed
Checks the file's mtime on each poll cycle (already running every 30s) rather than adding a separate file-watcher or admin UI. Proxmox hosts already needed no config (auto-discovered every poll); this covers the two lists that did. A parse failure logs and keeps the previous config running instead of crashing the poller. Closes #14. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+47
-11
@@ -1,8 +1,9 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
import { statSync } from "node:fs";
|
||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import fastifyCookie from "@fastify/cookie";
|
import fastifyCookie from "@fastify/cookie";
|
||||||
import fastifySession from "@fastify/session";
|
import fastifySession from "@fastify/session";
|
||||||
import { loadConfig } from "./config/index.js";
|
import { loadConfig, type AppConfig } from "./config/index.js";
|
||||||
import { openDb, upsertSnapshots, pruneOldSnapshots } from "./db/index.js";
|
import { openDb, upsertSnapshots, pruneOldSnapshots } from "./db/index.js";
|
||||||
import { seedAdminUser } from "./auth/local.js";
|
import { seedAdminUser } from "./auth/local.js";
|
||||||
import { ProxmoxCollector } from "./collectors/proxmox.js";
|
import { ProxmoxCollector } from "./collectors/proxmox.js";
|
||||||
@@ -17,15 +18,8 @@ import { initOidc } from "./auth/oidc.js";
|
|||||||
|
|
||||||
const HOSTS_CONFIG_PATH = process.env.HOSTS_CONFIG_PATH ?? "../../config/hosts.yaml";
|
const HOSTS_CONFIG_PATH = process.env.HOSTS_CONFIG_PATH ?? "../../config/hosts.yaml";
|
||||||
|
|
||||||
async function main() {
|
function buildCollectors(cfg: AppConfig): Collector[] {
|
||||||
const cfg = loadConfig(HOSTS_CONFIG_PATH);
|
return [
|
||||||
const db = openDb(cfg.dbPath);
|
|
||||||
|
|
||||||
if (cfg.adminPassword) {
|
|
||||||
seedAdminUser(db, cfg.adminUsername, cfg.adminPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectors: Collector[] = [
|
|
||||||
new ProxmoxCollector({
|
new ProxmoxCollector({
|
||||||
host: cfg.proxmox.host,
|
host: cfg.proxmox.host,
|
||||||
node: cfg.proxmox.node,
|
node: cfg.proxmox.node,
|
||||||
@@ -34,8 +28,50 @@ async function main() {
|
|||||||
}),
|
}),
|
||||||
...cfg.sshHosts.map((h) => new SshHostCollector(h)),
|
...cfg.sshHosts.map((h) => new SshHostCollector(h)),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const cfg = loadConfig(HOSTS_CONFIG_PATH);
|
||||||
|
const db = openDb(cfg.dbPath);
|
||||||
|
|
||||||
|
if (cfg.adminPassword) {
|
||||||
|
seedAdminUser(db, cfg.adminUsername, cfg.adminPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxmox-monitored hosts (the node + every LXC) auto-discover on every poll
|
||||||
|
// already — no config needed there. sshHosts and knownDevices, though, are
|
||||||
|
// declared in hosts.yaml and used to need a container restart to pick up
|
||||||
|
// changes. Re-checking the file's mtime on each poll cycle (already running
|
||||||
|
// every 30s) avoids that without adding a separate file-watcher/admin UI.
|
||||||
|
let knownDevices = cfg.knownDevices;
|
||||||
|
let collectors = buildCollectors(cfg);
|
||||||
|
let hostsConfigMtimeMs = statSync(HOSTS_CONFIG_PATH).mtimeMs;
|
||||||
|
|
||||||
|
function reloadHostsConfigIfChanged() {
|
||||||
|
let mtimeMs: number;
|
||||||
|
try {
|
||||||
|
mtimeMs = statSync(HOSTS_CONFIG_PATH).mtimeMs;
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mtimeMs === hostsConfigMtimeMs) return;
|
||||||
|
hostsConfigMtimeMs = mtimeMs;
|
||||||
|
try {
|
||||||
|
const reloaded = loadConfig(HOSTS_CONFIG_PATH);
|
||||||
|
collectors = buildCollectors(reloaded);
|
||||||
|
knownDevices = reloaded.knownDevices;
|
||||||
|
app.log.info(
|
||||||
|
{ sshHosts: reloaded.sshHosts.length, knownDevices: reloaded.knownDevices.size },
|
||||||
|
"config/hosts.yaml changed, reloaded"
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error({ err }, "failed to reload config/hosts.yaml, keeping previous config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function pollOnce() {
|
async function pollOnce() {
|
||||||
|
reloadHostsConfigIfChanged();
|
||||||
|
|
||||||
for (const collector of collectors) {
|
for (const collector of collectors) {
|
||||||
try {
|
try {
|
||||||
const snapshots = await collector.collect();
|
const snapshots = await collector.collect();
|
||||||
@@ -47,7 +83,7 @@ async function main() {
|
|||||||
pruneOldSnapshots(db, cfg.snapshotRetentionHours);
|
pruneOldSnapshots(db, cfg.snapshotRetentionHours);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await refreshDevices(db, cfg.discoveryFilePath, cfg.knownDevices);
|
await refreshDevices(db, cfg.discoveryFilePath, knownDevices);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
app.log.error({ err }, "device discovery refresh failed");
|
app.log.error({ err }, "device discovery refresh failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Editing monitored hosts without a redeploy
|
||||||
|
|
||||||
|
`config/hosts.yaml`'s `sshHosts` and `knownDevices` lists are re-read on the next poll
|
||||||
|
cycle (checks the file's mtime, reloads if changed) — no container restart needed.
|
||||||
|
|
||||||
|
- **Proxmox-monitored hosts** (the node + every LXC) need no config at all — they're
|
||||||
|
auto-discovered from the Proxmox API on every poll already.
|
||||||
|
- **SSH-monitored hosts** (`sshHosts`): edit `config/hosts.yaml` on the deploy host
|
||||||
|
directly (`/opt/homelab-monitor/config/hosts.yaml` on CT122), save, wait up to one
|
||||||
|
poll interval (`POLL_INTERVAL_SECONDS`, default 30s). Adding a *new* SSH host still
|
||||||
|
needs a one-time manual step first — installing the forced-command key on the target
|
||||||
|
(see `docs/ssh-collector-key-setup.md`) — that part can't be automated away, so a web
|
||||||
|
form wouldn't make this fully self-service regardless of hot-reload.
|
||||||
|
- **`knownDevices`** (device discovery labeling): same file, same reload path, no
|
||||||
|
manual step needed since it's just a label list.
|
||||||
|
|
||||||
|
If the file fails to parse (bad YAML), the error is logged and the **previous**
|
||||||
|
in-memory config keeps running rather than crashing the poller.
|
||||||
Reference in New Issue
Block a user