feat(chv): 架构与代码质量提升——WarmPool 定案、长函数提取、严格测试替身、Docker 同步守卫、配置统一校验、类型注解与 ADR
堆叠于 #246(feat-chv-secfix),只到 PR OPEN,未合并、未部署。 安全语义不变:egress、key seed 投递、磁盘删除、默认 provisioned、 tap 校验一律保留;前端零改动。 - WarmPool 定案为实验性(ADR-5):dispatch 永远冷启动, wired_to_dispatch 恒 False,acquire/resume 只保留实验接口, worker 仍调 reconcile_orphans 清理残留。 - 长函数提取:preflight 126→31、wake_vm 135→36、__init__ 69→31、 build_chv_config 159→58;错误文案逐字不变。 - 严格测试替身:fake API 校验 VmConfig(任务盘+seed 盘、net 无重复),fake SSH 校验 known_hosts 钉死;新增 5 项测试。 - Docker 同步守卫:8 个真实标记用例下 Docker 内联版与正典 parse_goal_result 状态全等,改分叉即 fail。 - 配置统一校验:preflight 以 config_invalid 统一兜底端口/配额/ 超时/快照/host-key 模式(构造时已校验正常值,preflight 兜底 事后改写);CHV_NET 与 CHV_NET_JSON 双设仍警告不硬拒。 - 类型与 lint:chv 叶子模块公开 API 补类型注解;stdlib 扫描 揪出 executor 内 1 个真死 import(host_key_paths)并删除。 - ADR:docs/chv-architecture.md(ADR-1 至 ADR-7)。 测试:CHV 163 项全过;cloud 全量 1593 项三跑结果为 8 errors (首跑详细日志因 tail 截断丢失)、全过、1 失败(浏览器代理 时序测试,与 CHV 无关),全量本身存在时序偶发,CHV 套件 确定性全过。
This commit is contained in:
1 parent
baf26beb78
commit
c908a0147a
12 files changed
+725
-239
No files matched your search
+221
-112
@@ -61,6 +61,13 @@ from cloud.chv.ssh_transport import write_known_hosts
|
||||
_LOGGER = logging.getLogger("cloud.cloud_hypervisor")
|
||||
|
||||
|
||||
def _default_popen_launcher(cmd):
|
||||
"""默认 VMM 启动器:独立会话子进程,输出丢弃(纯移动)。"""
|
||||
return subprocess.Popen(
|
||||
cmd, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
|
||||
|
||||
class CloudHypervisorExecutor(Executor):
|
||||
"""Cloud Hypervisor 执行器 v1(命名:cloud-hypervisor,绝不用
|
||||
cloud/ch 缩写——cloud 已是通用 HTTP runner 占位,撞名会让
|
||||
@@ -267,20 +274,12 @@ class CloudHypervisorExecutor(Executor):
|
||||
"ready_timeout": ready_timeout,
|
||||
"ready_interval": ready_interval,
|
||||
"host_key_mode": host_key_mode}, self._env)
|
||||
for _field in (
|
||||
"binary", "kernel", "rootfs", "cpus", "memory",
|
||||
"state_dir", "ssh_key", "ssh_user", "ssh_port",
|
||||
"guest_host_py", "cmdline", "net", "net_json",
|
||||
"kvm_path", "cpuinfo_path", "ch_remote", "model",
|
||||
"max_vms", "disk_mode", "disk_gb", "egress_proxy",
|
||||
"allow_direct_egress", "task_timeout", "snapshot_url",
|
||||
"warm_pool_size", "api_timeout", "ready_timeout",
|
||||
"ready_interval", "host_key_mode", "ssh_host",
|
||||
"ssh_forward", "reuse_experiment",
|
||||
"single_tenant_project", "snapshot_max_count",
|
||||
"snapshot_max_total_gb", "snapshot_retention_days",
|
||||
"snapshot_dir", "allow_remote_snapshot"):
|
||||
setattr(self, _field, getattr(_cfg, _field))
|
||||
self._copy_config_fields(_cfg)
|
||||
self._init_runtime(runner, launcher, api_client, sleeper)
|
||||
|
||||
def _init_runtime(self, runner, launcher, api_client, sleeper) -> None:
|
||||
"""运行时注入:seed 投递集合、API 客户端、睡眠器、
|
||||
runner/launcher 默认值(纯移动)。"""
|
||||
# feat-chv-secfix(H2):本实例已 seed 投递密钥的 VM
|
||||
# 名 → 已投递密钥名集合。_run_guest 只对集合内的密钥
|
||||
# 名 source 客内文件,未投递的密钥名一律拒绝(防止
|
||||
@@ -301,13 +300,26 @@ class CloudHypervisorExecutor(Executor):
|
||||
if launcher is not None:
|
||||
self.launcher = launcher
|
||||
else:
|
||||
def _popen(cmd):
|
||||
return subprocess.Popen(
|
||||
cmd, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
self.launcher = _popen
|
||||
self.launcher = _default_popen_launcher
|
||||
self._vms = {}
|
||||
|
||||
def _copy_config_fields(self, _cfg) -> None:
|
||||
"""把 build_chv_config 的字段抄到 self(纯移动,字段集不变)。"""
|
||||
for _field in (
|
||||
"binary", "kernel", "rootfs", "cpus", "memory",
|
||||
"state_dir", "ssh_key", "ssh_user", "ssh_port",
|
||||
"guest_host_py", "cmdline", "net", "net_json",
|
||||
"kvm_path", "cpuinfo_path", "ch_remote", "model",
|
||||
"max_vms", "disk_mode", "disk_gb", "egress_proxy",
|
||||
"allow_direct_egress", "task_timeout", "snapshot_url",
|
||||
"warm_pool_size", "api_timeout", "ready_timeout",
|
||||
"ready_interval", "host_key_mode", "ssh_host",
|
||||
"ssh_forward", "reuse_experiment",
|
||||
"single_tenant_project", "snapshot_max_count",
|
||||
"snapshot_max_total_gb", "snapshot_retention_days",
|
||||
"snapshot_dir", "allow_remote_snapshot"):
|
||||
setattr(self, _field, getattr(_cfg, _field))
|
||||
|
||||
# -- 名字 / 路径 ---------------------------------------------------
|
||||
def _host_name(self, task):
|
||||
return re.sub(r"[^A-Za-z0-9_.-]", "-",
|
||||
@@ -338,7 +350,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return os.path.isfile(path) and os.access(path, os.X_OK)
|
||||
return shutil.which(path) is not None
|
||||
|
||||
def _preflight_config_checks(self, missing):
|
||||
def _preflight_config_checks(self, missing: list) -> None:
|
||||
"""preflight 的配置校验块(feat-chv-fix,P2-1 小提取:
|
||||
仅把 reuse 拦截与 CPU/内存校验从 preflight() 里抽出,
|
||||
语义逐行不变;其余 preflight 检查项与 config.py 的
|
||||
@@ -378,24 +390,70 @@ class CloudHypervisorExecutor(Executor):
|
||||
except ValueError:
|
||||
missing.append(("config_invalid",
|
||||
"CHV_MEM 格式非法:%r" % (self.memory,)))
|
||||
self._preflight_numeric_checks(missing)
|
||||
def _preflight_numeric_checks(self, missing: list) -> None:
|
||||
"""preflight 数值面:端口/配额/超时/快照统一校验,纯移动。
|
||||
构造时已校验正常值;这里兜底属性事后被改写的情形。"""
|
||||
# feat-chv-arch95(配置统一校验):构造时 build_chv_config
|
||||
# 已校验过这些字段的正常值;这里再以 config_invalid
|
||||
# 统一兜底属性被事后改写/注入非法值的情形,preflight
|
||||
# 是生产路径最后一道统一校验,不再等到 build_vm_config
|
||||
# 或 wake 时才在不同位置以不同文案报错。
|
||||
try:
|
||||
_port = int(getattr(self, "ssh_port", 2222))
|
||||
if not 1 <= _port <= 65535:
|
||||
raise ValueError("ssh_port")
|
||||
except (TypeError, ValueError):
|
||||
missing.append(("config_invalid",
|
||||
"CHV_SSH_PORT 必须在 1-65535 之间"))
|
||||
try:
|
||||
_max_vms = int(getattr(self, "max_vms", 4))
|
||||
if _max_vms < 0:
|
||||
raise ValueError("max_vms")
|
||||
except (TypeError, ValueError):
|
||||
missing.append(("config_invalid",
|
||||
"CHV_MAX_VMS 必须是非负整数"))
|
||||
try:
|
||||
_disk_gb = float(getattr(self, "disk_gb", 50.0))
|
||||
if _disk_gb <= 0:
|
||||
raise ValueError("disk_gb")
|
||||
except (TypeError, ValueError):
|
||||
missing.append(("config_invalid",
|
||||
"CHV_DISK_GB 必须是大于 0 的数字"))
|
||||
try:
|
||||
if float(getattr(self, "api_timeout", 30)) <= 0 \
|
||||
or float(getattr(self, "ready_timeout", 120)) <= 0 \
|
||||
or float(getattr(self, "ready_interval", 5)) <= 0:
|
||||
raise ValueError("timeout")
|
||||
except (TypeError, ValueError):
|
||||
missing.append(("config_invalid",
|
||||
"CHV_*_TIMEOUT/INTERVAL 必须是正数"))
|
||||
try:
|
||||
if int(getattr(self, "warm_pool_size", 0)) < 0:
|
||||
raise ValueError("warm_pool_size")
|
||||
except (TypeError, ValueError):
|
||||
missing.append(("config_invalid",
|
||||
"CHV_WARM_POOL_SIZE 必须是非负整数"))
|
||||
self._preflight_snapshot_checks(missing)
|
||||
def _preflight_snapshot_checks(self, missing: list) -> None:
|
||||
"""preflight 快照/host-key 面:统一校验,纯移动。"""
|
||||
try:
|
||||
if int(getattr(self, "snapshot_max_count", 10)) < 0 \
|
||||
or float(getattr(self, "snapshot_max_total_gb", 20.0)) < 0 \
|
||||
or float(getattr(self, "snapshot_retention_days", 30.0)) < 0:
|
||||
raise ValueError("snapshot")
|
||||
except (TypeError, ValueError):
|
||||
missing.append(("config_invalid",
|
||||
"CHV_SNAPSHOT_* 不能为负数"))
|
||||
if getattr(self, "host_key_mode", "provisioned") \
|
||||
not in ("provisioned", "keyscan"):
|
||||
missing.append(("config_invalid",
|
||||
"CHV_HOST_KEY_MODE 只能是 provisioned 或 keyscan"))
|
||||
|
||||
def preflight(self):
|
||||
"""预检(缺一不可则 fail-fast):收集全部缺失项一次报出
|
||||
具体缺哪一项,绝不笼统报错;失败为 ChvFailure(带 kind,
|
||||
供审计与失败分类用),绝不回落 Docker/Local。
|
||||
检查项与 kind:
|
||||
- binary_missing:cloud-hypervisor 二进制不存在/不可执行
|
||||
- binary_missing:ch-remote 二进制不存在/不可执行
|
||||
- kvm_missing:/dev/kvm 不存在
|
||||
- kvm_missing:/dev/kvm 不可读/不可写(分开报)
|
||||
- cpu_flag_missing:/proc/cpuinfo 无 vmx/svm 标志
|
||||
- kernel_missing:CHV_KERNEL 未设/文件不存在/不可读
|
||||
- image_missing:CHV_ROOTFS 未设/文件不存在/不可读
|
||||
- ssh_key_missing:CHV_SSH_KEY 未设/文件不存在/不可读
|
||||
- egress_not_configured:CHV_EGRESS_PROXY 未设且未显式
|
||||
允许直连(默认 fail-closed,VM 出口必须过白名单代理)
|
||||
"""
|
||||
missing = [] # (kind, 文本) 列表,全部收集完再一次抛
|
||||
|
||||
|
||||
def _preflight_host_checks(self, missing: list) -> None:
|
||||
"""preflight 主机面:二进制/KVM/CPU 标志,纯移动。"""
|
||||
if not self._binary_ok(self.binary):
|
||||
missing.append(("binary_missing",
|
||||
"cloud-hypervisor 二进制缺失或不可执行:%s"
|
||||
@@ -432,6 +490,9 @@ class CloudHypervisorExecutor(Executor):
|
||||
missing.append(("cpu_flag_missing",
|
||||
"CPU 无虚拟化标志(无 vmx/svm):%s"
|
||||
% self.cpuinfo_path))
|
||||
|
||||
def _preflight_file_checks(self, missing: list) -> None:
|
||||
"""preflight 文件面:内核/镜像/SSH key,纯移动。"""
|
||||
if not self.kernel:
|
||||
missing.append(("kernel_missing", "CHV_KERNEL 未设"))
|
||||
elif not os.path.isfile(self.kernel):
|
||||
@@ -456,6 +517,9 @@ class CloudHypervisorExecutor(Executor):
|
||||
elif not os.access(self.ssh_key, os.R_OK):
|
||||
missing.append(("ssh_key_missing",
|
||||
"SSH key 文件不可读:%s" % self.ssh_key))
|
||||
|
||||
def _preflight_net_checks(self, missing: list) -> None:
|
||||
"""preflight 网络/目录面:egress/安全目录/net,纯移动。"""
|
||||
if not self.egress_proxy and not self.allow_direct_egress:
|
||||
missing.append(("egress_not_configured",
|
||||
"CHV_EGRESS_PROXY 未设且未显式允许直连:"
|
||||
@@ -494,6 +558,26 @@ class CloudHypervisorExecutor(Executor):
|
||||
validate_net_entries(_entries, [])
|
||||
except ChvFailure as exc:
|
||||
missing.append((exc.kind, str(exc)))
|
||||
def preflight(self):
|
||||
"""预检(缺一不可则 fail-fast):收集全部缺失项一次报出
|
||||
具体缺哪一项,绝不笼统报错;失败为 ChvFailure(带 kind,
|
||||
供审计与失败分类用),绝不回落 Docker/Local。
|
||||
检查项与 kind:
|
||||
- binary_missing:cloud-hypervisor 二进制不存在/不可执行
|
||||
- binary_missing:ch-remote 二进制不存在/不可执行
|
||||
- kvm_missing:/dev/kvm 不存在
|
||||
- kvm_missing:/dev/kvm 不可读/不可写(分开报)
|
||||
- cpu_flag_missing:/proc/cpuinfo 无 vmx/svm 标志
|
||||
- kernel_missing:CHV_KERNEL 未设/文件不存在/不可读
|
||||
- image_missing:CHV_ROOTFS 未设/文件不存在/不可读
|
||||
- ssh_key_missing:CHV_SSH_KEY 未设/文件不存在/不可读
|
||||
- egress_not_configured:CHV_EGRESS_PROXY 未设且未显式
|
||||
允许直连(默认 fail-closed,VM 出口必须过白名单代理)
|
||||
"""
|
||||
missing = [] # (kind, 文本) 列表,全部收集完再一次抛
|
||||
self._preflight_host_checks(missing)
|
||||
self._preflight_file_checks(missing)
|
||||
self._preflight_net_checks(missing)
|
||||
self._preflight_config_checks(missing)
|
||||
if missing:
|
||||
kind = missing[0][0]
|
||||
@@ -612,7 +696,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
旧 key 对)。成功后审计 host_key_generated(只含
|
||||
VM 名与密钥名,不含任何 key 值)。"""
|
||||
from cloud.chv.hostkeys import (
|
||||
dirty_marker_path, generate_host_keypair, host_key_paths,
|
||||
dirty_marker_path, generate_host_keypair,
|
||||
known_hosts_line, write_seed)
|
||||
from cloud.chv.ssh_transport import write_known_hosts
|
||||
vm = self.vm_name(task)
|
||||
@@ -1077,25 +1161,9 @@ class CloudHypervisorExecutor(Executor):
|
||||
"VMM api-socket 组/其它可写(%o),拒绝启动:%s"
|
||||
% (mode, socket_path))
|
||||
|
||||
def wake_vm(self, task, extra_env=None):
|
||||
"""主唤醒路径(API 模式,dispatch/run/goal 的前置步骤):
|
||||
preflight → 容量门 → 磁盘配额门 → 每任务磁盘供给 →
|
||||
launcher 只起 VMM(--api-socket)→ 等 vmm.ping 通 →
|
||||
PUT vm.create(VmConfig)→ PUT vm.boot → 等 guest
|
||||
SSH 就绪。任一步失败都关 VM(stop_vm)再抛
|
||||
ChvFailure,绝不带着半醒的 VM 去跑任务。
|
||||
成功返回 {vm, api_socket, state: "running", ready: True,
|
||||
mode: "cold"},并审计 vm_create/vm_boot/vm_ready。
|
||||
单任务单 VM:同一任务重复 wake 时先关旧 VM 再起新
|
||||
的,绝不两个 VM 共用一个任务名。
|
||||
feat-chv-secfix:extra_env(_run_with_vm 透传)里的
|
||||
密钥子集在 provisioned 模式下写进 seed 客内文件,
|
||||
keyscan 模式下有密钥子集即 fail-closed;起 VM 前的
|
||||
preflight/容量/配额失败也发 vm_failed 审计(L3);
|
||||
net 隔离在起 VMM 前校验(H7),成功后登记 net.json。"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
name = self.vm_name(task)
|
||||
def _wake_prechecks(self, task: dict, name: str, extra_env) -> tuple:
|
||||
"""wake 前置:preflight/容量/配额/net/密钥模式,纯移动。
|
||||
失败时发 vm_failed 审计并原样抛 ChvFailure。"""
|
||||
try:
|
||||
self.preflight()
|
||||
# feat-chv-fix:SSH 目标与 reuse 放行在启动前就定性。
|
||||
@@ -1129,6 +1197,70 @@ class CloudHypervisorExecutor(Executor):
|
||||
"vm=%s wake 前置失败 kind=%s" % (name, exc.kind),
|
||||
exc.kind)
|
||||
raise
|
||||
return secrets, net_entries
|
||||
|
||||
def _provision_seed_disk(self, task: dict, name: str, secrets) -> str:
|
||||
"""provisioned 模式的 seed 盘供给:生成→制盘→挂盘,
|
||||
任一步失败即抛(调用方 stop_vm 清理),纯移动。"""
|
||||
seed_disk = None
|
||||
if getattr(self, "host_key_mode", "provisioned") == "provisioned":
|
||||
# 强方案:启动前预置每 VM host key 并钉死,
|
||||
# 再把 seed 目录做成 CIDATA seed 磁盘并挂进
|
||||
# VmConfig(P0-1 闭环:生成→制盘→挂盘三步缺
|
||||
# 一即 fail-fast,绝不回落 keyscan、绝不只
|
||||
# 生成不注入)。
|
||||
# feat-chv-secfix(H2/H5):secret_env 随 seed
|
||||
# 写进客内密钥文件;seed.iso 建好(0600,见
|
||||
# build_seed_disk)后立即删宿主侧私钥——此后
|
||||
# 宿主上只剩公钥 + known_hosts,私钥只活在客
|
||||
# 机与 seed.iso(随 VM 销毁而删)里。删私钥
|
||||
# 失败即 fail-fast(VM 尚未启动,此时拒绝最
|
||||
# 安全,except 里 stop_vm 会清掉 seed)。
|
||||
from cloud.chv.hostkeys import (
|
||||
build_seed_disk, delete_host_private_key,
|
||||
seed_disk_path)
|
||||
provisioned = self.provision_host_key(
|
||||
task, secret_env=secrets or None)
|
||||
seed_disk = seed_disk_path(self.state_dir, name)
|
||||
build_seed_disk(
|
||||
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)
|
||||
return seed_disk
|
||||
|
||||
def _boot_vm(self, task, name, task_disk, seed_disk,
|
||||
net_entries) -> None:
|
||||
"""VMM 启动→vm.create→vm.boot→SSH 就绪,纯移动。"""
|
||||
socket_path = self.api_socket(task)
|
||||
self._launch_api_only(task)
|
||||
self.wait_api_ready(task)
|
||||
self._check_api_socket_permissions(socket_path)
|
||||
status, _ = self._api_call(
|
||||
socket_path, "PUT", "/vm.create",
|
||||
self.build_vm_config(task_disk, seed_disk=seed_disk))
|
||||
if status not in (200, 204):
|
||||
raise ChvFailure(
|
||||
"vm_create_failed",
|
||||
"vm.create 失败:status=%s" % status)
|
||||
self._emit("vm_create", task,
|
||||
"vm=%s disk=%s" % (name, task_disk))
|
||||
if net_entries:
|
||||
self._record_net_entries(name, net_entries)
|
||||
status, _ = self._api_call(socket_path, "PUT", "/vm.boot")
|
||||
if status not in (200, 204):
|
||||
raise ChvFailure(
|
||||
"vm_boot_failed",
|
||||
"vm.boot 失败:status=%s" % status)
|
||||
self._emit("vm_boot", task, "vm=%s" % name)
|
||||
self.wait_guest_ready(task)
|
||||
|
||||
def _wake_prepare(self, task: dict, name: str) -> tuple:
|
||||
"""wake 准备:reuse 审计、旧 VM 清理、任务盘供给、
|
||||
直连 egress 审计,纯移动。返回 (task_disk, socket_path)。"""
|
||||
if getattr(self, "disk_mode", "copy") == "reuse":
|
||||
self._emit("disk_reuse_experiment_allowed", task,
|
||||
"vm=%s CHV_REUSE_SINGLE_TENANT_EXPERIMENT=1 "
|
||||
@@ -1144,62 +1276,35 @@ class CloudHypervisorExecutor(Executor):
|
||||
task_disk = self._provision_task_disk(task)
|
||||
socket_path = self.api_socket(task)
|
||||
if self.allow_direct_egress:
|
||||
# (4) 直连 egress 只在显式 CHV_ALLOW_DIRECT_EGRESS=1
|
||||
# 时放行,且必须审计;默认 fail-closed 在 preflight
|
||||
# 里已拦(无代理且未显式允许即缺项)。tap 桥接
|
||||
# 本身是否真只走代理仍需宿主实测,代码不声称已验证。
|
||||
self._emit("egress_direct_allowed", task,
|
||||
"vm=%s CHV_ALLOW_DIRECT_EGRESS=1 explicit" % name)
|
||||
return task_disk, socket_path
|
||||
|
||||
|
||||
def wake_vm(self, task, extra_env=None):
|
||||
"""主唤醒路径(API 模式,dispatch/run/goal 的前置步骤):
|
||||
preflight → 容量门 → 磁盘配额门 → 每任务磁盘供给 →
|
||||
launcher 只起 VMM(--api-socket)→ 等 vmm.ping 通 →
|
||||
PUT vm.create(VmConfig)→ PUT vm.boot → 等 guest
|
||||
SSH 就绪。任一步失败都关 VM(stop_vm)再抛
|
||||
ChvFailure,绝不带着半醒的 VM 去跑任务。
|
||||
成功返回 {vm, api_socket, state: "running", ready: True,
|
||||
mode: "cold"},并审计 vm_create/vm_boot/vm_ready。
|
||||
单任务单 VM:同一任务重复 wake 时先关旧 VM 再起新
|
||||
的,绝不两个 VM 共用一个任务名。
|
||||
feat-chv-secfix:extra_env(_run_with_vm 透传)里的
|
||||
密钥子集在 provisioned 模式下写进 seed 客内文件,
|
||||
keyscan 模式下有密钥子集即 fail-closed;起 VM 前的
|
||||
preflight/容量/配额失败也发 vm_failed 审计(L3);
|
||||
net 隔离在起 VMM 前校验(H7),成功后登记 net.json。"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
name = self.vm_name(task)
|
||||
secrets, net_entries = self._wake_prechecks(task, name, extra_env)
|
||||
task_disk, socket_path = self._wake_prepare(task, name)
|
||||
try:
|
||||
seed_disk = None
|
||||
if getattr(self, "host_key_mode", "provisioned") == "provisioned":
|
||||
# 强方案:启动前预置每 VM host key 并钉死,
|
||||
# 再把 seed 目录做成 CIDATA seed 磁盘并挂进
|
||||
# VmConfig(P0-1 闭环:生成→制盘→挂盘三步缺
|
||||
# 一即 fail-fast,绝不回落 keyscan、绝不只
|
||||
# 生成不注入)。
|
||||
# feat-chv-secfix(H2/H5):secret_env 随 seed
|
||||
# 写进客内密钥文件;seed.iso 建好(0600,见
|
||||
# build_seed_disk)后立即删宿主侧私钥——此后
|
||||
# 宿主上只剩公钥 + known_hosts,私钥只活在客
|
||||
# 机与 seed.iso(随 VM 销毁而删)里。删私钥
|
||||
# 失败即 fail-fast(VM 尚未启动,此时拒绝最
|
||||
# 安全,except 里 stop_vm 会清掉 seed)。
|
||||
from cloud.chv.hostkeys import (
|
||||
build_seed_disk, delete_host_private_key,
|
||||
seed_disk_path)
|
||||
provisioned = self.provision_host_key(
|
||||
task, secret_env=secrets or None)
|
||||
seed_disk = seed_disk_path(self.state_dir, name)
|
||||
build_seed_disk(
|
||||
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._launch_api_only(task)
|
||||
self.wait_api_ready(task)
|
||||
self._check_api_socket_permissions(socket_path)
|
||||
status, _ = self._api_call(
|
||||
socket_path, "PUT", "/vm.create",
|
||||
self.build_vm_config(task_disk, seed_disk=seed_disk))
|
||||
if status not in (200, 204):
|
||||
raise ChvFailure(
|
||||
"vm_create_failed",
|
||||
"vm.create 失败:status=%s" % status)
|
||||
self._emit("vm_create", task,
|
||||
"vm=%s disk=%s" % (name, task_disk))
|
||||
if net_entries:
|
||||
self._record_net_entries(name, net_entries)
|
||||
status, _ = self._api_call(socket_path, "PUT", "/vm.boot")
|
||||
if status not in (200, 204):
|
||||
raise ChvFailure(
|
||||
"vm_boot_failed",
|
||||
"vm.boot 失败:status=%s" % status)
|
||||
self._emit("vm_boot", task, "vm=%s" % name)
|
||||
self.wait_guest_ready(task)
|
||||
seed_disk = self._provision_seed_disk(task, name, secrets)
|
||||
self._boot_vm(task, name, task_disk, seed_disk, net_entries)
|
||||
except Exception:
|
||||
try:
|
||||
self.stop_vm(task)
|
||||
@@ -1779,8 +1884,12 @@ class CloudHypervisorExecutor(Executor):
|
||||
|
||||
def _run_with_vm(self, task, inner_args, goal=False, extra_env=None,
|
||||
**kwargs):
|
||||
"""每任务 VM 生命周期:wake_vm(API 模式 + SSH 就绪)
|
||||
→ 跑客内命令 → finally shutdown_vm。wake 失败时不跑
|
||||
"""每任务 VM 生命周期(冷启动正式路径,feat-chv-arch95
|
||||
定案):wake_vm(API 模式 + SSH 就绪)→ 跑客内命令 →
|
||||
finally shutdown_vm。本方法绝不查 WarmPool:预热池是
|
||||
实验性的(见 pool.py 头定案与 ADR-5),dispatch 永远
|
||||
冷启动,warm_pool_wired_to_dispatch 恒 False。
|
||||
wake 失败时不跑
|
||||
任务、直接抛分类失败。本方法不做内部重试:重试是
|
||||
worker 的 max_retries 语义,执行器内部重试会把一次
|
||||
失败放大成多次计费与多次 VM 启动。
|
||||
|
||||
Reference in new issue
Block a user