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:
1 parent
ba5df980bf
commit
c39715659e
7 files changed
+850
-51
No files matched your search
+98
-20
@@ -37,6 +37,7 @@ import time
|
||||
from collections.abc import Callable
|
||||
|
||||
from cloud.chv.api_client import _unix_socket_api, normalize_api_result
|
||||
from cloud.chv.atomic_file import atomic_write_text
|
||||
from cloud.chv.config import _parse_memory_bytes, build_chv_config
|
||||
from cloud.chv.disk import (
|
||||
check_disk_quota,
|
||||
@@ -366,6 +367,16 @@ class CloudHypervisorExecutor(Executor):
|
||||
# 名 source 客内文件,未投递的密钥名一律拒绝(防止
|
||||
# “wake 没投递、执行时却按名 source 一个旧文件”)。
|
||||
self._guest_env_delivered = {}
|
||||
# feat-chv-closeup(P2-20):_guest_env_delivered 是
|
||||
# 跨 wake/build_ssh_command/stop 的共享字典,wake 与
|
||||
# 执行可并发在不同线程——此前无锁读写,set/get/pop
|
||||
# 交错会丢投递记录(密钥被误判未投递而拒跑,或已清
|
||||
# 的记录被误读为已投递)。专用锁护住全部访问(与
|
||||
# _vms_lock 分开:投递记录与进程登记是两把不同的锁,
|
||||
# 合一把会在 stop 路径上不必要地串行);所有访问点
|
||||
# 必须走下面三个小方法,禁止直接读写字典(新方法
|
||||
# 是唯一入口,回归测试钉死它)。
|
||||
self._guest_env_lock = threading.Lock()
|
||||
self.api_client = api_client if api_client is not None \
|
||||
else _unix_socket_api
|
||||
self.sleeper = sleeper if sleeper is not None else time.sleep
|
||||
@@ -385,6 +396,25 @@ class CloudHypervisorExecutor(Executor):
|
||||
self.launcher = _default_popen_launcher
|
||||
self._vms = {}
|
||||
|
||||
def _delivered_secrets(self, name: str) -> set:
|
||||
"""P2-20 唯一读入口:该 VM 已 seed 投递的密钥名集合。"""
|
||||
with self._guest_env_lock:
|
||||
return set(self._guest_env_delivered.get(name, set()))
|
||||
|
||||
def _mark_guest_env_delivered(self, name: str, secrets) -> None:
|
||||
"""P2-20 唯一写入口:记下该 VM 已投递的密钥名集合
|
||||
(secrets 为空即清记录,与旧语义一致)。"""
|
||||
with self._guest_env_lock:
|
||||
if secrets:
|
||||
self._guest_env_delivered[name] = set(secrets)
|
||||
else:
|
||||
self._guest_env_delivered.pop(name, None)
|
||||
|
||||
def _clear_guest_env_delivered(self, name: str) -> None:
|
||||
"""P2-20 唯一清除入口:stop 时清掉该 VM 的投递记录。"""
|
||||
with self._guest_env_lock:
|
||||
self._guest_env_delivered.pop(name, None)
|
||||
|
||||
def _copy_config_fields(self, _cfg) -> None:
|
||||
"""把 build_chv_config 的字段抄到 self(纯移动,字段集不变)。"""
|
||||
for _field in (
|
||||
@@ -736,6 +766,11 @@ class CloudHypervisorExecutor(Executor):
|
||||
"task_id": task_id})
|
||||
with open(path, "a") as fh:
|
||||
fh.write(line + "\n")
|
||||
# feat-chv-closeup(P2-18):spill 是审计丢事
|
||||
# 件后的唯一取证,写完即 fsync,否则宿主崩溃
|
||||
# 后 spill 本身也可能丢。失败只记日志,不炸任务。
|
||||
fh.flush()
|
||||
os.fsync(fh.fileno())
|
||||
except Exception as exc:
|
||||
_LOGGER.error("chv_audit_spill_failed error=%s", exc)
|
||||
|
||||
@@ -942,8 +977,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
guest_env_file = None
|
||||
if secrets:
|
||||
from cloud.chv.hostkeys import GUEST_ENV_PATH
|
||||
delivered = self._guest_env_delivered.get(
|
||||
self.vm_name(task), set())
|
||||
delivered = self._delivered_secrets(self.vm_name(task))
|
||||
missing: list = [name for name in secrets if name not in delivered]
|
||||
if missing:
|
||||
raise ChvFailure(
|
||||
@@ -1296,18 +1330,34 @@ class CloudHypervisorExecutor(Executor):
|
||||
return registered
|
||||
|
||||
def _record_net_entries(self, vm, entries):
|
||||
"""wake 成功后登记本 VM 的 net 条目(best-effort 写,
|
||||
失败只记日志——登记缺失的后果是漏判冲突,见上文)。"""
|
||||
"""wake 成功后登记本 VM 的 net 条目(P2-19 收尾)。
|
||||
|
||||
返回 True=登记成功,False=登记失败。失败时三件事
|
||||
同时做:warning 日志(旧行为保留)+ 独立审计事件
|
||||
net_registry_write_failed(调用方与运维都能看见,
|
||||
不再只有一条日志)+ 返回 False 让调用方感知。
|
||||
调用方 _boot_vm 见 False 即拒绝启动(ChvFailure,
|
||||
wake 随后 stop_vm 清掉半醒 VM):登记缺失的后果
|
||||
是后来的 wake 漏判 tap/mac/ip 冲突(H7 的第二道
|
||||
防线没了),继续启动等于带着缺口跑,不如不起;
|
||||
state_dir 可写本就是 preflight 的 0700 门,走到
|
||||
这里才写失败是真故障,不是预期内的可容忍情形。
|
||||
写路径走 atomic_file(P2-18):随机 tmp 名 + 双 fsync。
|
||||
"""
|
||||
try:
|
||||
vm_dir = os.path.join(self.state_dir, vm)
|
||||
os.makedirs(vm_dir, exist_ok=True)
|
||||
tmp_path = os.path.join(vm_dir, "net.json.tmp")
|
||||
with open(tmp_path, "w") as fh:
|
||||
json.dump(entries, fh)
|
||||
os.replace(tmp_path, os.path.join(vm_dir, "net.json"))
|
||||
atomic_write_text(
|
||||
os.path.join(vm_dir, "net.json"),
|
||||
json.dumps(entries))
|
||||
return True
|
||||
except OSError as exc:
|
||||
_LOGGER.warning("chv_net_registry_write_failed vm=%s "
|
||||
"error=%s", vm, exc)
|
||||
self._emit("net_registry_write_failed", None,
|
||||
"vm=%s net 登记写失败:%s" % (vm, exc),
|
||||
"net_registry_write_failed")
|
||||
return False
|
||||
|
||||
def _check_api_socket_permissions(self, socket_path):
|
||||
"""VMM api-socket 权限门(H6):socket 文件存在时,
|
||||
@@ -1462,10 +1512,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
provisioned["seed_dir"], seed_disk,
|
||||
runner=lambda cmd: self._call_runner(cmd))
|
||||
delete_host_private_key(self.state_dir, name)
|
||||
if secrets:
|
||||
self._guest_env_delivered[name] = set(secrets)
|
||||
else:
|
||||
self._guest_env_delivered.pop(name, None)
|
||||
self._mark_guest_env_delivered(name, secrets)
|
||||
return seed_disk
|
||||
|
||||
def _boot_vm(self, task, name, task_disk, seed_disk,
|
||||
@@ -1485,7 +1532,13 @@ class CloudHypervisorExecutor(Executor):
|
||||
self._emit("vm_create", task,
|
||||
"vm=%s disk=%s" % (name, task_disk))
|
||||
if net_entries:
|
||||
self._record_net_entries(name, net_entries)
|
||||
# feat-chv-closeup(P2-19):登记失败即拒起,
|
||||
# 见 _record_net_entries(审计 + 返回 False)。
|
||||
if not self._record_net_entries(name, net_entries):
|
||||
raise ChvFailure(
|
||||
"net_registry_write_failed",
|
||||
"vm=%s net 登记写失败,拒绝启动(缺登记会"
|
||||
"漏判 tap/mac/ip 冲突)" % name)
|
||||
status, _ = self._api_call(socket_path, "PUT", "/vm.boot")
|
||||
if status not in (200, 204):
|
||||
raise ChvFailure(
|
||||
@@ -1771,24 +1824,49 @@ class CloudHypervisorExecutor(Executor):
|
||||
_net_path = os.path.join(self.state_dir, name, "net.json")
|
||||
if os.path.exists(_net_path):
|
||||
os.remove(_net_path)
|
||||
except Exception:
|
||||
except Exception as _net_exc:
|
||||
# feat-chv-closeup(P2-17):net.json 删失败
|
||||
# 此前只记 failed=True、无独立审计——删不掉的
|
||||
# 登记会让后来的 wake 误判该 tap/mac/ip 仍被
|
||||
# 占用(误拒)或与脏标记混在一起查不出;现发
|
||||
# 独立审计 vm_net_registry_delete_failed。
|
||||
failed = True
|
||||
self._guest_env_delivered.pop(name, None)
|
||||
self._emit("vm_net_registry_delete_failed", task,
|
||||
"vm=%s error=%s" % (name, _net_exc))
|
||||
self._clear_guest_env_delivered(name)
|
||||
return failed
|
||||
|
||||
def _write_dirty_marker(self, name, failed):
|
||||
"""M2 脏标记:清理失败写标记、成功清标记;写失败不炸关闭。"""
|
||||
"""M2 脏标记:清理失败写标记、成功清标记(P2-17 收尾)。
|
||||
|
||||
返回 True=标记状态已如愿落盘(失败时标记已写、
|
||||
成功时标记已清/本来就没有),False=写/删标记
|
||||
本身失败。失败时发独立审计 dirty_marker_write_failed
|
||||
+ error 日志,不再全静默——标记写不下去时拒起链
|
||||
本身就不存在(provision/wake 读不到标记),部署
|
||||
必须保证 state_dir 可写(preflight 的 0700 门)
|
||||
并靠审计告警发现;stop 路径仍不抛错(stop_vm
|
||||
“尽力而为、不抛异常”的契约不变,False 只作
|
||||
调用方可观测的信号)。写路径走 atomic_file
|
||||
(P2-18):随机 tmp 名 + 双 fsync。
|
||||
"""
|
||||
from cloud.chv.hostkeys import dirty_marker_path
|
||||
try:
|
||||
_marker = dirty_marker_path(self.state_dir, name)
|
||||
if failed:
|
||||
os.makedirs(os.path.dirname(_marker), exist_ok=True)
|
||||
with open(_marker, "w") as fh:
|
||||
fh.write("cleanup failed\n")
|
||||
atomic_write_text(_marker, "cleanup failed\n")
|
||||
elif os.path.exists(_marker):
|
||||
os.remove(_marker)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
except Exception as exc:
|
||||
_LOGGER.error("chv_dirty_marker_write_failed vm=%s "
|
||||
"failed=%s error=%s", name, failed, exc)
|
||||
self._emit("dirty_marker_write_failed", None,
|
||||
"vm=%s 脏标记写失败(failed=%s):%s"
|
||||
% (name, failed, exc),
|
||||
"dirty_marker_write_failed")
|
||||
return False
|
||||
|
||||
def stop_vm(self, task_or_name: dict | str) -> bool:
|
||||
"""停止 VM(尽力而为,不抛异常):已死进程直接清登记,
|
||||
|
||||
Reference in new issue
Block a user