feat(chv-guest): seed 附加项供客机用户与静态网络(wake 路径前置)

wake_vm 在 WSL 台架上跑通需要客机身份与网络:镜像本身不建
用户、tap 无 DHCP,缺 users 块的 VM 登不进去、缺 network-config
的 VM 拿不到静态 IP。最小补法(复用 write_seed 机制):
- hostkeys.write_seed 新增 user_data_extra(原样追加进
  user-data,供 users: 块)与 network_config(写 seed
  network-config 文件,0600);附加项含 runcmd: 即 ValueError
- config _seed_fragment:CHV_SEED_USER_DATA_EXTRA_FILE /
  CHV_SEED_NETWORK_CONFIG_FILE(只存路径)
- executor._seed_extras 在 wake 起 VMM 前读入,缺失即
  ChvFailure seed_extra_missing;provision_host_key 透传
- README §7 残留更新:guest-env 集成已做
- 测试:test_chv_gitea_sync 新增 5 项(写盘/禁 runcmd/
  配置映射/wake 带附加项/缺失 fail-closed)
This commit is contained in:
host committed 2026-11-06 16:58:27 +00:00
1 parent e7f699c62b
commit d7cc482ed5
5 files changed
+184 -11

No files matched your search

+18
View File
@@ -259,6 +259,23 @@ def _gitea_fragment(env: dict) -> dict:
return {"gitea_sync_env": config_gitea_sync_env(env)}
def _seed_fragment(params: dict, env: dict) -> dict:
"""seed 附加项片段:只存宿主文件路径,不读文件内容
(读文件在 wake 时 fail-closed,见执行器 _seed_extras;
构造时不读,免得路径配错时连执行器对象都建不出来)。
CHV_SEED_USER_DATA_EXTRA_FILE 指向含 `users:` 块的文件,
CHV_SEED_NETWORK_CONFIG_FILE 指向 NoCloud network-config
文件;皆空即不附加(行为与旧 seed 一致)。"""
return {
"seed_user_data_extra_file":
params.get("seed_user_data_extra_file")
or env.get("CHV_SEED_USER_DATA_EXTRA_FILE"),
"seed_network_config_file":
params.get("seed_network_config_file")
or env.get("CHV_SEED_NETWORK_CONFIG_FILE"),
}
def _assemble_chv_config(fragments: list) -> "ChvConfig":
"""把各片段 dict 合并成 ChvConfig(纯合并,不改值)。
@@ -292,4 +309,5 @@ def build_chv_config(params: dict, env: dict) -> "ChvConfig":
_network_fragment(params, env),
_timeout_fragment(params, env),
_gitea_fragment(env),
_seed_fragment(params, env),
])
+40 -6
View File
@@ -400,7 +400,8 @@ class CloudHypervisorExecutor(Executor):
"single_tenant_project", "snapshot_max_count",
"snapshot_max_total_gb", "snapshot_retention_days",
"snapshot_dir", "allow_remote_snapshot",
"gitea_sync_env"):
"gitea_sync_env", "seed_user_data_extra_file",
"seed_network_config_file"):
setattr(self, _field, getattr(_cfg, _field))
# -- 名字 / 路径 ---------------------------------------------------
@@ -830,7 +831,9 @@ class CloudHypervisorExecutor(Executor):
host=ssh_host)])
def provision_host_key(self, task: dict, secret_env: dict | None = None,
guest_env: dict | None = None) -> dict:
guest_env: dict | None = None,
user_data_extra: str | None = None,
network_config: str | None = None) -> dict:
"""按 VM 预置 host key(补缺口轨强方案,见 hostkeys.py):
启动前为该 VM 生成一次性 ed25519 host key 对,公钥
立即写进该 VM 的 known_hosts(钉死),私钥只写进
@@ -868,7 +871,9 @@ class CloudHypervisorExecutor(Executor):
seed_dir = write_seed(paths["seed_dir"], vm,
private_text, public_text,
secret_env=secret_env,
guest_env=guest_env)
guest_env=guest_env,
user_data_extra=user_data_extra,
network_config=network_config)
self._emit("host_key_generated", task,
"vm=%s secret_names=%s"
% (vm, ",".join(sorted(secret_env or {}))))
@@ -1311,6 +1316,30 @@ class CloudHypervisorExecutor(Executor):
"VMM api-socket 组/其它可写(%o),拒绝启动:%s"
% (mode, socket_path))
def _seed_extras(self) -> tuple:
"""读 seed 附加项文件(CHV_SEED_* 配置的路径):
返回 (user_data_extra, network_config),皆空即
(None, None)。路径已配但文件缺失/读不出时
ChvFailure(seed_extra_missing),wake 在起 VMM
之前拒绝——缺 users 块的 VM 起了也登不进去,缺
network-config 的 VM 在无 DHCP 的 tap 上拿不到 IP,
不如不起。错误只含路径,不含文件内容。"""
out = []
for attr in ("seed_user_data_extra_file",
"seed_network_config_file"):
path = getattr(self, attr, None)
if not path:
out.append(None)
continue
try:
with open(path) as fh:
out.append(fh.read())
except OSError as exc:
raise ChvFailure(
"seed_extra_missing",
"seed 附加文件读不出:%s:%s" % (path, exc))
return out[0], out[1]
def _effective_extra_env(self, task: dict,
extra_env: dict | None) -> dict | None:
"""wake 生效的 extra_env:把 Gitea 同步变量并进来。
@@ -1381,7 +1410,8 @@ class CloudHypervisorExecutor(Executor):
return secrets, net_entries
def _provision_seed_disk(self, task: dict, name: str, secrets,
guest_env: dict | None = None) -> str | None:
guest_env: dict | None = None,
seed_extras: tuple | None = None) -> str | None:
"""provisioned 模式的 seed 盘供给:生成→制盘→挂盘,
任一步失败即抛(调用方 stop_vm 清理),纯移动。"""
seed_disk = None
@@ -1403,9 +1433,11 @@ class CloudHypervisorExecutor(Executor):
delete_host_private_key,
seed_disk_path,
)
extra, netcfg = seed_extras or (None, None)
provisioned = self.provision_host_key(
task, secret_env=secrets or None,
guest_env=guest_env or None)
guest_env=guest_env or None,
user_data_extra=extra, network_config=netcfg)
seed_disk = seed_disk_path(self.state_dir, name)
build_seed_disk(
provisioned["seed_dir"], seed_disk,
@@ -1487,6 +1519,7 @@ class CloudHypervisorExecutor(Executor):
name = self.vm_name(task)
try:
extra_env = self._effective_extra_env(task, extra_env)
seed_extras = self._seed_extras()
except ChvFailure as exc:
self._emit("vm_failed", task,
"vm=%s wake 失败 kind=%s" % (name, exc.kind),
@@ -1497,7 +1530,8 @@ class CloudHypervisorExecutor(Executor):
task_disk, socket_path = self._wake_prepare(task, name)
try:
seed_disk = self._provision_seed_disk(
task, name, secrets, guest_env=guest_env or None)
task, name, secrets, guest_env=guest_env or None,
seed_extras=seed_extras)
self._boot_vm(task, name, task_disk, seed_disk, net_entries)
except Exception:
try:
+37 -2
View File
@@ -185,10 +185,41 @@ def _guest_env_entry(merged_env: dict) -> list:
return lines
def _apply_seed_extras(seed_dir: str, user_data: str,
user_data_extra: str | None,
network_config: str | None) -> str:
"""seed 可选附加项(执行器 wake 路径的客机身份/网络供给):
user_data_extra 原样追加进 user-data(调用方用它供
`users:` 块——chv 用户 + authorized_keys,镜像本身不
建用户);network_config 写成 seed 目录的 network-config
文件(NoCloud 静态 IP,tap 无 DHCP 时客机才有地址)。
两者皆空即原样返回。user_data_extra 里含 runcmd: 行即
ValueError——seed 禁 runcmd 是 2026-11-06 根因禁令,
附加项也不得绕过(镜像的 99-chv-sync.cfg 靠它活着)。"""
if user_data_extra:
for line in user_data_extra.splitlines():
if line.strip().startswith("runcmd:"):
raise ValueError(
"seed 附加 user-data 里不得带 runcmd:")
if not user_data.endswith("\n"):
user_data += "\n"
user_data += user_data_extra
if not user_data.endswith("\n"):
user_data += "\n"
if network_config:
net_path = os.path.join(seed_dir, "network-config")
with open(net_path, "w") as fh:
fh.write(network_config)
os.chmod(net_path, 0o600)
return user_data
def write_seed(seed_dir: str, vm_name: str, private_key_text: str,
public_key_text: str,
secret_env: dict | None = None,
guest_env: dict | None = None) -> str:
guest_env: dict | None = None,
user_data_extra: str | None = None,
network_config: str | None = None) -> str:
"""写 seed 目录:meta-data + user-data(均为 0600),
返回 seed 目录路径。调用方负责随 VM 销毁删掉它。
@@ -198,7 +229,9 @@ def write_seed(seed_dir: str, vm_name: str, private_key_text: str,
个条目(secret 优先于同名键,防两行同名);guest_env
装 boot 时要用的非密钥同步变量(GITEA_URL 等,见
gitea_sync):只走 SSH 前缀到不了 cloud-init 时运行
的 chv-sync boot。两者皆空时不写该条目。"""
的 chv-sync boot。两者皆空时不写该条目。
user_data_extra / network_config 见 _apply_seed_extras
(客机用户与静态网络的供给,皆空即不写)。"""
os.makedirs(seed_dir, exist_ok=True)
meta_path = os.path.join(seed_dir, "meta-data")
user_path = os.path.join(seed_dir, "user-data")
@@ -221,6 +254,8 @@ def write_seed(seed_dir: str, vm_name: str, private_key_text: str,
user_data = head + "\n".join(lines) + "\n" + marker + tail
else:
user_data = user_data + "\n".join(lines) + "\n"
user_data = _apply_seed_extras(seed_dir, user_data,
user_data_extra, network_config)
with open(user_path, "w") as fh:
fh.write(user_data)
os.chmod(user_path, 0o600)