feat(chv-hybrid-c): autoscaler 池目标数 + 回收同步门 + 审计事件目录
- 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
This commit is contained in:
1 parent
3cdfd0f79e
commit
39c51b7bf2
4 files changed
+953
-11
No files matched your search
@@ -0,0 +1,146 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user