Files
codex-app-server/cloud/tests/test_chv_gitea_sync.py
T
host 717efc4688 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)。
2026-11-07 00:48:25 +00:00

722 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""CHV guest-env 集成测试:Gitea 同步变量供给链路。
只用标准库与 mock runner/launcher/api_client,绝不启动真实
VM、不联网、不含任何真实 token(测试值全是假值)。
覆盖:
- gitea_sync 纯函数:三源合并优先级、白名单清洗、完整性
校验(fail-closed)与空=不同步;
- ssh_transport:GITEA_TOKEN 进密钥集(argv 拒绝)、其余
5 键进 guest-env 集;
- hostkeys.write_seed:guest_env 与 secret_env 合并写同一
条目、仍无 runcmd、0600;
- 执行器 wake_vm:完整同步变量进 seed(5 键 guest-env +
GITEA_TOKEN 走 authd-seed);缺失必填
键时起 VMM 前 ChvFailure 且不启动;无同步变量时正常启动
且不写 guest-env;seed 投递后 build_ssh_command 才能用
GITEA_TOKEN 且 argv 里绝无 token 值;
- worker:_gitea_sync_env 取值;_run_attempt 只对 CHV 执行器
把同步变量并进 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 sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
import yaml
try:
from cloud.chv.gitea_sync import (
GITEA_SYNC_KEYS,
config_gitea_sync_env,
extract_gitea_sync_env,
validate_gitea_sync_env,
)
from cloud.chv.hostkeys import write_seed
from cloud.chv.ssh_transport import (
_CHV_SECRET_ENV_NAMES,
build_ssh_command,
remote_command,
split_guest_env,
split_secret_env,
)
from cloud.sandbox import ChvFailure, CloudHypervisorExecutor
from cloud.worker import Worker
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv.gitea_sync import (
GITEA_SYNC_KEYS,
config_gitea_sync_env,
extract_gitea_sync_env,
validate_gitea_sync_env,
)
from cloud.chv.hostkeys import write_seed
from cloud.chv.ssh_transport import (
_CHV_SECRET_ENV_NAMES,
build_ssh_command,
remote_command,
split_guest_env,
split_secret_env,
)
from cloud.sandbox import ChvFailure, CloudHypervisorExecutor
from cloud.worker import Worker
FAKE_SYNC = {
"GITEA_URL": "https://gitea.tose.top",
"GITEA_USER": "admin",
"GITEA_TOKEN": "fake-token-123",
"SYNC_REPO": "admin/chv-sync-test",
"SYNC_BRANCH": "main",
"GITEA_EMAIL": "admin@tose.top",
}
class FakeResult:
def __init__(self, returncode=0, stdout="", stderr=""):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
class FakeProc:
def __init__(self, alive=True):
self._alive = alive
def poll(self):
return None if self._alive else 1
def terminate(self):
self._alive = False
def wait(self, timeout=None):
return 0
def kill(self):
self._alive = False
def _write(path, content=b"x"):
with open(path, "wb") as fh:
fh.write(content)
os.chmod(path, 0o755)
return path
def _seed_user_text(state_dir, vm_name):
with open(os.path.join(state_dir, vm_name, "seed", "user-data")) as fh:
return fh.read()
def _guest_env_lines(user_text):
parsed = yaml.safe_load(user_text)
entries = parsed["write_files"]
guest = [e for e in entries if e["path"] == "/etc/chv/guest-env"]
assert len(guest) == 1, entries
return guest[0]["content"], guest[0]["permissions"]
def _authd_seed_lines(user_text):
parsed = yaml.safe_load(user_text)
entries = parsed["write_files"]
authd = [e for e in entries if e["path"] == "/etc/chv/authd-seed"]
assert len(authd) == 1, entries
return authd[0]["content"], authd[0]["permissions"]
# -- (1) gitea_sync 纯函数 ---------------------------------------------
class GiteaSyncPureTest(unittest.TestCase):
def test_keys_are_six(self):
self.assertEqual(GITEA_SYNC_KEYS, set(FAKE_SYNC))
def test_config_mapping(self):
env = {"CHV_GITEA_URL": "https://gitea.tose.top",
"CHV_GITEA_USER": "admin",
"CHV_GITEA_TOKEN": "tok",
"CHV_SYNC_REPO": "admin/chv-sync-test",
"CHV_SYNC_BRANCH": "main",
"CHV_GITEA_EMAIL": "admin@tose.top",
"CHV_UNRELATED": "x"}
self.assertEqual(config_gitea_sync_env(env), {
"GITEA_URL": "https://gitea.tose.top",
"GITEA_USER": "admin", "GITEA_TOKEN": "tok",
"SYNC_REPO": "admin/chv-sync-test",
"SYNC_BRANCH": "main", "GITEA_EMAIL": "admin@tose.top"})
def test_extract_priority_config_lt_task_lt_extra_env(self):
task = {"id": "t1", "gitea_sync": {
"GITEA_URL": "https://task.example",
"GITEA_USER": "task-user", "GITEA_TOKEN": "task-tok",
"SYNC_REPO": "task/repo"}}
out = extract_gitea_sync_env(
task=task,
extra_env={"SYNC_REPO": "extra/repo"},
config_sync={"GITEA_URL": "https://config.example",
"GITEA_USER": "config-user",
"GITEA_TOKEN": "config-tok",
"SYNC_REPO": "config/repo",
"SYNC_BRANCH": "dev"})
self.assertEqual(out["GITEA_URL"], "https://task.example")
self.assertEqual(out["GITEA_USER"], "task-user")
self.assertEqual(out["GITEA_TOKEN"], "task-tok")
self.assertEqual(out["SYNC_REPO"], "extra/repo")
self.assertEqual(out["SYNC_BRANCH"], "dev")
def test_extract_drops_unknown_and_empty_and_nonstr(self):
task = {"id": "t1", "gitea_sync": {
"GITEA_URL": "https://gitea.tose.top",
"EVIL": "nope", "SYNC_REPO": " ",
"GITEA_TOKEN": 12345}}
out = extract_gitea_sync_env(task=task)
self.assertEqual(out, {"GITEA_URL": "https://gitea.tose.top"})
self.assertNotIn("gitea_sync", out)
def test_build_chv_config_maps_gitea_env(self):
from cloud.chv.config import build_chv_config
cfg = build_chv_config({}, {
"CHV_GITEA_URL": "https://gitea.tose.top",
"CHV_GITEA_USER": "admin",
"CHV_GITEA_TOKEN": "tok",
"CHV_SYNC_REPO": "admin/chv-sync-test"})
self.assertEqual(cfg.gitea_sync_env, {
"GITEA_URL": "https://gitea.tose.top",
"GITEA_USER": "admin", "GITEA_TOKEN": "tok",
"SYNC_REPO": "admin/chv-sync-test"})
cfg2 = build_chv_config({}, {})
self.assertEqual(cfg2.gitea_sync_env, {})
def test_validate_empty_passes_partial_fails_with_names_only(self):
validate_gitea_sync_env({})
validate_gitea_sync_env(None)
validate_gitea_sync_env(dict(FAKE_SYNC))
with self.assertRaises(ValueError) as ctx:
validate_gitea_sync_env({"GITEA_URL": "https://gitea.tose.top",
"SYNC_REPO": "admin/chv-sync-test"})
msg = str(ctx.exception)
self.assertIn("GITEA_TOKEN", msg)
self.assertIn("GITEA_USER", msg)
self.assertNotIn("gitea.tose.top", msg)
self.assertNotIn("chv-sync-test", msg)
# -- (2) ssh_transport -------------------------------------------------
class SshTransportSyncTest(unittest.TestCase):
def test_gitea_token_is_secret(self):
self.assertIn("GITEA_TOKEN", _CHV_SECRET_ENV_NAMES)
self.assertIn("SUB2API_API_KEY", _CHV_SECRET_ENV_NAMES)
secrets, plain = split_secret_env(
{"GITEA_TOKEN": "tok", "GITEA_URL": "https://x",
"OTHER": "1"})
self.assertEqual(secrets, {"GITEA_TOKEN": "tok"})
self.assertEqual(plain, {"GITEA_URL": "https://x", "OTHER": "1"})
def test_split_guest_env_covers_all_six_keys(self):
guest, plain = split_guest_env(dict(FAKE_SYNC, OTHER="1"))
self.assertEqual(set(guest), set(FAKE_SYNC))
self.assertEqual(guest["GITEA_TOKEN"], "fake-token-123")
# 非密钥子集仍含同步普通变量(任务期 SSH 前缀照常带它们)。
self.assertIn("GITEA_URL", plain)
self.assertNotIn("GITEA_TOKEN", plain)
self.assertIn("OTHER", plain)
self.assertNotIn("OTHER", guest)
def test_remote_command_rejects_gitea_token(self):
with self.assertRaises(ValueError):
remote_command(["true"], extra_env={"GITEA_TOKEN": "tok"})
with self.assertRaises(ValueError):
build_ssh_command(2222, "/tmp/key", "root", None,
"/tmp/known_hosts", ["true"],
extra_env={"GITEA_TOKEN": "tok"})
# -- (3) hostkeys.write_seed -------------------------------------------
class WriteSeedGuestEnvTest(unittest.TestCase):
def test_guest_env_and_secret_merge_into_one_entry_no_runcmd(self):
# feat-chv-muse-p3:GITEA_TOKEN 不再进 guest-env,改走
# authd_env(独立的 authd-seed 条目,0600);guest-env
# 条目里只剩非 authd 密钥与同步普通变量。guest_env 里
# 直接给 GITEA_TOKEN 即 ValueError(调用方须先
# split_authd_env 拆出来,旧的“同名键 secret 优先”语义
# 已由“authd 通道只装 authd 密钥”取代)。
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": "gw-key"},
guest_env={"GITEA_URL": "https://gitea.tose.top",
"GITEA_USER": "admin",
"SYNC_REPO": "admin/chv-sync-test"},
authd_env={"GITEA_TOKEN": "dup-should-lose"})
with open(os.path.join(seed_dir, "user-data")) as fh:
content = fh.read()
self.assertEqual(content.count("write_files:"), 1)
parsed = yaml.safe_load(content)
self.assertNotIn("runcmd", parsed)
self.assertEqual(len(parsed["write_files"]), 4)
text, perms = _guest_env_lines(content)
self.assertEqual(perms, "0600")
self.assertIn("GITEA_URL=https://gitea.tose.top", text)
self.assertIn("SUB2API_API_KEY=gw-key", text)
self.assertNotIn("GITEA_TOKEN", text)
authd_text, authd_perms = _authd_seed_lines(content)
self.assertEqual(authd_perms, "0600")
self.assertIn("GITEA_TOKEN=dup-should-lose", authd_text)
with tempfile.TemporaryDirectory() as tmp:
seed_dir = os.path.join(tmp, "seed")
with self.assertRaises(ValueError):
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
guest_env={"GITEA_TOKEN": "dup-should-lose"})
def test_guest_env_only_still_no_runcmd(self):
with tempfile.TemporaryDirectory() as tmp:
seed_dir = os.path.join(tmp, "seed")
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
guest_env={"GITEA_URL": "https://gitea.tose.top"})
with open(os.path.join(seed_dir, "user-data")) as fh:
content = fh.read()
parsed = yaml.safe_load(content)
self.assertNotIn("runcmd", parsed)
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 -------------------------------------------------
class WakeGuestEnvCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
self.binary = _write(os.path.join(self.dir, "cloud-hypervisor"))
self.ch_remote = _write(os.path.join(self.dir, "ch-remote"))
self.kernel = _write(os.path.join(self.dir, "vmlinux"))
self.rootfs = _write(os.path.join(self.dir, "disk.raw"),
b"R" * 100)
self.ssh_key = _write(os.path.join(self.dir, "id_chv"))
self.kvm = _write(os.path.join(self.dir, "kvm"))
self.cpuinfo = os.path.join(self.dir, "cpuinfo")
with open(self.cpuinfo, "w") as fh:
fh.write("flags : fpu vmx sse\n")
self.state_dir = os.path.join(self.dir, "state")
self.calls = []
self.audit = []
self.launches = []
def tearDown(self):
self.tmp.cleanup()
def _runner(self, cmd, timeout=None):
self.calls.append(list(cmd))
if cmd and cmd[0] == "ssh-keygen":
priv = cmd[cmd.index("-f") + 1]
with open(priv, "w") as fh:
fh.write("-----BEGIN OPENSSH PRIVATE KEY-----\nFAKE\n")
os.chmod(priv, 0o600)
with open(priv + ".pub", "w") as fh:
fh.write("ssh-ed25519 AAAAfake1 vm-key\n")
return FakeResult(0, "", "")
if cmd and cmd[0] == "genisoimage":
out = cmd[cmd.index("-output") + 1]
with open(out, "wb") as fh:
fh.write(b"ISO")
return FakeResult(0, "", "")
return FakeResult(0, "", "")
def _launcher(self, cmd):
self.launches.append(list(cmd))
return FakeProc(alive=True)
def make_exec(self, env_extra=None, runner=None, ssh_user=None):
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
"CHV_HOST_KEY_MODE": "provisioned",
"CHV_SSH_FORWARD": "1",
# feat-chv-muse-p3:wake 前 Sentinel 预审要求
# Gitea 主机在白名单内;解析器用假的(测试不联网)。
"CHV_SENTINEL_ALLOW_HOSTS": "gitea.tose.top"}
if env_extra:
env.update(env_extra)
ex = CloudHypervisorExecutor(
binary=self.binary, ch_remote=self.ch_remote,
kernel=self.kernel, rootfs=self.rootfs,
ssh_key=self.ssh_key, kvm_path=self.kvm,
cpuinfo_path=self.cpuinfo, state_dir=self.state_dir,
runner=runner if runner is not None else self._runner,
launcher=self._launcher,
api_client=lambda s, m, p, payload=None: (200, {}),
sleeper=lambda s: None, env=env,
ssh_user=ssh_user,
sentinel_resolver=lambda host, port: ["93.184.216.34"])
ex.audit_callback = lambda e, t, d, k=None: self.audit.append(
(e, t.get("id") if isinstance(t, dict) else t, d, k))
return ex
def test_wake_with_task_gitea_sync_writes_all_six_to_seed(self):
ex = self.make_exec()
task = {"id": "sync1", "gitea_sync": dict(FAKE_SYNC)}
out = ex.wake_vm(task)
self.assertEqual(out["state"], "running")
text = _seed_user_text(self.state_dir, "chv-sync1")
self.assertNotIn("runcmd", yaml.safe_load(text))
guest_text, perms = _guest_env_lines(text)
self.assertEqual(perms, "0600")
# feat-chv-muse-p3:六键仍全进 seed,但分两路——
# 5 个普通键进 guest-env,GITEA_TOKEN 只进 authd-seed。
for key in ("GITEA_URL", "GITEA_USER", "SYNC_REPO",
"SYNC_BRANCH", "GITEA_EMAIL"):
self.assertIn("%s=%s" % (key, FAKE_SYNC[key]), guest_text)
self.assertNotIn("GITEA_TOKEN", guest_text)
authd_text, authd_perms = _authd_seed_lines(text)
self.assertEqual(authd_perms, "0600")
self.assertIn("GITEA_TOKEN=fake-token-123", authd_text)
# seed.iso 已制盘且 0600。
seed_iso = os.path.join(self.state_dir, "chv-sync1", "seed.iso")
self.assertTrue(os.path.exists(seed_iso))
self.assertEqual(oct(os.stat(seed_iso).st_mode & 0o777), "0o600")
def test_wake_with_config_env_sync(self):
ex = self.make_exec(env_extra={
"CHV_GITEA_URL": "https://gitea.tose.top",
"CHV_GITEA_USER": "admin",
"CHV_GITEA_TOKEN": "fake-token-123",
"CHV_SYNC_REPO": "admin/chv-sync-test",
"CHV_SYNC_BRANCH": "main"})
self.assertEqual(ex.gitea_sync_env["SYNC_REPO"],
"admin/chv-sync-test")
out = ex.wake_vm({"id": "sync2"})
self.assertEqual(out["state"], "running")
guest_text, _ = _guest_env_lines(
_seed_user_text(self.state_dir, "chv-sync2"))
# feat-chv-muse-p3:GITEA_TOKEN 不再进 guest-env,
# 改走 authd-seed;其余同步变量仍在 guest-env 里。
self.assertNotIn("GITEA_TOKEN", guest_text)
self.assertIn("SYNC_BRANCH=main", guest_text)
authd_text, authd_perms = _authd_seed_lines(
_seed_user_text(self.state_dir, "chv-sync2"))
self.assertEqual(authd_perms, "0600")
self.assertIn("GITEA_TOKEN=fake-token-123", authd_text)
def test_wake_partial_sync_fails_closed_before_launch(self):
ex = self.make_exec()
task = {"id": "sync3", "gitea_sync": {
"GITEA_URL": "https://gitea.tose.top",
"SYNC_REPO": "admin/chv-sync-test"}}
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm(task)
self.assertEqual(ctx.exception.kind, "gitea_sync_incomplete")
self.assertEqual(self.launches, [])
self.assertFalse(os.path.exists(
os.path.join(self.state_dir, "chv-sync3", "seed.iso")))
kinds = [a[0] for a in self.audit]
self.assertIn("vm_failed", kinds)
def test_wake_partial_sync_from_config_also_fails_closed(self):
ex = self.make_exec(env_extra={
"CHV_GITEA_URL": "https://gitea.tose.top"})
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "sync4"})
self.assertEqual(ctx.exception.kind, "gitea_sync_incomplete")
self.assertEqual(self.launches, [])
def test_wake_without_sync_writes_no_guest_env(self):
ex = self.make_exec()
out = ex.wake_vm({"id": "plain1"})
self.assertEqual(out["state"], "running")
parsed = yaml.safe_load(
_seed_user_text(self.state_dir, "chv-plain1"))
self.assertEqual(len(parsed["write_files"]), 2)
def test_build_ssh_command_after_wake_sources_guest_env(self):
ex = self.make_exec()
ex.wake_vm({"id": "sync5", "gitea_sync": dict(FAKE_SYNC)})
# feat-chv-muse-p3:GITEA_TOKEN 走 authd 投递——任务期
# 认“已投递、不报错”,但绝不 source 进 codex 进程环境,
# 所以命令里既无 token 值、也不 source 任何客内密钥文件。
cmd = ex.build_ssh_command(
{"id": "sync5"}, ["true"],
extra_env={"GITEA_TOKEN": "fake-token-123"})
joined = " ".join(cmd)
self.assertNotIn("fake-token-123", joined)
self.assertNotIn("/etc/chv/guest-env", joined)
self.assertNotIn("/etc/chv/authd-seed", joined)
# 非 authd 密钥仍走 guest-env:wake 时投递,任务期 source。
ex.wake_vm({"id": "sync5b"},
extra_env={"SUB2API_API_KEY": "gw-key"})
cmd = ex.build_ssh_command(
{"id": "sync5b"}, ["true"],
extra_env={"SUB2API_API_KEY": "gw-key"})
joined = " ".join(cmd)
self.assertNotIn("gw-key", joined)
self.assertIn("/etc/chv/guest-env", joined)
# 未 wake 投递的 VM 用密钥即拒绝。
with self.assertRaises(ChvFailure) as ctx:
ex.build_ssh_command(
{"id": "other"}, ["true"],
extra_env={"GITEA_TOKEN": "fake-token-123"})
self.assertEqual(ctx.exception.kind, "secret_env_not_delivered")
def test_wake_keyscan_mode_with_token_fails_closed(self):
ex = self.make_exec(env_extra={"CHV_HOST_KEY_MODE": "keyscan"})
with self.assertRaises(ChvFailure):
ex.wake_vm({"id": "sync6", "gitea_sync": dict(FAKE_SYNC)})
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)------------------------
class SeedExtrasTest(unittest.TestCase):
USERS_BLOCK = ("users:\n"
" - name: chv\n"
" sudo: ALL=(ALL) NOPASSWD:ALL\n"
" shell: /bin/bash\n"
" ssh_authorized_keys:\n"
" - ssh-ed25519 AAAAfake chv-guest\n")
NETCFG = ("version: 2\nethernets:\n ens4:\n"
" addresses: [192.168.10.10/24]\n")
def test_write_seed_extras(self):
with tempfile.TemporaryDirectory() as tmp:
seed_dir = os.path.join(tmp, "seed")
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
user_data_extra=self.USERS_BLOCK,
network_config=self.NETCFG)
with open(os.path.join(seed_dir, "user-data")) as fh:
content = fh.read()
with open(os.path.join(seed_dir, "network-config")) as fh:
net = fh.read()
net_mode = oct(os.stat(
os.path.join(seed_dir, "network-config")).st_mode
& 0o777)
parsed = yaml.safe_load(content)
self.assertNotIn("runcmd", parsed)
self.assertIn("users:", content)
self.assertIn("ssh-ed25519 AAAAfake chv-guest", content)
self.assertEqual(net, self.NETCFG)
self.assertEqual(net_mode, "0o600")
def test_write_seed_extra_with_runcmd_rejected(self):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(ValueError):
write_seed(os.path.join(tmp, "seed"), "vm1",
"PRIV", "ssh-ed25519 AAAAx",
user_data_extra="runcmd:\n - echo hi\n")
def test_config_fragment_maps_seed_files(self):
from cloud.chv.config import build_chv_config
cfg = build_chv_config({}, {
"CHV_SEED_USER_DATA_EXTRA_FILE": "/tmp/users.yaml",
"CHV_SEED_NETWORK_CONFIG_FILE": "/tmp/net.yaml"})
self.assertEqual(cfg.seed_user_data_extra_file, "/tmp/users.yaml")
self.assertEqual(cfg.seed_network_config_file, "/tmp/net.yaml")
self.assertIsNone(build_chv_config({}, {}).seed_user_data_extra_file)
class WakeSeedExtrasCase(WakeGuestEnvCase):
def test_wake_with_seed_extras(self):
users_path = os.path.join(self.dir, "users.yaml")
net_path = os.path.join(self.dir, "net.yaml")
with open(users_path, "w") as fh:
fh.write(SeedExtrasTest.USERS_BLOCK)
with open(net_path, "w") as fh:
fh.write(SeedExtrasTest.NETCFG)
ex = self.make_exec(env_extra={
"CHV_SEED_USER_DATA_EXTRA_FILE": users_path,
"CHV_SEED_NETWORK_CONFIG_FILE": net_path})
out = ex.wake_vm({"id": "extra1"})
self.assertEqual(out["state"], "running")
seed_dir = os.path.join(self.state_dir, "chv-extra1", "seed")
with open(os.path.join(seed_dir, "user-data")) as fh:
self.assertIn("ssh-ed25519 AAAAfake chv-guest", fh.read())
with open(os.path.join(seed_dir, "network-config")) as fh:
self.assertEqual(fh.read(), SeedExtrasTest.NETCFG)
def test_wake_missing_seed_extra_fails_closed(self):
ex = self.make_exec(env_extra={
"CHV_SEED_USER_DATA_EXTRA_FILE": "/nonexistent/users.yaml"})
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "extra2"})
self.assertEqual(ctx.exception.kind, "seed_extra_missing")
self.assertEqual(self.launches, [])
# -- (5) worker ---------------------------------------------------------
class WorkerGiteaSyncTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
from cloud.server import init_db
init_db(self.db_path)
def tearDown(self):
self.tmp.cleanup()
def _worker(self, executor, env_extra=None):
env = {"XCM_API_KEY": "test-placeholder"}
if env_extra:
env.update(env_extra)
return Worker(self.db_path, "/fake/host.py",
executor=executor, env=env)
def test_gitea_sync_env_from_worker_env_and_task(self):
worker = self._worker(None, env_extra={
"CHV_GITEA_URL": "https://gitea.tose.top",
"CHV_GITEA_USER": "admin",
"CHV_GITEA_TOKEN": "env-tok",
"CHV_SYNC_REPO": "admin/chv-sync-test"})
out = worker._gitea_sync_env({"id": "t1"})
self.assertEqual(out["GITEA_TOKEN"], "env-tok")
out2 = worker._gitea_sync_env(
{"id": "t1", "gitea_sync": {"GITEA_TOKEN": "task-tok"}})
self.assertEqual(out2["GITEA_TOKEN"], "task-tok")
self.assertEqual(out2["SYNC_REPO"], "admin/chv-sync-test")
worker2 = self._worker(None)
self.assertEqual(worker2._gitea_sync_env({"id": "t1"}), {})
def test_run_attempt_merges_sync_env_only_for_chv(self):
import tempfile as _tf
case_dir = _tf.mkdtemp()
binary = _write(os.path.join(case_dir, "cloud-hypervisor"))
ch_remote = _write(os.path.join(case_dir, "ch-remote"))
kernel = _write(os.path.join(case_dir, "vmlinux"))
rootfs = _write(os.path.join(case_dir, "disk.raw"), b"R" * 100)
ssh_key = _write(os.path.join(case_dir, "id_chv"))
kvm = _write(os.path.join(case_dir, "kvm"))
cpuinfo = os.path.join(case_dir, "cpuinfo")
with open(cpuinfo, "w") as fh:
fh.write("flags : fpu vmx sse\n")
ex = CloudHypervisorExecutor(
binary=binary, ch_remote=ch_remote, kernel=kernel,
rootfs=rootfs, ssh_key=ssh_key, kvm_path=kvm,
cpuinfo_path=cpuinfo,
state_dir=os.path.join(case_dir, "state"),
runner=lambda cmd, timeout=None: FakeResult(0, "", ""),
launcher=lambda cmd: FakeProc(alive=True),
api_client=lambda s, m, p, payload=None: (200, {}),
sleeper=lambda s: None,
env={"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
"CHV_HOST_KEY_MODE": "provisioned",
"CHV_SSH_FORWARD": "1"})
seen = {}
orig_dispatch = ex.dispatch_task
def fake_dispatch(task, extra_env=None, model_provider=None):
seen["extra_env"] = dict(extra_env or {})
return {"executor": "cloud-hypervisor", "task_id": task["id"],
"status": "completed", "stdout": "", "stderr": "",
"returncode": 0, "host_status": "completed",
"usage": None}
ex.dispatch_task = fake_dispatch
worker = self._worker(ex, env_extra={
"CHV_GITEA_URL": "https://gitea.tose.top",
"CHV_GITEA_USER": "admin",
"CHV_GITEA_TOKEN": "fake-token-123",
"CHV_SYNC_REPO": "admin/chv-sync-test"})
worker._ensure_gateway = lambda task: (None, {})
task = {"id": "w1", "project_id": "p1", "prompt": "做事"}
result, error = worker._run_attempt(task)
self.assertIsNone(error)
self.assertEqual(result["status"], "completed")
self.assertEqual(seen["extra_env"]["GITEA_TOKEN"],
"fake-token-123")
self.assertEqual(seen["extra_env"]["SYNC_REPO"],
"admin/chv-sync-test")
worker.close()
if __name__ == "__main__":
unittest.main()