feat: Agent 支持 etcdctl/etcdutl 诊断命令

etcd 是 K8S 控制平面的关键组件,缺少 etcd 诊断会导致很多故障无法
继续排查。本次将 etcdctl 和 etcdutl 纳入命令白名单:

只读 (自动执行):
- etcdctl endpoint status/health/hashkv - 端点健康
- etcdctl member list - 成员列表
- etcdctl alarm list - 告警
- etcdctl auth/user/role status|list - 认证与权限查看
- etcdctl check perf/datascale - 性能检查
- etcdctl get/watch - 读取 key
- etcdctl version
- etcdutl snapshot status/hash - 快照检查
- etcdutl version

写操作 (人工审批):
- etcdctl put/del/compact/txn - 数据修改
- etcdctl snapshot save - 保存快照
- etcdctl member add/remove/update - 成员管理
- etcdctl alarm disarm - 解除告警
- etcdctl auth enable/disable - 认证开关
- etcdctl user/role add/delete/grant/revoke - 权限管理
- etcdutl snapshot restore - 恢复快照

实现细节:
- etcdctl/etcdutl 命令支持全局 flag (--endpoints/--cacert 等在子命令前)
- 两段式命令按 (verb, subverb) 组合精确分类,避免 member list (只读)
  和 member remove (写) 混淆
- 新增 12 个 etcd 命令分类测试,全部 56 个测试通过
This commit is contained in:
cnbugs
2026-07-25 13:50:34 +08:00
parent 32bea0d4ea
commit 4f37c65f18
3 changed files with 126 additions and 0 deletions
+60
View File
@@ -127,6 +127,34 @@ SHELL_READ_ONLY_COMMANDS: dict[str, set[str] | None] = {
"timedatectl": None, "date": None, "stat": None, "file": None, "blkid": None,
}
# etcdctl / etcdutl 由 classify_shell_command 中的专用块处理 (两段式命令)
# 从通用白名单中移除,避免被单段式逻辑误判
# etcdctl 单段式只读 verb
_ETCDCTL_READ_VERBS = {"get", "watch", "version", "lock", "lease", "move-leader", "leader"}
# etcdctl 单段式写 verb (需人工审批)
_ETCDCTL_WRITE_VERBS = {"put", "del", "delete", "compact", "txn", "snapshot"}
# etcdctl 两段式 (verb, subverb) 只读组合
_ETCDCTL_READ_SUBVERBS = {
"endpoint": {"status", "health", "hashkv"},
"member": {"list", "list-urls"},
"alarm": {"list"},
"auth": {"status"},
"user": {"list", "get"},
"role": {"list", "get"},
"check": {"perf", "datascale"},
}
# etcdctl 两段式 (verb, subverb) 写组合 (需人工审批)
_ETCDCTL_WRITE_SUBVERBS = {
"member": {"add", "remove", "update"},
"alarm": {"disarm"},
"auth": {"enable", "disable"},
"user": {"add", "delete", "grant-role", "revoke-role", "passwd"},
"role": {"add", "delete", "grant-permission", "revoke-permission"},
}
# etcdutl snapshot 只读子命令
_ETCDUTL_SNAPSHOT_READ = {"status", "hash"}
# 系统修改命令白名单:需要人工审批
SHELL_WRITE_COMMANDS: dict[str, set[str] | None] = {
"systemctl": {"restart", "start", "stop", "reload", "enable", "disable", "daemon-reload"},
@@ -178,6 +206,38 @@ def classify_shell_command(command: str) -> CommandDecision:
if allowed is None or (verb is not None and verb in allowed):
return CommandDecision("write", True, binary, parts[1:])
# etcdctl: 单段式或两段式命令,verb/subverb 均跳过全局 flag (--endpoints 等)
# member/alarm/auth 等同时有只读和写子命令,需按 (verb, subverb) 组合精确判断
if binary == "etcdctl":
non_flags = [a for a in parts[1:] if not a.startswith("-")]
ev = non_flags[0].lower() if non_flags else None
esub = non_flags[1].lower() if len(non_flags) > 1 else None
# 单段式 verb
if ev in _ETCDCTL_READ_VERBS:
return CommandDecision("read", False, ev, parts[1:])
if ev in _ETCDCTL_WRITE_VERBS:
return CommandDecision("write", True, ev, parts[1:])
# 两段式: 先查写子命令 (精确匹配 subverb),再查只读子命令
if ev in _ETCDCTL_WRITE_SUBVERBS and esub in _ETCDCTL_WRITE_SUBVERBS[ev]:
return CommandDecision("write", True, ev, parts[1:])
if ev in _ETCDCTL_READ_SUBVERBS:
if esub is None or esub in _ETCDCTL_READ_SUBVERBS[ev]:
return CommandDecision("read", False, ev, parts[1:])
raise ValueError(f"etcdctl {ev} 不支持子命令: {esub}")
raise ValueError(f"etcdctl 不支持该子命令: {ev}")
if binary == "etcdutl":
non_flags = [a for a in parts[1:] if not a.startswith("-")]
ev = non_flags[0].lower() if non_flags else None
esub = non_flags[1].lower() if len(non_flags) > 1 else None
if ev == "snapshot":
if esub in _ETCDUTL_SNAPSHOT_READ:
return CommandDecision("read", False, ev, parts[1:])
return CommandDecision("write", True, ev, parts[1:])
if ev == "version":
return CommandDecision("read", False, ev, parts[1:])
raise ValueError(f"etcdutl 不支持该子命令: {ev}")
if binary in SHELL_READ_ONLY_COMMANDS:
allowed = SHELL_READ_ONLY_COMMANDS[binary]
if allowed is not None and (verb is None or verb not in allowed):