e26a3e559f
完整 shell 能力支持: - VAR=value command 语法(自动跳过赋值检测实际 binary) - export VAR=value 支持(export 加入只读白名单) - 多行命令(\n 分割后逐段检查) - && || ; 等连接符的武器级命令检测 - _split_into_commands 重写:使用 while 索引遍历,正确处理 && / || / | - _command_has_dangerous_cmds 跳过 VAR=value 和 export 前缀 - classify_agent_command 检测 VAR=value kubectl 语法路由到 kubectl 分类器 - execute_command 支持变量赋值后的 binary 定位 - 新增 shell 内置白名单:echo printf cd pwd export source . sleep timeout nohup unset shift return local readonly trap type command set shopt eval exec alias unalias declare typeset read mapfile - 白名单只读命令新增:timeout、nohup、sleep 等
577 lines
24 KiB
Python
577 lines
24 KiB
Python
"""AI Agent 可用的命令工具和安全策略。
|
||
|
||
支持两类命令:
|
||
1. kubectl 命令 - 通过 classify_kubectl_command 分类
|
||
2. 系统诊断 shell 命令 - 通过 classify_shell_command 分类 (白名单)
|
||
|
||
classify_agent_command 统一分发,execute_command 自动选择执行方式。
|
||
所有命令均通过 subprocess_exec 执行 (无 shell),禁止管道、重定向和命令组合。
|
||
"""
|
||
import asyncio
|
||
import shlex
|
||
from dataclasses import dataclass
|
||
|
||
from backend.diagnosis import KUBECTL
|
||
|
||
# ── kubectl 命令分类 ──────────────────────────────────────
|
||
|
||
READ_ONLY_VERBS = {
|
||
"api-resources", "api-versions", "auth", "cluster-info", "config", "describe",
|
||
"diff", "explain", "get", "logs", "top", "version", "wait",
|
||
}
|
||
WRITE_OR_INTERACTIVE_VERBS = {
|
||
"annotate", "apply", "attach", "autoscale", "cordon", "cp", "create",
|
||
"debug", "delete", "drain", "edit", "exec", "expose", "label", "patch",
|
||
"port-forward", "replace", "rollout", "run", "scale", "set", "taint",
|
||
"uncordon",
|
||
}
|
||
GLOBAL_FLAGS_WITH_VALUE = {
|
||
"--as", "--as-group", "--as-uid", "--cache-dir", "--certificate-authority",
|
||
"--client-certificate", "--client-key", "--cluster", "--context", "--kubeconfig",
|
||
"--namespace", "-n", "--profile", "--request-timeout", "--server", "-s",
|
||
"--tls-server-name", "--token", "--user",
|
||
}
|
||
FORBIDDEN_TOKENS = set() # 全部 shell 操作符均允许;安全靠白名单+危险命令检查
|
||
# 永远拒绝的武器级危险命令(无论是否在白名单中)
|
||
_ALWAYS_DENY_CMDS = {"sh", "bash", "dash", "zsh",
|
||
"dd", "format", "mkfs", "fdisk", "parted"}
|
||
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class CommandDecision:
|
||
mode: str # "read" (只读,自动执行) 或 "write" (修改,需审批)
|
||
requires_approval: bool
|
||
verb: str # kubectl 子命令 或 shell 命令名
|
||
args: list[str]
|
||
needs_shell: bool = False # True 时需用 shell=True 执行
|
||
|
||
|
||
def _has_shell_operators(command: str) -> bool:
|
||
"""检测命令是否包含 shell 操作符(管道、重定向、分号、逻辑连接符、多行等)。"""
|
||
stripped = command.strip()
|
||
if not stripped:
|
||
return False
|
||
if "\n" in stripped or "\r" in stripped:
|
||
return True # 多行命令需要 shell=True
|
||
in_single = in_double = False
|
||
for i, ch in enumerate(stripped):
|
||
if ch == "'" and not in_double:
|
||
in_single = not in_single
|
||
elif ch == '"' and not in_single:
|
||
in_double = not in_double
|
||
if in_single or in_double:
|
||
continue
|
||
if ch in ('|', '>', ';', '&', '<'):
|
||
# 单纯 & 不算(后台任务标记)
|
||
if ch == '&':
|
||
if i + 1 < len(stripped) and stripped[i + 1] in ('&',):
|
||
return True # &&
|
||
continue
|
||
return True
|
||
return False
|
||
|
||
|
||
def _split_into_commands(command: str) -> list[str]:
|
||
"""按 shell 连接符(; && || | \n 等)分割命令块。
|
||
|
||
返回每个可执行段,忽略引号内的操作符。重定向符号 > < 不分割。
|
||
"""
|
||
segments = []
|
||
buf = []
|
||
in_single = in_double = False
|
||
i = 0
|
||
while i < len(command):
|
||
ch = command[i]
|
||
if ch == "'" and not in_double:
|
||
in_single = not in_single
|
||
elif ch == '"' and not in_single:
|
||
in_double = not in_double
|
||
if not in_single and not in_double:
|
||
if ch in (';', '\n'):
|
||
if buf:
|
||
segments.append(''.join(buf).strip())
|
||
buf = []
|
||
i += 1
|
||
continue
|
||
if ch == '|':
|
||
if i + 1 < len(command) and command[i + 1] == '|':
|
||
# || — 分割命令
|
||
if buf:
|
||
segments.append(''.join(buf).strip())
|
||
buf = []
|
||
i += 2
|
||
continue
|
||
# 单 | — 分割命令
|
||
if buf:
|
||
segments.append(''.join(buf).strip())
|
||
buf = []
|
||
i += 1
|
||
continue
|
||
if ch == '&':
|
||
if i + 1 < len(command) and command[i + 1] == '&':
|
||
# && — 分割命令
|
||
if buf:
|
||
segments.append(''.join(buf).strip())
|
||
buf = []
|
||
i += 2
|
||
continue
|
||
# 单独 & — 保留
|
||
buf.append(ch)
|
||
i += 1
|
||
if buf:
|
||
segments.append(''.join(buf).strip())
|
||
return [s for s in segments if s]
|
||
|
||
|
||
def _command_has_dangerous_cmds(command: str) -> bool:
|
||
"""检查命令中任何可执行段是否包含武器级危险命令。"""
|
||
for segment in _split_into_commands(command):
|
||
try:
|
||
seg_args = shlex.split(segment)
|
||
except ValueError:
|
||
continue
|
||
if not seg_args:
|
||
continue
|
||
# 跳过 VAR=value 赋值
|
||
idx = 0
|
||
while idx < len(seg_args) and "=" in seg_args[idx] and not seg_args[idx].startswith("-"):
|
||
idx += 1
|
||
if idx < len(seg_args) and seg_args[idx] in _ALWAYS_DENY_CMDS:
|
||
return True
|
||
# 也检查 export VAR=value 后的命令
|
||
if idx < len(seg_args) and seg_args[idx] == "export":
|
||
idx2 = idx + 1
|
||
while idx2 < len(seg_args) and "=" in seg_args[idx2]:
|
||
idx2 += 1
|
||
if idx2 < len(seg_args) and seg_args[idx2] in _ALWAYS_DENY_CMDS:
|
||
return True
|
||
return False
|
||
|
||
|
||
def _parse_command(command: str) -> list[str]:
|
||
"""解析命令,检查合法性。
|
||
|
||
允许所有 shell 操作符(| > ; && || < 等)以及多行命令,安全靠白名单检查。
|
||
武器级危险命令 sh/bash/dd/mkfs 等在 classify 阶段处理。
|
||
"""
|
||
if not isinstance(command, str) or not command.strip():
|
||
raise ValueError("命令不能为空")
|
||
if _command_has_dangerous_cmds(command):
|
||
raise ValueError("命令包含不允许的危险命令(sh/bash/dd/mkfs 等),请手动执行")
|
||
try:
|
||
return shlex.split(command)
|
||
except ValueError as exc:
|
||
raise ValueError(f"命令格式错误: {exc}") from exc
|
||
|
||
|
||
def parse_kubectl_command(command: str) -> list[str]:
|
||
"""仅解析单条 kubectl 命令,不允许 shell、重定向或命令组合。"""
|
||
parts = _parse_command(command)
|
||
if not parts or parts[0] != "kubectl":
|
||
raise ValueError("只允许执行 kubectl 命令")
|
||
if len(parts) < 2:
|
||
raise ValueError("kubectl 命令缺少操作类型")
|
||
return parts[1:]
|
||
|
||
|
||
def _find_verb(args: list[str]) -> str:
|
||
index = 0
|
||
while index < len(args):
|
||
arg = args[index]
|
||
if arg == "--":
|
||
break
|
||
if arg in GLOBAL_FLAGS_WITH_VALUE:
|
||
index += 2
|
||
continue
|
||
if any(arg.startswith(flag + "=") for flag in GLOBAL_FLAGS_WITH_VALUE if flag.startswith("--")):
|
||
index += 1
|
||
continue
|
||
if arg.startswith("-"):
|
||
index += 1
|
||
continue
|
||
return arg.lower()
|
||
raise ValueError("无法识别 kubectl 操作类型")
|
||
|
||
|
||
READ_ONLY_CONFIG_SUBCOMMANDS = {"current-context", "get-clusters", "get-contexts", "get-users", "view"}
|
||
|
||
|
||
def classify_kubectl_command(command: str) -> CommandDecision:
|
||
args = parse_kubectl_command(command)
|
||
verb = _find_verb(args)
|
||
if verb == "config":
|
||
try:
|
||
verb_index = args.index(next(arg for arg in args if arg.lower() == "config"))
|
||
subcommand = args[verb_index + 1].lower()
|
||
except (StopIteration, ValueError, IndexError):
|
||
return CommandDecision("write", True, verb, args)
|
||
if subcommand in READ_ONLY_CONFIG_SUBCOMMANDS:
|
||
return CommandDecision("read", False, verb, args)
|
||
return CommandDecision("write", True, verb, args)
|
||
if verb in READ_ONLY_VERBS:
|
||
return CommandDecision("read", False, verb, args)
|
||
if verb in WRITE_OR_INTERACTIVE_VERBS:
|
||
return CommandDecision("write", True, verb, args)
|
||
return CommandDecision("write", True, verb, args)
|
||
|
||
|
||
# ── 系统诊断 shell 命令分类 (白名单) ─────────────────────
|
||
|
||
# 只读诊断命令白名单:value 为 None 表示该命令本身只读,任意参数均可;
|
||
# 为集合时,第一个非 flag 参数必须属于该集合。
|
||
SHELL_READ_ONLY_COMMANDS: dict[str, set[str] | None] = {
|
||
"systemctl": {
|
||
"status", "is-active", "is-enabled", "is-failed", "list-units",
|
||
"list-sockets", "list-jobs", "show", "cat", "list-unit-files",
|
||
"list-dependencies",
|
||
},
|
||
"journalctl": None, # 只读,任意参数 (-u, -n, --no-pager, --since 等)
|
||
"crictl": {"ps", "inspect", "logs", "info", "version", "images", "stats"},
|
||
"ctr": None,
|
||
"nerdctl": {"ps", "logs", "inspect", "info", "version", "images", "stats", "top", "events", "system", "namespace", "network", "volume"},
|
||
"docker": {"ps", "logs", "inspect", "stats", "version", "info", "images", "top"},
|
||
"df": None, "du": None, "free": None, "uptime": None, "uname": None, "hostname": None,
|
||
"ip": {"addr", "address", "route", "link", "neighbor", "neigh", "rule", "maddr", "monitor", "tunnel", "tuntap"},
|
||
"ss": None, "netstat": None, "lsblk": None, "ls": None,
|
||
"cat": None, "head": None, "tail": None, "wc": None, "grep": None, "egrep": None, "fgrep": None,
|
||
"ps": None, "pstree": None, "mount": None, "lsmod": None, "lscpu": None, "lsof": None,
|
||
"dmesg": None, "ping": None, "nslookup": None, "dig": None, "getent": None,
|
||
"timedatectl": None, "date": None, "stat": None, "file": None, "blkid": None,
|
||
"top": None, "htop": None, "iostat": None, "vmstat": None, "mpstat": None,
|
||
"pidstat": None, "sar": None, "nproc": None, "env": None, "which": None,
|
||
"readlink": None, "realpath": None, "find": None, "sort": None, "uniq": None,
|
||
"awk": None, "sed": None, "cut": None, "tr": None, "tee": None,
|
||
"basename": None, "dirname": None, "xargs": None, "bc": None, "expr": None,
|
||
"curl": None, "wget": None, "traceroute": None, "tracepath": None, "mtr": None,
|
||
"nc": None, "nmap": None, "telnet": None, "tcpdump": None, "ethtool": None,
|
||
"lspci": None, "lsusb": None, "dmidecode": None, "sysctl": None, "udevadm": None,
|
||
"echo": None, "printf": None, "cd": None, "pwd": None, "export": None,
|
||
"test": None, "[": None, "sleep": None, "source": None, ".": None,
|
||
"timeout": None, "nohup": None, "unset": None, "shift": None, "return": None,
|
||
"local": None, "readonly": None, "trap": None, "type": None, "command": None,
|
||
"set": None, "shopt": None, "eval": None, "exec": None, "alias": None,
|
||
"unalias": None, "declare": None, "typeset": None, "read": None, "mapfile": None,
|
||
}
|
||
|
||
# etcdctl 由 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", "defrag"}
|
||
# 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"},
|
||
}
|
||
# 系统修改命令白名单:需要人工审批
|
||
SHELL_WRITE_COMMANDS: dict[str, set[str] | None] = {
|
||
"systemctl": {"restart", "start", "stop", "reload", "enable", "disable", "daemon-reload"},
|
||
# 文件操作命令:任意参数都需审批
|
||
"rm": None,
|
||
"cp": None,
|
||
"mv": None,
|
||
"chmod": None,
|
||
"chown": None,
|
||
"mkdir": None,
|
||
"touch": None,
|
||
"ln": None,
|
||
}
|
||
|
||
# ip 命令的写操作动词 (出现即拒绝自动执行)
|
||
_IP_WRITE_ACTIONS = {"set", "add", "del", "delete", "change", "replace", "append", "prepend"}
|
||
|
||
|
||
|
||
def _first_non_flag(args: list[str]) -> str | None:
|
||
for arg in args:
|
||
if not arg.startswith("-"):
|
||
return arg.lower()
|
||
return None
|
||
|
||
|
||
def parse_shell_command(command: str) -> list[str]:
|
||
"""解析单条 shell 诊断命令,禁止 shell 操作符和命令组合。"""
|
||
parts = _parse_command(command)
|
||
if not parts:
|
||
raise ValueError("命令不能为空")
|
||
return parts
|
||
|
||
|
||
def classify_shell_command(command: str) -> CommandDecision:
|
||
"""对白名单内的系统诊断命令进行分类。
|
||
|
||
systemctl 同时出现在只读和写白名单中:restart/stop 等走写表(需审批),
|
||
status/is-active 等走只读表(自动执行)。因此先检查写表,再检查只读表。
|
||
|
||
含管道 (|) 或重定向 (>) 的命令:只检查管道前的 binary 是否在白名单内,
|
||
并设置 needs_shell=True 以便用 shell 方式执行。
|
||
"""
|
||
needs_shell = _has_shell_operators(command)
|
||
|
||
if needs_shell:
|
||
# 含 shell 操作符或为多行命令:先检查武器级危险命令
|
||
if _command_has_dangerous_cmds(command):
|
||
raise ValueError("命令包含不允许的危险命令(sh/bash/dd/mkfs 等),请手动执行")
|
||
|
||
# 多行或含操作符的命令:按行分割后,对每行逐段检查,取最高权限模式
|
||
lines = command.split("\n") if "\n" in command else [command]
|
||
overall_mode = "read"
|
||
overall_approval = False
|
||
first_binary = None
|
||
first_segment = ""
|
||
|
||
for line in lines:
|
||
line = line.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
segments = _split_into_commands(line)
|
||
for segment in segments:
|
||
# 检测 kubectl 命令
|
||
if segment.startswith("kubectl ") or segment == "kubectl":
|
||
try:
|
||
kdec = classify_kubectl_command(segment)
|
||
except (ValueError, IndexError) as exc:
|
||
raise ValueError(str(exc))
|
||
if first_binary is None:
|
||
first_binary = "kubectl"
|
||
first_segment = segment
|
||
if kdec.mode == "write":
|
||
overall_mode = "write"
|
||
overall_approval = True
|
||
continue
|
||
|
||
try:
|
||
seg_parts = shlex.split(segment)
|
||
except ValueError:
|
||
continue
|
||
if not seg_parts:
|
||
continue
|
||
# 跳过 VAR=value 赋值前缀(含 export)
|
||
idx = 0
|
||
while idx < len(seg_parts):
|
||
if "=" in seg_parts[idx] and not seg_parts[idx].startswith("-"):
|
||
idx += 1
|
||
elif seg_parts[idx] == "export":
|
||
idx += 1
|
||
else:
|
||
break
|
||
if idx >= len(seg_parts):
|
||
continue # 纯赋值段,跳过
|
||
binary = seg_parts[idx]
|
||
check_parts = seg_parts[idx:]
|
||
if first_binary is None:
|
||
first_binary = binary
|
||
|
||
if binary not in SHELL_READ_ONLY_COMMANDS and binary not in SHELL_WRITE_COMMANDS:
|
||
raise ValueError(f"不允许执行该命令: {binary}")
|
||
|
||
if binary in SHELL_WRITE_COMMANDS:
|
||
allowed = SHELL_WRITE_COMMANDS[binary]
|
||
if allowed is None:
|
||
overall_mode = "write"
|
||
overall_approval = True
|
||
else:
|
||
verb = _first_non_flag(check_parts[1:])
|
||
if verb is not None and verb in allowed:
|
||
overall_mode = "write"
|
||
overall_approval = True
|
||
elif binary in SHELL_READ_ONLY_COMMANDS:
|
||
ro_allowed = SHELL_READ_ONLY_COMMANDS[binary]
|
||
if ro_allowed is None or (verb is not None and verb in ro_allowed):
|
||
pass # 该段是只读,不改变 overall
|
||
else:
|
||
raise ValueError(f"命令 {binary} 不支持该子命令: {verb}")
|
||
else:
|
||
raise ValueError(f"命令 {binary} 不支持该子命令: {verb}")
|
||
|
||
if first_binary is None:
|
||
raise ValueError("命令不能为空")
|
||
|
||
return CommandDecision(overall_mode, overall_approval, first_binary, [command], needs_shell=True)
|
||
|
||
parts = parse_shell_command(command)
|
||
binary = parts[0]
|
||
# 支持 VAR=value command 语法
|
||
start_idx = 0
|
||
while start_idx < len(parts) and "=" in parts[start_idx] and not parts[start_idx].startswith("-"):
|
||
start_idx += 1
|
||
if start_idx > 0:
|
||
if start_idx < len(parts):
|
||
binary = parts[start_idx]
|
||
effective_parts = parts[start_idx:]
|
||
else:
|
||
# 纯赋值行如 LOG_DIR=/tmp/log — 放行
|
||
return CommandDecision("read", False, parts[0], parts[1:])
|
||
verb = _first_non_flag(effective_parts[1:])
|
||
else:
|
||
effective_parts = parts
|
||
verb = _first_non_flag(parts[1:])
|
||
|
||
if binary in SHELL_WRITE_COMMANDS:
|
||
allowed = SHELL_WRITE_COMMANDS[binary]
|
||
if allowed is None or (verb is not None and verb in allowed):
|
||
return CommandDecision("write", True, binary, effective_parts[1:])
|
||
|
||
# etcdctl: 单段式或两段式命令,verb/subverb 均跳过全局 flag (--endpoints 等)
|
||
# member/alarm/auth 等同时有只读和写子命令,需按 (verb, subverb) 组合精确判断
|
||
if binary == "etcdctl":
|
||
non_flags = [a for a in effective_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, effective_parts[1:])
|
||
if ev in _ETCDCTL_WRITE_VERBS:
|
||
return CommandDecision("write", True, ev, effective_parts[1:])
|
||
# 两段式: 先查写子命令 (精确匹配 subverb),再查只读子命令
|
||
if ev in _ETCDCTL_WRITE_SUBVERBS and esub in _ETCDCTL_WRITE_SUBVERBS[ev]:
|
||
return CommandDecision("write", True, ev, effective_parts[1:])
|
||
if ev in _ETCDCTL_READ_SUBVERBS:
|
||
if esub is None or esub in _ETCDCTL_READ_SUBVERBS[ev]:
|
||
return CommandDecision("read", False, ev, effective_parts[1:])
|
||
raise ValueError(f"etcdctl {ev} 不支持子命令: {esub}")
|
||
raise ValueError(f"etcdctl 不支持该子命令: {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):
|
||
raise ValueError(f"命令 {binary} 不支持该子命令: {verb}")
|
||
if binary == "ip" and any(arg in _IP_WRITE_ACTIONS for arg in effective_parts[1:]):
|
||
raise ValueError("ip 命令包含写操作,请使用只读形式如 'ip addr show'")
|
||
return CommandDecision("read", False, binary, effective_parts[1:])
|
||
|
||
raise ValueError(f"不允许执行该命令: {binary}")
|
||
|
||
|
||
def classify_agent_command(command: str) -> CommandDecision:
|
||
"""统一分类 Agent 命令:kubectl 或白名单系统诊断命令。"""
|
||
stripped = (command or "").strip()
|
||
# 检测 VAR=value kubectl 语法(有变量赋值时)
|
||
rest = stripped
|
||
while True:
|
||
try:
|
||
first = rest.split(None, 1)[0]
|
||
except IndexError:
|
||
break
|
||
if "=" in first and not first.startswith("-"):
|
||
idx = rest.index("=")
|
||
# 必须是合法变量赋值 VAR=value
|
||
parts_split = rest.split(None, 1)
|
||
if len(parts_split) < 2:
|
||
break
|
||
rest = parts_split[1]
|
||
elif first == "export":
|
||
parts_split = rest.split(None, 1)
|
||
if len(parts_split) < 2:
|
||
break
|
||
rest = parts_split[1]
|
||
else:
|
||
break
|
||
if rest.startswith("kubectl ") or rest == "kubectl":
|
||
return classify_kubectl_command(rest)
|
||
if stripped.startswith("kubectl ") or stripped == "kubectl":
|
||
return classify_kubectl_command(stripped)
|
||
return classify_shell_command(stripped)
|
||
|
||
|
||
# ── 命令执行 ──────────────────────────────────────────────
|
||
|
||
async def execute_command(command: str, timeout: int = 30) -> dict:
|
||
"""执行 Agent 命令 (kubectl 或白名单系统诊断命令)。
|
||
|
||
含管道/重定向的命令通过 shell=True 执行,其他用 subprocess_exec 直接执行。
|
||
"""
|
||
decision = classify_agent_command(command)
|
||
parts = _parse_command(command)
|
||
# 支持 VAR=value command 语法,跳过赋值找实际 binary
|
||
cmd_idx = 0
|
||
while cmd_idx < len(parts) and "=" in parts[cmd_idx] and not parts[cmd_idx].startswith("-"):
|
||
cmd_idx += 1
|
||
if cmd_idx > 0 and cmd_idx < len(parts):
|
||
parts = parts[cmd_idx:]
|
||
if parts[0] == "kubectl":
|
||
executable = KUBECTL
|
||
args = decision.args
|
||
else:
|
||
executable = parts[0]
|
||
args = parts[1:]
|
||
|
||
if decision.needs_shell:
|
||
proc = await asyncio.create_subprocess_shell(
|
||
command,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
)
|
||
else:
|
||
proc = await asyncio.create_subprocess_exec(
|
||
executable,
|
||
*args,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
)
|
||
try:
|
||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||
return {
|
||
"command": command,
|
||
"mode": decision.mode,
|
||
"returncode": proc.returncode,
|
||
"stdout": stdout.decode(errors="replace")[:20000].strip(),
|
||
"stderr": stderr.decode(errors="replace")[:10000].strip(),
|
||
}
|
||
except asyncio.TimeoutError:
|
||
proc.kill()
|
||
await proc.communicate()
|
||
return {
|
||
"command": command,
|
||
"mode": decision.mode,
|
||
"returncode": -1,
|
||
"stdout": "",
|
||
"stderr": f"命令执行超时 ({timeout}s)",
|
||
}
|
||
|
||
|
||
async def execute_kubectl(command: str, timeout: int = 30) -> dict:
|
||
"""执行单条 kubectl 命令 (兼容旧接口)。"""
|
||
decision = classify_kubectl_command(command)
|
||
proc = await asyncio.create_subprocess_exec(
|
||
KUBECTL,
|
||
*decision.args,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
)
|
||
try:
|
||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||
return {
|
||
"command": command,
|
||
"mode": decision.mode,
|
||
"returncode": proc.returncode,
|
||
"stdout": stdout.decode(errors="replace")[:20000].strip(),
|
||
"stderr": stderr.decode(errors="replace")[:10000].strip(),
|
||
}
|
||
except asyncio.TimeoutError:
|
||
proc.kill()
|
||
await proc.communicate()
|
||
return {
|
||
"command": command,
|
||
"mode": decision.mode,
|
||
"returncode": -1,
|
||
"stdout": "",
|
||
"stderr": f"命令执行超时 ({timeout}s)",
|
||
}
|