guanghulab/scripts/verify/auto-sync.js

130 lines
5.6 KiB
JavaScript

// 铸渊自动同步验证Agent v1.0 · ICE-GL-ZY001
const http = require('http');
const fs = require('fs');
const path = require('path');
const REPO = '/opt/guanghulab-repo';
function apiGet(p) {
return new Promise((ok, no) => {
http.get({ hostname: '127.0.0.1', port: 3920, path: p, timeout: 15000 }, r => {
let b = '';
r.on('data', c => b += c);
r.on('end', () => { try { ok(JSON.parse(b)) } catch (e) { no(e) } });
}).on('error', no).on('timeout', () => no(new Error('timeout')));
});
}
function gkExec(srv, cmd, to = 10000) {
return new Promise(ok => {
const d = JSON.stringify({ cmd });
const o = {
hostname: srv.ip, port: srv.port || 3910, path: '/exec', method: 'POST',
headers: {
'Authorization': 'Bearer ' + srv.key,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(d)
}, timeout: to
};
const r = http.request(o, res => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => { try { ok({ ok: res.statusCode === 200, body: JSON.parse(b) }) } catch (e) { ok({ ok: false, body: b, error: e.message }) } });
});
r.on('error', e => ok({ ok: false, error: e.message }));
r.on('timeout', () => { r.destroy(); ok({ ok: false, error: 'timeout' }) });
r.write(d); r.end();
});
}
function load(f) {
try { return JSON.parse(fs.readFileSync(path.join(REPO, f), 'utf-8')) } catch (e) { return null }
}
async function main() {
const t0 = Date.now();
console.log('铸渊自动同步Agent v1.0 · 启动验证...');
console.log('[1/5] 获取真实服务器状态...');
let real;
try { real = await apiGet('/api/servers?hw=0'); console.log(' OK ' + real.servers.length + '台') }
catch (e) { console.log(' FAIL ' + e.message); process.exit(1) }
console.log('[2/5] 读取声明配置...');
const topo = load('brain/pool-topology.json');
const gk = load('brain/gatekeeper-deployment.json');
console.log(' topo v' + (topo?.version || '?') + ' · gk v' + (gk?.version || '?'));
console.log('[3/5] 逐台对比声明vs实际...');
let alive = 0, off = 0;
const issues = [];
const codes = new Set();
gk?.servers?.forEach(s => codes.add(s.code));
if (topo?.pools) for (const p of Object.values(topo.pools)) if (p.nodes) Object.keys(p.nodes).forEach(c => codes.add(c));
for (const code of codes) {
const r = real.servers.find(s => s.code === code);
const g = gk?.servers?.find(s => s.code === code);
let t = null;
for (const [k, p] of Object.entries(topo?.pools || {})) { if (p.nodes?.[code]) { t = p.nodes[code]; break } }
const diffs = [];
if (t && t.online !== undefined && t.online !== (r?.alive || false)) {
diffs.push({ f: 'online', d: t.online, a: r?.alive || false, s: t.online === false ? 'info' : 'critical' });
}
if (!g?.key && r) diffs.push({ f: 'gk_key', d: 'present', a: 'missing', s: 'critical' });
if (r?.alive) alive++; else if (r) off++;
if (diffs.length > 0) issues.push({ code, name: g?.name || code, diffs });
}
console.log('[4/5] 深度验证 (仓库/密钥/gatekeeper进程)...');
const deep = [];
const ps = gk?.servers?.filter(s => s.persona) || [];
for (const s of ps) {
const r = real.servers.find(x => x.code === s.code);
if (!r?.alive) { deep.push({ code: s.code, name: s.name, status: 'offline' }); continue }
const ck = {};
try {
const x = await gkExec(s, 'ls ~/ 2>/dev/null && echo FOUND || echo NF', 5000);
ck.home = x.ok ? 'ok' : '?';
} catch (e) { ck.home = 'err' }
try {
const x = await gkExec(s, 'ls ~/repos/ 2>/dev/null && echo FOUND || echo NF', 5000);
ck.repo = x.ok ? (x.body?.stdout?.includes('FOUND') ? 'found' : 'none') : '?';
} catch (e) { ck.repo = 'err' }
try {
const x = await gkExec(s, 'test -f ~/.ssh/authorized_keys && wc -l < ~/.ssh/authorized_keys || echo 0', 5000);
ck.keys = x.ok ? (parseInt(x.body?.stdout?.trim()) || 0) : '?';
} catch (e) { ck.keys = 'err' }
try {
const x = await gkExec(s, 'pm2 list 2>/dev/null | grep -c gatekeeper || echo 0', 5000);
ck.gk_pm2 = x.ok ? (parseInt(x.body?.stdout?.trim()) || 0) : '?';
} catch (e) { ck.gk_pm2 = 'err' }
deep.push({ code: s.code, name: s.name, persona: s.persona, status: 'online', checks: ck });
console.log(' ' + s.code + ': home=' + ck.home + ' repo=' + ck.repo + ' keys=' + ck.keys + ' gk=' + ck.gk_pm2);
}
console.log('[5/5] 生成报告...');
const dur = Date.now() - t0;
console.log('');
console.log('═══ 铸渊验证报告 ═══');
console.log('时间: ' + new Date().toISOString());
console.log(alive + '/' + codes.size + ' 在线 · ' + issues.length + ' 差异 · ' + deep.length + ' 深度检查');
if (issues.length === 0) console.log('所有节点声明与实际状态一致');
else for (const i of issues) for (const d of i.diffs) {
console.log(' ' + (d.s === 'critical' ? '!' : '~') + ' ' + i.code + ' [' + d.f + ']: ' + d.d + ' → ' + d.a);
}
console.log('深度检查:');
for (const d of deep) {
console.log(' ' + d.code + '(' + (d.persona || '?') + '): home=' + d.checks.home + ' repo=' + d.checks.repo + ' keys=' + d.checks.keys + ' gk_pm2=' + d.checks.gk_pm2);
}
console.log(dur + 'ms · 报告完毕');
const rep = { ts: new Date().toISOString(), summary: { total: codes.size, alive, off, diffs: issues.length, deep: deep.length, ms: dur }, issues, deep };
fs.writeFileSync(path.join(REPO, 'brain/auto-sync-report.json'), JSON.stringify(rep, null, 2));
console.log('brain/auto-sync-report.json');
}
main().catch(e => { console.error('FAIL: ' + e.message); process.exit(1) });