package stress import ( "fmt" "strings" "auto-check/pkg/sshclient" "golang.org/x/crypto/ssh" ) // ============================ // Docker 环境检测与特权执行(压测统一在容器中执行,工具预装在镜像里) // ============================ // DockerInfo Docker 运行环境信息 type DockerInfo struct { Version string // Docker 版本号(如 24.0.7) Available bool // 是否可用 } // IsDockerAvailable 检测远程 Docker 是否可用(飞牛 NAS 默认已安装)。 // 非 root 用户(如 fnOS admin)无 docker.sock 权限,需通过 sudo 执行。 func IsDockerAvailable(client *ssh.Client, sudoPwd string) DockerInfo { out, err := runPrivileged(client, "docker version --format '{{.Server.Version}}' 2>/dev/null", sudoPwd) if err != nil || strings.TrimSpace(out) == "" { return DockerInfo{Available: false} } return DockerInfo{ Version: strings.TrimSpace(out), Available: true, } } // runPrivileged 以特权执行命令:root 直接执行;非 root 用 sudo, // 若提供了 sudo 密码则通过 stdin 喂入(无需 NOPASSWD 配置)。 // fnOS 的 admin 用户无 docker.sock 权限,docker 命令均需走此路径。 func runPrivileged(client *ssh.Client, cmd, sudoPwd string) (string, error) { // 已是 root 则直接执行 if out, err := sshclient.RunCommand(client, "id -u"); err == nil && strings.TrimSpace(out) == "0" { return sshclient.RunCommand(client, cmd) } // 非 root:尝试 sudo if sudoPwd != "" { // echo 密码 | sudo -S 执行;整条命令用双引号包裹,避免命令内部单引号嵌套冲突。 // 仅对命令内的双引号转义(docker 命令均不含双引号,安全)。 quoted := strings.ReplaceAll(singleLine(cmd), `"`, `\"`) wrapped := fmt.Sprintf("echo %s | sudo -S -- sh -c \"%s\"", shellQuote(sudoPwd), quoted) return sshclient.RunCommand(client, wrapped) } // 无密码:先尝试免密 sudo (-n),失败则回退普通 sudo(会提示需要密码) quoted := strings.ReplaceAll(singleLine(cmd), `"`, `\"`) if out, err := sshclient.RunCommand(client, "sudo -n -- sh -c \""+quoted+"\""); err == nil { return out, nil } return sshclient.RunCommand(client, "sudo -- sh -c \""+quoted+"\"") } // shellQuote 转义单引号,避免密码破坏命令结构 func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } // singleLine 去掉换行,保证能塞进 sh -c '...' func singleLine(s string) string { return strings.ReplaceAll(strings.ReplaceAll(s, "\n", " "), "\r", "") } // truncate 截断过长的输出,便于日志展示 func truncate(s string, n int) string { // 去掉 ANSI/多余空行,只保留前 n 个字符 s = strings.TrimSpace(s) if len([]rune(s)) > n { return string([]rune(s)[:n]) + "...(truncated)" } return s }