Some checks are pending
offline / test (push) Waiting to run
NS1 is the Proxmox host. verae-proxmox creates LXC 510 (verae-px-worker 10.10.10.20 on vmbr1) with a private NATS proxy on 10.10.10.1:4222. verae-uptime GET-watches public doors; verae-backup snapshots SQLite and worm/tree data; verae-deploy does host-deps + checkout + npm ci. Fleet overlays/ns1 are checked in (start.sh no longer rewrites JSON). User systemd + linger for keep and fleet survive reboot.
117 lines
3.9 KiB
JavaScript
117 lines
3.9 KiB
JavaScript
/**
|
|
* Host catalog: where replicas may run. Least-loaded placement.
|
|
* @module machines
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
export function defaultMachines() {
|
|
return [
|
|
{
|
|
id: 'local',
|
|
title: 'Control plane (this host)',
|
|
kind: 'local',
|
|
host: '127.0.0.1',
|
|
enabled: true,
|
|
capacity: 32,
|
|
roles: ['*'],
|
|
},
|
|
];
|
|
}
|
|
|
|
export function loadMachines(root, overlayPath) {
|
|
const file = process.env.FLEET_MACHINES
|
|
? path.resolve(process.env.FLEET_MACHINES)
|
|
: path.join(root, 'machines.json');
|
|
let list = defaultMachines();
|
|
if (fs.existsSync(file)) {
|
|
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
if (Array.isArray(raw.machines) && raw.machines.length) list = raw.machines;
|
|
}
|
|
if (overlayPath && fs.existsSync(overlayPath)) {
|
|
const extra = JSON.parse(fs.readFileSync(overlayPath, 'utf8'));
|
|
const incoming = Array.isArray(extra) ? extra : extra.machines || [];
|
|
for (const m of incoming) upsertMachine(list, m);
|
|
}
|
|
const secretsPath = path.join(root, 'machines.secrets.json');
|
|
if (fs.existsSync(secretsPath)) {
|
|
const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8'));
|
|
const byId = secrets.machines || secrets;
|
|
for (const m of list) {
|
|
const extra = byId[m.id];
|
|
if (extra && typeof extra === 'object') Object.assign(m, extra);
|
|
}
|
|
}
|
|
return list.map((m) => {
|
|
const n = normalizeMachine(m);
|
|
if (n.id === 'lan-134' && process.env.FLEET_ENABLE_LAN134 !== '1') n.enabled = false;
|
|
return n;
|
|
});
|
|
}
|
|
|
|
export function normalizeMachine(m) {
|
|
const kind = m.kind === 'agent' || m.kind === 'ssh' ? m.kind : 'local';
|
|
return {
|
|
id: String(m.id || '').trim(),
|
|
title: m.title || m.id,
|
|
kind,
|
|
host: m.host || '127.0.0.1',
|
|
user: m.user || (kind === 'ssh' ? 'marchon' : ''),
|
|
sshPort: Number(m.sshPort || 22),
|
|
identityFile: m.identityFile || (kind === 'ssh' ? '~/.ssh/id_ed25519' : ''),
|
|
remoteDir: m.remoteDir || '~/verae-fleet-runtime',
|
|
agentPort: Number(m.agentPort || 3851),
|
|
enabled: m.enabled !== false,
|
|
capacity: Number(m.capacity || 8),
|
|
roles: Array.isArray(m.roles) && m.roles.length ? m.roles : ['*'],
|
|
notes: m.notes || '',
|
|
};
|
|
}
|
|
|
|
export function upsertMachine(list, spec) {
|
|
const m = normalizeMachine(spec);
|
|
if (!m.id) throw new Error('machine id required');
|
|
const i = list.findIndex((x) => x.id === m.id);
|
|
if (i >= 0) list[i] = { ...list[i], ...m };
|
|
else list.push(m);
|
|
return m;
|
|
}
|
|
|
|
export function canHost(machine, serviceId, role) {
|
|
if (!machine?.enabled) return false;
|
|
const roles = machine.roles || ['*'];
|
|
if (roles.includes('*')) return true;
|
|
return roles.includes(serviceId) || (role && roles.includes(role));
|
|
}
|
|
|
|
/**
|
|
* Pick the enabled machine with the most free capacity (fewest running / capacity).
|
|
* @param {object[]} machines
|
|
* @param {Array<{ machine?: string }>} instances
|
|
* @param {string} serviceId
|
|
* @param {string} [role]
|
|
*/
|
|
export function pickMachine(machines, instances, serviceId, role, exclude = [], opts = {}) {
|
|
const skip = new Set(exclude);
|
|
let eligible = machines.filter((m) => canHost(m, serviceId, role) && !skip.has(m.id));
|
|
if (opts.preferLocal) {
|
|
const local = eligible.filter((m) => m.kind === 'local');
|
|
if (local.length) eligible = local;
|
|
}
|
|
if (!eligible.length) return null;
|
|
const scored = eligible.map((m) => {
|
|
const running = instances.filter((i) => i.machine === m.id && i.pid).length;
|
|
const free = m.capacity - running;
|
|
const load = m.capacity <= 0 ? 1 : running / m.capacity;
|
|
return { m, running, free, load };
|
|
});
|
|
scored.sort((a, b) => a.load - b.load || a.running - b.running || a.m.id.localeCompare(b.m.id));
|
|
const best = scored.find((s) => s.free > 0);
|
|
return best ? best.m : null;
|
|
}
|
|
|
|
export function saveOverlay(overlayPath, machines) {
|
|
fs.mkdirSync(path.dirname(overlayPath), { recursive: true });
|
|
fs.writeFileSync(overlayPath, JSON.stringify({ machines }, null, 2));
|
|
}
|