feat(chv-muse-round11): 移植 V-9 真机修复(9f929b1/912edef/9fb0f4b)
- lockfile 两脚本 DECLARED_SOURCES 加 P2 三件套 chv-cell/broker/ browser -> repo_commit(9f929b1)。 - build.sh 99-chv-sync.cfg runcmd 在 chv-sync boot 之前加 chown 行:guest-env 由 write_files 以 root:root 写(建用户之前), 须在 final 阶段改归 chv(912edef;本分支 runcmd 多一行 start chv-authd,chown 插在它之后、boot 之前)。 - executor._wait_guest_env_usable:密钥已投递时等客内 guest-env 对 SSH 用户可读再报 ready,超时关 VM 抛 guest_env_not_ready (9fb0f4b;runcmd chown 比 SSH 就绪晚约 30 秒)。 - hostkeys.py:本分支 _guest_env_entry 本来就写死 owner root:root(P3 重构时已无 ssh_user 分支),912edef 的 hostkeys 改动无内容可移,只补 V-9 根因注释防回退。 - 测试(5 个新测试,8 个运行):owner 恒 root:root、build.sh chown 位置、chv 用户下 wake 后 owner 仍 root、等到可读、 永不可读抛 guest_env_not_ready。 全量 cloud 套件 2019 OK(skipped=8)。
This commit is contained in:
1 parent
29d3a5a474
commit
717efc4688
6 files changed
+165
-6
No files matched your search
+1
-1
@@ -304,7 +304,7 @@ VC_ARGS=(-a "$OUT/disk.raw"
|
|||||||
--copy-in "$SCRIPT_DIR/chv-authd.service:/etc/systemd/system/"
|
--copy-in "$SCRIPT_DIR/chv-authd.service:/etc/systemd/system/"
|
||||||
--run-command 'mv /usr/local/bin/authd.py /usr/local/bin/chv-authd && chmod +x /usr/local/bin/chv-authd /usr/local/bin/git-credential-chv-authd'
|
--run-command 'mv /usr/local/bin/authd.py /usr/local/bin/chv-authd && chmod +x /usr/local/bin/chv-authd /usr/local/bin/git-credential-chv-authd'
|
||||||
--run-command 'systemctl enable chv-authd || true'
|
--run-command 'systemctl enable chv-authd || true'
|
||||||
--run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - systemctl start chv-authd || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg'
|
--run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - systemctl start chv-authd || true\n - test -f /etc/chv/guest-env && chown chv:chv /etc/chv/guest-env || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg'
|
||||||
--run-command 'git config --system user.name chv && git config --system user.email chv@localhost && git config --system credential.helper store && git config --system init.defaultBranch main'
|
--run-command 'git config --system user.name chv && git config --system user.email chv@localhost && git config --system credential.helper store && git config --system init.defaultBranch main'
|
||||||
--run-command "$GT_INSTALL_CMD"
|
--run-command "$GT_INSTALL_CMD"
|
||||||
--run-command '/usr/local/bin/gt version > /app/guest-versions-gt.txt'
|
--run-command '/usr/local/bin/gt version > /app/guest-versions-gt.txt'
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ DECLARED_SOURCES = {
|
|||||||
"/app/host.py": "repo_commit",
|
"/app/host.py": "repo_commit",
|
||||||
"gt": "gt_version",
|
"gt": "gt_version",
|
||||||
"chv-sync": "repo_commit",
|
"chv-sync": "repo_commit",
|
||||||
|
# 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理,
|
||||||
|
# 均是仓库内脚本,版本随 repo_commit(与 chv-sync 同款)。
|
||||||
|
"chv-cell": "repo_commit",
|
||||||
|
"chv-broker": "repo_commit",
|
||||||
|
"chv-browser": "repo_commit",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ DECLARED_SOURCES = {
|
|||||||
"/app/host.py": "repo_commit",
|
"/app/host.py": "repo_commit",
|
||||||
"gt": "gt_version",
|
"gt": "gt_version",
|
||||||
"chv-sync": "repo_commit",
|
"chv-sync": "repo_commit",
|
||||||
|
# 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理,
|
||||||
|
# 均是仓库内脚本,版本随 repo_commit(与 chv-sync 同款)。
|
||||||
|
"chv-cell": "repo_commit",
|
||||||
|
"chv-broker": "repo_commit",
|
||||||
|
"chv-browser": "repo_commit",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+45
-2
@@ -1415,6 +1415,43 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
"guest SSH 就绪超时:%d 次轮询内 sshd 未通,已关 VM"
|
"guest SSH 就绪超时:%d 次轮询内 sshd 未通,已关 VM"
|
||||||
% attempts)
|
% attempts)
|
||||||
|
|
||||||
|
def _wait_guest_env_usable(self, task: dict) -> None:
|
||||||
|
"""密钥已投递时,等客内 guest-env 对 SSH 用户可读再返回:
|
||||||
|
write_files 以 root:root 写文件(见 hostkeys.py——
|
||||||
|
write_files 在 users-groups 之前跑,owner 只能写 root),
|
||||||
|
客内 final 阶段的 99-chv-sync.cfg runcmd 才 chown 给
|
||||||
|
SSH 用户;SSH 就绪(vm_ready)比 final 阶段早约 30 秒
|
||||||
|
(V-9 真机:runcmd 脚本在 vm_ready 后约 31 秒才执行),
|
||||||
|
派发若在这窗口内 source 文件会 Permission denied。
|
||||||
|
判定用 `test -r`(SSH 用户本人可读与否,正是派发
|
||||||
|
source 的前置条件),不比对 owner 名。无密钥投递时
|
||||||
|
不等待、不轮询。超时先关 VM 再抛 guest_env_not_ready,
|
||||||
|
绝不带着读不到的密钥去跑任务。移植自 V-9(9fb0f4b)。"""
|
||||||
|
if not self._delivered_secrets(self.vm_name(task)):
|
||||||
|
return
|
||||||
|
from cloud.chv.hostkeys import GUEST_ENV_PATH
|
||||||
|
attempts = max(1, int(self.ready_timeout / self.ready_interval))
|
||||||
|
for attempt in range(attempts):
|
||||||
|
try:
|
||||||
|
result = self._call_runner(self.build_ssh_command(
|
||||||
|
task, ["test", "-r", GUEST_ENV_PATH]))
|
||||||
|
except Exception:
|
||||||
|
result = None
|
||||||
|
if (result is not None
|
||||||
|
and getattr(result, "returncode", 1) == 0):
|
||||||
|
return
|
||||||
|
if attempt + 1 < attempts:
|
||||||
|
self.sleeper(self.ready_interval)
|
||||||
|
try:
|
||||||
|
self.stop_vm(task)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise ChvFailure(
|
||||||
|
"guest_env_not_ready",
|
||||||
|
"guest-env 在 %d 次轮询内仍对 SSH 用户不可读,已关 VM:"
|
||||||
|
"runcmd chown 未生效或 cloud-init 未完成"
|
||||||
|
% attempts)
|
||||||
|
|
||||||
def _guest_host_command(self, *args, model=None, effort=None,
|
def _guest_host_command(self, *args, model=None, effort=None,
|
||||||
approval=None, sandbox=None,
|
approval=None, sandbox=None,
|
||||||
model_provider=None):
|
model_provider=None):
|
||||||
@@ -1820,7 +1857,9 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
|
|
||||||
def _boot_vm(self, task, name, task_disk, seed_disk,
|
def _boot_vm(self, task, name, task_disk, seed_disk,
|
||||||
net_entries) -> None:
|
net_entries) -> None:
|
||||||
"""VMM 启动→vm.create→vm.boot→SSH 就绪,纯移动。"""
|
"""VMM 启动→vm.create→vm.boot→SSH 就绪→密钥已投递时
|
||||||
|
等 guest-env 对 SSH 用户可读(V-9 移植,见
|
||||||
|
_wait_guest_env_usable),纯移动。"""
|
||||||
socket_path = self.api_socket(task)
|
socket_path = self.api_socket(task)
|
||||||
self._launch_api_only(task)
|
self._launch_api_only(task)
|
||||||
self.wait_api_ready(task)
|
self.wait_api_ready(task)
|
||||||
@@ -1849,6 +1888,9 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
"vm.boot 失败:status=%s" % status)
|
"vm.boot 失败:status=%s" % status)
|
||||||
self._emit("vm_boot", task, "vm=%s" % name)
|
self._emit("vm_boot", task, "vm=%s" % name)
|
||||||
self.wait_guest_ready(task)
|
self.wait_guest_ready(task)
|
||||||
|
# V-9 移植:密钥已投递时等 guest-env 对 SSH 用户可读再报
|
||||||
|
# 就绪(runcmd chown 比 SSH 就绪晚约 30 秒)。
|
||||||
|
self._wait_guest_env_usable(task)
|
||||||
|
|
||||||
def _wake_prepare(self, task: dict, name: str) -> tuple:
|
def _wake_prepare(self, task: dict, name: str) -> tuple:
|
||||||
"""wake 准备:reuse 审计、旧 VM 清理、任务盘供给、
|
"""wake 准备:reuse 审计、旧 VM 清理、任务盘供给、
|
||||||
@@ -1878,7 +1920,8 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
preflight → 容量门 → 磁盘配额门 → 每任务磁盘供给 →
|
preflight → 容量门 → 磁盘配额门 → 每任务磁盘供给 →
|
||||||
launcher 只起 VMM(--api-socket)→ 等 vmm.ping 通 →
|
launcher 只起 VMM(--api-socket)→ 等 vmm.ping 通 →
|
||||||
PUT vm.create(VmConfig)→ PUT vm.boot → 等 guest
|
PUT vm.create(VmConfig)→ PUT vm.boot → 等 guest
|
||||||
SSH 就绪。任一步失败都关 VM(stop_vm)再抛
|
SSH 就绪 → 密钥已投递时再等 guest-env 对 SSH 用户可读
|
||||||
|
(见 _wait_guest_env_usable,V-9 移植)。任一步失败都关 VM(stop_vm)再抛
|
||||||
ChvFailure,绝不带着半醒的 VM 去跑任务。
|
ChvFailure,绝不带着半醒的 VM 去跑任务。
|
||||||
成功返回 {vm, api_socket, state: "running", ready: True,
|
成功返回 {vm, api_socket, state: "running", ready: True,
|
||||||
mode: "cold"},并审计 vm_create/vm_boot/vm_ready。
|
mode: "cold"},并审计 vm_create/vm_boot/vm_ready。
|
||||||
|
|||||||
@@ -213,7 +213,13 @@ def _guest_env_entry(merged_env: dict) -> list:
|
|||||||
"""guest-env 的 write_files 条目行(0600 root,值经
|
"""guest-env 的 write_files 条目行(0600 root,值经
|
||||||
shlex.quote 引用):只能并进已有的 write_files 列表,
|
shlex.quote 引用):只能并进已有的 write_files 列表,
|
||||||
绝不能再写一个 write_files: 键(YAML 重复键会覆盖掉
|
绝不能再写一个 write_files: 键(YAML 重复键会覆盖掉
|
||||||
host key 的两条写文件条目,预置即失效)。"""
|
host key 的两条写文件条目,预置即失效)。
|
||||||
|
owner 恒为 root:root(V-9 真机根因:cloud-init 的 config
|
||||||
|
阶段里 write_files 在 users-groups 之前跑,owner 写
|
||||||
|
chv:chv 时 getpwnam('chv') 尚不存在,整个 write_files
|
||||||
|
模块失败、guest-env 文件根本不产生)。文件改归 SSH 用户
|
||||||
|
由客内 final 阶段 99-chv-sync.cfg runcmd 在 chv-sync
|
||||||
|
boot 之前 chown(用户那时已建好),见 chv-guest/build.sh。"""
|
||||||
lines = [
|
lines = [
|
||||||
" - path: %s" % GUEST_ENV_PATH,
|
" - path: %s" % GUEST_ENV_PATH,
|
||||||
" permissions: '0600'",
|
" permissions: '0600'",
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ VM、不联网、不含任何真实 token(测试值全是假值)。
|
|||||||
GITEA_TOKEN 且 argv 里绝无 token 值;
|
GITEA_TOKEN 且 argv 里绝无 token 值;
|
||||||
- worker:_gitea_sync_env 取值;_run_attempt 只对 CHV 执行器
|
- worker:_gitea_sync_env 取值;_run_attempt 只对 CHV 执行器
|
||||||
把同步变量并进 gateway_env。
|
把同步变量并进 gateway_env。
|
||||||
|
- V-9 移植:guest-env owner 恒 root:root(write_files 先于建用户)
|
||||||
|
+ 客内 runcmd chown(build.sh 位置断言)+ wake 等 guest-env
|
||||||
|
可读再报 ready(超时关 VM 抛 guest_env_not_ready)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -279,6 +282,47 @@ class WriteSeedGuestEnvTest(unittest.TestCase):
|
|||||||
self.assertNotIn("runcmd", parsed)
|
self.assertNotIn("runcmd", parsed)
|
||||||
self.assertEqual(len(parsed["write_files"]), 3)
|
self.assertEqual(len(parsed["write_files"]), 3)
|
||||||
|
|
||||||
|
def test_guest_env_owner_always_root(self):
|
||||||
|
# V-9 移植:write_files 在 users-groups 之前跑,owner
|
||||||
|
# 写 chv:chv 时 getpwnam 失败、整个模块失败,文件根本
|
||||||
|
# 不产生;故 owner 恒为 root:root,改归 SSH 用户由客内
|
||||||
|
# final 阶段 99-chv-sync.cfg runcmd chown(本分支的
|
||||||
|
# write_seed 本来就没有 ssh_user 参数,owner 写死
|
||||||
|
# root:root——此测试是防回退断言)。
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
seed_dir = os.path.join(tmp, "seed")
|
||||||
|
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
|
||||||
|
secret_env={"SUB2API_API_KEY": "s3cr3t"})
|
||||||
|
with open(os.path.join(seed_dir, "user-data")) as fh:
|
||||||
|
parsed = yaml.safe_load(fh.read())
|
||||||
|
guest = [e for e in parsed["write_files"]
|
||||||
|
if e["path"] == "/etc/chv/guest-env"]
|
||||||
|
self.assertEqual(len(guest), 1)
|
||||||
|
self.assertEqual(guest[0]["owner"], "root:root")
|
||||||
|
self.assertEqual(guest[0]["permissions"], "0600")
|
||||||
|
|
||||||
|
|
||||||
|
# -- (3b) build.sh:客内 runcmd chown --------------------------------------
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildShGuestEnvChownTest(unittest.TestCase):
|
||||||
|
def test_build_sh_runcmd_chowns_guest_env_before_sync_boot(self):
|
||||||
|
# V-9 移植:guest-env 由 write_files 以 root:root 写
|
||||||
|
# (建用户之前),须在 final 阶段 chv-sync boot 之前
|
||||||
|
# 改归 chv,否则 SSH 派发 source 它会 Permission
|
||||||
|
# denied;无文件时 test -f 护栏使该行空操作,runcmd
|
||||||
|
# 失败不影响启动。
|
||||||
|
text = (ROOT / "chv-guest" / "build.sh").read_text()
|
||||||
|
chown = ("test -f /etc/chv/guest-env && "
|
||||||
|
"chown chv:chv /etc/chv/guest-env || true")
|
||||||
|
self.assertIn(chown, text)
|
||||||
|
# "chv-sync boot" 在文件前部注释里先出现过一次,故从
|
||||||
|
# chown 位置往后找同一 runcmd 行里的 boot。
|
||||||
|
chown_at = text.index(chown)
|
||||||
|
boot_at = text.index("/usr/local/bin/chv-sync boot", chown_at)
|
||||||
|
self.assertLess(chown_at, boot_at)
|
||||||
|
|
||||||
|
|
||||||
# -- (4) 执行器 wake_vm -------------------------------------------------
|
# -- (4) 执行器 wake_vm -------------------------------------------------
|
||||||
class WakeGuestEnvCase(unittest.TestCase):
|
class WakeGuestEnvCase(unittest.TestCase):
|
||||||
@@ -324,7 +368,7 @@ class WakeGuestEnvCase(unittest.TestCase):
|
|||||||
self.launches.append(list(cmd))
|
self.launches.append(list(cmd))
|
||||||
return FakeProc(alive=True)
|
return FakeProc(alive=True)
|
||||||
|
|
||||||
def make_exec(self, env_extra=None):
|
def make_exec(self, env_extra=None, runner=None, ssh_user=None):
|
||||||
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
|
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
|
||||||
"CHV_HOST_KEY_MODE": "provisioned",
|
"CHV_HOST_KEY_MODE": "provisioned",
|
||||||
"CHV_SSH_FORWARD": "1",
|
"CHV_SSH_FORWARD": "1",
|
||||||
@@ -338,9 +382,11 @@ class WakeGuestEnvCase(unittest.TestCase):
|
|||||||
kernel=self.kernel, rootfs=self.rootfs,
|
kernel=self.kernel, rootfs=self.rootfs,
|
||||||
ssh_key=self.ssh_key, kvm_path=self.kvm,
|
ssh_key=self.ssh_key, kvm_path=self.kvm,
|
||||||
cpuinfo_path=self.cpuinfo, state_dir=self.state_dir,
|
cpuinfo_path=self.cpuinfo, state_dir=self.state_dir,
|
||||||
runner=self._runner, launcher=self._launcher,
|
runner=runner if runner is not None else self._runner,
|
||||||
|
launcher=self._launcher,
|
||||||
api_client=lambda s, m, p, payload=None: (200, {}),
|
api_client=lambda s, m, p, payload=None: (200, {}),
|
||||||
sleeper=lambda s: None, env=env,
|
sleeper=lambda s: None, env=env,
|
||||||
|
ssh_user=ssh_user,
|
||||||
sentinel_resolver=lambda host, port: ["93.184.216.34"])
|
sentinel_resolver=lambda host, port: ["93.184.216.34"])
|
||||||
ex.audit_callback = lambda e, t, d, k=None: self.audit.append(
|
ex.audit_callback = lambda e, t, d, k=None: self.audit.append(
|
||||||
(e, t.get("id") if isinstance(t, dict) else t, d, k))
|
(e, t.get("id") if isinstance(t, dict) else t, d, k))
|
||||||
@@ -456,6 +502,60 @@ class WakeGuestEnvCase(unittest.TestCase):
|
|||||||
ex.wake_vm({"id": "sync6", "gitea_sync": dict(FAKE_SYNC)})
|
ex.wake_vm({"id": "sync6", "gitea_sync": dict(FAKE_SYNC)})
|
||||||
self.assertEqual(self.launches, [])
|
self.assertEqual(self.launches, [])
|
||||||
|
|
||||||
|
def test_wake_with_chv_ssh_user_guest_env_owner_still_root(self):
|
||||||
|
# V-9 移植:owner 恒为 root:root(write_files 先于建用户),
|
||||||
|
# 改归 chv 由客内 99-chv-sync.cfg runcmd chown——即使
|
||||||
|
# SSH 用户就是 chv,seed 里的 owner 也不许写 chv:chv。
|
||||||
|
ex = self.make_exec(ssh_user="chv")
|
||||||
|
out = ex.wake_vm({"id": "envown1"},
|
||||||
|
extra_env={"SUB2API_API_KEY": "s3cr3t"})
|
||||||
|
self.assertEqual(out["state"], "running")
|
||||||
|
parsed = yaml.safe_load(
|
||||||
|
_seed_user_text(self.state_dir, "chv-envown1"))
|
||||||
|
guest = [e for e in parsed["write_files"]
|
||||||
|
if e["path"] == "/etc/chv/guest-env"]
|
||||||
|
self.assertEqual(len(guest), 1)
|
||||||
|
self.assertEqual(guest[0]["owner"], "root:root")
|
||||||
|
|
||||||
|
def _readable_runner(self, answers):
|
||||||
|
"""Fake runner:`test -r /etc/chv/guest-env` 按 answers
|
||||||
|
依次回可读与否(用完后重复最后一个),其余命令走默认
|
||||||
|
runner。answers 是 bool 列表。移植自 V-9(9fb0f4b)。"""
|
||||||
|
state = {"n": 0}
|
||||||
|
|
||||||
|
def run(cmd, timeout=None):
|
||||||
|
if ("test" in cmd
|
||||||
|
and cmd[-1] == "/etc/chv/guest-env"):
|
||||||
|
idx = min(state["n"], len(answers) - 1)
|
||||||
|
state["n"] += 1
|
||||||
|
return FakeResult(0 if answers[idx] else 1, "", "")
|
||||||
|
return self._runner(cmd, timeout)
|
||||||
|
run.readable_calls = lambda: state["n"]
|
||||||
|
return run
|
||||||
|
|
||||||
|
def test_wake_waits_for_guest_env_readable(self):
|
||||||
|
# V-9 移植:runcmd chown 比 SSH 就绪晚约 30 秒,wake 须等
|
||||||
|
# guest-env 对 SSH 用户可读再报 ready,否则首个派发
|
||||||
|
# source 它即 Permission denied。
|
||||||
|
runner = self._readable_runner([False, False, True])
|
||||||
|
ex = self.make_exec(runner=runner, ssh_user="chv")
|
||||||
|
out = ex.wake_vm({"id": "envwait1"},
|
||||||
|
extra_env={"SUB2API_API_KEY": "s3cr3t"})
|
||||||
|
self.assertEqual(out["state"], "running")
|
||||||
|
self.assertEqual(runner.readable_calls(), 3)
|
||||||
|
|
||||||
|
def test_wake_fails_when_guest_env_never_readable(self):
|
||||||
|
# V-9 移植:guest-env 一直对 SSH 用户不可读时 wake 超时即
|
||||||
|
# 关 VM 再抛 guest_env_not_ready,绝不带着读不到的密钥
|
||||||
|
# 跑任务。
|
||||||
|
runner = self._readable_runner([False])
|
||||||
|
ex = self.make_exec(runner=runner, ssh_user="chv")
|
||||||
|
with self.assertRaises(ChvFailure) as ctx:
|
||||||
|
ex.wake_vm({"id": "envwait2"},
|
||||||
|
extra_env={"SUB2API_API_KEY": "s3cr3t"})
|
||||||
|
self.assertEqual(ctx.exception.kind, "guest_env_not_ready")
|
||||||
|
self.assertGreater(runner.readable_calls(), 1)
|
||||||
|
|
||||||
|
|
||||||
# -- (4b) seed 附加项(users 块 + network-config)------------------------
|
# -- (4b) seed 附加项(users 块 + network-config)------------------------
|
||||||
class SeedExtrasTest(unittest.TestCase):
|
class SeedExtrasTest(unittest.TestCase):
|
||||||
|
|||||||
Reference in new issue
Block a user