diff --git a/auto-check/Dockerfile b/auto-check/Dockerfile new file mode 100644 index 0000000..5feef79 --- /dev/null +++ b/auto-check/Dockerfile @@ -0,0 +1,47 @@ +# stress-tools Dockerfile +# 用于 auto-check 压力测试的容器镜像 +# 预装所有压测工具,避免目标机在线安装依赖 +# +# 构建: docker build -t stress-tools:latest . +# 推送: docker push registry.example.com/stress-tools:latest +# 离线导入: docker save stress-tools:latest | gzip > stress-tools.tar.gz +# docker load < stress-tools.tar.gz + +FROM ubuntu:22.04 + +LABEL maintainer="auto-check" +LABEL description="压力测试工具集(stress-ng, fio, stressapptest, lm-sensors)" +LABEL version="1.0.0" + +# 避免交互式提示 +ENV DEBIAN_FRONTEND=noninteractive + +# 安装压测核心工具 +RUN apt-get update && apt-get install -y --no-install-recommends \ + # 压力测试 + stress-ng \ + stressapptest \ + # 磁盘测试 + fio \ + # 温度监控 + lm-sensors \ + # 系统信息 + sysstat \ + procps \ + coreutils \ + util-linux \ + # 脚本工具 + bash \ + awk \ + sed \ + grep \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# 初始化 sensors(加载内核模块) +RUN sensors --detect 2>/dev/null || true + +WORKDIR /workspace + +# 默认使用 bash(脚本通过 stdin 传入) +ENTRYPOINT ["/bin/bash"] diff --git a/auto-check/auto-check.yaml b/auto-check/auto-check.yaml index 9399c1f..3b9b1a6 100644 --- a/auto-check/auto-check.yaml +++ b/auto-check/auto-check.yaml @@ -30,6 +30,11 @@ stress: auto_install: true # 缺工具时在线安装(apt/dnf/yum 等),需要目标机可联网 use_mirror: false # 在线安装是否切换国内镜像源(清华TUNA)。国内网络卡顿/超时再改 true +# --- Docker 容器化压测 --- +docker: + enabled: true # 启用 Docker 模式(目标机需已安装 Docker,如飞牛系统) + image: "stress-tools:latest" # 镜像名,支持 registry/xxx:tag + # --- 工作流 --- workflow: interval: 180s diff --git a/auto-check/pkg/stress/runner.go b/auto-check/pkg/stress/runner.go index 80706f4..e2bcb50 100644 --- a/auto-check/pkg/stress/runner.go +++ b/auto-check/pkg/stress/runner.go @@ -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、stressapptest(Docker 镜像预装) + 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) } diff --git a/auto-check/pkg/stress/tools.go b/auto-check/pkg/stress/tools.go index f6cf578..d9b8d67 100644 --- a/auto-check/pkg/stress/tools.go +++ b/auto-check/pkg/stress/tools.go @@ -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 +} + // ============================ // 远程工具检测 // ============================