part 5 修复 2:默认 dispatch 在只读 /app 下建不了 worktree——workspace 绑定 /app/work
RETEST4 新发现 2:make_worktree 把 worktree 建在 HERE/work/ host-worktrees,单元内 HERE=/app 而 /app 是 --bind-ro,带 repo 的默认 dispatch 撞 OSError Errno 30(--no-worktree 的同任务 越过此关、跑到真实 turn)。 二选一取 (a) 给 worktree 一条单元内可写路径:客机 workspace 下的 .chv-worktrees 以可写 --bind 挂到单元内 /app/work 上 (build_nspawn_argv 新增 worktrees_dir 参数,默认 WORKTREE_DIR=/home/chv/workspace/.chv-worktrees;run_cell 起 单元前 _ensure_bind_source 建好该目录并 chown 回 chv)。 取舍: - 不选 (b) 执行器默认 dispatch 带 --no-worktree:worktree 隔离就丢了——任务改动直接落进 repo 本身,多任务同仓互撞, merge/discard/seed-dir 语义全坏;且只修 dispatch,goal/fork 同样调 make_worktree,照样撞 Errno 30。 - (a) 内不选可写 tmpfs:tmpfs 随单元退出即丢 worktree,跨 cell 的 run/merge 续跑找不着工作区;workspace 绑定随客机 磁盘持久化,worktree 语义、隔离与落盘位置(仍是 /app/work/host-worktrees/<任务>)全都不变,也不碰 P2 的 /usr 只读约束。 - bind 必须排在 --bind-ro=/app 之后(nspawn 按顺序挂载,后挂 的才盖住只读子树下的子路径),单测钉住该顺序。 未决(须 RETEST5 真机验证):nspawn 对 --bind 目的路径的 处理(目的不存在时是否自动建挂载点)与挂载叠加顺序只能在 真机上验证,单测只能钉住 argv 形状;本轮不建镜像、不跑真机。 新测试 cloud/tests/test_chv_muse_p2.py::CellPart5WorktreeTest (4 项):可写 bind 在位且在 workspace 下、顺序在 --bind-ro=/app 之后、自定义目录参数生效。
This commit is contained in:
1 parent
39084ce2bf
commit
adc7bf6666
2 files changed
+65
-3
No files matched your search
+32
-3
@@ -33,7 +33,10 @@ volatile 空根(part 4):--volatile=yes 下单元根是基本空的
|
|||||||
tmpfs,/app、/etc/resolv.conf、/etc/hosts、/etc/ssl/certs 都须
|
tmpfs,/app、/etc/resolv.conf、/etc/hosts、/etc/ssl/certs 都须
|
||||||
显式绑进去(/app 与 CA 只读绑;resolv/hosts 是 run_cell 按客机
|
显式绑进去(/app 与 CA 只读绑;resolv/hosts 是 run_cell 按客机
|
||||||
有效 DNS 生成的单元专属文件,只读绑;不绑 nsswitch.conf,
|
有效 DNS 生成的单元专属文件,只读绑;不绑 nsswitch.conf,
|
||||||
glibc 默认 files+dns 正是单元需要的)。
|
glibc 默认 files+dns 正是单元需要的)。part 5 补一条:
|
||||||
|
/app/work 以可写 bind 盖住(backing 在客机 workspace 下),
|
||||||
|
否则 host.py 的 make_worktree 在只读 /app 下建不了 worktree
|
||||||
|
(RETEST4 新发现 2,OSError Errno 30)。
|
||||||
|
|
||||||
环境透传:nspawn 不透传调用方环境,本脚本只转发白名单变量(模型
|
环境透传:nspawn 不透传调用方环境,本脚本只转发白名单变量(模型
|
||||||
网关与代理变量)。GITEA_TOKEN 等凭证变量不在白名单里,绝不进单元。
|
网关与代理变量)。GITEA_TOKEN 等凭证变量不在白名单里,绝不进单元。
|
||||||
@@ -70,6 +73,17 @@ CELL_GID = 1000
|
|||||||
WORKSPACE = "/home/chv/workspace"
|
WORKSPACE = "/home/chv/workspace"
|
||||||
CODEX_HOME = "/home/chv/.codex"
|
CODEX_HOME = "/home/chv/.codex"
|
||||||
BROKER_DIR = "/run/chv"
|
BROKER_DIR = "/run/chv"
|
||||||
|
# part 5:单元内 worktree 的可写落点(RETEST4 新发现 2)。
|
||||||
|
# host.py 的 make_worktree 把 worktree 建在 HERE/work(单元内
|
||||||
|
# HERE=/app,而 /app 是 --bind-ro),带 repo 的默认 dispatch
|
||||||
|
# 撞 OSError Errno 30。把客机 workspace 下的 .chv-worktrees
|
||||||
|
# 以可写 bind 挂到单元内的 /app/work 上:worktree 的语义、隔离
|
||||||
|
# 与落盘位置都不变(仍是 /app/work/host-worktrees/<任务>),
|
||||||
|
# 只是 backing store 落在 bind 出去的 workspace 下、随客机磁盘
|
||||||
|
# 持久化(跨 cell 的 run/merge 续跑仍找得到 worktree);不碰
|
||||||
|
# /usr 的只读约束。必须排在 --bind-ro=/app 之后:nspawn 按顺序
|
||||||
|
# 挂载,后挂的才盖住只读子树下的该子路径。
|
||||||
|
WORKTREE_DIR = "/home/chv/workspace/.chv-worktrees"
|
||||||
CELL_PID_FILE = "/run/chv/cell.pid"
|
CELL_PID_FILE = "/run/chv/cell.pid"
|
||||||
|
|
||||||
# 单元网络(与客机 tap 网 192.168.10.0/24 不重叠的独立 /24)。
|
# 单元网络(与客机 tap 网 192.168.10.0/24 不重叠的独立 /24)。
|
||||||
@@ -210,14 +224,17 @@ def build_nspawn_argv(command, network="veth",
|
|||||||
workspace=WORKSPACE, codex_home=CODEX_HOME,
|
workspace=WORKSPACE, codex_home=CODEX_HOME,
|
||||||
broker_dir=BROKER_DIR,
|
broker_dir=BROKER_DIR,
|
||||||
playwright_package_dir=None,
|
playwright_package_dir=None,
|
||||||
cell_net=None, extra_ro_binds=None):
|
cell_net=None, extra_ro_binds=None,
|
||||||
|
worktrees_dir=WORKTREE_DIR):
|
||||||
"""产 nspawn 参数表(纯函数,可单测)。command 是单元内要跑的命令
|
"""产 nspawn 参数表(纯函数,可单测)。command 是单元内要跑的命令
|
||||||
(list,如 ["python3", "/app/host.py", ...])。
|
(list,如 ["python3", "/app/host.py", ...])。
|
||||||
cell_net 为 None 时用旧常量身份(argv 子命令与旧测试);
|
cell_net 为 None 时用旧常量身份(argv 子命令与旧测试);
|
||||||
run_cell 一律传 allocate_cell_net() 的唯一身份。
|
run_cell 一律传 allocate_cell_net() 的唯一身份。
|
||||||
extra_ro_binds 是 [(src, dest)],只读绑进单元(如单元专属
|
extra_ro_binds 是 [(src, dest)],只读绑进单元(如单元专属
|
||||||
resolv.conf/hosts;/app 与 /etc/ssl/certs 是固定只读绑定,
|
resolv.conf/hosts;/app 与 /etc/ssl/certs 是固定只读绑定,
|
||||||
直接列在下面)。"""
|
直接列在下面)。worktrees_dir 是客机侧 worktree backing
|
||||||
|
目录(part 5:以可写 bind 挂到单元内 /app/work 上,见
|
||||||
|
WORKTREE_DIR;排在 --bind-ro=/app 之后才能盖住只读子树)。"""
|
||||||
if not command:
|
if not command:
|
||||||
raise ValueError("chv-cell: 单元内命令不能为空")
|
raise ValueError("chv-cell: 单元内命令不能为空")
|
||||||
if network not in ("veth", "shared"):
|
if network not in ("veth", "shared"):
|
||||||
@@ -269,6 +286,14 @@ def build_nspawn_argv(command, network="veth",
|
|||||||
# 单元内无 resolved/dbus,绑了反而可能挂起;glibc 缺
|
# 单元内无 resolved/dbus,绑了反而可能挂起;glibc 缺
|
||||||
# nsswitch.conf 时默认 files+dns,正是单元需要的。
|
# nsswitch.conf 时默认 files+dns,正是单元需要的。
|
||||||
"--bind-ro=/app",
|
"--bind-ro=/app",
|
||||||
|
# part 5(RETEST4 新发现 2):/app/work 以可写 bind 盖住
|
||||||
|
# 只读 /app 下的该子路径——host.py 的 make_worktree 在
|
||||||
|
# /app/work/host-worktrees 下建目录,绑了才写得进;必须在
|
||||||
|
# --bind-ro=/app 之后(nspawn 按顺序挂载),且必须是
|
||||||
|
# --bind(可写),不是 --bind-ro。backing 目录在客机
|
||||||
|
# workspace 下(run_cell 起单元前 _ensure_bind_source
|
||||||
|
# 建好),worktree 随客机磁盘持久化。
|
||||||
|
"--bind=%s:/app/work" % worktrees_dir,
|
||||||
"--bind-ro=/etc/ssl/certs",
|
"--bind-ro=/etc/ssl/certs",
|
||||||
"--tmpfs=/tmp",
|
"--tmpfs=/tmp",
|
||||||
]
|
]
|
||||||
@@ -658,6 +683,10 @@ def run_cell(command, network="veth"):
|
|||||||
_ensure_bind_source(WORKSPACE)
|
_ensure_bind_source(WORKSPACE)
|
||||||
_ensure_bind_source(CODEX_HOME)
|
_ensure_bind_source(CODEX_HOME)
|
||||||
_ensure_bind_source(BROKER_DIR, uid=0, gid=0)
|
_ensure_bind_source(BROKER_DIR, uid=0, gid=0)
|
||||||
|
# part 5:worktree backing 目录须在 Popen 前存在(nspawn
|
||||||
|
# --bind 的源不存在即起不来),且属 chv(单元内 root 即客机
|
||||||
|
# chv,属主不对就写不进 worktree)。
|
||||||
|
_ensure_bind_source(WORKTREE_DIR)
|
||||||
net = None
|
net = None
|
||||||
files = None
|
files = None
|
||||||
uplink = None
|
uplink = None
|
||||||
|
|||||||
@@ -854,6 +854,39 @@ class CellPart4ArgvTest(unittest.TestCase):
|
|||||||
self.assertEqual(net.subnet, "10.200.0.0/24")
|
self.assertEqual(net.subnet, "10.200.0.0/24")
|
||||||
|
|
||||||
|
|
||||||
|
class CellPart5WorktreeTest(unittest.TestCase):
|
||||||
|
"""part 5 修复 2(RETEST4 新发现 2):/app/work 以可写 bind
|
||||||
|
盖住只读 /app——默认 dispatch 的 make_worktree 才写得进。"""
|
||||||
|
|
||||||
|
def test_worktree_bind_is_writable_under_workspace(self):
|
||||||
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
|
self.assertIn(
|
||||||
|
"--bind=/home/chv/workspace/.chv-worktrees:/app/work",
|
||||||
|
argv)
|
||||||
|
# 必须是可写 bind,不是只读绑。
|
||||||
|
self.assertNotIn(
|
||||||
|
"--bind-ro=/home/chv/workspace/.chv-worktrees:/app/work",
|
||||||
|
argv)
|
||||||
|
|
||||||
|
def test_worktree_bind_stacks_after_readonly_app(self):
|
||||||
|
# nspawn 按顺序挂载:work bind 必须在 --bind-ro=/app 之后,
|
||||||
|
# 才能盖住只读子树下的 /app/work。
|
||||||
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
|
self.assertLess(
|
||||||
|
argv.index("--bind-ro=/app"),
|
||||||
|
argv.index(
|
||||||
|
"--bind=/home/chv/workspace/.chv-worktrees:/app/work"))
|
||||||
|
|
||||||
|
def test_worktree_bind_custom_dir(self):
|
||||||
|
argv = CELL.build_nspawn_argv(["true"],
|
||||||
|
worktrees_dir="/tmp/wt")
|
||||||
|
self.assertIn("--bind=/tmp/wt:/app/work", argv)
|
||||||
|
|
||||||
|
def test_worktree_dir_constant_under_workspace(self):
|
||||||
|
self.assertTrue(
|
||||||
|
CELL.WORKTREE_DIR.startswith(CELL.WORKSPACE + "/"))
|
||||||
|
|
||||||
|
|
||||||
class CellFilesTest(unittest.TestCase):
|
class CellFilesTest(unittest.TestCase):
|
||||||
"""part 4:单元专属 resolv.conf/hosts 生成。"""
|
"""part 4:单元专属 resolv.conf/hosts 生成。"""
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user