diff --git a/scripts/deploy-push-heartbeat.js b/scripts/deploy-push-heartbeat.js new file mode 100644 index 0000000..8fbc851 --- /dev/null +++ b/scripts/deploy-push-heartbeat.js @@ -0,0 +1,87 @@ +// 铸渊 push-heartbeat 批量部署脚本 v1.0 +// 在 BS-GZ-006 上运行,通过Gatekeeper exec部署到所有远程服务器 +// 用法: node deploy-push-heartbeat.js + +const http = require('http'); +const fs = require('fs'); + +const SERVERS = [ + { code: "BS-SG-001", name: "新加坡·铸渊大脑", ip: "43.156.237.110", key: "zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9", port: 3911 }, + { code: "BS-SG-002", name: "新加坡·面孔", ip: "43.134.16.246", key: "zy_gtw_d1f6d2b8cb4ea44292bd036e4dd03a70745ea502d4a3ae40", port: 3910 }, + { code: "BS-SG-003", name: "新加坡·中继", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910 }, + { code: "ZY-SG-006", name: "新加坡·语料", ip: "43.153.203.105", key: "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", port: 3910 }, + { code: "BS-SH-005", name: "上海·国内节点", ip: "124.223.10.33", key: "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", port: 3910 }, +]; + +const MASTER = "43.139.217.141"; +const MASTER_PORT = 3920; + +function gkExec(srv, cmd, timeoutMs) { + timeoutMs = timeoutMs || 10000; + const d = JSON.stringify({ cmd }); + const opts = { + hostname: srv.ip, port: srv.port, path: '/exec', method: 'POST', + headers: { 'Authorization': 'Bearer ' + srv.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) }, + timeout: timeoutMs + }; + return new Promise((resolve) => { + const req = http.request(opts, (res) => { + let b = ''; res.on('data', c => b += c); + res.on('end', () => { try { resolve({ ok: res.statusCode === 200, body: JSON.parse(b) }); } catch(e) { resolve({ ok: false, error: e.message }); } }); + }); + req.on('error', (e) => resolve({ ok: false, error: e.message })); + req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); }); + req.write(d); req.end(); + }); +} + +// 推送脚本模板(内联,避免文件传输) +function pushScript(code, name, key) { + return ` +const http=require("http"),os=require("os"); +const MASTER="${MASTER}",PORT=${MASTER_PORT},CODE="${code}",NAME="${name}",KEY="${key}"; +function hw(){const c=os.cpus().length,l=os.loadavg(),t=os.totalmem(),f=os.freemem();return{cpu_cores:c,cpu_load:parseFloat(l[0].toFixed(2)),mem_total_mb:Math.floor(t/1048576),mem_used_mb:Math.floor((t-f)/1048576),mem_used_pct:parseFloat(((t-f)/t*100).toFixed(1)),uptime_h:Math.floor(os.uptime()/3600),hostname:os.hostname()}} +function post(d){return new Promise(r=>{const p=JSON.stringify(d),o={hostname:MASTER,port:PORT,path:"/api/heartbeat",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(p),"X-Server-Code":CODE,"X-Server-Key":KEY},timeout:8000};const q=http.request(o,res=>{let b="";res.on("data",c=>b+=c);res.on("end",()=>r(res.statusCode===200))});q.on("error",()=>r(false));q.on("timeout",()=>{q.destroy();r(false)});q.write(p);q.end()})} +(async()=>{const d={server_code:CODE,server_name:NAME,timestamp:new Date().toISOString(),hardware:hw()};const ok=await post(d);console.log(new Date().toISOString()+" "+(ok?"OK":"FAIL"))})(); +`.trim(); +} + +async function deployOne(srv) { + console.log(`\n[${srv.code}] ${srv.name} ...`); + + // 1. 写脚本 + const scriptCmd = `mkdir -p /opt/push-heartbeat && echo '${pushScript(srv.code, srv.name, srv.key).replace(/'/g, "'\\''")}' > /opt/push-heartbeat/push.js && echo "SCRIPT_OK"`; + const r1 = await gkExec(srv, scriptCmd); + if (!r1.ok) { console.log(` ❌ 写入失败: ${r1.error}`); return false; } + console.log(` ✅ 脚本写入`); + + // 2. 配置cron(不覆盖已有cron,去重) + const cronLine = `*/5 * * * * node /opt/push-heartbeat/push.js >> /opt/push-heartbeat/push.log 2>&1`; + const cronCmd = `(crontab -l 2>/dev/null | grep -v "/opt/push-heartbeat/push.js"; echo "${cronLine}") | crontab - && echo "CRON_OK"`; + const r2 = await gkExec(srv, cronCmd); + if (!r2.ok) { console.log(` ❌ Cron失败: ${r2.error}`); return false; } + console.log(` ✅ Cron配置`); + + // 3. 立即触发一次 + const r3 = await gkExec(srv, `node /opt/push-heartbeat/push.js 2>&1`, 8000); + if (r3.ok) console.log(` ✅ 首次上报: ${r3.body?.stdout?.trim() || 'sent'}`); + else console.log(` ⚠️ 首次上报失败: ${r3.error || r3.body?.stdout?.trim()}`); + + return true; +} + +async function main() { + console.log('铸渊 · push-heartbeat 批量部署'); + console.log(`主控台: ${MASTER}:${MASTER_PORT}`); + console.log(`目标: ${SERVERS.length}台远程服务器\n`); + + let ok = 0, fail = 0; + for (const srv of SERVERS) { + const r = await deployOne(srv); + if (r) ok++; else fail++; + } + + console.log(`\n═══ 部署完成: ${ok}成功 ${fail}失败 ═══`); +} + +main().catch(e => { console.error('FAIL:', e.message); process.exit(1); });