- autoscaler: desired_workers += pool_target(W=2 台/宿主, pool_target_for_hosts);Autoscaler/CLI 支持 --pool-target / --online-hosts;L1 其余语义(30s 轮、scale up fast/down slow、 fail-closed、veto、LIFO)原样复用,L2 不自动加主机 - executor: recycle_vm/recycle_persistent_vm 前加 Gitea 同步门 (chv-sync push 失败即 state/status=sync_failed、VM 不回收、 记 vm_recycle_sync_failed 审计);未配同步/VM 已死走旧路径 - 新增 cloud/chv/hybrid_events.py:9 个设计事件名一字不差, 全部必填 host_id、密钥字段名拒绝入目录 - orphans facade 已存在且复用 registry/orphan 语义,不动 experimental_pool(wired_to_dispatch 恒 False) - 测试:cloud/tests/test_chv_hybrid_c.py 32 项;全量 4611 项 0 失败 0 错误 15 skipped
147 lines
5.9 KiB
Python
147 lines
5.9 KiB
Python
"""CHV 混合模式审计事件目录(C 轨,feat-chv-hybrid-c)。
|
||
|
||
唯一依据:~/workspace/chv-multi-host/DESIGN-HYBRID.md
|
||
§7 与 §9.1 第 13 项。事件名字符串与设计原文一字不差,
|
||
供 A/B 两轨的发射点 import(A/B 工作树里还没有本文件,
|
||
他们先按设计原文的事件名字符串直接发射,协调员集成时
|
||
统一收到本模块名下)。
|
||
|
||
规则(设计 §7):
|
||
- 全部事件带 host_id(宿主名,默认 socket.gethostname())。
|
||
- 绝不带密钥值:字段名命中密钥名(见 SECRET_FIELD_NAMES)
|
||
即拒绝,错误信息只含字段名、不含值;调用方仍须遵守
|
||
executor._emit 的既有契约(detail 只含 VM 名、路径、
|
||
URL、kind,不含任何 key 值)。
|
||
- 本模块只定名 + 必填字段 + 校验/格式化,不发事件、
|
||
不读库、不碰 VM;发射仍走各轨现有的 _emit / 日志路径。
|
||
|
||
另有一枚 C 轨事件 vm_recycle_sync_failed:不在 §9.1-13
|
||
的名单里,但 §3.3 要求“同步失败 → 任务标同步失败
|
||
(审计)”,回收同步失败的审计须有个名字,故在此单列
|
||
并注明来源,不混进设计名单。
|
||
"""
|
||
|
||
import socket
|
||
|
||
# -- 设计原文事件名(§9.1 第 13 项 / §7,一字不差) ------------
|
||
WARM_POOL_ACQUIRE = "warm_pool_acquire"
|
||
WARM_POOL_MISS = "warm_pool_miss"
|
||
WARM_POOL_RELEASE = "warm_pool_release"
|
||
VM_RESIDENT_SUSPENDED = "vm_resident_suspended"
|
||
VM_RESIDENT_DEMOTED = "vm_resident_demoted"
|
||
VM_RESIDENT_PROMOTED = "vm_resident_promoted"
|
||
RESIDENT_UPGRADE_HINT = "resident_upgrade_hint"
|
||
SNAPSHOT_TENANT_MISMATCH = "snapshot_tenant_mismatch"
|
||
VM_RESTORE_DENIED = "vm_restore_denied"
|
||
|
||
# 设计 §9.1-13 名单一共 9 个事件(acquire/miss/release
|
||
# 是 §7“warm_pool_acquire/miss/release”的三枚展开,
|
||
# miss 的全名 warm_pool_miss 见 §7“须记 warm_pool_miss
|
||
# 审计”)。
|
||
HYBRID_EVENTS = (
|
||
WARM_POOL_ACQUIRE,
|
||
WARM_POOL_MISS,
|
||
WARM_POOL_RELEASE,
|
||
VM_RESIDENT_SUSPENDED,
|
||
VM_RESIDENT_DEMOTED,
|
||
VM_RESIDENT_PROMOTED,
|
||
RESIDENT_UPGRADE_HINT,
|
||
SNAPSHOT_TENANT_MISMATCH,
|
||
VM_RESTORE_DENIED,
|
||
)
|
||
|
||
# -- C 轨回收同步审计(§3.3,非 §9.1-13 名单) ---------------
|
||
VM_RECYCLE_SYNC_FAILED = "vm_recycle_sync_failed"
|
||
|
||
ALL_KNOWN_EVENTS = HYBRID_EVENTS + (VM_RECYCLE_SYNC_FAILED,)
|
||
|
||
# -- 必填字段:全部事件都须带 host_id -------------------------
|
||
# 其余字段按事件语义:vm = VM 名,tenant_id = 租户,
|
||
# reason = 失败原因短文本(已截断,绝不含密钥值)。
|
||
REQUIRED_FIELDS = {
|
||
WARM_POOL_ACQUIRE: ("host_id", "vm"),
|
||
WARM_POOL_MISS: ("host_id",),
|
||
WARM_POOL_RELEASE: ("host_id", "vm"),
|
||
VM_RESIDENT_SUSPENDED: ("host_id", "tenant_id", "vm"),
|
||
VM_RESIDENT_DEMOTED: ("host_id", "tenant_id", "vm"),
|
||
VM_RESIDENT_PROMOTED: ("host_id", "tenant_id", "vm"),
|
||
RESIDENT_UPGRADE_HINT: ("host_id", "tenant_id"),
|
||
SNAPSHOT_TENANT_MISMATCH: ("host_id", "tenant_id", "vm"),
|
||
VM_RESTORE_DENIED: ("host_id", "tenant_id", "vm"),
|
||
VM_RECYCLE_SYNC_FAILED: ("host_id", "vm"),
|
||
}
|
||
|
||
# 密钥字段名:命中即拒绝。exact 匹配 + token/secret/
|
||
# password 子串匹配(host_key 等公开指纹名不在此列,
|
||
# 不可误拦)。
|
||
SECRET_FIELD_NAMES = frozenset({
|
||
"gitea_token", "token", "secret", "password", "api_key",
|
||
"apikey", "private_key", "access_key", "session_key",
|
||
"credential", "credentials", "seed", "auth",
|
||
})
|
||
_SECRET_SUBSTRINGS = ("token", "secret", "password")
|
||
|
||
|
||
def _is_secret_field(name: str) -> bool:
|
||
lowered = str(name).lower()
|
||
if lowered in SECRET_FIELD_NAMES:
|
||
return True
|
||
return any(part in lowered for part in _SECRET_SUBSTRINGS)
|
||
|
||
|
||
def default_host_id() -> str:
|
||
"""宿主名:审计事件的 host_id 缺省值。"""
|
||
return socket.gethostname()
|
||
|
||
|
||
def validate_event_fields(event: str, fields: dict) -> dict:
|
||
"""校验一个事件的字段,返回清洗后的副本(去 None 值)。
|
||
|
||
未知事件名、缺必填字段、host_id 为空、字段名命中密钥
|
||
名时一律 ValueError;错误信息只含事件名/字段名,
|
||
绝不含字段值。"""
|
||
if event not in ALL_KNOWN_EVENTS:
|
||
raise ValueError("未知审计事件:%s" % event)
|
||
cleaned = {k: v for k, v in dict(fields or {}).items()
|
||
if v is not None}
|
||
secret = sorted(k for k in cleaned if _is_secret_field(k))
|
||
if secret:
|
||
raise ValueError(
|
||
"审计事件 %s 不得带密钥字段:%s" % (event, ",".join(secret)))
|
||
missing = [k for k in REQUIRED_FIELDS[event]
|
||
if k not in cleaned
|
||
or (isinstance(cleaned[k], str)
|
||
and not cleaned[k].strip())]
|
||
if missing:
|
||
raise ValueError(
|
||
"审计事件 %s 缺必填字段:%s" % (event, ",".join(missing)))
|
||
return cleaned
|
||
|
||
|
||
def build_event(event: str, fields: dict | None = None,
|
||
host_id: str | None = None) -> dict:
|
||
"""组装一个事件的字段 dict:host_id 未给时取本机宿主名,
|
||
再过 validate_event_fields。返回的 dict 可直接用于
|
||
format_detail 或交审计回调。"""
|
||
merged = dict(fields or {})
|
||
if host_id is not None:
|
||
merged["host_id"] = host_id
|
||
elif not merged.get("host_id"):
|
||
merged["host_id"] = default_host_id()
|
||
return validate_event_fields(event, merged)
|
||
|
||
|
||
def format_detail(event: str, fields: dict | None = None,
|
||
host_id: str | None = None) -> str:
|
||
"""把事件字段格式化成 executor._emit 的 detail 字符串
|
||
("k=v k=v",键排序,值截断 200 字符、去换行)。
|
||
先过 build_event 校验,所以带 host_id、不带密钥字段。"""
|
||
cleaned = build_event(event, fields, host_id=host_id)
|
||
parts = []
|
||
for key in sorted(cleaned):
|
||
value = str(cleaned[key]).replace("\n", " ").replace("\r", " ")
|
||
if len(value) > 200:
|
||
value = value[:200]
|
||
parts.append("%s=%s" % (key, value))
|
||
return " ".join(parts)
|