This commit is contained in:
zw@118
2026-08-25 22:33:33 +08:00
parent 0a0e5fdf7f
commit 5e650491a2
4 changed files with 180 additions and 2 deletions

View File

@@ -31,6 +31,9 @@ type Runner struct {
tools ToolSet
metrics []Metric
installFailed []string // 自动安装仍失败的工具
useDocker bool // 是否使用 Docker 模式执行
dockerImage string // Docker 镜像名(如 registry/stress-tools:latest
sudoPwd string // sudo 密码(用于 docker 命令)
}
// NewRunner 创建执行器(检查/在线安装 → 构建指标 → 生成脚本 → 执行)
@@ -41,6 +44,31 @@ func NewRunner(cfg Config, types []TestType, client *ssh.Client, autoInstall boo
return &Runner{cfg: cfg, types: types, client: client, tools: tools, metrics: metrics, installFailed: failed}
}
// NewRunnerDocker 创建 Docker 模式执行器(工具预装在镜像中,无需在线安装)
// image 为 Docker 镜像名sudoPwd 用于非 root 用户执行 docker 命令。
func NewRunnerDocker(cfg Config, types []TestType, client *ssh.Client, image string, sudoPwd string) *Runner {
// Docker 模式下,镜像内已包含所有工具,直接标记全部可用。
// 这样 BuildMetrics 不会因缺少工具而禁用测试项。
tools := ToolSet{Tools: make(map[string]ToolInfo)}
for _, name := range requiredTools {
tools.Tools[name] = ToolInfo{Name: name, Path: "/usr/bin/" + name, Version: "docker"}
}
// 额外标记 lm-sensors、stressapptestDocker 镜像预装)
tools.Tools["lm-sensors"] = ToolInfo{Name: "lm-sensors", Path: "/usr/bin/sensors", Version: "docker"}
tools.Tools["stressapptest"] = ToolInfo{Name: "stressapptest", Path: "/usr/bin/stressapptest", Version: "docker"}
metrics := BuildMetrics(types, tools)
return &Runner{
cfg: cfg,
types: types,
client: client,
tools: tools,
metrics: metrics,
useDocker: true,
dockerImage: image,
sudoPwd: sudoPwd,
}
}
// Run 执行压力测试(生成脚本 → 远程执行 → 解析结果)
func (r *Runner) Run(ip string) *Report {
report := &Report{IP: ip, StartTime: time.Now()}
@@ -74,8 +102,14 @@ func (r *Runner) Run(ip string) *Report {
script := r.assembleScript()
fmt.Printf(" [脚本] 拼装完成,%d 字节\n", len(script))
// 通过 heredoc 传到远程执行
scriptCmd := fmt.Sprintf("cat <<'AUTOCHECKSCRIPT' > /tmp/auto-check.sh\n%s\nAUTOCHECKSCRIPT\nchmod +x /tmp/auto-check.sh && bash /tmp/auto-check.sh 2>&1", script)
// 通过 heredoc 传到远程执行Docker 模式或原生模式)
var scriptCmd string
if r.useDocker {
scriptCmd = r.buildDockerCmd(script)
fmt.Printf(" [模式] Docker 容器执行(镜像: %s\n", r.dockerImage)
} else {
scriptCmd = fmt.Sprintf("cat <<'AUTOCHECKSCRIPT' > /tmp/auto-check.sh\n%s\nAUTOCHECKSCRIPT\nchmod +x /tmp/auto-check.sh && bash /tmp/auto-check.sh 2>&1", script)
}
output, err := sshclient.RunCommand(r.client, scriptCmd)
if err != nil {
@@ -366,6 +400,39 @@ func (r *Runner) readScript(name string) []byte {
return data
}
// buildDockerCmd 构造 Docker 执行命令。
// 将脚本通过 heredoc 传入 docker run在容器内执行。
// 容器需要挂载 /sys温度传感器、/volume1飞牛存储卷等。
func (r *Runner) buildDockerCmd(script string) string {
// 构造 docker run 命令:
// --rm 容器结束后自动清理
// -i 交互式输入(通过 stdin 传入脚本)
// --privileged 允许访问硬件(温度传感器、内核日志)
// --pid=host 共享宿主机进程空间top 可见)
// -v /sys:/sys:ro 温度传感器(只读)
// -v /volume1:/volume1 飞牛存储卷(磁盘测试)
dockerCmd := fmt.Sprintf(
"docker run --rm -i --privileged --pid=host "+
"-v /sys:/sys:ro "+
"-v /volume1:/volume1 "+
"%s bash -s",
r.dockerImage,
)
// 通过 heredoc 将脚本传入容器执行(使用 sudo 以获取 docker 权限)
heredoc := fmt.Sprintf("cat <<'AUTOCHECKSCRIPT' | %s\n%s\nAUTOCHECKSCRIPT", dockerCmd, script)
// 如果需要 sudo非 root 用户)
runner := heredoc
if out, err := sshclient.RunCommand(r.client, "id -u"); err == nil && strings.TrimSpace(out) != "0" {
if r.sudoPwd != "" {
runner = fmt.Sprintf("echo '%s' | sudo -S bash -c %s", r.sudoPwd, shellQuote(heredoc))
} else {
runner = "sudo " + heredoc
}
}
return runner
}
// testPriority 测试执行顺序:单项测试在前,综合测试(full)最后,监控项最后
func testPriority(m Metric) int {
if m.IsMonitor {
@@ -432,6 +499,28 @@ func TestDevice(ip string, cfg config.Config) *Report {
stressCfg.TempLimit = cfg.Stress.TempLimit
stressCfg.UseMirror = cfg.Stress.UseMirror
// 检测是否使用 Docker 模式(配置启用且远程主机支持)
if cfg.Docker.Enabled {
dockerInfo := DetectDocker(conn, cfg.Docker.Image, sudoPwd)
if dockerInfo.Available {
if dockerInfo.HasImage {
fmt.Printf(" [Docker] 镜像 %s 已存在,使用容器模式\n", cfg.Docker.Image)
return NewRunnerDocker(stressCfg, ParseTypes(cfg.Stress.Types), conn, cfg.Docker.Image, sudoPwd).Run(ip)
}
// 镜像不存在,尝试拉取(如果失败则回退到原生模式)
fmt.Printf(" [Docker] 镜像 %s 不存在,尝试拉取...\n", cfg.Docker.Image)
if out, err := runPrivileged(conn, fmt.Sprintf("docker pull %s 2>&1", cfg.Docker.Image), sudoPwd); err == nil {
fmt.Printf(" [Docker] 镜像拉取成功\n")
return NewRunnerDocker(stressCfg, ParseTypes(cfg.Stress.Types), conn, cfg.Docker.Image, sudoPwd).Run(ip)
} else {
fmt.Printf(" [Docker] 镜像拉取失败,回退到原生模式: %v\n%s\n", err, truncate(out, 200))
}
} else {
fmt.Printf(" [Docker] 目标机未安装 Docker回退到原生模式\n")
}
}
// 原生模式:在线安装工具(回退路径)
return NewRunner(stressCfg, ParseTypes(cfg.Stress.Types), conn, cfg.Stress.AutoInstall, sudoPwd).Run(ip)
}

View File

@@ -63,6 +63,43 @@ echo OK`
}
}
// ============================
// Docker 容器化压测支持
// ============================
// DockerInfo 远程 Docker 环境信息
type DockerInfo struct {
Available bool // Docker 是否可用(已安装且有权限)
Image string // 镜像名(检测时传入,确认存在时回填)
HasImage bool // 目标镜像是否已存在本地(避免每次 pull
}
// DetectDocker 检测远程主机的 Docker 环境。
// 包括docker 命令是否存在、是否需要 sudo、镜像是否已存在。
// sudoPwd 为非 root 用户执行时的密码(用于 sudo docker
func DetectDocker(client *ssh.Client, image string, sudoPwd string) DockerInfo {
info := DockerInfo{Image: image}
// 1. 检测 docker 命令是否存在(支持 root 和 sudo
if out, err := runPrivileged(client, "command -v docker", sudoPwd); err != nil || strings.TrimSpace(out) == "" {
return info
}
info.Available = true
// 2. 检测 docker 服务是否运行docker info 成功即可)
if _, err := runPrivileged(client, "docker info >/dev/null 2>&1", sudoPwd); err != nil {
info.Available = false
return info
}
// 3. 检测目标镜像是否已存在本地(避免每次拉取)
if out, err := runPrivileged(client, fmt.Sprintf("docker image inspect %s >/dev/null 2>&1 && echo EXIST", image), sudoPwd); err == nil && strings.Contains(out, "EXIST") {
info.HasImage = true
}
return info
}
// ============================
// 远程工具检测
// ============================