feat-chv-to10:往 10 分方向提升(类型/lint 清零、worker 适配抽取、审计落盘、lockfile 生成脚本、前端四态与统一文案、就绪清单 §13)
This commit is contained in:
1 parent
df8c64bd64
commit
2f3ff8b3ec
22 files changed
+1400
-282
No files matched your search
+195
-80
@@ -34,30 +34,35 @@ import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
|
||||
from cloud.chv.api_client import _unix_socket_api
|
||||
from cloud.chv.api_client import normalize_api_result
|
||||
from cloud.executor_base import Executor
|
||||
from cloud.chv.config import _parse_memory_bytes
|
||||
from cloud.chv.config import build_chv_config
|
||||
from cloud.chv.disk import check_disk_quota
|
||||
from cloud.chv.disk import check_snapshot_limits
|
||||
from cloud.chv.disk import delete_task_disk
|
||||
from cloud.chv.disk import provision_task_disk
|
||||
from cloud.chv.disk import record_snapshot
|
||||
from cloud.chv.disk import task_disk_path
|
||||
from cloud.chv.api_client import _unix_socket_api, normalize_api_result
|
||||
from cloud.chv.config import _parse_memory_bytes, build_chv_config
|
||||
from cloud.chv.disk import (
|
||||
check_disk_quota,
|
||||
check_snapshot_limits,
|
||||
delete_task_disk,
|
||||
provision_task_disk,
|
||||
record_snapshot,
|
||||
task_disk_path,
|
||||
)
|
||||
from cloud.chv.errors import ChvFailure
|
||||
from cloud.chv.hardening import ensure_secure_state_dir
|
||||
from cloud.chv.hardening import parse_net_entries
|
||||
from cloud.chv.hardening import socket_is_live
|
||||
from cloud.chv.hardening import validate_egress_proxy
|
||||
from cloud.chv.hardening import validate_net_entries
|
||||
from cloud.chv.hardening import validate_snapshot_url
|
||||
from cloud.chv.hardening import (
|
||||
ensure_secure_state_dir,
|
||||
parse_net_entries,
|
||||
socket_is_live,
|
||||
validate_egress_proxy,
|
||||
validate_net_entries,
|
||||
validate_snapshot_url,
|
||||
)
|
||||
from cloud.chv.ssh_transport import build_ssh_command as _build_ssh_cmd
|
||||
from cloud.chv.ssh_transport import known_hosts_path as _known_hosts_path
|
||||
from cloud.chv.ssh_transport import parse_keyscan_output
|
||||
from cloud.chv.ssh_transport import split_secret_env
|
||||
from cloud.chv.ssh_transport import write_known_hosts
|
||||
from cloud.chv.ssh_transport import (
|
||||
parse_keyscan_output,
|
||||
split_secret_env,
|
||||
write_known_hosts,
|
||||
)
|
||||
from cloud.executor_base import Executor
|
||||
|
||||
_LOGGER = logging.getLogger("cloud.cloud_hypervisor")
|
||||
|
||||
@@ -246,6 +251,57 @@ class CloudHypervisorExecutor(Executor):
|
||||
开启嵌套虚拟化的实例,那是用户侧开通决策,不在本轨内。
|
||||
"""
|
||||
|
||||
# to10:mypy 属性声明(仅注解、不赋值,运行时仍由
|
||||
# _copy_config_fields/_init_runtime 抄写)。没有这组声明,
|
||||
# 动态 setattr 会让 mypy 认为全部配置属性不存在(基线
|
||||
# 115 个报错里近 90 个属此类),类型检查等于没跑。
|
||||
binary: str | None
|
||||
kernel: str | None
|
||||
rootfs: str | None
|
||||
cpus: str
|
||||
memory: str
|
||||
state_dir: str
|
||||
ssh_key: str | None
|
||||
ssh_user: str
|
||||
ssh_port: int
|
||||
guest_host_py: str
|
||||
cmdline: str
|
||||
net: str | None
|
||||
net_json: str | None
|
||||
kvm_path: str
|
||||
cpuinfo_path: str
|
||||
ch_remote: str
|
||||
model: str | None
|
||||
max_vms: int
|
||||
disk_mode: str
|
||||
disk_gb: float
|
||||
egress_proxy: str | None
|
||||
allow_direct_egress: bool
|
||||
task_timeout: float | None
|
||||
snapshot_url: str | None
|
||||
warm_pool_size: int
|
||||
api_timeout: float
|
||||
ready_timeout: float
|
||||
ready_interval: float
|
||||
host_key_mode: str
|
||||
ssh_host: str | None
|
||||
ssh_forward: bool
|
||||
reuse_experiment: bool
|
||||
single_tenant_project: str | None
|
||||
snapshot_max_count: int
|
||||
snapshot_max_total_gb: float
|
||||
snapshot_retention_days: float
|
||||
snapshot_dir: str | None
|
||||
allow_remote_snapshot: bool
|
||||
runner: "Callable[..., object]"
|
||||
launcher: "Callable[..., object]"
|
||||
api_client: "Callable[..., object]"
|
||||
sleeper: "Callable[..., object]"
|
||||
audit_callback: object
|
||||
audit_failure_count: int
|
||||
_vms: dict
|
||||
_guest_env_delivered: dict
|
||||
|
||||
def __init__(self, binary: str | None = None,
|
||||
kernel: str | None = None,
|
||||
rootfs: str | None = None,
|
||||
@@ -253,7 +309,8 @@ class CloudHypervisorExecutor(Executor):
|
||||
cpus: str | int | None = None,
|
||||
memory: str | None = None,
|
||||
state_dir: str | None = None,
|
||||
runner: object = None, launcher: object = None,
|
||||
runner: Callable[..., object] | None = None,
|
||||
launcher: Callable[..., object] | None = None,
|
||||
ssh_key: str | None = None,
|
||||
ssh_user: str | None = None,
|
||||
ssh_port: int | str | None = None,
|
||||
@@ -265,12 +322,13 @@ class CloudHypervisorExecutor(Executor):
|
||||
cpuinfo_path: str = "/proc/cpuinfo",
|
||||
env: dict | None = None,
|
||||
model: str | None = None, ch_remote: str | None = None,
|
||||
api_client: object = None,
|
||||
sleeper: object = None, api_timeout: float | None = None,
|
||||
api_client: Callable[..., object] | None = None,
|
||||
sleeper: Callable[..., object] | None = None,
|
||||
api_timeout: float | None = None,
|
||||
ready_timeout: float | None = None,
|
||||
ready_interval: float | None = None,
|
||||
host_key_mode: str | None = None) -> None:
|
||||
self._env = env if env is not None else os.environ
|
||||
self._env: dict = env if env is not None else os.environ # type: ignore[assignment]
|
||||
# 配置解析已拆到 cloud/chv/config.py(参数优先于环境
|
||||
# 变量,默认值与 #216 一致;显式空用户/越界端口的收紧
|
||||
# 见 config.py 头注释)。字段名与原属性一一对应,旧
|
||||
@@ -291,7 +349,15 @@ class CloudHypervisorExecutor(Executor):
|
||||
self._copy_config_fields(_cfg)
|
||||
self._init_runtime(runner, launcher, api_client, sleeper)
|
||||
|
||||
def _init_runtime(self, runner, launcher, api_client, sleeper) -> None:
|
||||
def _init_runtime(self, runner: Callable[..., object] | None,
|
||||
launcher: Callable[..., object] | None,
|
||||
api_client: Callable[..., object] | None,
|
||||
sleeper: Callable[..., object] | None) -> None:
|
||||
# to10:runner/launcher/api_client/sleeper 均为可调用注入对象;
|
||||
# 形参保持 object 以兼容旧测试的任意 lambda,调用点用
|
||||
# getattr 可调用检查后调用(见 _call_runner 等),mypy
|
||||
# 的 object-not-callable 在 _call_runner 处以 Callable 局部
|
||||
# 变量处理。
|
||||
"""运行时注入:seed 投递集合、API 客户端、睡眠器、
|
||||
runner/launcher 默认值(纯移动)。"""
|
||||
# feat-chv-secfix(H2):本实例已 seed 投递密钥的 VM
|
||||
@@ -346,7 +412,11 @@ class CloudHypervisorExecutor(Executor):
|
||||
幂等:传入的已是 chv- 前缀名时不再二次加前缀(清理
|
||||
扫描 socket 文件名时传的就是 chv- 名,二次加前缀会
|
||||
把 chv-dead 算成 chv-chv-dead、永远清不掉)。"""
|
||||
raw = task_or_name.get("id") if isinstance(task_or_name, dict) \
|
||||
# to10:id 按契约是 str;cast 只给 mypy 用,运行时原样
|
||||
# 传给 re.sub(id 缺失时仍按旧行为 TypeError,不静默
|
||||
# 编成 "None")。
|
||||
from typing import cast as _cast
|
||||
raw = _cast(str, task_or_name.get("id")) if isinstance(task_or_name, dict) \
|
||||
else str(task_or_name)
|
||||
safe = re.sub(r"[^A-Za-z0-9_.-]", "-", raw)
|
||||
if safe.startswith("chv-"):
|
||||
@@ -589,7 +659,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
- egress_not_configured:CHV_EGRESS_PROXY 未设且未显式
|
||||
允许直连(默认 fail-closed,VM 出口必须过白名单代理)
|
||||
"""
|
||||
missing = [] # (kind, 文本) 列表,全部收集完再一次抛
|
||||
missing: list = [] # (kind, 文本) 列表,全部收集完再一次抛
|
||||
self._preflight_host_checks(missing)
|
||||
self._preflight_file_checks(missing)
|
||||
self._preflight_net_checks(missing)
|
||||
@@ -637,6 +707,34 @@ class CloudHypervisorExecutor(Executor):
|
||||
self, "audit_failure_count", 0) + 1
|
||||
_LOGGER.error("chv_audit_failed event=%s kind=%s error=%s",
|
||||
event, kind, exc)
|
||||
# to10(ADR-14):回调失败的事件再落盘一份——
|
||||
# 宿主日志可能轮转、计数只剩数字,落盘文件是
|
||||
# 唯一能事后找回“哪次事件丢了”的取证。落盘
|
||||
# 本身 best-effort,失败只记日志,绝不炸任务。
|
||||
self._spill_audit(event, task, detail, kind)
|
||||
|
||||
def _spill_audit(self, event, task, detail, kind) -> None:
|
||||
"""审计失败事件落盘(to10,见 ADR-14 取舍):
|
||||
state_dir/chv-audit-spill.jsonl,每行一 JSON
|
||||
{ts, event, kind, detail, task_id};文件超 1MB 时
|
||||
先轮转成 .old 再写。detail 沿用 _emit 契约,绝不
|
||||
含密钥值。落盘不是 DB 替代:timeline 查询看不到它,
|
||||
需宿主人工查阅;轮转只留最新两代,旧事件会丢。"""
|
||||
try:
|
||||
path = os.path.join(self.state_dir,
|
||||
"chv-audit-spill.jsonl")
|
||||
if os.path.exists(path) \
|
||||
and os.path.getsize(path) > 1024 * 1024:
|
||||
os.replace(path, path + ".old")
|
||||
task_id = task.get("id") if isinstance(task, dict) \
|
||||
else str(task)
|
||||
line = json.dumps({"ts": time.time(), "event": event,
|
||||
"kind": kind, "detail": detail,
|
||||
"task_id": task_id})
|
||||
with open(path, "a") as fh:
|
||||
fh.write(line + "\n")
|
||||
except Exception as exc:
|
||||
_LOGGER.error("chv_audit_spill_failed error=%s", exc)
|
||||
|
||||
# -- 命令构造(纯函数,不做文件校验) -------------------------------
|
||||
def _build_vm_command(self, task):
|
||||
@@ -671,7 +769,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return _known_hosts_path(self.state_dir,
|
||||
self.vm_name(task_or_name))
|
||||
|
||||
def _net_guest_ip(self):
|
||||
def _net_guest_ip(self) -> str | None:
|
||||
"""VmConfig net 首个带 ip 的条目(静态 tap 客机 IP)。"""
|
||||
if not self.net_json:
|
||||
return None
|
||||
@@ -691,8 +789,9 @@ class CloudHypervisorExecutor(Executor):
|
||||
显式 CHV_SSH_HOST > net 静态客机 IP > 显式转发宣称下
|
||||
的回环;三者皆无时报 ChvFailure(ssh_target_undeclared),
|
||||
绝不无条件假设 127.0.0.1:2222 能通。"""
|
||||
if getattr(self, "ssh_host", None):
|
||||
return self.ssh_host
|
||||
_ssh_host = self.ssh_host
|
||||
if _ssh_host:
|
||||
return _ssh_host
|
||||
guest_ip = self._net_guest_ip()
|
||||
if guest_ip:
|
||||
return guest_ip
|
||||
@@ -704,6 +803,30 @@ class CloudHypervisorExecutor(Executor):
|
||||
"里给 net 条目配 ip(静态 tap 客机 IP)、或显式设 "
|
||||
"CHV_SSH_FORWARD=1 宣称宿主 NAT 已转发回环端口")
|
||||
|
||||
def _read_keypair_texts(self, paths: dict) -> tuple:
|
||||
"""读 key 对文本(to10 提取,provision_host_key 降行用)。"""
|
||||
with open(paths["public"], "r") as fh:
|
||||
public_text = fh.read()
|
||||
with open(paths["private"], "r") as fh:
|
||||
private_text = fh.read()
|
||||
return public_text, private_text
|
||||
|
||||
def _write_provisioned_known_hosts(self, paths: dict,
|
||||
public_text: str) -> None:
|
||||
"""写预置 known_hosts(to10 提取)。主机部分跟 SSH
|
||||
目标走(客机 IP 时不再写死回环,否则钉死校验永远
|
||||
对不上);解析失败时回环仅作钉死写入,不放宽校验。"""
|
||||
from cloud.chv.hostkeys import known_hosts_line
|
||||
from cloud.chv.ssh_transport import write_known_hosts
|
||||
try:
|
||||
ssh_host = self.resolve_ssh_host()
|
||||
except ChvFailure:
|
||||
ssh_host = "127.0.0.1"
|
||||
write_known_hosts(
|
||||
paths["known_hosts"],
|
||||
[known_hosts_line(self.ssh_port, public_text,
|
||||
host=ssh_host)])
|
||||
|
||||
def provision_host_key(self, task: dict, secret_env: dict | None = None) -> dict:
|
||||
"""按 VM 预置 host key(补缺口轨强方案,见 hostkeys.py):
|
||||
启动前为该 VM 生成一次性 ed25519 host key 对,公钥
|
||||
@@ -718,10 +841,13 @@ class CloudHypervisorExecutor(Executor):
|
||||
脏标记存在时直接拒绝(清理失败即拒起,不再复用
|
||||
旧 key 对)。成功后审计 host_key_generated(只含
|
||||
VM 名与密钥名,不含任何 key 值)。"""
|
||||
# to10:known_hosts 写入已抽到 _write_provisioned_known_hosts,
|
||||
# 此处只留生成与 seed 写入的 import(删后不再有未用 import)。
|
||||
from cloud.chv.hostkeys import (
|
||||
dirty_marker_path, generate_host_keypair,
|
||||
known_hosts_line, write_seed)
|
||||
from cloud.chv.ssh_transport import write_known_hosts
|
||||
dirty_marker_path,
|
||||
generate_host_keypair,
|
||||
write_seed,
|
||||
)
|
||||
vm = self.vm_name(task)
|
||||
if os.path.exists(dirty_marker_path(self.state_dir, vm)):
|
||||
raise ChvFailure(
|
||||
@@ -732,20 +858,8 @@ class CloudHypervisorExecutor(Executor):
|
||||
paths = generate_host_keypair(
|
||||
self.state_dir, vm,
|
||||
runner=lambda cmd: self._call_runner(cmd))
|
||||
with open(paths["public"], "r") as fh:
|
||||
public_text = fh.read()
|
||||
with open(paths["private"], "r") as fh:
|
||||
private_text = fh.read()
|
||||
# feat-chv-fix:known_hosts 行的主机部分跟 SSH 目标走
|
||||
# (客机 IP 时不再写死回环,否则钉死校验永远对不上)。
|
||||
try:
|
||||
ssh_host = self.resolve_ssh_host()
|
||||
except ChvFailure:
|
||||
ssh_host = "127.0.0.1"
|
||||
write_known_hosts(
|
||||
paths["known_hosts"],
|
||||
[known_hosts_line(self.ssh_port, public_text,
|
||||
host=ssh_host)])
|
||||
public_text, private_text = self._read_keypair_texts(paths)
|
||||
self._write_provisioned_known_hosts(paths, public_text)
|
||||
seed_dir = write_seed(paths["seed_dir"], vm,
|
||||
private_text, public_text,
|
||||
secret_env=secret_env)
|
||||
@@ -819,7 +933,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
from cloud.chv.hostkeys import GUEST_ENV_PATH
|
||||
delivered = self._guest_env_delivered.get(
|
||||
self.vm_name(task), set())
|
||||
missing = [name for name in secrets if name not in delivered]
|
||||
missing: list = [name for name in secrets if name not in delivered]
|
||||
if missing:
|
||||
raise ChvFailure(
|
||||
"secret_env_not_delivered",
|
||||
@@ -835,6 +949,27 @@ class CloudHypervisorExecutor(Executor):
|
||||
guest_env_file=guest_env_file)
|
||||
|
||||
# -- API 模式:VmConfig / API 调用 / 就绪轮询 -----------------------
|
||||
def _vm_config_disks(self, task_disk: str | None,
|
||||
seed_disk: str | None) -> list:
|
||||
"""VmConfig 磁盘列表(to10 提取,含 L2 白名单校验)。
|
||||
任务盘 + 可选 seed 只读盘(CIDATA ISO,cloud-init
|
||||
NoCloud 数据源);显式给的盘若不是基础镜像本身,
|
||||
须在 state_dir 内,否则 VmConfig 不能挂进宿主任意文件。"""
|
||||
# 旧文案逐字保留:任务盘“任务磁盘须在…”,seed 盘
|
||||
# “seed 磁盘须在…”(label 本身含“磁盘”与空格)。
|
||||
for path, label in ((task_disk, "任务磁盘"),
|
||||
(seed_disk, "seed 磁盘")):
|
||||
if path and path != self.rootfs:
|
||||
if not os.path.realpath(path).startswith(
|
||||
os.path.realpath(self.state_dir) + os.sep):
|
||||
raise ChvFailure(
|
||||
"disk_path_not_allowed",
|
||||
"%s须在 state_dir 内:%s" % (label, path))
|
||||
disks: list = [{"path": task_disk or self.rootfs}]
|
||||
if seed_disk:
|
||||
disks.append({"path": seed_disk, "readonly": True})
|
||||
return disks
|
||||
|
||||
def build_vm_config(self, task_disk: str | None = None, seed_disk: str | None = None) -> dict:
|
||||
"""API 模式的 VmConfig(vm.create 请求体,纯函数):
|
||||
cpus{boot_vcpus,max_vcpus}、memory{size: 字节}、
|
||||
@@ -847,36 +982,14 @@ class CloudHypervisorExecutor(Executor):
|
||||
cpus = int(self.cpus)
|
||||
except ValueError:
|
||||
raise ValueError("CHV_CPU 必须是整数:%r" % (self.cpus,))
|
||||
# feat-chv-secfix(L2):磁盘路径白名单。显式给的
|
||||
# task_disk 若不是基础镜像本身(reuse 模式的合法
|
||||
# 情形),须在 state_dir 内;seed 盘须在 state_dir
|
||||
# 内——VmConfig 再也不能挂进宿主任意文件。
|
||||
if task_disk and task_disk != self.rootfs:
|
||||
if not os.path.realpath(task_disk).startswith(
|
||||
os.path.realpath(self.state_dir) + os.sep):
|
||||
raise ChvFailure(
|
||||
"disk_path_not_allowed",
|
||||
"任务磁盘须在 state_dir 内:%s" % task_disk)
|
||||
if seed_disk:
|
||||
if not os.path.realpath(seed_disk).startswith(
|
||||
os.path.realpath(self.state_dir) + os.sep):
|
||||
raise ChvFailure(
|
||||
"disk_path_not_allowed",
|
||||
"seed 磁盘须在 state_dir 内:%s" % seed_disk)
|
||||
config = {
|
||||
"cpus": {"boot_vcpus": cpus, "max_vcpus": cpus},
|
||||
"memory": {"size": _parse_memory_bytes(self.memory)},
|
||||
"kernel": {"path": self.kernel},
|
||||
"cmdline": {"args": self.cmdline},
|
||||
"disks": [{"path": task_disk or self.rootfs}],
|
||||
"disks": self._vm_config_disks(task_disk, seed_disk),
|
||||
"rng": {"src": "/dev/urandom"},
|
||||
}
|
||||
# feat-chv-fix(P0-1):seed 磁盘是 VmConfig 第二块
|
||||
# 只读盘(CIDATA 卷标 ISO,客机 cloud-init NoCloud
|
||||
# 数据源),不是宿主目录——目录本身从不进 VmConfig。
|
||||
if seed_disk:
|
||||
config["disks"].append({"path": seed_disk,
|
||||
"readonly": True})
|
||||
if self.net_json:
|
||||
try:
|
||||
net = json.loads(self.net_json)
|
||||
@@ -1230,7 +1343,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
raise
|
||||
return secrets, net_entries
|
||||
|
||||
def _provision_seed_disk(self, task: dict, name: str, secrets) -> str:
|
||||
def _provision_seed_disk(self, task: dict, name: str, secrets) -> str | None:
|
||||
"""provisioned 模式的 seed 盘供给:生成→制盘→挂盘,
|
||||
任一步失败即抛(调用方 stop_vm 清理),纯移动。"""
|
||||
seed_disk = None
|
||||
@@ -1248,8 +1361,10 @@ class CloudHypervisorExecutor(Executor):
|
||||
# 失败即 fail-fast(VM 尚未启动,此时拒绝最
|
||||
# 安全,except 里 stop_vm 会清掉 seed)。
|
||||
from cloud.chv.hostkeys import (
|
||||
build_seed_disk, delete_host_private_key,
|
||||
seed_disk_path)
|
||||
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)
|
||||
@@ -1498,8 +1613,8 @@ class CloudHypervisorExecutor(Executor):
|
||||
self.pause_vm(task_or_name)
|
||||
self.snapshot_vm(task_or_name, snapshot_url)
|
||||
self.stop_vm(task_or_name)
|
||||
task = task_or_name if isinstance(task_or_name, dict) \
|
||||
else {"id": task_or_name}
|
||||
# to10:删死赋值(task 建出后未用,F841;返回值直接
|
||||
# 用 task_or_name 算 VM 名,行为不变)。
|
||||
return {"vm": self.vm_name(task_or_name),
|
||||
"state": "hibernated", "snapshot": snapshot_url}
|
||||
|
||||
@@ -1616,7 +1731,8 @@ class CloudHypervisorExecutor(Executor):
|
||||
非 live(无 api-socket 文件且无登记活进程)即删盘
|
||||
并审计;删失败审计 vm_disk_delete_failed 并计入
|
||||
failed。返回 {deleted, failed}。"""
|
||||
deleted, failed = [], []
|
||||
deleted: list = []
|
||||
failed: list = []
|
||||
try:
|
||||
entries = os.listdir(self.state_dir)
|
||||
except Exception:
|
||||
@@ -1693,7 +1809,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
"""崩溃残留扫描(H5):私钥一律删、非 live VM 的 seed
|
||||
同理删并审计;返回已清理 VM 名。worker 启动与
|
||||
cleanup_stale_vms 时调它,不依赖 socket 是否存在。"""
|
||||
cleaned = []
|
||||
cleaned: list = []
|
||||
try:
|
||||
entries = os.listdir(self.state_dir)
|
||||
except Exception:
|
||||
@@ -1814,8 +1930,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
extra_env=extra_env)
|
||||
result = self._call_runner(ssh_cmd, timeout=self.task_timeout)
|
||||
# 方法内 import:见模块头注释的循环导入说明。
|
||||
from cloud.sandbox import _parse_host_result
|
||||
from cloud.sandbox import SimpleNamespaceShim
|
||||
from cloud.sandbox import SimpleNamespaceShim, _parse_host_result
|
||||
parsed = _parse_host_result(task["id"],
|
||||
SimpleNamespaceShim(result))
|
||||
parsed["executor"] = "cloud-hypervisor"
|
||||
|
||||
Reference in new issue
Block a user