Files
dsh_shenxian/scripts/verify-cluster-cross.mjs
T
admin 452924d89c feat(config): 涉密内容外置到配置目录(档案 140)
把散落在代码里的真实部署值统一收进 config/,代码改为引用配置,
使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。

新增 config/:platform.env.example(模板)· load.sh(shell 加载器)·
index.cjs(node 加载器)· README.md(键一览与优先级)。
真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。

TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用):
platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。
config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由
DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径,
src/** 注释中性化 116 行/53 文件。

scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径
一律改从配置取;web/wake.html 的注册域白名单改为运行时从
location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737
保留值,并把「内置种子必须为空」固化为回归断言。

取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有);
全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归
(/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
2026-09-19 15:12:19 +08:00

186 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const cfg = require('../config/index.cjs')
/**
* T08 · **真跨机演练**驱动脚本(在 Manager 那台机器上运行)。
*
* 与 `verify-cluster-live.mjs`(同机、脚本自己起进程)的区别:这里**假设两侧都已部署好**:
* · Manager 运行在**本机**(47)`http://127.0.0.1:13080`
* · Worker agent 运行在**另一台机器**(106),经 **SSH 反向隧道**出现在本机 `127.0.0.1:<worker-port-b>`
* · 控制面 PG 也在**另一台机器**(106)上,经隧道出现在本机 `127.0.0.1:<pg-port>`
* 它回答的是本次演练的核心问题:**跨机到底能不能用**(含跨机代理取页面、跨 worker 迁移)。
*
* 运行(在 47 上):MANAGER=http://127.0.0.1:13080 AGENT_TOKEN=cross-machine-token \
* AGENT2=http://127.0.0.1:19001 node scripts/verify-cluster-cross.mjs
*/
const MANAGER = process.env.MANAGER ?? 'http://127.0.0.1:13080'
const TOKEN = process.env.AGENT_TOKEN ?? 'cross-machine-token'
const AGENT1 = process.env.AGENT1 ?? 'http://127.0.0.1:19000'
const AGENT2 = process.env.AGENT2 ?? ''
const ADMIN_PW = process.env.ADMIN_PW ?? 'crossmgr123'
const USER_PW = 'crossuser123'
function assert(condition, message) {
if (!condition) throw new Error('ASSERT: ' + message)
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const json = async (path, { method = 'GET', body, cookie } = {}) => {
const res = await fetch(MANAGER + path, {
method,
headers: { ...(body ? { 'content-type': 'application/json' } : {}), ...(cookie ? { cookie } : {}) },
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(30_000),
})
const text = await res.text()
let parsed = null
try {
parsed = text === '' ? null : JSON.parse(text)
} catch {
parsed = { raw: text.slice(0, 200) }
}
return { status: res.status, body: parsed, setCookie: res.headers.get('set-cookie') }
}
/** 直接问 worker(绕过 Manager)—— 证明实例真的落在**那台机器**上。 */
const agent = async (base, path) => {
const res = await fetch(base + path, { headers: { 'x-dsh-agent-token': TOKEN }, signal: AbortSignal.timeout(10_000) })
const text = await res.text()
return { status: res.status, body: text === '' ? null : JSON.parse(text) }
}
/** 取页面:跟随重定向(dsh 首页 303),并对启动窗口的断连做重试。 */
async function fetchPage(url, cookie, tries = 40) {
let status = 0
let snippet = ''
for (let i = 0; i < tries; i += 1) {
try {
const res = await fetch(MANAGER + url, { headers: { cookie }, redirect: 'follow', signal: AbortSignal.timeout(20_000) })
status = res.status
if (res.status === 200) {
snippet = (await res.text()).slice(0, 200)
break
}
} catch {
status = 0
}
await sleep(1000)
}
return { status, snippet }
}
async function waitRunning(cookie, tries = 60) {
for (let i = 0; i < tries; i += 1) {
const st = await json('/api/dsh/status', { cookie })
if (st.body?.running === true) return st.body
if (st.body?.instance?.status === 'crashed') return st.body
await sleep(1000)
}
return await json('/api/dsh/status', { cookie }).then((r) => r.body)
}
try {
console.log('=== 跨机演练:Manager=%s Worker=%s ===', MANAGER, AGENT1)
// ── 0) 两侧可达性(跨机链路的第一层证据)─────────────────────────────
const h1 = await agent(AGENT1, '/healthz')
assert(h1.status === 200 && h1.body.hostId === 'w-2', `Worker w-2 应可达(实际 ${JSON.stringify(h1.body)})`)
assert(h1.body.tunnel?.ready === true, `Worker 侧隧道应就绪(实际 ${JSON.stringify(h1.body.tunnel)})`)
console.log('⓪ worker 可达 -> %s(隧道 ready,已转发 %s)', h1.body.hostId, JSON.stringify(h1.body.tunnel.ports))
// ── 1) 管理面:注册 worker(join 脚本干的事)──────────────────────────
const adm = await json('/api/auth/login', { method: 'POST', body: { username: 'root', password: ADMIN_PW } })
assert(adm.status === 200, `管理员登录失败 ${adm.status}`)
const adminCookie = adm.setCookie.split(';')[0]
let r = await json('/api/admin/hosts', {
method: 'POST',
cookie: adminCookie,
body: { id: 'w-2', endpoint: AGENT1, token: TOKEN, capacityMb: 4096 },
})
assert(r.status === 200, `注册 w-2 失败 ${r.status}`)
const hosts = await json('/api/admin/hosts', { cookie: adminCookie })
assert(hosts.body.hosts.some((h) => h.id === 'w-2'), 'w-2 出现在 worker 目录')
assert(!('agentToken' in (hosts.body.hosts[0] ?? {})), '**绝不下发 agentToken**')
console.log('① 注册 -> w-2(列表不含 agentToken)')
// ── 2) 用户流程 ───────────────────────────────────────────────────────
const uname = `crossuser${Date.now() % 100000}`
r = await json('/api/auth/register', { method: 'POST', body: { username: uname, password: USER_PW } })
assert(r.status === 201, `注册应 201(实际 ${r.status})`)
const users = await json('/api/admin/users', { cookie: adminCookie })
const target = users.body.users.find((u) => u.username === uname)
assert(target !== undefined, '管理员能看到待审用户')
r = await json(`/api/admin/users/${target.id}/approve`, { method: 'POST', cookie: adminCookie })
assert(r.status === 200, `审批应 200(实际 ${r.status})`)
const login = await json('/api/auth/login', { method: 'POST', body: { username: uname, password: USER_PW } })
assert(login.status === 200, `用户登录失败 ${login.status}`)
const cookie = login.setCookie.split(';')[0]
console.log('② 用户流程 -> 注册→审批→登录(uid=%s)', target.id)
// ── 3) 文件面跨机(Manager 在 47、目录落在 106)───────────────────────
r = await json('/api/fs/mkdir', { method: 'POST', cookie, body: { path: 'proj' } })
assert(r.status === 200, `mkdir 应 200(实际 ${r.status} ${JSON.stringify(r.body)})`)
console.log('③ 文件面 -> mkdir 经隧道落到 106 的 worker')
// ── 4) 拉起实例(真 dsh 在 **106** 上)────────────────────────────────
r = await json('/api/dsh/launch', { method: 'POST', cookie, body: { folder: 'proj' } })
assert(r.status === 200, `launch 应 200(实际 ${r.status} ${JSON.stringify(r.body)})`)
const st = await waitRunning(cookie)
assert(st?.running === true, `实例应 running(实际 ${JSON.stringify(st)?.slice(0, 300)})`)
const onAgent = await agent(AGENT1, '/instances')
assert(onAgent.body.instances.length === 1, 'worker(106) 上有 1 个实例')
console.log('④ 拉起 -> running=true,**实例在 106 上**(worker /instances=%d)', onAgent.body.instances.length)
// ── 5) 登录直达 + **跨机取页面**(本演练的核心证据)───────────────────
const enter = await json('/api/dsh/enter', { method: 'POST', cookie })
assert(enter.status === 200, `enter 应 200(实际 ${enter.status})`)
const url = enter.body.url
assert(typeof url === 'string' && url.includes('token='), `enter 应带 token(实际 ${url})`)
const page = await fetchPage(url, cookie)
assert(page.status === 200, `**跨机页面**应 200(实际 ${page.status})`)
console.log('⑤ 跨机页面 -> 200(47 的 Manager 代理到 106 的实例;片段 %s)', page.snippet.replace(/\s+/g, ' ').slice(0, 60))
// ── 6) 第二台 worker(106 上模拟的第二台服务器)+ 迁移 ────────────────
if (AGENT2 !== '') {
const h2 = await agent(AGENT2, '/healthz')
assert(h2.status === 200, `第二台 worker 应可达(实际 ${h2.status})`)
r = await json('/api/admin/hosts', {
method: 'POST',
cookie: adminCookie,
body: { id: 'w-2b', endpoint: AGENT2, token: TOKEN, capacityMb: 4096 },
})
assert(r.status === 200, `注册 w-2b 失败 ${r.status}`)
console.log('⑥ 第二台 -> %s(模拟的第二台服务器)已注册', h2.body.hostId)
r = await json(`/api/admin/users/${target.id}/dsh/migrate`, {
method: 'POST',
cookie: adminCookie,
body: { targetHost: 'w-2b' },
})
assert(r.status === 200, `迁移应 200(实际 ${r.status} ${JSON.stringify(r.body)})`)
assert(r.body.to === 'w-2b', `迁移目标应为 w-2b(实际 ${r.body.to})`)
await waitRunning(cookie)
const a1 = await agent(AGENT1, '/instances')
const a2 = await agent(AGENT2, '/instances')
assert(a1.body.instances.length === 0 && a2.body.instances.length === 1, '实例应从 w-2 移到 w-2b')
console.log('⑦ 跨机迁移 -> %s → %s(epoch=%d),源机已空、目标机有 1 个实例', r.body.from, r.body.to, r.body.epoch)
const enter2 = await json('/api/dsh/enter', { method: 'POST', cookie })
assert(enter2.status === 200 && enter2.body.url !== url, '迁移后 enter 应给**新** URL')
const page2 = await fetchPage(enter2.body.url, cookie)
assert(page2.status === 200, `迁移后页面应 200(实际 ${page2.status})`)
console.log('⑧ 迁移后 -> 新 token URL 页面 200')
} else {
console.log('⑥⑦⑧ 跳过(未提供 AGENT2)')
}
// ── 9) 收尾 ───────────────────────────────────────────────────────────
r = await json('/api/dsh/stop', { method: 'POST', cookie })
assert(r.status === 200, `stop 应 200(实际 ${r.status})`)
console.log('⑨ 停止 -> ok')
console.log('\nOK: **真跨机**(47 当 Manager / 106 当 Worker,隧道跨界)演练通过')
console.log(' ✓ worker 可达 ✓ 注册 ✓ 用户流程 ✓ 文件面跨机 ✓ 实例在 106 ✓ 跨机取页面 ✓ 跨 worker 迁移')
} finally {
/* 不主动清理:实例由调用方决定留或停(演练后要观察现场) */
}