fix(chv): P2-20/P2-19/P2-17/P2-18 残留收尾 + NO_PROXY 合并

- P2-20:_guest_env_delivered 加专用锁,读写只走
  _delivered_secrets/_mark/_clear 三个入口,并发 hammer 测试钉死
- P2-19:_record_net_entries 失败发 net_registry_write_failed
  独立审计并返回 False;_boot_vm 见 False 即拒起(wake 随后清半醒 VM)
- P2-17:net.json 删失败发 vm_net_registry_delete_failed 审计;
  _write_dirty_marker 失败发 dirty_marker_write_failed 审计并返回
  False,stop 不抛的契约不变
- P2-18:新增 cloud/chv/atomic_file.py(mkstemp 随机名 + 文件
  fsync + rename + 目录 fsync),net.json/known_hosts/
  snapshots.json/pool registry/脏标记/spill 全部走它,固定 .tmp 清零
- H1 附带:NO_PROXY 实测确有覆盖 bug(env 重复赋值以后者为准,
  调用方 NO_PROXY 被 egress 的 localhost 值静默覆盖),改为合并
  单条发出;egress 已设时调用方代理变量(含小写)一律不发
- 测试:新增 cloud/tests/test_chv_residual.py(23 项),旧 closeup
  两处钉死旧语义的测试按新语义更新
This commit is contained in:
host committed 2026-11-06 18:41:50 +00:00
1 parent ba5df980bf
commit c39715659e
7 files changed
+850 -51

No files matched your search

+64 -18
View File
@@ -44,6 +44,7 @@ StrictHostKeyChecking=no + UserKnownHostsFile=/dev/null):
import os
import shlex
from cloud.chv.atomic_file import atomic_write_text
from cloud.chv.gitea_sync import (
GITEA_SYNC_PLAIN_KEYS,
GITEA_SYNC_SECRET_KEYS,
@@ -93,6 +94,63 @@ def split_guest_env(extra_env: dict | None) -> tuple[dict, dict]:
return guest_env, plain
def _merge_no_proxy(extra_env, egress_proxy):
"""调用方 NO_PROXY/no_proxy 与 egress 的 localhost、
127.0.0.1 去重合并(单条 NO_PROXY 的值)。"""
parts = []
if extra_env:
for key in ("NO_PROXY", "no_proxy"):
val = extra_env.get(key)
if val:
parts += [p.strip() for p in str(val).split(",")
if p.strip()]
merged = []
for part in parts:
if part not in merged:
merged.append(part)
if egress_proxy:
for part in ("localhost", "127.0.0.1"):
if part not in merged:
merged.append(part)
return merged
def _env_assignments(extra_env, egress_proxy):
"""env 前缀的赋值列表(feat-chv-closeup,H1 附带)。
NO_PROXY 覆盖顺序已核实并修掉:旧拼装是 extra_env 的
赋值在前、egress 的 NO_PROXY 在后,`env` 遇重复赋值以
后者为准(实测 env NO_PROXY=a NO_PROXY=b → b),调用方
传的 NO_PROXY 被 egress 的 localhost,127.0.0.1 静默覆盖。
现改为合并(见 _merge_no_proxy),单条 NO_PROXY 发出。
egress 代理本身须赢:egress 已设时调用方的
HTTPS_PROXY/HTTP_PROXY(含小写,curl 等工具读小写优先,
留着会架空 egress)一律不发出,只发 egress 的三条;
无 egress 时调用方的值原样保留(只去重,不丢)。"""
skip = {"NO_PROXY", "no_proxy"}
if egress_proxy:
skip |= {"HTTPS_PROXY", "HTTP_PROXY",
"https_proxy", "http_proxy"}
assignments = []
if extra_env:
for key in sorted(extra_env):
if key in skip:
continue
assignments.append("%s=%s"
% (key, shlex.quote(
str(extra_env[key]))))
merged = _merge_no_proxy(extra_env, egress_proxy)
if egress_proxy:
assignments += [
"HTTPS_PROXY=%s" % shlex.quote(egress_proxy),
"HTTP_PROXY=%s" % shlex.quote(egress_proxy),
"NO_PROXY=%s" % shlex.quote(",".join(merged))]
elif merged:
assignments += [
"NO_PROXY=%s" % shlex.quote(",".join(merged))]
return assignments
def remote_command(inner_cmd: list, egress_proxy: str | None = None,
extra_env: dict | None = None,
guest_env_file: str | None = None) -> list:
@@ -104,7 +162,8 @@ def remote_command(inner_cmd: list, egress_proxy: str | None = None,
set +a`(把 seed 投递的密钥文件 source 进环境并 export),
再接 env 前缀与客内命令;文件路径本身不是密钥。
extra_env 含密钥名时 ValueError(调用方须先 split 出
密钥子集走 seed 投递,见 split_secret_env)。"""
密钥子集走 seed 投递,见 split_secret_env)。env 前缀的
拼装见 _env_assignments(NO_PROXY 合并、egress 优先)。"""
if extra_env:
secrets, _ = split_secret_env(extra_env)
if secrets:
@@ -115,17 +174,7 @@ def remote_command(inner_cmd: list, egress_proxy: str | None = None,
if guest_env_file:
tokens += ["set", "-a;", ".",
shlex.quote(guest_env_file) + ";", "set", "+a;"]
env_assignments = []
if extra_env:
for key in sorted(extra_env):
env_assignments.append("%s=%s"
% (key, shlex.quote(
str(extra_env[key]))))
if egress_proxy:
env_assignments += [
"HTTPS_PROXY=%s" % shlex.quote(egress_proxy),
"HTTP_PROXY=%s" % shlex.quote(egress_proxy),
"NO_PROXY=localhost,127.0.0.1"]
env_assignments = _env_assignments(extra_env, egress_proxy)
inner = [shlex.quote(str(arg)) for arg in inner_cmd]
if env_assignments:
tokens += ["env"] + env_assignments + inner
@@ -198,13 +247,10 @@ def parse_keyscan_output(text: str, host: str = "127.0.0.1") -> list:
def write_known_hosts(path: str, key_lines: list) -> None:
"""原子写 known_hosts:先写同目录临时文件再 rename,
"""原子写 known_hosts:随机 tmp 名 + fsync 后 rename
(feat-chv-closeup,P2-18:旧固定 `<path>.tmp` 无 fsync),
文件权限 0600(只含公钥行,仍按敏感文件对待)。"""
directory = os.path.dirname(path)
if directory:
os.makedirs(directory, exist_ok=True)
tmp_path = path + ".tmp"
with open(tmp_path, "w") as fh:
fh.write("\n".join(key_lines) + "\n")
os.chmod(tmp_path, 0o600)
os.replace(tmp_path, path)
atomic_write_text(path, "\n".join(key_lines) + "\n", mode=0o600)