feat(worker): D2 建号自动化 ensureOsAccount + D1 节点自举脚本 + 5 例测试
This commit is contained in:
1 parent
500011d329
commit
c8b514832f
4 files changed
+286
-1
No files matched your search
@@ -18,7 +18,9 @@
|
||||
*
|
||||
* @module dshs/worker/agent
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { timingSafeEqual } from 'node:crypto'
|
||||
import { existsSync } from 'node:fs'
|
||||
import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify'
|
||||
import type { ServerConfig } from '../config.js'
|
||||
import { LocalUserFs } from '../fs/local-user-fs.js'
|
||||
@@ -61,6 +63,57 @@ const OP_CACHE_MAX = 512
|
||||
/** patch 内容长度上限(防把 agent 当大对象存储)。 */
|
||||
const MAX_PATCH_BYTES = 256 * 1024
|
||||
|
||||
/** 按 uid 查 passwd 条目(查不到返回 undefined)。 */
|
||||
function passwdEntryForUid(uid: number): string | undefined {
|
||||
try {
|
||||
const out = execFileSync('getent', ['passwd', String(uid)], { encoding: 'utf8' }).trim()
|
||||
return out === '' ? undefined : out
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保该用户在本机的 OS 账号存在(幂等)—— **集群化后「建号」的责任落点**(2026-09-16 定)。
|
||||
*
|
||||
* **为什么必须落在这里**
|
||||
* - 账号是**机器本地状态**(`/etc/passwd`),而 uid 由 Manager 从控制面库分配、随
|
||||
* `POST /launch` 投递 ⇒ **只有 agent 同时握有「uid」与「这台机器」**。
|
||||
* - 单机时代的 `dsh-provision.path`(观察目录出现 ⇒ 建号)靠本地控制面库算 uid,
|
||||
* 集群下算不出来:实测 `hashUid` 兜底把 guest 算成 **184656**(真实 100002),
|
||||
* 而 PG 是 Manager 专属(Worker 不连控制面库,见本文件头「边界」)⇒
|
||||
* **不能把建号留在 Worker 本地被动触发**。
|
||||
*
|
||||
* **规格**与 `provision-new-users.sh` 一致(`dsh-<短id>` / `-M` / `nologin` / 同 uid),
|
||||
* 因此与存量节点兼容且可重复执行(幂等)。
|
||||
*
|
||||
* ⚠️ 这是**受控固定动作**,不接受调用方传命令或路径 ⇒ 不违反「最小接口」纪律:
|
||||
* `userId` 必须是 UUID 形状、`uid` 必须是 `>= baseUid` 的整数。
|
||||
*/
|
||||
export function ensureOsAccount(config: ServerConfig, userId: string, uid: number): void {
|
||||
// ① 参数校验先行(与平台无关):非法输入在任何平台都该被拒绝
|
||||
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId)) {
|
||||
throw new Error(`ensureOsAccount: invalid userId ${userId}`)
|
||||
}
|
||||
if (!Number.isInteger(uid) || uid < config.baseUid) {
|
||||
throw new Error(`ensureOsAccount: invalid uid ${uid} (baseUid=${config.baseUid})`)
|
||||
}
|
||||
// ② 非 Linux 节点没有 useradd 语义(客户端节点形态另议)
|
||||
if (process.platform !== 'linux') return
|
||||
// ⚠️ 判定**只看 uid**:uid 由 Manager 全局唯一分配 ⇒「同 uid」即「同一用户」,
|
||||
// 因此**不校验账号名**。存量账号存在历史命名差异(实测 47 上 guest 的账号是
|
||||
// `dsh-eeccbc638afc46bdb663`,而按规则本该是 `dsh-4092b9652f6849779989`)——
|
||||
// 名字只是本机标识,权限判据始终是 uid;为它改名反而要动 /etc/passwd + 全量 chown。
|
||||
const entry = passwdEntryForUid(uid)
|
||||
if (entry === undefined) {
|
||||
const name = `dsh-${userId.replace(/-/g, '').slice(0, 20)}`
|
||||
execFileSync('useradd', ['-u', String(uid), '-M', '-s', '/usr/sbin/nologin', name], { stdio: 'pipe' })
|
||||
}
|
||||
// 目录已存在则对齐属主(目录可能尚未创建 ⇒ 留给 spawner 建)
|
||||
const root = userRoot(config.dataRoot, userId)
|
||||
if (existsSync(root)) execFileSync('chown', ['-R', `${uid}:${uid}`, root], { stdio: 'pipe' })
|
||||
}
|
||||
|
||||
interface OpCache {
|
||||
order: string[]
|
||||
results: Map<string, unknown>
|
||||
@@ -229,6 +282,16 @@ export function buildWorkerAgent(
|
||||
if (body.apiKey !== undefined && body.apiKey !== null) apiKeys.set(body.userId, body.apiKey)
|
||||
if (body.uid !== undefined) uids.set(body.userId, body.uid)
|
||||
if (body.epoch !== undefined) epochs.set(body.userId, body.epoch)
|
||||
// 建号必须先于 spawn:OS 账号缺席时 bwrap 里的 `setpriv --reuid` 会直接失败
|
||||
// (现象是"实例起不来",不会报权限错)。幂等:已建过只对账属主;uid 被他人占用则 fail-loud。
|
||||
if (body.uid !== undefined) {
|
||||
try {
|
||||
ensureOsAccount(config, body.userId, body.uid)
|
||||
} catch (err) {
|
||||
const why = err instanceof Error ? err.message : String(err)
|
||||
return reply.code(500).send({ error: `provision-failed: ${why}` })
|
||||
}
|
||||
}
|
||||
try {
|
||||
const instance = await spawner.launch(body.userId, body.folder ?? '', body.patch)
|
||||
// 跨机:把该实例端口经隧道打到 Manager 侧(失败不阻断 —— 本机仍可用)
|
||||
|
||||
Reference in new issue
Block a user