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 未被误建)。
This commit is contained in:
1 parent
9c2e7975ac
commit
452924d89c
100 files changed
+1167
-453
No files matched your search
@@ -1,15 +1,16 @@
|
||||
#!/bin/bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# DSH 平台一键备份(2026-09-11 加固:可执行位 + SQLite 一致性快照 + 纳入运维资产)
|
||||
# 用法:/opt/dsh/backup.sh 产物:/opt/dsh/backups/dsh-platform-backup-<TS>.tar.gz
|
||||
# 用法:<platform-dir>/backup.sh 产物:<platform-dir>/backups/dsh-platform-backup-<TS>.tar.gz
|
||||
set -euo pipefail
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
OUT=/opt/dsh/backups/dsh-platform-backup-${TS}.tar.gz
|
||||
OUT="$DSH_BACKUP_DIR"/dsh-platform-backup-${TS}.tar.gz
|
||||
TMP=$(mktemp -d /tmp/dsh-bk-XXXXXX)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# 1) SQLite 一致性快照(运行中服务用 .backup,避免只拷到 -wal 而数据库不一致)
|
||||
if command -v sqlite3 >/dev/null 2>&1 && [ -f /var/lib/dshs/dshs.db ]; then
|
||||
sqlite3 /var/lib/dshs/dshs.db ".backup '$TMP/dshs.db'"
|
||||
if command -v sqlite3 >/dev/null 2>&1 && [ -f "$DSH_DB_FILE" ]; then
|
||||
sqlite3 "$DSH_DB_FILE" ".backup '$TMP/dshs.db'"
|
||||
DB_SNAP=1
|
||||
else
|
||||
DB_SNAP=0
|
||||
@@ -18,8 +19,8 @@ fi
|
||||
# 2) 打包:用户数据 + 平台库/配置 + 运维资产(可重建)
|
||||
cd /
|
||||
ITEMS="var/lib/dshs etc/dshs.env etc/systemd/system/dshs.service etc/systemd/system/dsh-provision.path etc/systemd/system/dsh-provision.service"
|
||||
[ -d /opt/dsh/artifacts ] && ITEMS="$ITEMS opt/dsh/artifacts"
|
||||
[ -d /opt/dshs/scripts ] && ITEMS="$ITEMS opt/dshs/scripts"
|
||||
[ -d "$DSH_ARTIFACT_DIR" ] && ITEMS="$ITEMS opt/dsh/artifacts"
|
||||
[ -d "$DSH_INSTALL_DIR/scripts" ] && ITEMS="$ITEMS opt/dshs/scripts"
|
||||
for f in /etc/cron.d/dsh-maintenance /etc/cron.d/dsh-backup /etc/nftables-dsh-egress.nft; do
|
||||
[ -f "$f" ] && ITEMS="$ITEMS ${f#/}"
|
||||
done
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
#
|
||||
# bootstrap-worker.sh —— 把一台机器拉到「可承接实例的集群节点」状态
|
||||
#
|
||||
# 立稿 2026-09-16。起因:把 guest 从 w-47 迁到 w-106 时,节点侧缺的东西全是**手工**补的
|
||||
# 立稿 2026-09-16。起因:把 guest 从 <host-a> 迁到 <host-b> 时,节点侧缺的东西全是**手工**补的
|
||||
# (装 dsh、传 runtime、改目录权限、useradd)—— 用户明确要求「该谁处理就让对应功能处理」。
|
||||
# 本脚本就是那个「功能」:**幂等**、可重复执行、只做正向补齐。
|
||||
#
|
||||
@@ -10,7 +11,7 @@
|
||||
# 1. dsh 主程序 —— 缺则按指定版本从 npm 装(内网/镜像优先)
|
||||
# 2. dsh-runtime —— jq / rg / ffmpeg / ffprobe 走本机包管理(**不要从别的机器搬**)
|
||||
# python 见 §3 的「运行时路径契约」
|
||||
# 3. 数据根与权限 —— /var/lib/dshs 711、users 711(**漏了会让实例必崩,且不报权限错**)
|
||||
# 3. 数据根与权限 —— <data-root> 711、users 711(**漏了会让实例必崩,且不报权限错**)
|
||||
# 4. 旧角色残留 —— 可选(--prune-legacy)
|
||||
# 5. 自检 —— 四件套 + 权限 + 端口 + 数据根,任一项失败 ⇒ 退出码非 0
|
||||
#
|
||||
@@ -21,13 +22,13 @@
|
||||
#
|
||||
# 环境变量:
|
||||
# DSH_VERSION 要安装的 @deepseek-ai/dsh 版本(默认 0.1.5-rc.1,须与 Manager 一致)
|
||||
# DSHS_DATA_ROOT 数据根(默认 /var/lib/dshs)
|
||||
# DSHS_DATA_ROOT 数据根(默认 <data-root>)
|
||||
# DSHS_RUNTIME_DIR 运行时目录(默认 /usr/local/dsh-runtime)
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
DSH_VERSION="${DSH_VERSION:-0.1.5-rc.1}"
|
||||
DATA_ROOT="${DSHS_DATA_ROOT:-/var/lib/dshs}"
|
||||
DATA_ROOT="${DSHS_DATA_ROOT:-"$DSHS_DATA_ROOT"}"
|
||||
RUNTIME_DIR="${DSHS_RUNTIME_DIR:-/usr/local/dsh-runtime}"
|
||||
CHECK_ONLY=0
|
||||
PRUNE_LEGACY=0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* clean-ws-pollution.cjs —— 清理"平台安装插件"在用户工作区留下的污染(档案 28)
|
||||
*
|
||||
@@ -7,7 +8,7 @@
|
||||
* ws/.cache pnpm metadata 缓存
|
||||
* ws/.poc-backup PoC 备份
|
||||
* ws/poc PoC 源码副本
|
||||
* ws/*.tgz 安装用插件包(现在改为直接用 /opt/dsh/artifacts/ 不再复制)
|
||||
* ws/*.tgz 安装用插件包(现在改为直接用 <platform-dir>/artifacts/ 不再复制)
|
||||
*
|
||||
* 用法:
|
||||
* node clean-ws-pollution.cjs # dry-run(默认,只打印)
|
||||
@@ -18,14 +19,14 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { existsSync, mkdirSync, readdirSync, statSync } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
const Database = require('/opt/dshs/node_modules/better-sqlite3')
|
||||
const Database = require('better-sqlite3')
|
||||
|
||||
const APPLY = process.argv.includes('--apply')
|
||||
const idx = process.argv.indexOf('--user-id')
|
||||
const onlyId = idx >= 0 ? process.argv[idx + 1] : ''
|
||||
const STAMP = new Date().toISOString().slice(0, 10)
|
||||
|
||||
const db = new Database('/var/lib/dshs/dshs.db', { readonly: true })
|
||||
const db = new Database(cfg.dbFile(), { readonly: true })
|
||||
const users = db.prepare('SELECT id, username, uid, home_dir FROM users').all()
|
||||
.filter((u) => onlyId === '' || u.id === onlyId)
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
export PGPASSWORD=dshs_cluster_2026
|
||||
Q() { /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc "$1"; }
|
||||
Q() { /usr/bin/psql -h 127.0.0.1 -p "$DSHS_PG_PORT" -U dshs -d dshs -tAc "$1"; }
|
||||
echo "=== users ==="
|
||||
Q "select id || ' | ' || username || ' | ' || role || ' | uid=' || coalesce(uid::text,'-') from users order by row_id"
|
||||
echo "=== dsh_instances ==="
|
||||
|
||||
+13
-4
@@ -37,10 +37,19 @@ const ROOT = process.env.DSHLOG_ROOT || "E:/dsh-logs";
|
||||
const STATE = path.join(ROOT, "state.json");
|
||||
const HOSTS_FILE = path.join(ROOT, "hosts.json");
|
||||
|
||||
const DEFAULT_HOSTS = {
|
||||
"47": { ssh: ["-p", "22", "[email protected]"], label: "Manager + w-47" },
|
||||
"106": { ssh: ["-p", "22", "test106"], label: "w-106" },
|
||||
};
|
||||
// 主机表从配置读取(⛔ 不在代码里写死地址 / ssh 别名):
|
||||
// DSHLOG_HOSTS='{"mgr":{"ssh":["-p","22","[email protected]"],"label":"manager"}}'
|
||||
// 未配置 ⇒ 空表,`hosts` 子命令会提示先配置。
|
||||
const DEFAULT_HOSTS = (() => {
|
||||
const raw = process.env.DSHLOG_HOSTS ?? "";
|
||||
if (raw.trim() === "") return {};
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
console.error("DSHLOG_HOSTS 不是合法 JSON:", String(e));
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
// 巡检规则:只认**指向本项目自身故障**的信号。
|
||||
// ⚠️ 每条规则都是误报与漏报的取舍 —— 下面两组是实测调过的:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* ensure-anysearch-admin.cjs —— 给指定实例接入 AnySearch 联网搜索(档案 64 · B 方案)
|
||||
*
|
||||
@@ -47,10 +48,10 @@ const {
|
||||
chmodSync, chownSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync,
|
||||
} = require('node:fs')
|
||||
const { basename, dirname, join, resolve } = require('node:path')
|
||||
const Database = require('/opt/dshs/node_modules/better-sqlite3')
|
||||
const Database = require('better-sqlite3')
|
||||
|
||||
const DB_PATH = '/var/lib/dshs/dshs.db'
|
||||
const ART_DIR = process.env.DSH_PLATFORM_ARTIFACT_DIR ?? '/opt/dsh/artifacts'
|
||||
const DB_PATH = cfg.dbFile()
|
||||
const ART_DIR = cfg.artifactDir()
|
||||
const PKG = '@anysearch/anysearch-dsh'
|
||||
const PROFILE = 'web'
|
||||
const KEY_ENV = 'ANYSEARCH_API_KEY'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* ensure-anysearch-pool.mjs —— 把 AnySearch 插件投放进「功能插件」候选池(档案 64 / 65)
|
||||
*
|
||||
@@ -19,13 +20,13 @@
|
||||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import Database from '/opt/dshs/node_modules/better-sqlite3/lib/index.js'
|
||||
import Database from 'better-sqlite3'
|
||||
|
||||
const DATA_ROOT = process.env.DSH_DATA_ROOT ?? '/var/lib/dshs'
|
||||
const ART_DIR = process.env.DSH_PLATFORM_ARTIFACT_DIR ?? '/opt/dsh/artifacts'
|
||||
const DATA_ROOT = cfg.dataRoot()
|
||||
const ART_DIR = cfg.artifactDir()
|
||||
const POOL_DIR = join(DATA_ROOT, 'business-plugins')
|
||||
const DB_PATH = join(DATA_ROOT, 'dshs.db')
|
||||
const PLUGIN_ROUTES = '/opt/dshs/lib/web/routes/business-plugins.js'
|
||||
const PLUGIN_ROUTES = cfg.installPath('lib', 'web', 'routes', 'business-plugins.js')
|
||||
const PREFIX = 'anysearch-dsh-'
|
||||
|
||||
const APPLY = process.argv.includes('--apply')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* ensure-biz-plugins.cjs —— 给用户铺「功能插件」分区(档案 36 · 批次 2)
|
||||
*
|
||||
@@ -18,12 +19,12 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { chmodSync, chownSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } = require('node:fs')
|
||||
const { basename, dirname, join, resolve } = require('node:path')
|
||||
const Database = require('/opt/dshs/node_modules/better-sqlite3')
|
||||
const Database = require('better-sqlite3')
|
||||
|
||||
const DB_PATH = '/var/lib/dshs/dshs.db'
|
||||
const DB_PATH = cfg.dbFile()
|
||||
const BUNDLE = '@dsh-local/business-plugins'
|
||||
// 自动取产物目录里版本号最大的 business-plugins-*.tgz(升级只需丢新包,不用改脚本)
|
||||
const ART_DIR = process.env.DSH_PLATFORM_ARTIFACT_DIR ?? '/opt/dsh/artifacts'
|
||||
const ART_DIR = cfg.artifactDir()
|
||||
const ARTIFACT = (function () {
|
||||
const PREFIX = 'business-plugins-'
|
||||
const cands = readdirSync(ART_DIR).filter((f) => f.indexOf(PREFIX) === 0 && f.slice(-4) === '.tgz')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* ensure-portal-entry.cjs —— 给**所有**用户铺「设置面板 → 用户管理」入口
|
||||
*
|
||||
@@ -28,10 +29,10 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } = require('node:fs')
|
||||
const { basename, dirname, join } = require('node:path')
|
||||
const Database = require('/opt/dshs/node_modules/better-sqlite3')
|
||||
const Database = require('better-sqlite3')
|
||||
|
||||
const DB = '/var/lib/dshs/dshs.db'
|
||||
const ARTIFACTS = process.env.DSH_PLATFORM_ARTIFACT_DIR ?? '/opt/dsh/artifacts'
|
||||
const DB = cfg.dbFile()
|
||||
const ARTIFACTS = cfg.artifactDir()
|
||||
const PROFILE = 'web'
|
||||
const BUNDLE = '@dsh-local/portal-entry'
|
||||
const PKG_DIR = '@dsh-local/portal-entry'
|
||||
@@ -195,7 +196,7 @@ for (const u of users) {
|
||||
}
|
||||
|
||||
try {
|
||||
// ① 暂存产物到该用户自己的 home(/opt/dsh 是 drwx------ root,用户 uid 读不到其中文件)
|
||||
// ① 暂存产物到该用户自己的 home(<platform-dir> 是 drwx------ root,用户 uid 读不到其中文件)
|
||||
const stageDir = join(u.home_dir, '.dsh-stage')
|
||||
mkdirSync(stageDir, { recursive: true, mode: 0o755 })
|
||||
const staged = join(stageDir, WANT_BASE)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* gen-capabilities.cjs —— 生成「实例能力清单」(档案 56)
|
||||
*
|
||||
* 解决什么:agent 每开新会话都要**现场试一遍**才能知道能做什么(实测同一批探测重复 3 轮,
|
||||
* 撞同样 4 类墙:/etc 白名单、127.0.0.1 被 SSRF 拒、技能不存在、写边界),用户也跟着反复问
|
||||
* "能力有变化吗"。本脚本把**平台事实**固化成两份同源产物:
|
||||
* ① /opt/dsh/state/capabilities.json —— 给平台 API / 门户(机读)
|
||||
* ① <platform-dir>/state/capabilities.json —— 给平台 API / 门户(机读)
|
||||
* ② <bundled-skill-dir>/platform-capabilities/SKILL.md —— 给实例内 agent(它可被 skill 机制加载)
|
||||
*
|
||||
* 用法:node scripts/gen-capabilities.cjs # 生成
|
||||
@@ -17,10 +18,10 @@ const { execFileSync } = require('node:child_process')
|
||||
const { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
|
||||
const STATE = process.env.DSH_STATE_DIR ?? '/opt/dsh/state'
|
||||
const STATE = cfg.stateDir()
|
||||
const OUT_JSON = join(STATE, 'capabilities.json')
|
||||
const BUNDLED = process.env.DSH_BUNDLED_SKILL_DIR ?? '/var/lib/dshs/bundled-skills'
|
||||
const USERS = process.env.DSH_USERS_DIR ?? '/var/lib/dshs/users'
|
||||
const BUNDLED = cfg.bundledSkillsDir()
|
||||
const USERS = cfg.usersDir()
|
||||
const PERMISSION_MODE = process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'
|
||||
|
||||
const run = (cmd, args) => {
|
||||
@@ -96,7 +97,7 @@ const capabilities = {
|
||||
},
|
||||
fileDelivery: {
|
||||
hint: '把产出交给用户时:**用工作区相对路径**(如 `报告.md`),并提示「点会话页右下角『我的文件』可查看/下载」。',
|
||||
avoid: ['不要给 `/var/lib/dshs/users/...` 这类服务器绝对路径', '不要给 127.0.0.1:<port> 链接(用户浏览器打不开)'],
|
||||
avoid: ['不要给服务器绝对路径', '不要给 127.0.0.1:<port> 链接(用户浏览器打不开)'],
|
||||
howToShare: '用户在会话页右下角「我的文件」面板里可浏览工作区并下载任意文件(平台代理,不暴露宿主路径)。',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@ if [ "$(id -u)" != "0" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# ⛔ 本机地址不写死在 nft 规则里:规则体里用占位符,写完文件再注入实际值。
|
||||
HOST_ADDRS=""
|
||||
for a in "$DSH_HOST_LAN_IP" "$DSH_HOST_PUBLIC_IP"; do
|
||||
[ -n "$a" ] && HOST_ADDRS="${HOST_ADDRS:+$HOST_ADDRS, }$a"
|
||||
done
|
||||
|
||||
echo "==> 写入 /etc/nftables-dsh-egress.nft"
|
||||
cat > /etc/nftables-dsh-egress.nft <<'NFTEOF'
|
||||
#!/usr/sbin/nft -f
|
||||
@@ -50,18 +57,18 @@ table ip dsh_egress {
|
||||
# 封 4 类目的地址(仅实例主动发起的连接):
|
||||
# 127.0.0.0/8 → 宿主全部 loopback 服务(sshd 22/32022、nginx 80/443/888、
|
||||
# 门户 3080、BT-Panel 58888/8765)+ 其他实例的 127.0.0.1 监听
|
||||
# 172.18.16.212 → 宿主内网卡(eth0,同样到达 nginx/面板)
|
||||
# <host-lan-ip> → 宿主内网卡(eth0,同样到达 nginx/面板)
|
||||
# 172.17.0.1 → docker0 网桥网关
|
||||
# 47.77.182.89 → 公网 EIP(回环到本机 nginx/sshd)
|
||||
# <server-public-ip> → 公网 EIP(回环到本机 nginx/sshd)
|
||||
# 不影响:公网访问、阿里云内网 DNS(100.100.2.136/138)、pip/npm 下载、
|
||||
# 实例自身监听端口、nginx 回源(root 发包 + 实例回包带 ack)
|
||||
meta skuid 100000-199999 ip daddr { 127.0.0.0/8, 172.17.0.1, 172.18.16.212, 47.77.182.89 } \
|
||||
meta skuid 100000-199999 ip daddr { 127.0.0.0/8, 172.17.0.1, __HOST_ADDRS__ } \
|
||||
meta l4proto tcp tcp flags & (fin|syn|rst|ack) == syn \
|
||||
counter log prefix "dsh-egress-HOST " level warn limit rate 20/minute
|
||||
meta skuid 100000-199999 ip daddr { 127.0.0.0/8, 172.17.0.1, 172.18.16.212, 47.77.182.89 } \
|
||||
meta skuid 100000-199999 ip daddr { 127.0.0.0/8, 172.17.0.1, __HOST_ADDRS__ } \
|
||||
meta l4proto tcp tcp flags & (fin|syn|rst|ack) == syn \
|
||||
counter reject with tcp reset
|
||||
meta skuid 100000-199999 ip daddr { 127.0.0.0/8, 172.17.0.1, 172.18.16.212, 47.77.182.89 } \
|
||||
meta skuid 100000-199999 ip daddr { 127.0.0.0/8, 172.17.0.1, __HOST_ADDRS__ } \
|
||||
meta l4proto udp ct state new counter reject
|
||||
|
||||
# H4 · 观测实例新建外联(先记录不拦截;排除 loopback 与 DNS 降噪)
|
||||
@@ -71,6 +78,13 @@ table ip dsh_egress {
|
||||
}
|
||||
NFTEOF
|
||||
|
||||
# 注入本机地址(来自 config/platform.env);未配 ⇒ 摘掉该占位符,规则只剩回环与 docker 网桥。
|
||||
if [ -n "$HOST_ADDRS" ]; then
|
||||
sed -i "s|__HOST_ADDRS__|$HOST_ADDRS|g" /etc/nftables-dsh-egress.nft
|
||||
else
|
||||
sed -i "s|, __HOST_ADDRS__||g" /etc/nftables-dsh-egress.nft
|
||||
fi
|
||||
|
||||
echo "==> 写入 /etc/systemd/system/dsh-egress.service"
|
||||
cat > /etc/systemd/system/dsh-egress.service <<'SVCEOF'
|
||||
[Unit]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# 安装「dsh 实例共享运行时」—— 可移植 Python(python-build-standalone / install_only)
|
||||
#
|
||||
# 目的(档案 42):
|
||||
@@ -46,14 +47,14 @@ if [ -x "$PYDIR/bin/python3" ]; then
|
||||
echo "==> 已存在 $PYDIR,跳过解压"
|
||||
else
|
||||
if [ -z "$TARBALL" ]; then
|
||||
for c in "/opt/dsh/artifacts/cpython-$PY_VER.tar.gz" "$RT/cpython-$PY_VER.tar.gz"; do
|
||||
for c in ""$DSH_ARTIFACT_DIR"/cpython-$PY_VER.tar.gz" "$RT/cpython-$PY_VER.tar.gz"; do
|
||||
[ -f "$c" ] && TARBALL="$c" && break
|
||||
done
|
||||
fi
|
||||
if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then
|
||||
echo "==> 本地无 tarball,联网下载($PY_VER)"
|
||||
mkdir -p /opt/dsh/artifacts
|
||||
TARBALL="/opt/dsh/artifacts/cpython-$PY_VER.tar.gz"
|
||||
mkdir -p "$DSH_ARTIFACT_DIR"
|
||||
TARBALL=""$DSH_ARTIFACT_DIR"/cpython-$PY_VER.tar.gz"
|
||||
curl -fL --max-time 900 -o "$TARBALL" "$URL_DEFAULT"
|
||||
fi
|
||||
echo "==> 解压 $TARBALL"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# 安装「实例共享工具」到 /usr/local/dsh-runtime/bin(+ /usr/local/bin 软链)
|
||||
#
|
||||
# 为什么共享而不是每个用户装一份(档案 46):
|
||||
@@ -16,7 +17,7 @@ set -euo pipefail
|
||||
RT=/usr/local/dsh-runtime
|
||||
BIN="$RT/bin"
|
||||
LINK=/usr/local/bin
|
||||
ART=/opt/dsh/artifacts
|
||||
ART="$DSH_ARTIFACT_DIR"
|
||||
FORCE=0
|
||||
[ "${1:-}" = "--force" ] && FORCE=1
|
||||
|
||||
@@ -26,7 +27,7 @@ RG_VER=15.2.0
|
||||
FF_URL="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz"
|
||||
FF_SUM_URL="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/checksums.sha256"
|
||||
|
||||
mkdir -p "$BIN" "$ART" /opt/dsh/state
|
||||
mkdir -p "$BIN" "$ART" "$DSH_STATE_DIR"
|
||||
|
||||
say() { printf '==> %s\n' "$*"; }
|
||||
fail() { printf '!! %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# 安装 @dsh-local/workspace-scoped-picker 指定版本到所有用户 profile(幂等)
|
||||
set -euo pipefail
|
||||
TGZ_SRC="${1:?用法: install-wsp.sh <tgz路径>}"
|
||||
@@ -6,8 +7,8 @@ VER=$(basename "$TGZ_SRC" | sed -E 's/^workspace-scoped-picker-(.*)\.tgz$/\1/')
|
||||
echo "版本: $VER"
|
||||
for U in cce6d1cd-b376-4304-80f0-0e1c58c9ffde:114801:"" 4092b965-2f68-4977-9989-68b3966f7df0:100002:-w; do
|
||||
ID=$(echo "$U" | cut -d: -f1); UID_=$(echo "$U" | cut -d: -f2); FLAG=$(echo "$U" | cut -d: -f3)
|
||||
WS=/var/lib/dshs/users/$ID/ws
|
||||
PP=/var/lib/dshs/users/$ID/home/profiles/web
|
||||
WS="$DSH_USERS_DIR"/$ID/ws
|
||||
PP="$DSH_USERS_DIR"/$ID/home/profiles/web
|
||||
cp -f "$TGZ_SRC" "$WS/workspace-scoped-picker-$VER.tgz"
|
||||
chown "$UID_:$UID_" "$WS/workspace-scoped-picker-$VER.tgz"
|
||||
( cd "$PP" && setpriv --reuid "$UID_" --regid "$UID_" --clear-groups env HOME="$WS" pnpm add $FLAG "file:$WS/workspace-scoped-picker-$VER.tgz" >/tmp/pnpm-$ID.log 2>&1 ) && echo " [${ID:0:8}] pnpm OK" || { echo " [${ID:0:8}] pnpm FAILED"; tail -3 /tmp/pnpm-$ID.log; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* instance-mem-sample.cjs —— 实例内存用量采样 + 阈值告警(cron 每 10 分钟;只读 + 追加式写一个 json)
|
||||
*
|
||||
@@ -17,7 +18,7 @@
|
||||
* 而实例 OOM 常发生在分钟级(实测 guest 20:11:10 被 OOM kill,上一次采样还是 19:35),
|
||||
* 小时级采样根本抓不到,等于没有预警。10 分钟 × 3 次 = 30 分钟,才有实际提前量。
|
||||
*
|
||||
* 输出 `/opt/dsh/state/instance-mem-peak.json`:
|
||||
* 输出 `<platform-dir>/state/instance-mem-peak.json`:
|
||||
* { "updatedAt": <ms>, "uids": { "<uid>": { peakMiB, peakAt, lastMiB, samples, unit, limitMiB, pct, highStreak } } }
|
||||
*
|
||||
* 用法:node instance-mem-sample.cjs [--print]
|
||||
@@ -25,7 +26,7 @@
|
||||
const fs = require('node:fs')
|
||||
const { execFileSync } = require('node:child_process')
|
||||
|
||||
const STATE = process.env.DSH_PLATFORM_STATE_DIR ?? '/opt/dsh/state'
|
||||
const STATE = cfg.stateDir()
|
||||
const OUT = `${STATE}/instance-mem-peak.json`
|
||||
|
||||
// ⚠️ systemd 的 MemoryCurrent / MemoryMax 单位是**字节**(不是 KB)。
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* 5. `--dry-run` 只报行数,不写任何东西。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/migrate-sqlite-to-pg.mjs --sqlite /var/lib/dshs/dshs.db \
|
||||
* --pg postgres://dshs:***@127.0.0.1:15432/dshs [--dry-run]
|
||||
* node scripts/migrate-sqlite-to-pg.mjs --sqlite <data-root>/dshs.db \
|
||||
* --pg postgres://dshs:***@127.0.0.1:<pg-port>/dshs [--dry-run]
|
||||
*
|
||||
* @module dshs/scripts/migrate-sqlite-to-pg
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* 覆盖网络 **直连(打洞)观测面**(序㊵ · P2 · S5)—— **只读** + 一个**自检**子命令。
|
||||
*
|
||||
@@ -41,7 +42,7 @@
|
||||
*
|
||||
* ## 🆕 序㊸:**可跑性 = "节点形态也能跑"**(106 侧观测面缺口收口)
|
||||
*
|
||||
* **实测缺口**:106(worker/relay 节点)的 `/opt/dshs-cluster/lib` **不含 `registry.js`**
|
||||
* **实测缺口**:106(worker/relay 节点)的 `<install-dir>-cluster/lib` **不含 `registry.js`**
|
||||
* —— 那是**控制面注册表**模块,节点不落它。而本脚本原先在**顶层** `require` 了它
|
||||
* (外加同样依赖它的 `join.js`)⇒ 在 106 上**任何**子命令都跑不起来:
|
||||
* `Error: Cannot find module '../lib/net/relay/registry.js'`。
|
||||
@@ -225,11 +226,11 @@ async function switchCases(ctx) {
|
||||
new direct.DirectPath({ switchState: state, cooldownMs: ctx.cooldownMs, portBase: ctx.portBase, portSpan: ctx.portSpan, deadlineMs, maxAddrs: ctx.maxAddrs })
|
||||
|
||||
const good = cand.encodeDirectMessage({
|
||||
hostId: 'w-106',
|
||||
hostId: 'w-2',
|
||||
network: 'ops',
|
||||
addrs: [{ host: '10.0.0.9', port: ctx.portBase + 1 }],
|
||||
})
|
||||
const ctxOf = { dialers: ctx.dialers, from: { network: 'ops', hostId: 'w-106' }, selfHostId: 'manager' }
|
||||
const ctxOf = { dialers: ctx.dialers, from: { network: 'ops', hostId: 'w-2' }, selfHostId: 'manager' }
|
||||
|
||||
const defaultCase = direct.resolveDirectSwitch({})
|
||||
const onState = direct.resolveDirectSwitch({ [direct.DIRECT_ENV_KEY]: 'true' })
|
||||
@@ -305,8 +306,8 @@ function hintAudit() {
|
||||
function candidateMatrix(ctx) {
|
||||
const dialers = net.normalizeDialers(
|
||||
new Map([
|
||||
['ops', new Set(['manager', 'w-106'])],
|
||||
['u:5', new Set(['w-106'])],
|
||||
['ops', new Set(['manager', 'w-2'])],
|
||||
['u:5', new Set(['w-2'])],
|
||||
]),
|
||||
)
|
||||
const led = new cand.CandidateLedger()
|
||||
@@ -321,17 +322,17 @@ function candidateMatrix(ctx) {
|
||||
|
||||
const msg = (over = {}) =>
|
||||
cand.encodeDirectMessage({
|
||||
hostId: 'w-106',
|
||||
hostId: 'w-2',
|
||||
network: 'ops',
|
||||
addrs: [{ host: '10.0.0.9', port: ctx.portBase + 1 }],
|
||||
ts: Date.now(),
|
||||
...over,
|
||||
})
|
||||
const inOps = { dialers, from: { network: 'ops', hostId: 'w-106' }, selfHostId: 'manager', maxAddrs: ctx.maxAddrs }
|
||||
const inOps = { dialers, from: { network: 'ops', hostId: 'w-2' }, selfHostId: 'manager', maxAddrs: ctx.maxAddrs }
|
||||
|
||||
run('ops-单地址', msg(), inOps)
|
||||
run('ops-双地址', msg({ addrs: [{ host: '10.0.0.9', port: ctx.portBase + 1 }, { host: '2001:db8::1', port: ctx.portBase + 2 }] }), inOps)
|
||||
const u5 = { dialers, from: { network: 'u:5', hostId: 'w-106' }, selfHostId: 'w-106', maxAddrs: ctx.maxAddrs }
|
||||
const u5 = { dialers, from: { network: 'u:5', hostId: 'w-2' }, selfHostId: 'w-2', maxAddrs: ctx.maxAddrs }
|
||||
run('u:5-同名跨网可拨', msg({ network: 'u:5' }), u5)
|
||||
|
||||
run('跨网', msg({ network: 'u:5' }), inOps)
|
||||
@@ -342,7 +343,7 @@ function candidateMatrix(ctx) {
|
||||
run('形状非法', '{"kind":"DIRECT_CANDIDATE"}', inOps)
|
||||
run(
|
||||
'夹带密钥字段',
|
||||
JSON.stringify({ kind: 'DIRECT_CANDIDATE', hostId: 'w-106', network: 'ops', addrs: [{ host: '10.0.0.9', port: 21101 }], nodeKey: 'x' }),
|
||||
JSON.stringify({ kind: 'DIRECT_CANDIDATE', hostId: 'w-2', network: 'ops', addrs: [{ host: '10.0.0.9', port: 21101 }], nodeKey: 'x' }),
|
||||
inOps,
|
||||
)
|
||||
run('主机名当地址', msg({ addrs: [{ host: 'example.com', port: 80 }] }), inOps)
|
||||
@@ -368,8 +369,8 @@ function candidateMatrix(ctx) {
|
||||
/** 打洞(**判据 D5 / D6**)—— 成功路径走 NAT 模拟器(真 `dgram` + 同一份打洞代码)。 */
|
||||
async function punchCases(ctx) {
|
||||
const deadlineMs = 2500
|
||||
const ok = await punch.runPunchPair({ aPeer: 'ops/w-47', bPeer: 'ops/w-106', deadlineMs }, { sleep })
|
||||
const oneWay = await punch.runPunchPair({ aPeer: 'ops/w-47', bPeer: 'ops/w-106', deadlineMs: 1200, oneWay: 'a' }, { sleep })
|
||||
const ok = await punch.runPunchPair({ aPeer: 'ops/w-1', bPeer: 'ops/w-2', deadlineMs }, { sleep })
|
||||
const oneWay = await punch.runPunchPair({ aPeer: 'ops/w-1', bPeer: 'ops/w-2', deadlineMs: 1200, oneWay: 'a' }, { sleep })
|
||||
|
||||
const cd = new punch.DirectCooldown(ctx.cooldownMs)
|
||||
let openedSockets = 0
|
||||
@@ -453,7 +454,7 @@ async function punchCases(ctx) {
|
||||
*
|
||||
* 🆕 **序㊸:本条腿的依赖是"可选"的** —— 它要 `lib/net/relay/join.js` 与它 import 的
|
||||
* `lib/net/relay/registry.js`(**控制面注册表**模块)。**节点形态**(如 106 的
|
||||
* `/opt/dshs-cluster/lib`)**不落 `registry.js`** ⇒ 这里**具名降级**:
|
||||
* `<install-dir>-cluster/lib`)**不落 `registry.js`** ⇒ 这里**具名降级**:
|
||||
* `{ available:false, reason:'module-missing', missing:[…原文 message…] }`。
|
||||
* 🔴 ⛔ **不许静默返"没有"**(不填 `direct` / `readback` —— 那会让"没装"看起来像"读到了 null")。
|
||||
* 判据 = 「该腿不可用」与「该腿跑了但读数为空」在读数里**形状完全不同** ⇒ 可分。
|
||||
@@ -532,7 +533,7 @@ async function selfcheck() {
|
||||
portBase: punch.PUNCH_PORT_BASE,
|
||||
portSpan: punch.PUNCH_PORT_SPAN,
|
||||
maxAddrs: cand.DIRECT_CAND_MAX_ADDRS,
|
||||
dialers: net.normalizeDialers(new Map([['ops', new Set(['manager', 'w-106'])], ['u:5', new Set(['w-106'])]])),
|
||||
dialers: net.normalizeDialers(new Map([['ops', new Set(['manager', 'w-2'])], ['u:5', new Set(['w-2'])]])),
|
||||
}
|
||||
/** `--ss` 腿(真实机取证):① 基线 ② **关闭态应为 0** ③ **正对照**(绑一个真口 ⇒ 应为 ≥1)。 */
|
||||
const ssProbe = () => ssDirectUdpLines(ctx.portBase, ctx.portSpan)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
* 且把 47 的 relay 留在停用态。凡是**读状态**的远端命令一律 `|| true`。
|
||||
*
|
||||
* ## 运行(🆕 序㊸:**cwd 不再受限**;⛔ 阈值零硬编码)
|
||||
* `node "D:/github/dsh_shenxian/scripts/overlay-failover-drill.cjs" [--scene 1|2|3|all]`
|
||||
* `node "<repo>/scripts/overlay-failover-drill.cjs" [--scene 1|2|3|all]`
|
||||
* ⚠️ **参数表定位已与 cwd 解耦**(从代码仓根 / 任意目录都可跑):候选目录链 = `--table` > `--dir`
|
||||
* > `DSHS_OVERLAY_TABLE_DIR` > **注册文件**(缺省 `~/.dshs/overlay-table-dir`)> `cwd` > 脚本目录及上两级。
|
||||
* 🔴 `--scene trace` / `--scene sample` 的**明细落盘**仍按 `cwd` 写 `_中间产物_待清理/seq9-trace/`
|
||||
@@ -942,15 +942,15 @@ async function main() {
|
||||
|
||||
/* ═══ 幕 4(序⑧):**冷却语义拆分**的真机判据(E9 / 构 A) ═══
|
||||
*
|
||||
* 立项依据(上单 §8.8-4):生产目录 3 条候选里 **2 条同机**(`ai1net.com` + `relay-direct.ai1net.com`
|
||||
* 立项依据(上单 §8.8-4):生产目录 3 条候选里 **2 条同机**(`<base-domain>` + `relay-direct.<base-domain>`
|
||||
* 都在 47)⇒ 一次 47 故障会把它们**同时**耗进冷却 ⇒ 杀另一台时"唯一可能的出路"被自己设的冷却挡住
|
||||
* ⇒ 真机读数 `仍在冷却(剩 59201ms / 共 300000ms)` ⇒ **最长 ~300 s 不切流**。
|
||||
*
|
||||
* 序列:① 归零(重启 Manager ⇒ 通道回目录首位 47)→ ② 停 47 ⇒ 切 106
|
||||
* (47 的**两条**候选各按自己的原因进冷却:`relay-direct` = open-failed,`ai1net` = switched-away)
|
||||
* (manager 的**两条**候选各按自己的原因进冷却:`relay-direct` = open-failed,`<base-domain>` = switched-away)
|
||||
* → ③ 恢复 47 → ④ 停 106 ⇒ 此刻"当前挂了 + 所有候选都在冷却" = **D6 现场**。
|
||||
*
|
||||
* 🔴 **2026-09-19 域迁 `ai1net.com` 修正**:本幕判定"目标是不是 47 那台"原先**写死** `alotbuy.com`,
|
||||
* 🔴 **2026-09-19 域迁 `<base-domain>` 修正**:本幕判定"目标是不是 47 那台"原先**写死** `<legacy-domain>`,
|
||||
* 域一切就恒判失败(**假红**:豁免实际已生效并切回 47,仅字面匹配不上)。
|
||||
* ⇒ 现一律改用参数表的 `DRILL_KILLED_MATCH`(= 目录 `relays[]` 首位的 host),
|
||||
* ⛔ 不再在脚本里写死域名 —— 下次换域只改参数表一处。
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
*
|
||||
* 用法:
|
||||
* node overlay-holepunch.cjs --observer --port-x 21100 --port-y 21101 --token <t> --secs 90
|
||||
* node overlay-holepunch.cjs --probe --obs 47.77.182.89 --port-x 21100 --port-y 21101 \
|
||||
* --token <t> --name w-dev --peers w-47u,w-106u
|
||||
* node overlay-holepunch.cjs --probe --obs <server-public-ip> --port-x 21100 --port-y 21101 \
|
||||
* --token <t> --name w-dev --peers <host-a>u,<host-b>u
|
||||
*
|
||||
* @module scripts/overlay-holepunch
|
||||
*/
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
* 本线教训:「另一份实现 = 另一处静默失效」(取址链踩过两次)。
|
||||
*
|
||||
* 用法:
|
||||
* node overlay-jitter.cjs --icmp 106.54.21.172 --count 300 --interval 0.2
|
||||
* node overlay-jitter.cjs --tcp 106.54.21.172:22 --tcp-n 30
|
||||
* node overlay-jitter.cjs --icmp <peer-public-ip> --count 300 --interval 0.2
|
||||
* node overlay-jitter.cjs --tcp <peer-public-ip>:22 --tcp-n 30
|
||||
* node overlay-jitter.cjs --icmp H --count 300 --tcp H:22 --tcp-n 30 # 两者一起跑
|
||||
* node overlay-jitter.cjs --watch --targets 106.54.21.172:22,47.77.182.89:22 --rounds 40 --gap 400
|
||||
* node overlay-jitter.cjs --watch --targets <peer-public-ip>:22,<server-public-ip>:22 --rounds 40 --gap 400
|
||||
*
|
||||
* @module scripts/overlay-jitter
|
||||
*/
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
* node scripts/overlay-keyring.cjs init-signer --key /etc/dshs/overlay-signer-key.pem
|
||||
* node scripts/overlay-keyring.cjs sign-signerset --root-key <pem> --signers <pubhex,…> --network ops --out <json>
|
||||
* node scripts/overlay-keyring.cjs init-node --key /etc/dshs/node.key
|
||||
* node scripts/overlay-keyring.cjs issue-grant --signer-key <pem> --network ops --host w-106 --node-key <file|hex> --out <json>
|
||||
* node scripts/overlay-keyring.cjs issue-grant --signer-key <pem> --network ops --host <host-b> --node-key <file|hex> --out <json>
|
||||
* node scripts/overlay-keyring.cjs sign-revocations --signer-key <pem> --network ops --hosts a,b --out <json>
|
||||
* node scripts/overlay-keyring.cjs verify-grant --file <json> --signer-pub <hex> [--host w-106 --network ops]
|
||||
* node scripts/overlay-keyring.cjs verify-grant --file <json> --signer-pub <hex> [--host <host-b> --network ops]
|
||||
* node scripts/overlay-keyring.cjs recover-root --code <hex> --out <pem> [--expect-pub <hex>]
|
||||
* # 演练三判据:--expect-pub 比公钥;
|
||||
* # 再给 --signers <hex,…> --network <n> --issued-at <iso> ⇒ 用重建的根签 SignerSet 验通(判据②);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* 覆盖网络 **控制面侧** 命令行:网注册表 / 准入凭据 / 白名单派生(序㊱ · P1 · S1+S2+S4)。
|
||||
*
|
||||
@@ -24,7 +25,7 @@
|
||||
* ## 全局选项(**键名不可混用**)
|
||||
* | 选项 | 含义 | 备注 |
|
||||
* |---|---|---|
|
||||
* | `--dir` | 注册表**目录** | 优先于 env `DSHS_OVERLAY_REGISTRY_DIR`,缺省 `/var/lib/dshs/overlay` |
|
||||
* | `--dir` | 注册表**目录** | 优先于 env `DSHS_OVERLAY_REGISTRY_DIR`,缺省 `<data-root>/overlay` |
|
||||
* | `--registry` | 注册表**文件**(覆盖 `--dir` 下的 `nodes.json`) | 🔴 **⛔ 不要用 `--file`** —— 那是 `apply` 的申请单入参 |
|
||||
* | `--signer-pub` | 受信签名者公钥(可重复) | 未给 ⇒ 回落 env `DSHS_OVERLAY_SIGNER_PUBKEYS` |
|
||||
*
|
||||
@@ -79,7 +80,7 @@ function opt(name) {
|
||||
}
|
||||
|
||||
/** 注册表目录(`DSHS_OVERLAY_REGISTRY_DIR` 可覆盖;⛔ 生产值不在代码里写死第二份口径)。 */
|
||||
const REGISTRY_DIR = opt('dir') ?? process.env.DSHS_OVERLAY_REGISTRY_DIR ?? '/var/lib/dshs/overlay'
|
||||
const REGISTRY_DIR = opt('dir') ?? cfg.overlayDir()
|
||||
/**
|
||||
* 🔴 **覆盖注册表文件用 `--registry`,⛔ 不是 `--file`** —— 真机首轮实测踩坑:
|
||||
* `--file` 在本 CLI 里已被 **`apply --file <申请单>`** 占用(还有 `issue-invite --out`),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* `交接单_relay落地R2-R4` 的教训原文是「**静默失效靠判别器定位**」。要让判别器能被**脚本**
|
||||
* (而不是人读日志)用,就必须有「一条命令出 PASS/FAIL」的入口 —— 本文件就是那个入口:
|
||||
*
|
||||
* cd "E:/ProgramData/AI技能/aliyun-dsh-server" && node "D:/github/dsh_shenxian/scripts/overlay-probe.cjs"
|
||||
* cd "E:/ProgramData/AI技能/aliyun-dsh-server" && node "<repo>/scripts/overlay-probe.cjs"
|
||||
*
|
||||
* - **退出码**:全绿 `0` / 任一红 `1` / 用法或取数失败 `2`(可直接被 automation 消费)
|
||||
* - **输出**:≤ 12 行(每行 = 一条指标);红的条目**另外**写到 stderr,并**指名**是哪个 ID
|
||||
@@ -66,7 +66,7 @@
|
||||
* ## 🆕 观测面**并集**(序㊾:⛔ 单看 47 会把"合法拓扑态"判成红)
|
||||
* `OBS-01` / `OBS-08` / `OBS-09` 的绿**取决于 106 worker 挂在哪台中继** —— worker 通道按**抖动**换址
|
||||
* (`[relay-switch]`)⇒ 一旦落到 **106 自家中继**,47 视角就是
|
||||
* `used=1 / endpoints=[w-106:<PEER_AGENT_PORT> offline, w-106:<实例口> offline]` ⇒ `OBS-01` FAIL、`OBS-08` FAIL、
|
||||
* `used=1 / endpoints=[<host-b>:<PEER_AGENT_PORT> offline, <host-b>:<实例口> offline]` ⇒ `OBS-01` FAIL、`OBS-08` FAIL、
|
||||
* `OBS-09` SKIP —— 三条**全是假红 / 假 SKIP**(客户端好好的,只是"不在我这一台")。
|
||||
*
|
||||
* 口径 = **按 `hostId`(含 `network`)合并两台中继的视图**:
|
||||
@@ -182,7 +182,7 @@ function argOf(argv, name) {
|
||||
* 4. **注册文件**的每一行(`DSHS_OVERLAY_TABLE_REGISTRY`,缺省 `~/.dshs/overlay-table-dir`;
|
||||
* `#` 起注释;**机器本地、⛔ 不入库**)
|
||||
* 5. `cwd`(保持既有默认,⛔ 不破坏老用法)
|
||||
* 6. 脚本自身目录、其上一级、上两级(仓根 / `/opt/dshs` 这类部署形态都覆盖)
|
||||
* 6. 脚本自身目录、其上一级、上两级(仓根 / `<install-dir>` 这类部署形态都覆盖)
|
||||
*
|
||||
* 🔴 **"恰好 1 个才算合法"这条防错保留、且更严**:
|
||||
* 任一候选目录里 ≥2 份 ⇒ **立即报错**;跨候选目录合计 ≥2 份 ⇒ **也报错**(列出全部命中)。
|
||||
@@ -1637,7 +1637,7 @@ function main() {
|
||||
* ② **`count ≥ CAND_MIN`**(每连接候选数 ≥ 2)。
|
||||
*
|
||||
* 🔑 **`hosts`(主机名个数)只作信息输出、⛔ 不作判据** —— 且它**不是「独立物理路径数」**:
|
||||
* 观测器**不解析 DNS**(零网络),而生产目录前两条候选 `ai1net.com` 与 `relay-direct.ai1net.com`
|
||||
* 观测器**不解析 DNS**(零网络),而生产目录前两条候选 `<base-domain>` 与 `relay-direct.<base-domain>`
|
||||
* **摘名不同、同落 47** ⇒ **真机实测 `count=3` 时 `hosts` 也报 3**,而机器级独立路径只有 **2**(47 + 106)。
|
||||
* 故本项**照实判「条数」**,把 `hosts` 打出来供人判「冗余建成」,⛔ **不放宽判据凑绿**;
|
||||
* 真机首轮读数(2026-09-18 00:0x)已实证 **worker 侧 `count=1`**(根因见回报)。
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
set -euo pipefail
|
||||
for dir in /var/lib/dshs/users/*/; do
|
||||
for dir in "$DSH_USERS_DIR"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
id="$(basename "$dir")"
|
||||
[ "$id" = . ] && continue
|
||||
short="$(echo "$id" | tr -d '-' | cut -c1-20)"
|
||||
user="dsh-$short"
|
||||
if ! id "$user" &>/dev/null; then
|
||||
uid="$(node /opt/dshs/lib/cli.js uid-for-user "$id" --db /var/lib/dshs/dshs.db 2>/dev/null || echo 0)"
|
||||
uid="$(node "$DSH_INSTALL_DIR/lib"/cli.js uid-for-user "$id" --db "$DSH_DB_FILE" 2>/dev/null || echo 0)"
|
||||
[ "$uid" = 0 ] && { echo "SKIP $id (uid-for-user失败)"; continue; }
|
||||
# 若该uid已被其他账号占用则跳过
|
||||
if id "$uid" &>/dev/null; then echo "SKIP $id (uid $uid 已被占用)"; continue; fi
|
||||
@@ -22,8 +23,8 @@ done
|
||||
# 2026-09-11 追加(档案 18 v3 收尾):为该批新用户补齐「目录选择器收敛」——
|
||||
# ① 安装受限插件(逐用户 pnpm add)② 写平台段(cordis.patch.yml,幂等)。
|
||||
# best-effort:失败不影响上面的账号 provisioning;日志见 journalctl -u dsh-provision。
|
||||
if [ -f /opt/dshs/poc/workspace-scoped-picker/ensure-workspace-picker.cjs ]; then
|
||||
node /opt/dshs/poc/workspace-scoped-picker/ensure-workspace-picker.cjs 2>&1 | sed "s/^/[picker] /" || true
|
||||
if [ -f "$DSH_INSTALL_DIR"/poc/workspace-scoped-picker/ensure-workspace-picker.cjs ]; then
|
||||
node "$DSH_INSTALL_DIR"/poc/workspace-scoped-picker/ensure-workspace-picker.cjs 2>&1 | sed "s/^/[picker] /" || true
|
||||
fi
|
||||
|
||||
# 2026-09-11 追加:「设置面板 → 用户管理」入口(@dsh-local/portal-entry)全员兜底。
|
||||
@@ -31,6 +32,6 @@ fi
|
||||
# 普通用户=仅退出登录)。**普通用户没有它就没有任何退出登录入口**,故必须与新用户
|
||||
# 注册同步补齐(与上面的 picker 同一时机、同一 best-effort 策略)。
|
||||
# 幂等:按 node_modules 已装版本 + dep spec 判定,已是最新即跳过。
|
||||
if [ -f /opt/dshs/scripts/ensure-portal-entry.cjs ]; then
|
||||
node /opt/dshs/scripts/ensure-portal-entry.cjs 2>&1 | sed "s/^/[portal-entry] /" || true
|
||||
if [ -f "$DSH_INSTALL_DIR/scripts"/ensure-portal-entry.cjs ]; then
|
||||
node "$DSH_INSTALL_DIR/scripts"/ensure-portal-entry.cjs 2>&1 | sed "s/^/[portal-entry] /" || true
|
||||
fi
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# purge-trash.sh —— 清理各用户 trash/ 下超过 30 天的回收目录(档案 28)
|
||||
# 回收站是"清理动作的缓冲区":脚本只做 mv 进来,只有本脚本才真正 rm。
|
||||
set -uo pipefail
|
||||
KEEP_DAYS="${1:-30}"
|
||||
LOG=/var/log/dsh-trash-purge.log
|
||||
echo "[$(date -Is)] purge trash older than ${KEEP_DAYS}d" >> "$LOG"
|
||||
for t in /var/lib/dshs/users/*/trash; do
|
||||
for t in "$DSH_USERS_DIR"/*/trash; do
|
||||
[ -d "$t" ] || continue
|
||||
find "$t" -mindepth 1 -maxdepth 1 -mtime "+${KEEP_DAYS}" -print -exec rm -rf {} + >> "$LOG" 2>&1
|
||||
done
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# 存量数据并行搬运 47 → 106(4 路并发;瓶颈在源端小文件 IOPS,单流只 ~0.4MB/s)
|
||||
# 搬完写 /root/push-parallel.done,供后续 cutover 判断
|
||||
set -uo pipefail
|
||||
KEY=/root/.ssh/dshworker_ed25519
|
||||
DST=root@106.54.21.172
|
||||
DST=root@"$DSH_PEER_PUBLIC_IP"
|
||||
SSHO="-i $KEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o Compression=no"
|
||||
SRC=/var/lib/dshs
|
||||
SRC="$DSHS_DATA_ROOT"
|
||||
LOG=/root/push-parallel.log
|
||||
: > "$LOG"
|
||||
|
||||
ssh -n $SSHO "$DST" 'mkdir -p /var/lib/dshs'
|
||||
ssh -n $SSHO "$DST" "mkdir -p $DSHS_DATA_ROOT"
|
||||
echo "[$(date +%T)] 目标端就绪" >> "$LOG"
|
||||
|
||||
# 按"大小"分组:大用户单开一路,其余合流(每路一个 tar→ssh)
|
||||
@@ -19,7 +20,7 @@ one() { # $1=组名 其余=相对路径列表
|
||||
cd "$SRC" || exit 1
|
||||
{ for p in "$@"; do [ -e "$p" ] && echo "$p"; done; } > "/tmp/list-$name.txt"
|
||||
tar --numeric-owner --files-from="/tmp/list-$name.txt" -cf - \
|
||||
| ssh $SSHO "$DST" "tar -C /var/lib/dshs --numeric-owner -xf -"
|
||||
| ssh $SSHO "$DST" "tar -C "$DSHS_DATA_ROOT" --numeric-owner -xf -"
|
||||
echo "[$(date +%T)] $name 完成 rc=$?" >> "$LOG"
|
||||
) &
|
||||
}
|
||||
@@ -33,5 +34,5 @@ one g4 "users/7ba268be-6103-438c-8e6b-609b422ccbca" "users/ca3f36e0-937f-437e-b8
|
||||
|
||||
wait
|
||||
echo "[$(date +%T)] 全部完成" >> "$LOG"
|
||||
ssh -n $SSHO "$DST" 'du -sm /var/lib/dshs | cut -f1' >> "$LOG" 2>&1
|
||||
ssh -n $SSHO "$DST" "du -sm $DSHS_DATA_ROOT | cut -f1" >> "$LOG" 2>&1
|
||||
touch /root/push-parallel.done
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* 运行时版本基线巡检(档案 44)。
|
||||
*
|
||||
@@ -16,7 +17,7 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { existsSync, readFileSync, writeFileSync } = require('node:fs')
|
||||
|
||||
const BASE = '/opt/dsh/state/runtime-baseline.json'
|
||||
const BASE = require('node:path').join(cfg.stateDir(), 'runtime-baseline.json')
|
||||
const ACCEPT = process.argv.includes('--accept')
|
||||
|
||||
const run = (cmd, args) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* session-gc.cjs —— 会话记录保留期回收(档案 14 §H3 / 档案 28)
|
||||
* 规则(用户 2026-09-11 定):**每个用户的 sessions 达到阈值才触发**,清理 **超过 N 天** 的会话目录。
|
||||
@@ -11,7 +12,7 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { existsSync, mkdirSync, readdirSync, statSync } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
const Database = require('/opt/dshs/node_modules/better-sqlite3')
|
||||
const Database = require('better-sqlite3')
|
||||
|
||||
const argv = process.argv.slice(2)
|
||||
const APPLY = argv.includes('--apply')
|
||||
@@ -26,7 +27,7 @@ const CUTOFF = Date.now() - DAYS * 86400_000
|
||||
const duKB = (p) => { try { return Number(execFileSync('du', ['-sk', p], { encoding: 'utf8' }).split(/\s+/)[0]) * 1024 } catch { return 0 } }
|
||||
const fmt = (b) => (b >= 1048576 ? (b / 1048576).toFixed(1) + ' MB' : (b / 1024).toFixed(0) + ' KB')
|
||||
|
||||
const db = new Database('/var/lib/dshs/dshs.db', { readonly: true })
|
||||
const db = new Database(cfg.dbFile(), { readonly: true })
|
||||
const users = db.prepare('SELECT id, username, uid, home_dir FROM users').all().filter((u) => ONLY === '' || u.id === ONLY)
|
||||
|
||||
for (const u of users) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在 47 初始化「控制面 PG」:独立数据目录 /var/lib/dshs-pg + 专用 unit dshs-pg.service
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# 在 47 初始化「控制面 PG」:独立数据目录 <data-root>-pg + 专用 unit dshs-pg.service
|
||||
# 设计口径:控制面 DB 在 Manager 侧、仅 loopback、scram 认证。
|
||||
set -uo pipefail
|
||||
PGDATA=/var/lib/dshs-pg
|
||||
PGDATA="${DSH_PG_DATA_DIR}"
|
||||
PGPORT=15432
|
||||
DBPW="${DSHS_PG_PASSWORD:-dshs_cluster_2026}"
|
||||
PGVER=$(/usr/bin/postgres --version | grep -oE '[0-9]+' | head -1)
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# 起一台 **cluster 模式的 Manager**(T08 跨机演练用;在 Manager 那台机器上跑)。
|
||||
#
|
||||
# 现场的对照(2026-09-15 演练实测):
|
||||
# · Manager 在 **47**(本脚本所在机器),监听 `127.0.0.1:13080`(**不公网暴露**)
|
||||
# · Worker agent 在 **106**,经 SSH 反向隧道出现在本机 `127.0.0.1:19000` / `19001`
|
||||
# · 控制面 PG 也在 **106**,经同一条隧道出现在本机 `127.0.0.1:15432`
|
||||
# · Worker agent 在 **106**,经 SSH 反向隧道出现在本机 `127.0.0.1:<worker-port-b>` / `19001`
|
||||
# · 控制面 PG 也在 **106**,经同一条隧道出现在本机 `127.0.0.1:<pg-port>`
|
||||
# 用法:bash scripts/start-cluster-manager.sh (env 见下方 manager.env)
|
||||
在 47 上:建 manager.env、bootstrap 管理员、起 cluster Manager(127.0.0.1:13080)
|
||||
set -uo pipefail
|
||||
cd /opt/dshs-cluster || exit 1
|
||||
cd "$DSH_INSTALL_DIR"-cluster || exit 1
|
||||
|
||||
cat > /opt/dshs-cluster/manager.env <<'ENVEOF'
|
||||
cat > "$DSH_INSTALL_DIR"-cluster/manager.env <<ENVEOF
|
||||
DSHS_DEPLOY_MODE=cluster
|
||||
DSHS_DB_URL=postgres://dshs:[email protected]:15432/dshs_cross
|
||||
DSHS_DATA_ROOT=/opt/dshs-cluster/data
|
||||
DSHS_CLUSTER_HOST_ID=m-47
|
||||
DSHS_DB_URL=$DSHS_DB_URL
|
||||
DSHS_DATA_ROOT="$DSH_INSTALL_DIR"-cluster/data
|
||||
DSHS_CLUSTER_HOST_ID=${DSHS_CLUSTER_HOST_ID:-m-1}
|
||||
DSHS_CLUSTER_AGENT_URL=http://127.0.0.1:19000
|
||||
DSHS_CLUSTER_AGENT_TOKEN=cross-machine-token
|
||||
DSHS_CLUSTER_AGENT_TOKEN=$DSHS_CLUSTER_AGENT_TOKEN
|
||||
DSHS_CLUSTER_INSTANCE_HOST=127.0.0.1
|
||||
DSHS_CLUSTER_WORKER_DATA_ROOT=/opt/dshs-cluster/live-data
|
||||
DSHS_CLUSTER_WORKER_DATA_ROOT="$DSH_INSTALL_DIR"-cluster/live-data
|
||||
DSHS_CLUSTER_CAPACITY_MB=-1
|
||||
DSHS_CLUSTER_REGISTER_SELF=0
|
||||
DSHS_CLUSTER_LEASE_TTL_MS=30000
|
||||
@@ -26,7 +27,7 @@ ENVEOF
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. /opt/dshs-cluster/manager.env
|
||||
. "$DSH_INSTALL_DIR"-cluster/manager.env
|
||||
set +a
|
||||
|
||||
echo "--- bootstrap 管理员 ---"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* storage-report.cjs —— 每用户存储用量上报(档案 28)
|
||||
* 输出:/var/run/dsh-storage-report.json(供门户 GET /api/admin/storage 直接读取,避免每次请求都 du)
|
||||
@@ -7,14 +8,14 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { existsSync, writeFileSync } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
const Database = require('/opt/dshs/node_modules/better-sqlite3')
|
||||
const Database = require('better-sqlite3')
|
||||
|
||||
const OUT = process.env.DSH_STORAGE_REPORT ?? '/var/run/dsh-storage-report.json'
|
||||
const WS_MB = Number(process.env.DSH_WS_THRESHOLD_MB ?? 2048)
|
||||
const SESS_MB = Number(process.env.DSH_SESSIONS_THRESHOLD_MB ?? 1024)
|
||||
|
||||
const duKB = (p) => { try { return Number(execFileSync('du', ['-sk', p], { encoding: 'utf8' }).split(/\s+/)[0]) * 1024 } catch { return 0 } }
|
||||
const db = new Database('/var/lib/dshs/dshs.db', { readonly: true })
|
||||
const db = new Database(cfg.dbFile(), { readonly: true })
|
||||
const users = db.prepare('SELECT username, home_dir FROM users ORDER BY username').all()
|
||||
|
||||
const rows = users.map((u) => {
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# 切换 A 步(在 47 上跑):
|
||||
# ① 备份 /opt/dshs/lib → /opt/dsh/backups/lib-<ts>/
|
||||
# ② 覆盖 /opt/dshs/lib(T08 集群版代码)
|
||||
# ③ 装 **本地 Worker**(w-47,19100,无隧道 —— Manager 同机直连)
|
||||
# ④ 把既有用户(admin/guest)的归属**预置**为 w-47(否则粘性落点无处可粘、新老用户会被按容量随机调度)
|
||||
# ⑤ 在 PG 里注册 w-47 / w-106 两台 worker
|
||||
# ① 备份 <install-dir>/lib → <platform-dir>/backups/lib-<ts>/
|
||||
# ② 覆盖 <install-dir>/lib(T08 集群版代码)
|
||||
# ③ 装 **本地 Worker**(<host-a>,19100,无隧道 —— Manager 同机直连)
|
||||
# ④ 把既有用户(admin/guest)的归属**预置**为 <host-a>(否则粘性落点无处可粘、新老用户会被按容量随机调度)
|
||||
# ⑤ 在 PG 里注册 <host-a> / <host-b> 两台 worker
|
||||
set -uo pipefail
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
W47_TOKEN="dshs-worker-47-c4b7e19f"
|
||||
W106_TOKEN="dshs-worker-7f3a91c05e"
|
||||
PGURL="postgres://dshs:[email protected]:15432/dshs"
|
||||
# 凭据 / 地址一律来自配置(config/platform.env)—— ⛔ 不在脚本里写死
|
||||
W47_TOKEN="${DSHS_CLUSTER_AGENT_TOKEN:?config/platform.env 缺 DSHS_CLUSTER_AGENT_TOKEN}"
|
||||
W106_TOKEN="${DSH_PEER_AGENT_TOKEN:?config/platform.env 缺 DSH_PEER_AGENT_TOKEN}"
|
||||
PGURL="${DSHS_DB_URL:?config/platform.env 缺 DSHS_DB_URL}"
|
||||
TARBALL=/tmp/dshs-lib-new.tgz
|
||||
|
||||
echo "=== ① 备份 /opt/dshs/lib ==="
|
||||
mkdir -p "/opt/dsh/backups/lib-$TS"
|
||||
cp -a /opt/dshs/lib "/opt/dsh/backups/lib-$TS/lib" && echo " ✓ 备份到 /opt/dsh/backups/lib-$TS/lib($(find /opt/dsh/backups/lib-$TS -type f | wc -l) 文件)"
|
||||
echo "=== ① 备份 $DSH_INSTALL_DIR/lib ==="
|
||||
mkdir -p "$DSH_BACKUP_DIR/lib-$TS"
|
||||
cp -a "$DSH_INSTALL_DIR/lib" "$DSH_BACKUP_DIR/lib-$TS/lib" && echo " ✓ 备份到 $DSH_BACKUP_DIR/lib-$TS/lib($(find "$DSH_BACKUP_DIR/lib-$TS" -type f | wc -l) 文件)"
|
||||
|
||||
echo "=== ② 覆盖 lib ==="
|
||||
[ -f "$TARBALL" ] || { echo " ✗ 缺少 $TARBALL"; exit 1; }
|
||||
rm -rf /opt/dshs/lib && tar -xzf "$TARBALL" -C /opt/dshs
|
||||
echo " ✓ 已覆盖;cluster 特征检查: $(grep -l "DEPLOY_MODE" /opt/dshs/lib/config.js >/dev/null 2>&1 && echo '有 cluster 代码 ✓' || echo '✗ 未见 cluster 代码')"
|
||||
echo " lease/agent/tunnel: $(ls /opt/dshs/lib/supervisor/lease.js /opt/dshs/lib/worker/agent.js /opt/dshs/lib/worker/tunnel.js 2>/dev/null | wc -l)/3"
|
||||
rm -rf "$DSH_INSTALL_DIR"/lib && tar -xzf "$TARBALL" -C "$DSH_INSTALL_DIR"
|
||||
echo " ✓ 已覆盖;cluster 特征检查: $(grep -l "DEPLOY_MODE" "$DSH_INSTALL_DIR"/lib/config.js >/dev/null 2>&1 && echo '有 cluster 代码 ✓' || echo '✗ 未见 cluster 代码')"
|
||||
echo " lease/agent/tunnel: $(ls "$DSH_INSTALL_DIR"/lib/supervisor/lease.js "$DSH_INSTALL_DIR"/lib/worker/agent.js "$DSH_INSTALL_DIR"/lib/worker/tunnel.js 2>/dev/null | wc -l)/3"
|
||||
|
||||
echo "=== ③ 本地 Worker 单元(w-47,无隧道) ==="
|
||||
echo "=== ③ 本地 Worker 单元("$DSHS_CLUSTER_HOST_ID",无隧道) ==="
|
||||
cat > /etc/dshs-worker.env <<ENV
|
||||
DSHS_DATA_ROOT=/var/lib/dshs
|
||||
DSHS_DATA_ROOT=$DSHS_DATA_ROOT
|
||||
DSHS_ISOLATION_MODE=account
|
||||
DSHS_DSH_BIN=/usr/local/bin/dsh
|
||||
DSHS_BASE_UID=100000
|
||||
@@ -41,7 +43,7 @@ After=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/dshs-worker.env
|
||||
ExecStart=/usr/local/bin/node /opt/dshs/lib/cli.js worker --port 19100 --host 127.0.0.1 --host-id w-47 --instance-host 127.0.0.1 --log-level info
|
||||
ExecStart=/usr/local/bin/node $DSH_INSTALL_DIR/lib/cli.js worker --port 19100 --host 127.0.0.1 --host-id $DSHS_CLUSTER_HOST_ID --instance-host 127.0.0.1 --log-level info
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
KillMode=mixed
|
||||
@@ -53,24 +55,24 @@ systemctl daemon-reload; systemctl enable dshs-worker >/dev/null 2>&1
|
||||
systemctl restart dshs-worker; sleep 5
|
||||
echo " dshs-worker=$(systemctl is-active dshs-worker) healthz=$(curl -s -m 6 http://127.0.0.1:19100/healthz | head -c 120)"
|
||||
|
||||
echo "=== ④ 既有用户归属预置为 w-47(粘性锚点) ==="
|
||||
PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc \
|
||||
echo "=== ④ 既有用户归属预置为 $DSHS_CLUSTER_HOST_ID(粘性锚点) ==="
|
||||
PGPASSWORD="$DSHS_PG_PASSWORD" /usr/bin/psql -h 127.0.0.1 -p "$DSHS_PG_PORT" -U dshs -d dshs -tAc \
|
||||
"insert into dsh_instances (id, user_id, role, status, host_id, epoch, heartbeat_at, lease_until)
|
||||
select 'dsh-'||id, id, 'main', 'stopped', 'w-47', 0, 0, 0 from users
|
||||
on conflict (id) do update set host_id='w-47', lease_until=0" 2>&1 | tail -1
|
||||
PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc \
|
||||
select 'dsh-'||id, id, 'main', 'stopped', '$DSHS_CLUSTER_HOST_ID', 0, 0, 0 from users
|
||||
on conflict (id) do update set host_id='$DSHS_CLUSTER_HOST_ID', lease_until=0" 2>&1 | tail -1
|
||||
PGPASSWORD="$DSHS_PG_PASSWORD" /usr/bin/psql -h 127.0.0.1 -p "$DSHS_PG_PORT" -U dshs -d dshs -tAc \
|
||||
"select u.username||' -> '||coalesce(i.host_id,'NULL') from users u left join dsh_instances i on i.user_id=u.id" 2>&1 | sed 's/^/ /'
|
||||
|
||||
echo "=== ⑤ 注册两台 worker ==="
|
||||
PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc \
|
||||
PGPASSWORD="$DSHS_PG_PASSWORD" /usr/bin/psql -h 127.0.0.1 -p "$DSHS_PG_PORT" -U dshs -d dshs -tAc \
|
||||
"insert into dsh_hosts (id, endpoint, agent_token, capacity_mb, used_mb, status)
|
||||
values ('w-47','http://127.0.0.1:19100','$W47_TOKEN',1024,0,'up'),
|
||||
('w-106','http://127.0.0.1:19000','$W106_TOKEN',2560,0,'up')
|
||||
values ('$DSHS_CLUSTER_HOST_ID','http://127.0.0.1:19100','$W47_TOKEN',1024,0,'up'),
|
||||
('$DSH_PEER_HOST_ID','http://127.0.0.1:19000','$W106_TOKEN',2560,0,'up')
|
||||
on conflict (id) do update set endpoint=excluded.endpoint, agent_token=excluded.agent_token, capacity_mb=excluded.capacity_mb, status='up'" 2>&1 | tail -1
|
||||
PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc \
|
||||
PGPASSWORD="$DSHS_PG_PASSWORD" /usr/bin/psql -h 127.0.0.1 -p "$DSHS_PG_PORT" -U dshs -d dshs -tAc \
|
||||
"select id||' cap='||capacity_mb||' status='||status||' ep='||endpoint from dsh_hosts order by id" 2>&1 | sed 's/^/ /'
|
||||
|
||||
echo "=== 回滚剧本(现在就记下) ==="
|
||||
echo " rm -f /etc/systemd/system/dshs.service.d/cluster.conf && systemctl daemon-reload && \\"
|
||||
echo " systemctl restart dshs # 回 SQLite 单机;lib 回滚 = cp -a /opt/dsh/backups/lib-$TS/lib /opt/dshs/lib"
|
||||
echo " systemctl restart dshs # 回 SQLite 单机;lib 回滚 = cp -a $DSH_BACKUP_DIR/lib-$TS/lib $DSH_INSTALL_DIR/lib"
|
||||
echo " 备份时间戳: $TS"
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# A 步:把 47 的生产库 SQLite → 本机控制面 PG(停机窗口内做,避免迁移期间库变动)
|
||||
set -uo pipefail
|
||||
PGURL="postgres://dshs:[email protected]:15432/dshs"
|
||||
DB=/var/lib/dshs/dshs.db
|
||||
cd /opt/dshs-cluster || exit 1
|
||||
DB="$DSHS_DATA_ROOT"/dshs.db
|
||||
cd "$DSH_INSTALL_DIR"-cluster || exit 1
|
||||
|
||||
echo "=== 1) 停 dshs(窗口开始) ==="
|
||||
systemctl stop dshs
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# 校验:SQLite 与 PG 两侧的 users 明细 + 与磁盘目录对照(判断 2 vs 7 是孤儿目录还是迁移漏行)
|
||||
set -uo pipefail
|
||||
cd /opt/dshs-cluster || exit 1
|
||||
cd "$DSH_INSTALL_DIR"-cluster || exit 1
|
||||
echo "=== SQLite 侧(只读打开,含 WAL) ==="
|
||||
node -e '
|
||||
const D = require("better-sqlite3");
|
||||
const db = new D("/var/lib/dshs/dshs.db", { readonly: true });
|
||||
const db = new D(""$DSH_DB_FILE"", { readonly: true });
|
||||
const users = db.prepare("select id, username, role, uid from users order by rowid").all();
|
||||
console.log(" users 行数:", users.length);
|
||||
for (const u of users) console.log(` ${u.username} role=${u.role} uid=${u.uid} id=${u.id}`);
|
||||
@@ -13,9 +14,9 @@ console.log(" credential_vault:", db.prepare("select count(*) c from credential
|
||||
console.log(" business_plugins:", db.prepare("select count(*) c from business_plugins").get().c);
|
||||
'
|
||||
echo "=== PG 侧 ==="
|
||||
PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc \
|
||||
PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p "$DSHS_PG_PORT" -U dshs -d dshs -tAc \
|
||||
"select username||' role='||role||' uid='||coalesce(uid::text,'NULL')||' id='||id from users order by rowid" 2>&1 | sed 's/^/ /'
|
||||
echo " PG users 总数: $(PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc 'select count(*) from users')"
|
||||
echo " PG users 总数: $(PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p "$DSHS_PG_PORT" -U dshs -d dshs -tAc 'select count(*) from users')"
|
||||
echo "=== 磁盘目录 vs DB ==="
|
||||
echo " 目录($(ls /var/lib/dshs/users | wc -l) 个):"
|
||||
ls /var/lib/dshs/users | sed 's/^/ /'
|
||||
echo " 目录($(ls "$DSH_USERS_DIR" | wc -l) 个):"
|
||||
ls "$DSH_USERS_DIR" | sed 's/^/ /'
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# B1 步(在 47 上跑):uid 保真校验 + 建 47→106 专用密钥并打印公钥
|
||||
set -uo pipefail
|
||||
KEY=/root/.ssh/dshworker_ed25519
|
||||
@@ -6,16 +7,16 @@ KEY=/root/.ssh/dshworker_ed25519
|
||||
echo "=== 1) uid 保真校验(PG 与 SQLite 必须一致 —— 否则 106 上文件属主全错) ==="
|
||||
echo -n " PG : "; PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc \
|
||||
"select string_agg(username||'='||coalesce(uid::text,'NULL'), ' ' order by row_id) from users" 2>&1 | head -1
|
||||
echo -n " SQLite : "; node -e 'const D=require("better-sqlite3");const db=new D("/var/lib/dshs/dshs.db",{readonly:true});console.log(db.prepare("select username, uid from users order by rowid").all().map(u=>u.username+"="+u.uid).join(" "))'
|
||||
echo -n " SQLite : "; node -e 'const D=require("better-sqlite3");const db=new D("'"$DSHS_DATA_ROOT"'/dshs.db",{readonly:true});console.log(db.prepare("select username, uid from users order by rowid").all().map(u=>u.username+"="+u.uid).join(" "))'
|
||||
echo " --- 磁盘目录属主(与 uid 对照;多出的 5 个是已删用户孤儿目录) ---"
|
||||
for d in /var/lib/dshs/users/*/; do printf " %-38s uid=%s\n" "$(basename "$d")" "$(stat -c %u "$d")"; done
|
||||
for d in "$DSHS_DATA_ROOT"/users/*/; do printf " %-38s uid=%s\n" "$(basename "$d")" "$(stat -c %u "$d")"; done
|
||||
|
||||
echo "=== 2) 建 47→106 专用密钥(仅用于 rsync) ==="
|
||||
[ -f "$KEY" ] || ssh-keygen -t ed25519 -N "" -C "dshs-rsync-47to106" -f "$KEY" >/dev/null 2>&1
|
||||
echo " PUBKEY=$(cat "$KEY.pub")"
|
||||
|
||||
echo "=== 3) 试连通 106:22 ==="
|
||||
if ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 root@106.54.21.172 'echo ok' 2>/dev/null | grep -q ok; then
|
||||
if ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 root@"$DSH_PEER_PUBLIC_IP" 'echo ok' 2>/dev/null | grep -q ok; then
|
||||
echo " ✓ 已可连通(公钥已装)"
|
||||
else
|
||||
echo " ⏳ 尚不可连通 —— 需先把我本机把这个公钥装到 106"
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
# · 用 drop-in 而非改 unit:unit 本体 hash 不变 ⇒ 回滚只需删 drop-in
|
||||
# · 同时把 106 的 worker lib 也更新到同一版本(两侧代码必须一致)
|
||||
set -uo pipefail
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
mkdir -p /etc/systemd/system/dshs.service.d
|
||||
cat > /etc/systemd/system/dshs.service.d/cluster.conf <<CONF
|
||||
# T08 集群化(2026-09-15):Manager 在 47、实例落在 w-47(既有用户)/ w-106(新用户)
|
||||
# T08 集群化(2026-09-15):Manager 在 47、实例落在 <host-a>(既有用户)/ <host-b>(新用户)
|
||||
# 回滚:删除本文件 → systemctl daemon-reload → systemctl restart dshs
|
||||
[Service]
|
||||
Environment="DSHS_DEPLOY_MODE=cluster"
|
||||
Environment="DSHS_DB_URL=postgres://dshs:[email protected]:15432/dshs"
|
||||
Environment="DSHS_CLUSTER_HOST_ID=w-47"
|
||||
Environment="DSHS_DB_URL=$DSHS_DB_URL"
|
||||
Environment="DSHS_CLUSTER_HOST_ID=$DSHS_CLUSTER_HOST_ID"
|
||||
Environment="DSHS_CLUSTER_AGENT_URL=http://127.0.0.1:19100"
|
||||
Environment="DSHS_CLUSTER_AGENT_TOKEN=dshs-worker-47-c4b7e19f"
|
||||
Environment="DSHS_CLUSTER_AGENT_TOKEN=$DSHS_CLUSTER_AGENT_TOKEN"
|
||||
Environment="DSHS_CLUSTER_INSTANCE_HOST=127.0.0.1"
|
||||
Environment="DSHS_CLUSTER_WORKER_DATA_ROOT=/var/lib/dshs"
|
||||
Environment="DSHS_CLUSTER_WORKER_DATA_ROOT=$DSHS_DATA_ROOT"
|
||||
Environment="DSHS_CLUSTER_CAPACITY_MB=-1"
|
||||
Environment="DSHS_CLUSTER_REGISTER_SELF=0"
|
||||
Environment="DSHS_CLUSTER_LEASE_TTL_MS=30000"
|
||||
@@ -30,9 +31,9 @@ echo " dshs=$(systemctl is-active dshs) | dshs-pg=$(systemctl is-active dshs-
|
||||
echo " 生效 env(systemd 解析后):"
|
||||
systemctl show dshs -p Environment 2>/dev/null | tr ' ' '\n' | grep -E "DEPLOY_MODE|CLUSTER_HOST_ID|CLUSTER_AGENT_URL|DB_URL" | sed 's/^/ /'
|
||||
echo " 门户: login.html=$(curl -s -o /dev/null -w '%{http_code}' -m 8 http://127.0.0.1:3080/login.html)"
|
||||
echo " 公网: https://alotbuy.com/login.html = $(curl -s -o /dev/null -w '%{http_code}' -m 12 https://alotbuy.com/login.html)"
|
||||
echo " 公网: https://$DSHS_BASE_DOMAIN/login.html = $(curl -s -o /dev/null -w '%{http_code}' -m 12 https://$DSHS_BASE_DOMAIN/login.html)"
|
||||
echo " --- dshs cluster status ---"
|
||||
cd /opt/dshs && set -a && . /etc/dshs.env && set +a && set -a && . /etc/systemd/system/dshs.service.d/cluster.conf 2>/dev/null || true
|
||||
cd /opt/dshs && DSHS_DB_URL="postgres://dshs:[email protected]:15432/dshs" node lib/cli.js cluster status 2>&1 | head -8 | sed 's/^/ /'
|
||||
cd "$DSH_INSTALL_DIR" && set -a && . /etc/dshs.env && set +a && set -a && . /etc/systemd/system/dshs.service.d/cluster.conf 2>/dev/null || true
|
||||
cd "$DSH_INSTALL_DIR" && node lib/cli.js cluster status 2>&1 | head -8 | sed 's/^/ /'
|
||||
echo " --- 回滚命令(随时可用) ---"
|
||||
echo " rm -f /etc/systemd/system/dshs.service.d/cluster.conf && systemctl daemon-reload && systemctl restart dshs"
|
||||
+11
-10
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# B2 步(在 47 上跑):rsync 生产 dataRoot → 106
|
||||
# · --numeric-ids 保 uid/gid(否则 106 上文件属主全错 ⇒ 实例 EACCES)
|
||||
# · 排除 dshs.db*(DB 权威源已是 47 的 PG)与 secret.key(凭据主密钥不外扩到 Worker —— R5 最小面)
|
||||
# · ⚠️ 所有 ssh 调用带 -n:脚本本身经 stdin 传入,ssh 若不隔离 stdin 会把**脚本剩余部分**吃掉
|
||||
set -uo pipefail
|
||||
KEY=/root/.ssh/dshworker_ed25519
|
||||
DST=root@106.54.21.172
|
||||
SRC=/var/lib/dshs
|
||||
DST=root@"$DSH_PEER_PUBLIC_IP"
|
||||
SRC="$DSHS_DATA_ROOT"
|
||||
SSHOPT="-n -i $KEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10"
|
||||
|
||||
command -v rsync >/dev/null || { dnf -y install rsync >/tmp/dnf-rsync.log 2>&1 && echo " ✓ 47 装上 rsync"; }
|
||||
@@ -15,19 +16,19 @@ echo "=== 连通性 + 对端 rsync ==="
|
||||
ssh $SSHOPT "$DST" 'command -v rsync >/dev/null || dnf -y install rsync >/tmp/dnf-rs.log 2>&1; echo " 对端: $(rsync --version | head -1)"' 2>&1 | tail -2
|
||||
|
||||
echo "=== 目标端准备 ==="
|
||||
ssh $SSHOPT "$DST" 'mkdir -p /var/lib/dshs && ls -ld /var/lib/dshs' 2>&1 | sed 's/^/ /'
|
||||
ssh $SSHOPT "$DST" "mkdir -p $DSHS_DATA_ROOT && ls -ld $DSHS_DATA_ROOT" 2>&1 | sed 's/^/ /'
|
||||
|
||||
echo "=== rsync ==="
|
||||
rsync -a --numeric-ids --stats -e "ssh $SSHOPT" \
|
||||
--exclude 'dshs.db' --exclude 'dshs.db-shm' --exclude 'dshs.db-wal' --exclude 'secret.key' \
|
||||
"$SRC/" "$DST:/var/lib/dshs/" 2>&1 | grep -E "Number of regular files transferred|Total file size|sent [0-9]|total size is" | sed 's/^/ /'
|
||||
"$SRC/" "$DST:"$DSHS_DATA_ROOT"/" 2>&1 | grep -E "Number of regular files transferred|Total file size|sent [0-9]|total size is" | sed 's/^/ /'
|
||||
|
||||
echo "=== 目标端核对 ==="
|
||||
ssh $SSHOPT "$DST" 'bash -c "
|
||||
echo \" 顶层: \$(ls /var/lib/dshs | tr \"\n\" \" \")\"
|
||||
echo \" users 目录数: \$(ls /var/lib/dshs/users 2>/dev/null | wc -l)\"
|
||||
for d in /var/lib/dshs/users/*/; do printf \" %-38s uid=%s\n\" \"\$(basename \$d)\" \"\$(stat -c %u \$d)\"; done
|
||||
echo \" bundled-skills: \$(ls /var/lib/dshs/bundled-skills 2>/dev/null | wc -l) 项\"
|
||||
echo \" business-plugins: \$(ls /var/lib/dshs/business-plugins 2>/dev/null | wc -l) 项\"
|
||||
echo \" ⛔ 不应存在(dshs.db/secret.key): \$(ls /var/lib/dshs/dshs.db /var/lib/dshs/secret.key 2>/dev/null | wc -l) 个(应为 0)\"
|
||||
echo \" 顶层: \$(ls "$DSHS_DATA_ROOT" | tr \"\n\" \" \")\"
|
||||
echo \" users 目录数: \$(ls "$DSHS_DATA_ROOT"/users 2>/dev/null | wc -l)\"
|
||||
for d in "$DSHS_DATA_ROOT"/users/*/; do printf \" %-38s uid=%s\n\" \"\$(basename \$d)\" \"\$(stat -c %u \$d)\"; done
|
||||
echo \" bundled-skills: \$(ls "$DSHS_DATA_ROOT"/bundled-skills 2>/dev/null | wc -l) 项\"
|
||||
echo \" business-plugins: \$(ls "$DSHS_DATA_ROOT"/business-plugins 2>/dev/null | wc -l) 项\"
|
||||
echo \" ⛔ 不应存在(dshs.db/secret.key): \$(ls "$DSHS_DATA_ROOT"/dshs.db "$DSHS_DATA_ROOT"/secret.key 2>/dev/null | wc -l) 个(应为 0)\"
|
||||
"' 2>&1
|
||||
+11
-10
@@ -1,30 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# B2' 步:47 → 106 直推生产 dataRoot(tar-over-ssh;只用命令执行,绕开 rsync 协议问题)
|
||||
# · tar --numeric-owner 保 uid/gid(否则 106 上属主错 ⇒ 实例 EACCES)
|
||||
# · 排除 dshs.db*(权威源=47 的 PG)与 secret.key(主密钥不外扩 —— R5 最小面)
|
||||
set -uo pipefail
|
||||
KEY=/root/.ssh/dshworker_ed25519
|
||||
DST=root@106.54.21.172
|
||||
DST=root@"$DSH_PEER_PUBLIC_IP"
|
||||
SSHO="-i $KEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
|
||||
|
||||
echo "=== 1) 目标端准备(ssh -n 防抢 stdin) ==="
|
||||
ssh -n $SSHO "$DST" 'mkdir -p /var/lib/dshs && echo " ready: $(ls -ld /var/lib/dshs)"'
|
||||
ssh -n $SSHO "$DST" "mkdir -p $DSHS_DATA_ROOT && echo ready: \$(ls -ld $DSHS_DATA_ROOT)"
|
||||
|
||||
echo "=== 2) 推送(tar → ssh → tar --numeric-owner -x) ==="
|
||||
cd /var/lib/dshs
|
||||
cd "$DSHS_DATA_ROOT"
|
||||
tar --numeric-owner -cf - \
|
||||
--exclude=./dshs.db --exclude=./dshs.db-shm --exclude=./dshs.db-wal --exclude=./secret.key \
|
||||
. | ssh $SSHO "$DST" 'tar -C /var/lib/dshs --numeric-owner -xf -'
|
||||
. | ssh $SSHO "$DST" "tar -C $DSHS_DATA_ROOT --numeric-owner -xf -"
|
||||
rc=$?
|
||||
echo " 管道 rc=$rc"
|
||||
|
||||
echo "=== 3) 目标端核对 ==="
|
||||
ssh -n $SSHO "$DST" 'bash -c "
|
||||
echo \" 顶层: \$(ls /var/lib/dshs | tr \"\n\" \" \")\"
|
||||
echo \" 总量: \$(du -sh /var/lib/dshs | cut -f1)\"
|
||||
echo \" users 目录数: \$(ls /var/lib/dshs/users | wc -l)\"
|
||||
echo \" 顶层: \$(ls "$DSHS_DATA_ROOT" | tr \"\n\" \" \")\"
|
||||
echo \" 总量: \$(du -sh "$DSHS_DATA_ROOT" | cut -f1)\"
|
||||
echo \" users 目录数: \$(ls "$DSHS_DATA_ROOT"/users | wc -l)\"
|
||||
echo \" --- 属主抽样(应与 47 的 uid 一致) ---\"
|
||||
for d in /var/lib/dshs/users/*/; do printf \" %-38s uid=%s\n\" \"\$(basename \$d)\" \"\$(stat -c %u \$d)\"; done
|
||||
echo \" bundled-skills=\$(ls /var/lib/dshs/bundled-skills | wc -l) business-plugins=\$(ls /var/lib/dshs/business-plugins | wc -l)\"
|
||||
echo \" ⛔ 不该有 dshs.db/secret.key: \$(ls /var/lib/dshs/dshs.db /var/lib/dshs/secret.key 2>/dev/null | wc -l) 个(应 0)\"
|
||||
for d in "$DSHS_DATA_ROOT"/users/*/; do printf \" %-38s uid=%s\n\" \"\$(basename \$d)\" \"\$(stat -c %u \$d)\"; done
|
||||
echo \" bundled-skills=\$(ls "$DSHS_DATA_ROOT"/bundled-skills | wc -l) business-plugins=\$(ls "$DSHS_DATA_ROOT"/business-plugins | wc -l)\"
|
||||
echo \" ⛔ 不该有 dshs.db/secret.key: \$(ls "$DSHS_DATA_ROOT"/dshs.db "$DSHS_DATA_ROOT"/secret.key 2>/dev/null | wc -l) 个(应 0)\"
|
||||
"'
|
||||
+17
-16
@@ -1,15 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# 最终验证(在 47 上跑):清污染 → 重置锚点 → 重验两条路径
|
||||
# ① 既有用户 guest:留 w-47 + 工作区有历史数据 + 实例页正常
|
||||
# ② 新用户:落 w-106 + **文件真的写到 106 的盘** + 实例页正常
|
||||
# ① 既有用户 guest:留 <host-a> + 工作区有历史数据 + 实例页正常
|
||||
# ② 新用户:落 <host-b> + **文件真的写到 106 的盘** + 实例页正常
|
||||
set -uo pipefail
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
export PGPASSWORD=dshs_cluster_2026
|
||||
Q() { /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc "$1"; }
|
||||
M=http://127.0.0.1:3080
|
||||
DOMAIN=alotbuy.com
|
||||
T47=dshs-worker-47-c4b7e19f
|
||||
T106=dshs-worker-7f3a91c05e
|
||||
SSH106="ssh -n -i /root/.ssh/dshworker_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@106.54.21.172"
|
||||
DOMAIN="${DSHS_BASE_DOMAIN:?config/platform.env 缺 DSHS_BASE_DOMAIN}"
|
||||
T47="$DSHS_CLUSTER_AGENT_TOKEN"
|
||||
T106="$DSH_PEER_AGENT_TOKEN"
|
||||
SSH106="ssh -n -i /root/.ssh/dshworker_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@"$DSH_PEER_PUBLIC_IP""
|
||||
|
||||
mksess() {
|
||||
local u="$1" T H
|
||||
@@ -25,15 +26,15 @@ isapp() { grep -q '<base href=' <<<"$1" && echo "✓实例页" || echo "✗非
|
||||
echo "########## 0) 清污染:停所有实例 + 删测试用户 ##########"
|
||||
systemctl restart dshs-worker; sleep 4
|
||||
$SSH106 'systemctl restart dshs-worker' >/dev/null 2>&1; sleep 4
|
||||
echo " 重启后 w-47=$(agent 19100 $T47) w-106=$(agent 19000 $T106)"
|
||||
echo " 重启后 "$DSHS_CLUSTER_HOST_ID"=$(agent 19100 $T47) "$DSH_PEER_HOST_ID"=$(agent 19000 $T106)"
|
||||
AT=$(mksess admin); AC="sid=$AT"
|
||||
for u in $(Q "select id from users where username like 'switchtest%' or username like 'swtest%'"); do
|
||||
echo " 删测试用户 $u → $(curl -s -m 20 -X DELETE -b "$AC" "$M/api/admin/users/$u" -o /dev/null -w '%{http_code}')"
|
||||
done
|
||||
echo " 剩余用户: $(Q "select string_agg(username,', ') from users")"
|
||||
|
||||
echo "########## 1) 重置 guest 锚点(host_id=w-47) ##########"
|
||||
Q "update dsh_instances set host_id='w-47', epoch=0, lease_until=0, status='stopped'
|
||||
echo "########## 1) 重置 guest 锚点(host_id="$DSHS_CLUSTER_HOST_ID") ##########"
|
||||
Q "update dsh_instances set host_id='w-1', epoch=0, lease_until=0, status='stopped'
|
||||
where user_id=(select id from users where username='guest')" >/dev/null
|
||||
echo " $(owner guest)"
|
||||
|
||||
@@ -42,8 +43,8 @@ echo "########## 2) 路径①:既有用户 guest ##########"
|
||||
GT=$(mksess guest); GC="sid=$GT"
|
||||
E=$(curl -s -m 60 -X POST -b "$GC" -H 'content-type: application/json' -d '{}' "$M/api/dsh/enter")
|
||||
sleep 3
|
||||
echo " 归属: $(owner guest) ← 期望 w-47"
|
||||
echo " w-47=$(agent 19100 $T47) w-106=$(agent 19000 $T106)"
|
||||
echo " 归属: $(owner guest) ← 期望 "$DSHS_CLUSTER_HOST_ID""
|
||||
echo " "$DSHS_CLUSTER_HOST_ID"=$(agent 19100 $T47) "$DSH_PEER_HOST_ID"=$(agent 19000 $T106)"
|
||||
echo " 工作区条目: $(curl -s -m 10 -b "$GC" "$M/api/desktop/tree" | grep -o '"name":"[^"]*"' | head -4 | tr '\n' ' ')"
|
||||
PAGE=$(curl -s -m 25 -L -b "$GC" -H "Host: guest.$DOMAIN" "$M/" | head -c 300)
|
||||
echo " 实例页: $(isapp "$PAGE")"
|
||||
@@ -56,16 +57,16 @@ NID=$(Q "select id from users where username='$NU'")
|
||||
echo " approve=$(curl -s -m 15 -X POST -b "$AC" "$M/api/admin/users/$NID/approve" -o /dev/null -w '%{http_code}')"
|
||||
NC=$(curl -s -m 15 -X POST -H 'content-type: application/json' -d "{\"username\":\"$NU\",\"password\":\"SwitchTest123\"}" -D - "$M/api/auth/login" -o /dev/null | grep -i '^set-cookie' | head -1 | grep -oP 'sid=[^;]+')
|
||||
echo " mkdir=$(curl -s -m 15 -X POST -b "$NC" -H 'content-type: application/json' -d '{"path":"proj"}' "$M/api/fs/mkdir" -o /dev/null -w '%{http_code}') ← 首次触达应把归属钉住"
|
||||
echo " 钉住后归属: $(owner "$NU") ← 期望 w-106(与下面的 launch 必须同台)"
|
||||
echo " 钉住后归属: $(owner "$NU") ← 期望 "$DSH_PEER_HOST_ID"(与下面的 launch 必须同台)"
|
||||
echo " upload=$(curl -s -m 20 -X POST -b "$NC" -H 'content-type: application/json' -d "{\"path\":\"proj\",\"name\":\"hello.txt\",\"data\":\"$(printf 'hi-from-switch' | base64 -w0)\"}" "$M/api/fs/upload" -o /dev/null -w '%{http_code}')"
|
||||
echo " launch=$(curl -s -m 60 -X POST -b "$NC" -H 'content-type: application/json' -d '{"folder":"proj"}' "$M/api/dsh/launch" -o /dev/null -w '%{http_code}')"
|
||||
sleep 4
|
||||
echo " 归属: $(owner "$NU") ← 期望 w-106(粘性保持)"
|
||||
echo " w-106=$(agent 19000 $T106) w-47=$(agent 19100 $T47)"
|
||||
echo " 归属: $(owner "$NU") ← 期望 "$DSH_PEER_HOST_ID"(粘性保持)"
|
||||
echo " "$DSH_PEER_HOST_ID"=$(agent 19000 $T106) "$DSHS_CLUSTER_HOST_ID"=$(agent 19100 $T47)"
|
||||
echo " --- 落盘取证 ---"
|
||||
L47=$($SSH106 "ls /var/lib/dshs/users/$NID/ws/proj/hello.txt 2>/dev/null" 2>/dev/null || true)
|
||||
L47=$($SSH106 "ls "$DSHS_DATA_ROOT"/users/$NID/ws/proj/hello.txt 2>/dev/null" 2>/dev/null || true)
|
||||
echo " 106 盘: ${L47:-不存在}"
|
||||
echo " 47 盘: $(ls /var/lib/dshs/users/$NID/ws/proj/hello.txt 2>/dev/null || echo 不存在(应不存在 ✓))"
|
||||
echo " 47 盘: $(ls "$DSHS_DATA_ROOT"/users/$NID/ws/proj/hello.txt 2>/dev/null || echo 不存在(应不存在 ✓))"
|
||||
NP=$(curl -s -m 25 -L -b "$NC" -H "Host: $NU.$DOMAIN" "$M/" | head -c 300)
|
||||
echo " 实例页(Host: $NU.$DOMAIN): $(isapp "$NP")"
|
||||
|
||||
|
||||
+15
-14
@@ -1,14 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# 切换后功能验证 v2(在 47 上跑)
|
||||
# ① 既有用户 guest:应留 w-47,且**工作区有历史数据**(文件在 47 的盘上)
|
||||
# ② 新用户:应落 w-106,且**建的文件真的出现在 106 的盘上**(文件面路由已修)
|
||||
# ① 既有用户 guest:应留 <host-a>,且**工作区有历史数据**(文件在 47 的盘上)
|
||||
# ② 新用户:应落 <host-b>,且**建的文件真的出现在 106 的盘上**(文件面路由已修)
|
||||
# 判据:实例页必须带 <base href="/"(门户页不算);归属看 PG;文件落盘看两台机器磁盘
|
||||
set -uo pipefail
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
PG() { PGPASSWORD=dshs_cluster_2026 /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc "$1"; }
|
||||
M=http://127.0.0.1:3080
|
||||
DOMAIN=alotbuy.com
|
||||
T47=dshs-worker-47-c4b7e19f
|
||||
T106=dshs-worker-7f3a91c05e
|
||||
DOMAIN="${DSHS_BASE_DOMAIN:?config/platform.env 缺 DSHS_BASE_DOMAIN}"
|
||||
T47="$DSHS_CLUSTER_AGENT_TOKEN"
|
||||
T106="$DSH_PEER_AGENT_TOKEN"
|
||||
|
||||
mksess() {
|
||||
local u="$1" T H
|
||||
@@ -21,21 +22,21 @@ owner() { PG "select u.username||' -> '||coalesce(i.host_id,'NULL')||' epoch='||
|
||||
agent() { curl -s -m 6 -H "x-dsh-agent-token: $2" "http://127.0.0.1:$1/healthz" | grep -o '"instances":[0-9]*'; }
|
||||
isapp() { grep -q '<base href=' <<<"$1" && echo "✓实例页" || echo "✗非实例页"; }
|
||||
|
||||
echo "############ ① 既有用户 guest(应留 w-47 + 工作区有数据) ############"
|
||||
echo "############ ① 既有用户 guest(应留 "$DSHS_CLUSTER_HOST_ID" + 工作区有数据) ############"
|
||||
GT=$(mksess guest); GC="sid=$GT"
|
||||
E=$(curl -s -m 60 -X POST -b "$GC" -H 'content-type: application/json' -d '{}' "$M/api/dsh/enter")
|
||||
echo " enter → $(head -c 130 <<<"$E")"
|
||||
sleep 3
|
||||
echo " 归属: $(owner guest) ← 期望 w-47"
|
||||
echo " w-47=$(agent 19100 $T47) w-106=$(agent 19000 $T106)"
|
||||
echo " 归属: $(owner guest) ← 期望 "$DSHS_CLUSTER_HOST_ID""
|
||||
echo " "$DSHS_CLUSTER_HOST_ID"=$(agent 19100 $T47) "$DSH_PEER_HOST_ID"=$(agent 19000 $T106)"
|
||||
echo " 工作区首项: $(curl -s -m 10 -b "$GC" "$M/api/desktop/tree" | head -c 150)"
|
||||
URL=$(grep -o '"url":"[^"]*"' <<<"$E" | head -1 | cut -d'"' -f4)
|
||||
PAGE=$(curl -s -m 25 -L -b "$GC" -H "Host: guest.$DOMAIN" "$M/" | head -c 300)
|
||||
echo " 实例页: $(isapp "$PAGE")"
|
||||
echo " 47 盘上 guest 工作区条目: $(ls /var/lib/dshs/users/4092b965-2f68-4977-9989-68b3966f7df0/ws 2>/dev/null | wc -l) 项(>0 = 数据在 47 ✓)"
|
||||
echo " 47 盘上 guest 工作区条目: $(ls "$DSHS_DATA_ROOT"/users/4092b965-2f68-4977-9989-68b3966f7df0/ws 2>/dev/null | wc -l) 项(>0 = 数据在 47 ✓)"
|
||||
|
||||
echo
|
||||
echo "############ ② 新用户(应落 w-106 + 文件真的写到 106 盘) ############"
|
||||
echo "############ ② 新用户(应落 "$DSH_PEER_HOST_ID" + 文件真的写到 106 盘) ############"
|
||||
NU="swtest$(date +%H%M%S)"
|
||||
AT=$(mksess admin); AC="sid=$AT"
|
||||
echo " register=$(curl -s -m 15 -X POST -H 'content-type: application/json' -d "{\"username\":\"$NU\",\"password\":\"SwitchTest123\"}" "$M/api/auth/register" -o /dev/null -w '%{http_code}')"
|
||||
@@ -46,11 +47,11 @@ echo " mkdir=$(curl -s -m 15 -X POST -b "$NC" -H 'content-type: application/jso
|
||||
echo " upload=$(curl -s -m 20 -X POST -b "$NC" -H 'content-type: application/json' -d "{\"path\":\"proj\",\"name\":\"hello.txt\",\"data\":\"$(printf 'hi-from-switch' | base64 -w0)\"}" "$M/api/fs/upload" -o /dev/null -w '%{http_code}')"
|
||||
echo " launch=$(curl -s -m 60 -X POST -b "$NC" -H 'content-type: application/json' -d '{"folder":"proj"}' "$M/api/dsh/launch" -o /dev/null -w '%{http_code}')"
|
||||
sleep 4
|
||||
echo " 归属: $(owner "$NU") ← 期望 w-106"
|
||||
echo " w-106=$(agent 19000 $T106) w-47=$(agent 19100 $T47)"
|
||||
echo " 归属: $(owner "$NU") ← 期望 "$DSH_PEER_HOST_ID""
|
||||
echo " "$DSH_PEER_HOST_ID"=$(agent 19000 $T106) "$DSHS_CLUSTER_HOST_ID"=$(agent 19100 $T47)"
|
||||
echo " --- 文件落盘取证(这才是文件面路由修好的证据) ---"
|
||||
echo " 47 盘: $(ls /var/lib/dshs/users/$NU_ID/ws/proj/hello.txt 2>/dev/null && echo 存在 || echo '不存在 ✓(不应在 47)')"
|
||||
echo " 106 盘: $(ssh -n -i /root/.ssh/dshworker_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@106.54.21.172 "ls /var/lib/dshs/users/$NU_ID/ws/proj/hello.txt 2>/dev/null" 2>/dev/null && echo 存在✓ || echo '不存在 ✗')"
|
||||
echo " 47 盘: $(ls "$DSHS_DATA_ROOT"/users/$NU_ID/ws/proj/hello.txt 2>/dev/null && echo 存在 || echo '不存在 ✓(不应在 47)')"
|
||||
echo " 106 盘: $(ssh -n -i /root/.ssh/dshworker_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@"$DSH_PEER_PUBLIC_IP" "ls "$DSHS_DATA_ROOT"/users/$NU_ID/ws/proj/hello.txt 2>/dev/null" 2>/dev/null && echo 存在✓ || echo '不存在 ✗')"
|
||||
NU_PAGE=$(curl -s -m 25 -L -b "$NC" -H "Host: $NU.$DOMAIN" "$M/" | head -c 300)
|
||||
echo " 实例页(Host: $NU.$DOMAIN): $(isapp "$NU_PAGE")"
|
||||
|
||||
|
||||
@@ -4,19 +4,21 @@
|
||||
# · token 走 env 文件(600)而不是命令行,避免 ps 泄露
|
||||
# · 反向隧道复用演练时那把 key(其公钥已在 47 的 authorized_keys 里,restrict,port-forwarding)
|
||||
set -uo pipefail
|
||||
TOKEN="${WORKER_TOKEN:-dshs-worker-7f3a91c05e}"
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
# 凭据 / 地址一律来自配置(config/platform.env)—— ⛔ 不在脚本里写死
|
||||
TOKEN="${DSH_PEER_AGENT_TOKEN:?config/platform.env 缺 DSH_PEER_AGENT_TOKEN}"
|
||||
|
||||
cat > /etc/dshs-worker.env <<ENV
|
||||
# DSHS 集群 Worker(2026-09-15 切换)—— 与 47 /etc/dshs.env 的**实例侧**条目保持一致
|
||||
DSHS_DATA_ROOT=/var/lib/dshs
|
||||
DSHS_DATA_ROOT=$DSHS_DATA_ROOT
|
||||
DSHS_ISOLATION_MODE=account
|
||||
DSHS_DSH_BIN=/usr/bin/dsh
|
||||
DSHS_BASE_UID=100000
|
||||
DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160
|
||||
DSH_INSTANCE_UNIVER_SOCKET=auto
|
||||
# 控制通道:Worker 主动拨 47 的反向隧道(公网入方向被云安全组挡住 ⇒ 只能这个方向)
|
||||
# 控制通道:Worker 主动拨 Manager 的反向隧道(公网入方向被云安全组挡住 ⇒ 只能这个方向)
|
||||
DSHS_CLUSTER_AGENT_TOKEN=$TOKEN
|
||||
DSHS_TUNNEL_TARGET=[email protected]:32022
|
||||
DSHS_TUNNEL_TARGET=$DSHS_TUNNEL_TARGET
|
||||
DSHS_TUNNEL_IDENTITY=/root/.ssh/tunnel_ed25519
|
||||
ENV
|
||||
chmod 600 /etc/dshs-worker.env
|
||||
@@ -30,7 +32,7 @@ Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/dshs-worker.env
|
||||
ExecStart=/usr/bin/node /opt/dshs-cluster/lib/cli.js worker --port 19000 --host 127.0.0.1 --host-id w-106 --instance-host 127.0.0.1 --log-level info
|
||||
ExecStart=/usr/bin/node $DSH_INSTALL_DIR-cluster/lib/cli.js worker --port 19000 --host 127.0.0.1 --host-id $DSH_PEER_HOST_ID --instance-host 127.0.0.1 --log-level info
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
KillMode=mixed
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# 切换收尾:清理验证残留 + 健康检查(在 47 上跑)
|
||||
set -uo pipefail
|
||||
. "$(cd "$(dirname "$0")/.." && pwd)/config/load.sh"
|
||||
export PGPASSWORD=dshs_cluster_2026
|
||||
Q() { /usr/bin/psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -tAc "$1"; }
|
||||
M=http://127.0.0.1:3080
|
||||
@@ -17,16 +18,16 @@ echo " 剩余用户: $(Q "select string_agg(username||'('||role||')', ', ') fro
|
||||
|
||||
echo "=== 2) 停掉验证期间起的实例 ==="
|
||||
systemctl restart dshs-worker; sleep 4
|
||||
ssh -n -i /root/.ssh/dshworker_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@106.54.21.172 'systemctl restart dshs-worker' >/dev/null 2>&1
|
||||
ssh -n -i /root/.ssh/dshworker_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@"$DSH_PEER_PUBLIC_IP" 'systemctl restart dshs-worker' >/dev/null 2>&1
|
||||
sleep 4
|
||||
echo " w-47=$(curl -s -m 6 -H 'x-dsh-agent-token: dshs-worker-47-c4b7e19f' http://127.0.0.1:19100/healthz | grep -o '\"instances\":[0-9]*')"
|
||||
echo " w-106=$(ssh -n -i /root/.ssh/dshworker_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@106.54.21.172 'curl -s -m 6 -H "x-dsh-agent-token: dshs-worker-7f3a91c05e" http://127.0.0.1:19000/healthz | grep -o .instances.:[0-9]*' 2>/dev/null)"
|
||||
echo " "$DSHS_CLUSTER_HOST_ID"=$(curl -s -m 6 -H "x-dsh-agent-token: $DSHS_CLUSTER_AGENT_TOKEN" http://127.0.0.1:19100/healthz | grep -o '\"instances\":[0-9]*')"
|
||||
echo " "$DSH_PEER_HOST_ID"=$(ssh -n -i /root/.ssh/dshworker_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@"$DSH_PEER_PUBLIC_IP" "curl -s -m 6 -H 'x-dsh-agent-token: $DSH_PEER_AGENT_TOKEN' http://127.0.0.1:19000/healthz | grep -o .instances.:[0-9]*" 2>/dev/null)"
|
||||
|
||||
echo "=== 3) 健康检查 ==="
|
||||
echo " dshs=$(systemctl is-active dshs) dshs-pg=$(systemctl is-active dshs-pg) dshs-worker=$(systemctl is-active dshs-worker)"
|
||||
echo " 门户公网: $(curl -s -o /dev/null -w '%{http_code}' -m 12 https://alotbuy.com/login.html)"
|
||||
echo " 门户公网: $(curl -s -o /dev/null -w '%{http_code}' -m 12 https://$DSHS_BASE_DOMAIN/login.html)"
|
||||
echo " --- dshs cluster status ---"
|
||||
cd /opt/dshs && set -a && . /etc/dshs.env && set +a
|
||||
cd "$DSH_INSTALL_DIR" && set -a && . /etc/dshs.env && set +a
|
||||
DSHS_DEPLOY_MODE=cluster DSHS_DB_URL="postgres://dshs:[email protected]:15432/dshs" \
|
||||
node lib/cli.js cluster status 2>&1 | head -9 | sed 's/^/ /'
|
||||
echo " --- dshs doctor ---"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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:19000`
|
||||
* · 控制面 PG 也在**另一台机器**(106)上,经隧道出现在本机 `127.0.0.1:15432`
|
||||
* · 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 \
|
||||
@@ -81,7 +82,7 @@ try {
|
||||
|
||||
// ── 0) 两侧可达性(跨机链路的第一层证据)─────────────────────────────
|
||||
const h1 = await agent(AGENT1, '/healthz')
|
||||
assert(h1.status === 200 && h1.body.hostId === 'w-106', `Worker w-106 应可达(实际 ${JSON.stringify(h1.body)})`)
|
||||
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))
|
||||
|
||||
@@ -93,13 +94,13 @@ try {
|
||||
let r = await json('/api/admin/hosts', {
|
||||
method: 'POST',
|
||||
cookie: adminCookie,
|
||||
body: { id: 'w-106', endpoint: AGENT1, token: TOKEN, capacityMb: 4096 },
|
||||
body: { id: 'w-2', endpoint: AGENT1, token: TOKEN, capacityMb: 4096 },
|
||||
})
|
||||
assert(r.status === 200, `注册 w-106 失败 ${r.status}`)
|
||||
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-106'), 'w-106 出现在 worker 目录')
|
||||
assert(hosts.body.hosts.some((h) => h.id === 'w-2'), 'w-2 出现在 worker 目录')
|
||||
assert(!('agentToken' in (hosts.body.hosts[0] ?? {})), '**绝不下发 agentToken**')
|
||||
console.log('① 注册 -> w-106(列表不含 agentToken)')
|
||||
console.log('① 注册 -> w-2(列表不含 agentToken)')
|
||||
|
||||
// ── 2) 用户流程 ───────────────────────────────────────────────────────
|
||||
const uname = `crossuser${Date.now() % 100000}`
|
||||
@@ -145,22 +146,22 @@ try {
|
||||
r = await json('/api/admin/hosts', {
|
||||
method: 'POST',
|
||||
cookie: adminCookie,
|
||||
body: { id: 'w-106b', endpoint: AGENT2, token: TOKEN, capacityMb: 4096 },
|
||||
body: { id: 'w-2b', endpoint: AGENT2, token: TOKEN, capacityMb: 4096 },
|
||||
})
|
||||
assert(r.status === 200, `注册 w-106b 失败 ${r.status}`)
|
||||
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-106b' },
|
||||
body: { targetHost: 'w-2b' },
|
||||
})
|
||||
assert(r.status === 200, `迁移应 200(实际 ${r.status} ${JSON.stringify(r.body)})`)
|
||||
assert(r.body.to === 'w-106b', `迁移目标应为 w-106b(实际 ${r.body.to})`)
|
||||
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-106 移到 w-106b')
|
||||
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 })
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* T08 · **域名形态访问**验证(在演练环境做:不动生产、不动 DNS、不动证书)。
|
||||
*
|
||||
* 要回答的问题:生产切到 cluster(Manager 在 47、实例在 106)后,
|
||||
* **按域名形态访问**(`<用户名>.ai1net.com`)还能不能正常落到 106 上的实例?
|
||||
* **按域名形态访问**(`<用户名>.<base-domain>`)还能不能正常落到 106 上的实例?
|
||||
*
|
||||
* 做法:给演练 Manager 设一个**测试 baseDomain**,用**显式 `Host` 头**打进去。
|
||||
*
|
||||
@@ -10,15 +11,15 @@
|
||||
* (Fetch 规范把它列为禁止头,undici 直接忽略)⇒ 请求落到"无租户"的门户路由、回 200 门户页,
|
||||
* 看起来"验证通过"其实是假的。⇒ **必须用 curl(`-H Host:`)**,且判据不能只看状态码。
|
||||
*
|
||||
* 运行(在 47 上):MANAGER=http://127.0.0.1:13080 BASE_DOMAIN=test.ai1net.com \
|
||||
* AGENT=http://127.0.0.1:19000 AGENT_TOKEN=cross-machine-token \
|
||||
* 运行(在 47 上):MANAGER=http://127.0.0.1:13080 BASE_DOMAIN=test.<base-domain> \
|
||||
* AGENT=http://127.0.0.1:<worker-port-b> AGENT_TOKEN=cross-machine-token \
|
||||
* node scripts/verify-cluster-domain.mjs
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const MANAGER = process.env.MANAGER ?? 'http://127.0.0.1:13080'
|
||||
const BASE_DOMAIN = process.env.BASE_DOMAIN ?? 'test.ai1net.com'
|
||||
const BASE_DOMAIN = process.env.BASE_DOMAIN ?? 'test.example.net'
|
||||
const AGENT = process.env.AGENT ?? 'http://127.0.0.1:19000'
|
||||
const TOKEN = process.env.AGENT_TOKEN ?? 'cross-machine-token'
|
||||
const ADMIN_PW = process.env.ADMIN_PW ?? 'crossmgr123'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* ⑤ 顺带验**注册 + 心跳**:`dsh_hosts` 里有记录且 `last_heartbeat` 持续更新。
|
||||
*
|
||||
* 需要 PG(两个 Manager 必须共享 DB,否则谈不上"归属"):
|
||||
* CLUSTER_TEST_PG_URL=postgres://dshs:[email protected]:15432/dshs_smoke node scripts/verify-cluster-lease.mjs
|
||||
* CLUSTER_TEST_PG_URL=postgres://dshs:[email protected]:<pg-port>/dshs_smoke node scripts/verify-cluster-lease.mjs
|
||||
*/
|
||||
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
* ⑨ stop → ⑩ 起第二台 worker → 注册 → **迁移** → 再取一次页面
|
||||
* ⑪ `dshs doctor` / `dshs cluster status`(观测面)
|
||||
*
|
||||
* 需要:106 上 PG 已在 127.0.0.1:15432;以 root 运行(account 隔离要 setpriv/systemd-run)。
|
||||
* 运行:CLUSTER_LIVE_PG_URL=postgres://dshs:[email protected]:15432/dshs_live node scripts/verify-cluster-live.mjs
|
||||
* 需要:106 上 PG 已在 127.0.0.1:<pg-port>;以 root 运行(account 隔离要 setpriv/systemd-run)。
|
||||
* 运行:CLUSTER_LIVE_PG_URL=postgres://dshs:[email protected]:<pg-port>/dshs_live node scripts/verify-cluster-live.mjs
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process'
|
||||
import { createWriteStream, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
||||
@@ -35,7 +35,7 @@ const here = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = join(here, '..')
|
||||
const CLI = join(repoRoot, 'lib', 'cli.js')
|
||||
const TOKEN = 'live-cluster-agent-token'
|
||||
const DATA_ROOT = process.env.CLUSTER_LIVE_DATA_ROOT ?? '/opt/dshs-cluster/live-data'
|
||||
const DATA_ROOT = process.env.CLUSTER_LIVE_DATA_ROOT ?? cfg.installDir() + '-cluster/live-data'
|
||||
const ISO = process.env.CLUSTER_LIVE_ISOLATION ?? 'account'
|
||||
const M_PORT = Number(process.env.CLUSTER_LIVE_MANAGER_PORT ?? 13080)
|
||||
const A1_PORT = Number(process.env.CLUSTER_LIVE_AGENT1_PORT ?? 19000)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* ⑤ **数据不搬家也能用**:两台 worker **共享同一 dataRoot**(模拟共享存储 / 同路径基线)。
|
||||
*
|
||||
* 需要 PG(归属在 DB 里,两个 Manager/worker 共享):
|
||||
* CLUSTER_TEST_PG_URL=postgres://dshs:[email protected]:15432/dshs_smoke node scripts/verify-cluster-migrate.mjs
|
||||
* CLUSTER_TEST_PG_URL=postgres://dshs:[email protected]:<pg-port>/dshs_smoke node scripts/verify-cluster-migrate.mjs
|
||||
*/
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* verify-platform-admin-section.mjs —— 「系统管理」分区的**无浏览器**渲染验收(档案 82)
|
||||
*
|
||||
@@ -34,7 +35,7 @@ const jsxRuntime = { jsx, jsxs: jsx, Fragment: "Fragment" };
|
||||
let def = null;
|
||||
const win = {
|
||||
__ModuleLoader__: { load: (d) => { def = d; } },
|
||||
location: { hostname: "admin.ai1net.com", protocol: "https:", origin: "https://admin.ai1net.com" },
|
||||
location: { hostname: "admin.example.net", protocol: "https:", origin: "https://admin.example.net" },
|
||||
open: (u) => { globalThis.__OPENED = u; },
|
||||
document: {
|
||||
createElement: () => {
|
||||
@@ -82,7 +83,7 @@ const sandbox = {
|
||||
Math, Date, encodeURIComponent, decodeURIComponent,
|
||||
window: win, document: win.document,
|
||||
fetch: async (url) => {
|
||||
const p = String(url).replace("https://ai1net.com", "");
|
||||
const p = String(url).replace("https://example.net", "");
|
||||
const body = p === "/api/auth/me" ? { user: { id: "u1", username: ROLE, role: ROLE } } : DATA[p];
|
||||
return { ok: body !== undefined, status: body === undefined ? 404 : 200, json: async () => body };
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const cfg = require('../config/index.cjs')
|
||||
/**
|
||||
* ws-cleanup.cjs —— 用户工作区(ws)定期清理 + 磁盘画像(档案 28)
|
||||
*
|
||||
@@ -19,7 +20,7 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { existsSync, lchownSync, lstatSync, mkdirSync, readFileSync, readdirSync, statSync } = require('node:fs')
|
||||
const { join, extname, basename } = require('node:path')
|
||||
const Database = require('/opt/dshs/node_modules/better-sqlite3')
|
||||
const Database = require('better-sqlite3')
|
||||
|
||||
const argv = process.argv.slice(2)
|
||||
const APPLY = argv.includes('--apply')
|
||||
@@ -73,7 +74,7 @@ function reclaimOwnership(root, uid) {
|
||||
return fixed
|
||||
}
|
||||
|
||||
const db = new Database('/var/lib/dshs/dshs.db', { readonly: true })
|
||||
const db = new Database(cfg.dbFile(), { readonly: true })
|
||||
const users = db.prepare('SELECT id, username, uid, home_dir FROM users').all().filter((u) => ONLY === '' || u.id === ONLY)
|
||||
|
||||
for (const u of users) {
|
||||
|
||||
Reference in new issue
Block a user