Merge branch 'master' of http://gitea.stonelan.cn/admin/membank
# Conflicts: # auto-check/pkg/stress/runner.go # auto-check/pkg/stress/tools.go
This commit is contained in:
3
auto-check/.gitignore
vendored
3
auto-check/.gitignore
vendored
@@ -3,6 +3,9 @@ reports/
|
|||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
# 导出的镜像文件(体积大,不入库;维护时重新 docker save 生成)
|
||||||
|
docker/stress-test/*.tar
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# 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"]
|
|
||||||
@@ -70,6 +70,52 @@ go build -o auto-check .
|
|||||||
| `memnative` | stressapptest | 内存稳定性精压(数据完整性校验) |
|
| `memnative` | stressapptest | 内存稳定性精压(数据完整性校验) |
|
||||||
| `full` | stress-ng | CPU+内存+磁盘 三合一综合压测 |
|
| `full` | stress-ng | CPU+内存+磁盘 三合一综合压测 |
|
||||||
|
|
||||||
|
## 压测执行方式(Docker 容器)
|
||||||
|
|
||||||
|
压测统一在 Docker 容器中执行:工具与测试脚本预装在镜像里,无需在目标机在线安装,环境一致性好(飞牛 NAS 默认已安装 Docker)。
|
||||||
|
|
||||||
|
### 架构设计
|
||||||
|
```
|
||||||
|
auto-check/
|
||||||
|
├── docker/ # Docker 镜像维护目录(独立)
|
||||||
|
│ └── stress-test/
|
||||||
|
│ ├── Dockerfile # 镜像定义
|
||||||
|
│ ├── scripts/ # 测试脚本(打包进镜像)
|
||||||
|
│ │ ├── run.sh # 入口脚本(环境变量驱动)
|
||||||
|
│ │ ├── cpu.sh # CPU 压测
|
||||||
|
│ │ ├── memory.sh # 内存压测
|
||||||
|
│ │ ├── disk.sh # 磁盘压测
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── README.md
|
||||||
|
└── pkg/stress/
|
||||||
|
└── runner.go # 核心逻辑:送镜像 + 驱动容器测试
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
1. 检测目标机 Docker 可用性(不可用则判失败)
|
||||||
|
2. 检查镜像是否存在(不存在则上传工程目录中的镜像 tar 并 `docker load`,不依赖 registry)
|
||||||
|
3. 启动容器执行测试(脚本在镜像内,环境变量传参)
|
||||||
|
4. 解析容器输出 → 生成报告
|
||||||
|
|
||||||
|
### 配置方式(在 `auto-check.yaml` 中)
|
||||||
|
```yaml
|
||||||
|
stress:
|
||||||
|
docker_image: "stress-test:latest" # 压测镜像(飞牛 NAS 默认支持 Docker)
|
||||||
|
docker_tar: "docker/stress-test/stress-test.tar" # 本地镜像文件,目标机无镜像时自动上传并 load
|
||||||
|
docker_args: "--network=host --privileged"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建镜像(维护时使用)
|
||||||
|
```bash
|
||||||
|
cd docker/stress-test
|
||||||
|
docker build -t stress-test:latest .
|
||||||
|
# 导出到工程目录,供测试时上传分发(不依赖 registry)
|
||||||
|
docker save -o stress-test.tar stress-test:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 详细文档
|
||||||
|
镜像维护说明请参考:[docker/stress-test/README.md](docker/stress-test/README.md)
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -80,11 +126,11 @@ auto-check/
|
|||||||
├── pkg/
|
├── pkg/
|
||||||
│ ├── discovery/ # 设备发现
|
│ ├── discovery/ # 设备发现
|
||||||
│ ├── sshclient/ # SSH 客户端
|
│ ├── sshclient/ # SSH 客户端
|
||||||
│ ├── stress/ # 压力测试
|
│ ├── stress/ # 压力测试(Docker 容器驱动)
|
||||||
│ │ ├── types.go # 类型定义
|
│ │ ├── types.go # 类型定义
|
||||||
│ │ ├── tools.go # 工具检测 + 监控
|
│ │ ├── tools.go # Docker 环境检测
|
||||||
│ │ ├── runners.go # 压测实现
|
│ │ ├── metrics.go # 指标定义 + 配置
|
||||||
│ │ └── runner.go # 调度器
|
│ │ └── runner.go # 调度器(送镜像 + 驱动容器)
|
||||||
│ └── report/ # 报告模块
|
│ └── report/ # 报告模块
|
||||||
├── go.mod
|
├── go.mod
|
||||||
└── README.md
|
└── README.md
|
||||||
|
|||||||
BIN
auto-check/auto-check
Normal file
BIN
auto-check/auto-check
Normal file
Binary file not shown.
@@ -27,13 +27,11 @@ stress:
|
|||||||
disk_dir: "" # 磁盘压测目录,空=自动临时目录
|
disk_dir: "" # 磁盘压测目录,空=自动临时目录
|
||||||
temp_interval: 10s # 温度采样间隔
|
temp_interval: 10s # 温度采样间隔
|
||||||
temp_limit: 90 # 温度上限(°C),超过判失败,0=不判定
|
temp_limit: 90 # 温度上限(°C),超过判失败,0=不判定
|
||||||
auto_install: true # 缺工具时在线安装(apt/dnf/yum 等),需要目标机可联网
|
# --- Docker 镜像(压测统一在容器中执行,工具与脚本预装在镜像里)---
|
||||||
use_mirror: false # 在线安装是否切换国内镜像源(清华TUNA)。国内网络卡顿/超时再改 true
|
# 固定分发模式:目标机无镜像时,上传本地导出的 tar 并 docker load(不依赖 registry)
|
||||||
|
docker_image: "stress-test:latest" # 压测镜像名,镜像维护见 docker/stress-test/
|
||||||
# --- Docker 容器化压测 ---
|
docker_tar: "docker/stress-test/stress-test.tar" # 本地镜像文件(docker save -o 导出)
|
||||||
docker:
|
docker_args: "--network=host --privileged" # Docker 运行参数,--privileged 可访问完整资源
|
||||||
enabled: true # 启用 Docker 模式(目标机需已安装 Docker,如飞牛系统)
|
|
||||||
image: "stress-tools:latest" # 镜像名,支持 registry/xxx:tag
|
|
||||||
|
|
||||||
# --- 工作流 ---
|
# --- 工作流 ---
|
||||||
workflow:
|
workflow:
|
||||||
|
|||||||
45
auto-check/docker/stress-test/Dockerfile
Normal file
45
auto-check/docker/stress-test/Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 压力测试工具镜像
|
||||||
|
#
|
||||||
|
# 用途:为 auto-check 提供预装压测工具的容器环境
|
||||||
|
# 优势:工具预装在镜像中,无需在线安装,环境一致性好,适合国内网络环境
|
||||||
|
#
|
||||||
|
# 工作流程:
|
||||||
|
# 1. auto-check 检测目标机器的 Docker 可用性
|
||||||
|
# 2. 从 Registry 拉取镜像(或本地已存在则跳过)
|
||||||
|
# 3. 启动容器执行测试(脚本已打包在镜像中)
|
||||||
|
# 4. 收集测试结果并解析
|
||||||
|
#
|
||||||
|
# 构建命令:
|
||||||
|
# cd docker/stress-test
|
||||||
|
# docker build -t your-registry/stress-test:latest .
|
||||||
|
# docker push your-registry/stress-test:latest
|
||||||
|
#
|
||||||
|
# 使用方式(在 auto-check.yaml 中配置):
|
||||||
|
# stress:
|
||||||
|
# use_docker: true
|
||||||
|
# docker_image: "your-registry/stress-test:latest"
|
||||||
|
|
||||||
|
FROM debian:12-slim
|
||||||
|
|
||||||
|
# 清理缓存,减少镜像体积
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
stress-ng \
|
||||||
|
fio \
|
||||||
|
lm-sensors \
|
||||||
|
sysstat \
|
||||||
|
procps \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 验证工具安装
|
||||||
|
RUN stress-ng --version && fio --version && sensors --version
|
||||||
|
|
||||||
|
# 复制测试脚本到镜像中
|
||||||
|
COPY scripts/ /scripts/
|
||||||
|
|
||||||
|
# 设置脚本执行权限
|
||||||
|
RUN chmod +x /scripts/*.sh
|
||||||
|
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
|
# 默认执行入口(由 auto-check 传入脚本参数)
|
||||||
|
ENTRYPOINT ["/bin/bash", "/scripts/run.sh"]
|
||||||
100
auto-check/docker/stress-test/README.md
Normal file
100
auto-check/docker/stress-test/README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# stress-test 镜像
|
||||||
|
|
||||||
|
压力测试镜像,为 auto-check 提供预装压测工具与测试脚本的容器环境。压测统一在容器内执行,无需在目标机在线安装任何依赖。
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ auto-check 工作流程 │
|
||||||
|
│ │
|
||||||
|
│ 1. 检测目标机器的 Docker 可用性 │
|
||||||
|
│ 2. 检查镜像是否存在(不存在则上传本地导出的 tar 并 docker load)│
|
||||||
|
│ 3. 启动容器执行测试(脚本已打包在镜像中) │
|
||||||
|
│ 4. 收集测试结果并解析 │
|
||||||
|
│ 5. 生成报告 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
| 特性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 工具获取 | 预装在镜像中,无需在线安装 |
|
||||||
|
| 网络依赖 | 镜像已缓存后目标机无需联网 |
|
||||||
|
| 环境一致性 | 统一的容器环境,不受目标机发行版差异影响 |
|
||||||
|
|
||||||
|
## 构建与导出(维护时使用)
|
||||||
|
|
||||||
|
固定分发模式:不依赖 registry,镜像以 tar 文件形式放在工程目录,
|
||||||
|
目标机无镜像时由 auto-check 自动上传并 `docker load`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docker/stress-test
|
||||||
|
docker build -t stress-test:latest .
|
||||||
|
# 导出到工程目录(供 auto-check 上传分发,已加入 .gitignore)
|
||||||
|
docker save -o stress-test.tar stress-test:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
在 `auto-check.yaml` 中配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
stress:
|
||||||
|
docker_image: "stress-test:latest"
|
||||||
|
docker_tar: "docker/stress-test/stress-test.tar"
|
||||||
|
docker_args: "--network=host --privileged"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
容器运行时通过以下环境变量传入参数:
|
||||||
|
|
||||||
|
| 变量 | 说明 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `AUTOCHECK_TYPES` | 测试类型(逗号分隔) | `cpu,memory` |
|
||||||
|
| `AUTOCHECK_DURATION` | 持续时间(秒) | `30` |
|
||||||
|
| `AUTOCHECK_THREADS` | 线程数 | `4` |
|
||||||
|
| `AUTOCHECK_MEM_SIZE_MB` | 内存压测大小 | `0` (自动) |
|
||||||
|
| `AUTOCHECK_DISK_SIZE_MB` | 磁盘压测大小 | `1024` |
|
||||||
|
| `AUTOCHECK_DISK_DIR` | 磁盘压测目录 | `/tmp` |
|
||||||
|
| `AUTOCHECK_TEMP_INTERVAL` | 温度采样间隔 | `10` |
|
||||||
|
| `AUTOCHECK_SAMPLE_INTERVAL` | 运行时采样间隔 | `2` |
|
||||||
|
|
||||||
|
## 容器内执行流程(run.sh)
|
||||||
|
|
||||||
|
1. 系统信息(`===SYSTEM_INFO===`)
|
||||||
|
2. 温度监控后台采样(`temp_start.sh`)
|
||||||
|
3. 各项压测:先单项(cpu/memory/disk/memnative)、综合(full)最后
|
||||||
|
4. 温度监控收尾(`===MONITOR:temp===`)
|
||||||
|
5. 内核日志(`===MONITOR:dmesg===`)+ 结束标记
|
||||||
|
|
||||||
|
## 镜像内容
|
||||||
|
|
||||||
|
```
|
||||||
|
/stress-test/
|
||||||
|
├── Dockerfile # 镜像定义
|
||||||
|
├── scripts/ # 测试脚本
|
||||||
|
│ ├── run.sh # 入口脚本
|
||||||
|
│ ├── cpu.sh # CPU 压测
|
||||||
|
│ ├── memory.sh # 内存压测
|
||||||
|
│ ├── disk.sh # 磁盘压测
|
||||||
|
│ ├── full.sh # 综合压测
|
||||||
|
│ ├── memnative.sh # 内存精压
|
||||||
|
│ ├── sysinfo.sh # 系统信息
|
||||||
|
│ ├── temp_start.sh # 温度监控启动
|
||||||
|
│ ├── temp_end.sh # 温度监控收尾
|
||||||
|
│ └── dmesg.sh # 内核日志
|
||||||
|
└── README.md # 本文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依赖工具
|
||||||
|
|
||||||
|
| 工具 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `stress-ng` | CPU/内存/磁盘压测 |
|
||||||
|
| `fio` | 磁盘 IO 测试 |
|
||||||
|
| `lm-sensors` | 温度监控 |
|
||||||
|
| `sysstat` | 系统性能统计 |
|
||||||
|
| `procps` | 进程监控 |
|
||||||
48
auto-check/docker/stress-test/scripts/run.sh
Normal file
48
auto-check/docker/stress-test/scripts/run.sh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 压力测试入口脚本(打包在镜像内)
|
||||||
|
# auto-check 启动容器时通过环境变量传参,执行完整测试流程:
|
||||||
|
# 系统信息 → 温度监控启动 → 各项压测(先单项、后综合)→ 温度监控收尾 → 内核日志
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 默认值(docker -e 传入时覆盖)
|
||||||
|
export AUTOCHECK_TYPES="${AUTOCHECK_TYPES:-cpu,memory}"
|
||||||
|
export AUTOCHECK_DURATION="${AUTOCHECK_DURATION:-30}"
|
||||||
|
export AUTOCHECK_THREADS="${AUTOCHECK_THREADS:-4}"
|
||||||
|
export AUTOCHECK_MEM_SIZE_MB="${AUTOCHECK_MEM_SIZE_MB:-0}"
|
||||||
|
export AUTOCHECK_DISK_SIZE_MB="${AUTOCHECK_DISK_SIZE_MB:-1024}"
|
||||||
|
export AUTOCHECK_TEMP_INTERVAL="${AUTOCHECK_TEMP_INTERVAL:-10}"
|
||||||
|
export AUTOCHECK_SAMPLE_INTERVAL="${AUTOCHECK_SAMPLE_INTERVAL:-2}"
|
||||||
|
if [ -n "$AUTOCHECK_DISK_DIR" ]; then export AUTOCHECK_DISK_DIR; fi
|
||||||
|
|
||||||
|
# 1. 系统信息
|
||||||
|
source /scripts/sysinfo.sh
|
||||||
|
|
||||||
|
# 2. 温度监控(后台周期采样)
|
||||||
|
source /scripts/temp_start.sh
|
||||||
|
|
||||||
|
# 未知类型输出 skip,保证报告完整
|
||||||
|
for T in $(echo "$AUTOCHECK_TYPES" | tr ',' ' '); do
|
||||||
|
case "$T" in
|
||||||
|
cpu|memory|disk|memnative|full) ;;
|
||||||
|
*)
|
||||||
|
echo "===TEST:$T==="
|
||||||
|
echo "status:skip"
|
||||||
|
echo "output:未知测试类型"
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. 按固定顺序执行请求的测试(先单项、综合 full 最后)
|
||||||
|
REQ_TYPES=",${AUTOCHECK_TYPES// /},"
|
||||||
|
for TYPE in cpu memory disk memnative full; do
|
||||||
|
case "$REQ_TYPES" in
|
||||||
|
*",$TYPE,"*) source "/scripts/$TYPE.sh" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# 4. 温度监控收尾
|
||||||
|
source /scripts/temp_end.sh
|
||||||
|
|
||||||
|
# 5. 内核日志 + 结束标记
|
||||||
|
source /scripts/dmesg.sh
|
||||||
@@ -11,6 +11,8 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
|
github.com/pkg/sftp v1.13.11 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||||
|
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ type SSHConfig struct {
|
|||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
KeyFile string `yaml:"key"`
|
KeyFile string `yaml:"key"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
SudoPwd string `yaml:"sudo_password"` // 非 root 用户安装工具时使用的 sudo 密码
|
SudoPwd string `yaml:"sudo_password"` // 非 root 用户执行特权命令时的 sudo 密码
|
||||||
}
|
}
|
||||||
|
|
||||||
// StressConfig 压力测试
|
// StressConfig 压力测试
|
||||||
@@ -51,8 +51,10 @@ type StressConfig struct {
|
|||||||
DiskDir string `yaml:"disk_dir"` // 磁盘压测目录,空=自动临时目录
|
DiskDir string `yaml:"disk_dir"` // 磁盘压测目录,空=自动临时目录
|
||||||
TempInterval time.Duration `yaml:"temp_interval"` // 温度采样间隔,0=默认10s
|
TempInterval time.Duration `yaml:"temp_interval"` // 温度采样间隔,0=默认10s
|
||||||
TempLimit float64 `yaml:"temp_limit"` // 温度上限(°C),0=不判定
|
TempLimit float64 `yaml:"temp_limit"` // 温度上限(°C),0=不判定
|
||||||
AutoInstall bool `yaml:"auto_install"` // 缺工具时尝试在线安装(apt/dnf/yum 等)
|
// 压测统一在 Docker 容器中执行(工具与脚本预装在镜像里)
|
||||||
UseMirror bool `yaml:"use_mirror"` // 在线安装时切换国内镜像源(清华 TUNA),国内网络卡顿/超时再开启
|
DockerImage string `yaml:"docker_image"` // 压测镜像名称,如 stress-test:latest
|
||||||
|
DockerTar string `yaml:"docker_tar"` // 本地导出的镜像文件(docker save),目标机无镜像时上传并 load
|
||||||
|
DockerArgs string `yaml:"docker_args"` // Docker 运行参数,如 "--network=host --privileged"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReportConfig 报告
|
// ReportConfig 报告
|
||||||
@@ -97,8 +99,9 @@ func Default() *Config {
|
|||||||
DiskSizeMB: 1024,
|
DiskSizeMB: 1024,
|
||||||
TempInterval: 10 * time.Second,
|
TempInterval: 10 * time.Second,
|
||||||
TempLimit: 90,
|
TempLimit: 90,
|
||||||
AutoInstall: true,
|
DockerImage: "stress-test:latest",
|
||||||
UseMirror: false,
|
DockerTar: "docker/stress-test/stress-test.tar",
|
||||||
|
DockerArgs: "--network=host --privileged",
|
||||||
},
|
},
|
||||||
Workflow: WorkflowConfig{
|
Workflow: WorkflowConfig{
|
||||||
Interval: 10 * time.Second,
|
Interval: 10 * time.Second,
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package sshclient
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/sftp"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -113,6 +116,45 @@ func EnsureHome(client *ssh.Client) {
|
|||||||
RunCommand(client, fmt.Sprintf("mkdir -p %s", home))
|
RunCommand(client, fmt.Sprintf("mkdir -p %s", home))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UploadFile 通过 SFTP 把本地文件上传到远端指定路径(复用已有 SSH 连接)
|
||||||
|
func UploadFile(client *ssh.Client, localPath, remotePath string) error {
|
||||||
|
src, err := os.Open(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开本地文件失败: %w", err)
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
info, err := src.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取文件信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sftpCli, err := sftp.NewClient(client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建 SFTP 会话失败: %w", err)
|
||||||
|
}
|
||||||
|
defer sftpCli.Close()
|
||||||
|
|
||||||
|
// 确保远端目录存在(/tmp 一般已存在,兜底处理)
|
||||||
|
if dir := path.Dir(remotePath); dir != "" && dir != "/" {
|
||||||
|
_ = sftpCli.MkdirAll(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
dst, err := sftpCli.Create(remotePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建远端文件失败: %w", err)
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
fmt.Printf(" [上传] %s -> %s (%.1f MB)...\n", localPath, remotePath, float64(info.Size())/1024/1024)
|
||||||
|
start := time.Now()
|
||||||
|
if _, err := io.Copy(dst, src); err != nil {
|
||||||
|
return fmt.Errorf("文件传输失败: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" [上传] 完成,耗时 %.1fs\n", time.Since(start).Seconds())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// buildSSHConfig 构建认证配置
|
// buildSSHConfig 构建认证配置
|
||||||
func (c *Config) buildSSHConfig() (*ssh.ClientConfig, error) {
|
func (c *Config) buildSSHConfig() (*ssh.ClientConfig, error) {
|
||||||
var authMethods []ssh.AuthMethod
|
var authMethods []ssh.AuthMethod
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stress
|
package stress
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,8 +17,10 @@ type Metric struct {
|
|||||||
DisableReason string // 禁用时展示的原因
|
DisableReason string // 禁用时展示的原因
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildMetrics 根据配置和可用工具,生成要执行的指标列表
|
// BuildMetrics 根据测试类型生成要执行的指标列表。
|
||||||
func BuildMetrics(types []TestType, tools ToolSet) []Metric {
|
// 压测工具全部预装在 Docker 镜像中(stress-ng/fio/stressapptest/lm-sensors),
|
||||||
|
// 无需在宿主机检测工具可用性。
|
||||||
|
func BuildMetrics(types []TestType) []Metric {
|
||||||
var metrics []Metric
|
var metrics []Metric
|
||||||
|
|
||||||
for _, t := range types {
|
for _, t := range types {
|
||||||
@@ -30,15 +31,7 @@ func BuildMetrics(types []TestType, tools ToolSet) []Metric {
|
|||||||
case TestMemory:
|
case TestMemory:
|
||||||
m.Tool = "stress-ng"
|
m.Tool = "stress-ng"
|
||||||
case TestDiskIO:
|
case TestDiskIO:
|
||||||
// disk 优先 fio;无 fio 可降级 stress-ng
|
|
||||||
switch {
|
|
||||||
case tools.Has("fio"):
|
|
||||||
m.Tool = "fio"
|
m.Tool = "fio"
|
||||||
case tools.Has("stress-ng"):
|
|
||||||
m.Tool = "stress-ng"
|
|
||||||
default:
|
|
||||||
m.Tool = "fio"
|
|
||||||
}
|
|
||||||
case TestMemNative:
|
case TestMemNative:
|
||||||
m.Tool = "stressapptest"
|
m.Tool = "stressapptest"
|
||||||
case TestFull:
|
case TestFull:
|
||||||
@@ -47,25 +40,11 @@ func BuildMetrics(types []TestType, tools ToolSet) []Metric {
|
|||||||
m.Enabled = false
|
m.Enabled = false
|
||||||
m.DisableReason = "未知测试类型"
|
m.DisableReason = "未知测试类型"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据工具可用性决定是否禁用,并记录原因
|
|
||||||
switch t {
|
|
||||||
case TestDiskIO:
|
|
||||||
if !tools.Has("fio") && !tools.Has("stress-ng") {
|
|
||||||
m.Enabled = false
|
|
||||||
m.DisableReason = "缺少 fio 或 stress-ng"
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if m.Tool != "" && !tools.Has(m.Tool) {
|
|
||||||
m.Enabled = false
|
|
||||||
m.DisableReason = fmt.Sprintf("缺少 %s", m.Tool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
metrics = append(metrics, m)
|
metrics = append(metrics, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动附加监控指标
|
// 自动附加监控指标(容器内执行)
|
||||||
metrics = append(metrics, Metric{Name: "temp", Tool: "lm-sensors", Enabled: tools.Has("lm-sensors"), IsMonitor: true})
|
metrics = append(metrics, Metric{Name: "temp", Tool: "lm-sensors", Enabled: true, IsMonitor: true})
|
||||||
metrics = append(metrics, Metric{Name: "dmesg", Tool: "", Enabled: true, IsMonitor: true})
|
metrics = append(metrics, Metric{Name: "dmesg", Tool: "", Enabled: true, IsMonitor: true})
|
||||||
|
|
||||||
return metrics
|
return metrics
|
||||||
@@ -77,6 +56,7 @@ func BuildMetrics(types []TestType, tools ToolSet) []Metric {
|
|||||||
|
|
||||||
// Config 压力测试配置
|
// Config 压力测试配置
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Types string // 测试类型(逗号分隔),如 "cpu,memory,disk"
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
Threads int
|
Threads int
|
||||||
MemSizeMB int // 0=自动(可用60%)
|
MemSizeMB int // 0=自动(可用60%)
|
||||||
@@ -84,7 +64,10 @@ type Config struct {
|
|||||||
DiskDir string // 空=自动临时目录
|
DiskDir string // 空=自动临时目录
|
||||||
TempLogInt time.Duration
|
TempLogInt time.Duration
|
||||||
TempLimit float64 // 温度上限(°C),0=不判定
|
TempLimit float64 // 温度上限(°C),0=不判定
|
||||||
UseMirror bool // 在线安装时切换国内镜像源(清华TUNA)
|
// 压测统一在 Docker 容器中执行(工具预装在镜像里)
|
||||||
|
DockerImage string // 压测镜像名称,如 stress-test:latest
|
||||||
|
DockerTar string // 本地导出的镜像文件(docker save),目标机无镜像时上传并 load
|
||||||
|
DockerArgs string // Docker 运行参数,如 "--network=host --privileged"
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig 默认配置
|
// DefaultConfig 默认配置
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -23,68 +22,48 @@ var Results = make(map[string]*Report)
|
|||||||
// 执行器
|
// 执行器
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
// Runner 压力测试执行器
|
// Runner 压力测试执行器(统一在 Docker 容器中执行,工具预装在镜像里)
|
||||||
type Runner struct {
|
type Runner struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
types []TestType
|
|
||||||
client *ssh.Client
|
client *ssh.Client
|
||||||
tools ToolSet
|
|
||||||
metrics []Metric
|
metrics []Metric
|
||||||
installFailed []string // 自动安装仍失败的工具
|
sudoPwd string // 非 root 用户(如 fnOS admin)执行 docker 命令时的 sudo 密码
|
||||||
useDocker bool // 是否使用 Docker 模式执行
|
dockerOK bool // 目标机 Docker 是否可用
|
||||||
dockerImage string // Docker 镜像名(如 registry/stress-tools:latest)
|
|
||||||
sudoPwd string // sudo 密码(用于 docker 命令)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRunner 创建执行器(检查/在线安装 → 构建指标 → 生成脚本 → 执行)
|
// NewRunner 创建执行器(检测 Docker → 确保镜像 → 启动容器执行测试)
|
||||||
func NewRunner(cfg Config, types []TestType, client *ssh.Client, autoInstall bool, sudoPwd string) *Runner {
|
func NewRunner(cfg Config, types []TestType, client *ssh.Client, sudoPwd string) *Runner {
|
||||||
// 1. 检测工具,缺失时按发行版在线安装(可配置国内镜像源加速)
|
dockerInfo := IsDockerAvailable(client, sudoPwd)
|
||||||
tools, failed := EnsureTools(client, requiredTools, autoInstall, sudoPwd, cfg.UseMirror)
|
if dockerInfo.Available {
|
||||||
metrics := BuildMetrics(types, tools)
|
fmt.Printf(" [Docker] 检测到 Docker 版本 %s,压测将在容器中执行\n", dockerInfo.Version)
|
||||||
return &Runner{cfg: cfg, types: types, client: client, tools: tools, metrics: metrics, installFailed: failed}
|
} else {
|
||||||
|
fmt.Printf(" [Docker] 目标机未检测到 Docker,无法执行压测(飞牛默认已安装,请检查 Docker 服务状态)\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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{
|
return &Runner{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
types: types,
|
|
||||||
client: client,
|
client: client,
|
||||||
tools: tools,
|
metrics: BuildMetrics(types),
|
||||||
metrics: metrics,
|
|
||||||
useDocker: true,
|
|
||||||
dockerImage: image,
|
|
||||||
sudoPwd: sudoPwd,
|
sudoPwd: sudoPwd,
|
||||||
|
dockerOK: dockerInfo.Available,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run 执行压力测试(生成脚本 → 远程执行 → 解析结果)
|
// Run 执行压力测试(启动容器 → 解析结果 → 生成报告)
|
||||||
func (r *Runner) Run(ip string) *Report {
|
func (r *Runner) Run(ip string) *Report {
|
||||||
report := &Report{IP: ip, StartTime: time.Now()}
|
report := &Report{IP: ip, StartTime: time.Now()}
|
||||||
fmt.Printf("\n════════ [%s] 压力测试开始 ════════\n", ip)
|
fmt.Printf("\n════════ [%s] 压力测试开始 ════════\n", ip)
|
||||||
|
|
||||||
// 显示可用工具
|
// Docker 是唯一执行模式,不可用时直接判失败
|
||||||
fmt.Printf(" 工具: ")
|
if !r.dockerOK {
|
||||||
for name, info := range r.tools.Tools {
|
report.AddResult(Result{Type: "docker", Status: StatusFail, Error: "目标机 Docker 不可用"})
|
||||||
fmt.Printf("%s(%s) ", name, info.Version)
|
report.EndTime = time.Now()
|
||||||
}
|
report.Duration = report.EndTime.Sub(report.StartTime)
|
||||||
fmt.Println()
|
return report
|
||||||
if len(r.installFailed) > 0 {
|
|
||||||
fmt.Printf(" [安装] 以下工具自动安装失败,相关测试将跳过: %v\n", r.installFailed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示将执行的指标及被跳过的原因
|
fmt.Printf(" [模式] Docker 容器 (%s)\n", r.cfg.DockerImage)
|
||||||
|
|
||||||
|
// 显示将执行的指标及被跳过的原因(未知类型)
|
||||||
fmt.Printf(" 指标: ")
|
fmt.Printf(" 指标: ")
|
||||||
for _, m := range r.metrics {
|
for _, m := range r.metrics {
|
||||||
if m.Enabled {
|
if m.Enabled {
|
||||||
@@ -98,25 +77,13 @@ func (r *Runner) Run(ip string) *Report {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按需读取各测试脚本,拼装成完整脚本
|
// 启动容器执行测试(脚本已打包在镜像中,环境变量传参)
|
||||||
script := r.assembleScript()
|
output, err := r.runInDocker()
|
||||||
fmt.Printf(" [脚本] 拼装完成,%d 字节\n", len(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 {
|
if err != nil {
|
||||||
fmt.Printf(" [执行] 脚本执行出错: %v\n", err)
|
fmt.Printf(" [执行] 容器执行出错: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析结果
|
// 解析容器输出(与宿主机模式输出格式相同)
|
||||||
if output != "" {
|
if output != "" {
|
||||||
r.parseResults(report, output)
|
r.parseResults(report, output)
|
||||||
}
|
}
|
||||||
@@ -310,148 +277,82 @@ func (r *Runner) parseResults(report *Report, output string) {
|
|||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// assembleScript 根据 metrics 读取对应的独立脚本文件,拼装成完整脚本
|
// ============================
|
||||||
func (r *Runner) assembleScript() string {
|
// Docker 容器执行(工具与脚本均预装在镜像中)
|
||||||
var sb strings.Builder
|
// ============================
|
||||||
|
|
||||||
// shebang + 环境变量导出
|
// runInDocker 在 Docker 容器中执行压测(工具与脚本预装在镜像中)
|
||||||
sb.WriteString("#!/bin/bash\n")
|
// 固定分发模式:确保目标机有镜像(无则上传本地 tar + docker load)→ 启动容器 → 解析结果
|
||||||
sb.WriteString("set -e\n\n")
|
func (r *Runner) runInDocker() (string, error) {
|
||||||
|
// 1. 确保目标机存在压测镜像(不存在则上传本地导出的 tar 并 load)
|
||||||
// 导出参数变量
|
if err := r.ensureImage(); err != nil {
|
||||||
fmt.Fprintf(&sb, "export AUTOCHECK_DURATION=%d\n", int(r.cfg.Duration.Seconds()))
|
return "", err
|
||||||
fmt.Fprintf(&sb, "export AUTOCHECK_THREADS=%d\n", r.cfg.Threads)
|
|
||||||
fmt.Fprintf(&sb, "export AUTOCHECK_DISK_SIZE_MB=%d\n", r.cfg.DiskSizeMB)
|
|
||||||
fmt.Fprintf(&sb, "export AUTOCHECK_MEM_SIZE_MB=%d\n", r.cfg.MemSizeMB)
|
|
||||||
fmt.Fprintf(&sb, "export AUTOCHECK_TEMP_INTERVAL=%d\n", int(r.cfg.TempLogInt.Seconds()))
|
|
||||||
sb.WriteString("export AUTOCHECK_SAMPLE_INTERVAL=2\n")
|
|
||||||
if r.cfg.DiskDir != "" {
|
|
||||||
fmt.Fprintf(&sb, "export AUTOCHECK_DISK_DIR=%s\n", r.cfg.DiskDir)
|
|
||||||
}
|
|
||||||
sb.WriteString("\n")
|
|
||||||
|
|
||||||
// 系统信息
|
|
||||||
sb.Write(r.readScript("sysinfo"))
|
|
||||||
sb.WriteString("\n")
|
|
||||||
|
|
||||||
// 温度监控启动
|
|
||||||
hasTemp := false
|
|
||||||
for _, m := range r.metrics {
|
|
||||||
if m.Name == "temp" && m.Enabled {
|
|
||||||
hasTemp = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasTemp {
|
|
||||||
sb.Write(r.readScript("temp_start"))
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按"先单项、后综合"排序测试脚本
|
// 2. 构建 Docker 运行命令(通过环境变量传入参数)
|
||||||
ordered := make([]Metric, len(r.metrics))
|
containerName := fmt.Sprintf("auto-check-%d", time.Now().UnixNano())
|
||||||
copy(ordered, r.metrics)
|
dockerRunCmd := fmt.Sprintf("docker run --rm --name %s "+
|
||||||
sort.SliceStable(ordered, func(i, j int) bool {
|
"-e AUTOCHECK_TYPES=%s "+
|
||||||
return testPriority(ordered[i]) < testPriority(ordered[j])
|
"-e AUTOCHECK_DURATION=%d "+
|
||||||
})
|
"-e AUTOCHECK_THREADS=%d "+
|
||||||
|
"-e AUTOCHECK_MEM_SIZE_MB=%d "+
|
||||||
|
"-e AUTOCHECK_DISK_SIZE_MB=%d "+
|
||||||
|
"-e AUTOCHECK_DISK_DIR=%s "+
|
||||||
|
"-e AUTOCHECK_TEMP_INTERVAL=%d "+
|
||||||
|
"-e AUTOCHECK_SAMPLE_INTERVAL=2 "+
|
||||||
|
"%s %s",
|
||||||
|
containerName,
|
||||||
|
r.cfg.Types,
|
||||||
|
int(r.cfg.Duration.Seconds()),
|
||||||
|
r.cfg.Threads,
|
||||||
|
r.cfg.MemSizeMB,
|
||||||
|
r.cfg.DiskSizeMB,
|
||||||
|
r.cfg.DiskDir,
|
||||||
|
int(r.cfg.TempLogInt.Seconds()),
|
||||||
|
r.cfg.DockerArgs,
|
||||||
|
r.cfg.DockerImage)
|
||||||
|
|
||||||
// 逐个测试脚本
|
fmt.Printf(" [Docker] 启动容器: %s\n", containerName)
|
||||||
for _, m := range ordered {
|
output, err := runPrivileged(r.client, dockerRunCmd, r.sudoPwd)
|
||||||
if m.IsMonitor {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !m.Enabled {
|
|
||||||
// 缺少依赖时仍输出一个 skip 结果,让报告完整展示原因
|
|
||||||
fmt.Fprintf(&sb, "echo '===TEST:%s==='\n", m.Name)
|
|
||||||
sb.WriteString("echo 'status:skip'\n")
|
|
||||||
fmt.Fprintf(&sb, "echo 'output:%s'\n", m.DisableReason)
|
|
||||||
sb.WriteString("echo ''\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
data := r.readScript(m.Name)
|
|
||||||
if data == nil {
|
|
||||||
fmt.Fprintf(&sb, "echo '===TEST:%s==='\n", m.Name)
|
|
||||||
sb.WriteString("echo 'status:skip'\n")
|
|
||||||
fmt.Fprintf(&sb, "echo 'output:脚本文件 scripts/%s.sh 不存在'\n", m.Name)
|
|
||||||
sb.WriteString("echo ''\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sb.Write(data)
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 温度监控收尾
|
|
||||||
if hasTemp {
|
|
||||||
sb.Write(r.readScript("temp_end"))
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// dmesg + 结束标记
|
|
||||||
sb.Write(r.readScript("dmesg"))
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// readScript 读取 scripts/ 目录下的脚本文件
|
|
||||||
func (r *Runner) readScript(name string) []byte {
|
|
||||||
data, err := os.ReadFile(fmt.Sprintf("scripts/%s.sh", name))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureImage 确保目标机存在压测镜像(固定分发模式,不依赖 registry):
|
||||||
|
// 镜像已存在则直接使用;否则把工程目录中导出的镜像 tar 通过 SFTP
|
||||||
|
// 上传到目标机,再 docker load 载入。
|
||||||
|
func (r *Runner) ensureImage() error {
|
||||||
|
imageExists, _ := runPrivileged(r.client, fmt.Sprintf("docker images %s --format '{{.Repository}}:{{.Tag}}'", r.cfg.DockerImage), r.sudoPwd)
|
||||||
|
if strings.TrimSpace(imageExists) != "" {
|
||||||
|
fmt.Printf(" [Docker] 镜像已存在: %s\n", r.cfg.DockerImage)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return data
|
|
||||||
|
// 本地导出的镜像文件(docker save -o xxx.tar <image>)
|
||||||
|
if r.cfg.DockerTar == "" {
|
||||||
|
return fmt.Errorf("目标机不存在镜像 %s 且未配置 docker_tar", r.cfg.DockerImage)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(r.cfg.DockerTar); err != nil {
|
||||||
|
return fmt.Errorf("本地镜像文件不存在: %s(请先执行 docker save -o %s %s)",
|
||||||
|
r.cfg.DockerTar, r.cfg.DockerTar, r.cfg.DockerImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildDockerCmd 构造 Docker 执行命令。
|
fmt.Printf(" [Docker] 目标机镜像不存在,开始上传...\n")
|
||||||
// 将脚本通过 heredoc 传入 docker run,在容器内执行。
|
remoteTar := "/tmp/auto-check-image.tar"
|
||||||
// 容器需要挂载 /sys(温度传感器)、/volume1(飞牛存储卷)等。
|
if err := sshclient.UploadFile(r.client, r.cfg.DockerTar, remoteTar); err != nil {
|
||||||
func (r *Runner) buildDockerCmd(script string) string {
|
return fmt.Errorf("镜像上传失败: %w", err)
|
||||||
// 构造 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)最后,监控项最后
|
loadOut, err := runPrivileged(r.client, fmt.Sprintf("docker load -i %s 2>&1", remoteTar), r.sudoPwd)
|
||||||
func testPriority(m Metric) int {
|
sshclient.RunCommand(r.client, "rm -f "+remoteTar) // 清理远端临时 tar(忽略失败)
|
||||||
if m.IsMonitor {
|
if err != nil || strings.Contains(loadOut, "Error") {
|
||||||
return 1000
|
return fmt.Errorf("docker load 失败: %s", truncate(loadOut, 300))
|
||||||
}
|
|
||||||
switch TestType(m.Name) {
|
|
||||||
case TestCPU:
|
|
||||||
return 0
|
|
||||||
case TestMemory:
|
|
||||||
return 1
|
|
||||||
case TestDiskIO:
|
|
||||||
return 2
|
|
||||||
case TestMemNative:
|
|
||||||
return 3
|
|
||||||
case TestFull:
|
|
||||||
return 100
|
|
||||||
default:
|
|
||||||
return 50
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf(" [Docker] 镜像载入成功: %s\n", r.cfg.DockerImage)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@@ -490,6 +391,7 @@ func TestDevice(ip string, cfg config.Config) *Report {
|
|||||||
|
|
||||||
// 由默认配置 + yaml 映射构造(避免硬编码,便于后续扩展字段)
|
// 由默认配置 + yaml 映射构造(避免硬编码,便于后续扩展字段)
|
||||||
stressCfg := DefaultConfig()
|
stressCfg := DefaultConfig()
|
||||||
|
stressCfg.Types = cfg.Stress.Types
|
||||||
stressCfg.Duration = cfg.Stress.Duration
|
stressCfg.Duration = cfg.Stress.Duration
|
||||||
stressCfg.Threads = cfg.Stress.Threads
|
stressCfg.Threads = cfg.Stress.Threads
|
||||||
stressCfg.MemSizeMB = cfg.Stress.MemSizeMB
|
stressCfg.MemSizeMB = cfg.Stress.MemSizeMB
|
||||||
@@ -497,31 +399,12 @@ func TestDevice(ip string, cfg config.Config) *Report {
|
|||||||
stressCfg.DiskDir = cfg.Stress.DiskDir
|
stressCfg.DiskDir = cfg.Stress.DiskDir
|
||||||
stressCfg.TempLogInt = cfg.Stress.TempInterval
|
stressCfg.TempLogInt = cfg.Stress.TempInterval
|
||||||
stressCfg.TempLimit = cfg.Stress.TempLimit
|
stressCfg.TempLimit = cfg.Stress.TempLimit
|
||||||
stressCfg.UseMirror = cfg.Stress.UseMirror
|
// Docker 镜像配置(工具与脚本预装在镜像中,本地导出的 tar 上传到目标机后 load)
|
||||||
|
stressCfg.DockerImage = cfg.Stress.DockerImage
|
||||||
|
stressCfg.DockerTar = cfg.Stress.DockerTar
|
||||||
|
stressCfg.DockerArgs = cfg.Stress.DockerArgs
|
||||||
|
|
||||||
// 检测是否使用 Docker 模式(配置启用且远程主机支持)
|
return NewRunner(stressCfg, ParseTypes(cfg.Stress.Types), conn, sudoPwd).Run(ip)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSSHFailReport 构造 SSH 连接失败报告
|
// NewSSHFailReport 构造 SSH 连接失败报告
|
||||||
|
|||||||
@@ -10,199 +10,31 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// 国内镜像源
|
// Docker 环境检测与特权执行(压测统一在容器中执行,工具预装在镜像里)
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
// setupMirror 在安装前把对应包管理器的源切换为国内镜像(清华 TUNA)。
|
// DockerInfo Docker 运行环境信息
|
||||||
// useMirror=false 时直接跳过(先用系统自带源试装)。失败不致命,仅提示并继续用原源。
|
|
||||||
func setupMirror(client *ssh.Client, pm string, sudoPwd string, useMirror bool) {
|
|
||||||
if !useMirror {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const label = "清华 TUNA 镜像"
|
|
||||||
switch pm {
|
|
||||||
case "apt-get":
|
|
||||||
// 备份并替换为清华镜像(Debian/Ubuntu 通用,按 lsb_release 选版本)
|
|
||||||
script := `set -e
|
|
||||||
OS_ID=$(. /etc/os-release 2>/dev/null; echo "$ID")
|
|
||||||
OS_VER=$(. /etc/os-release 2>/dev/null; echo "$VERSION_CODENAME")
|
|
||||||
[ -z "$OS_VER" ] && OS_VER=$(. /etc/os-release 2>/dev/null; echo "$VERSION_ID")
|
|
||||||
MIRROR="https://mirrors.tuna.tsinghua.edu.cn"
|
|
||||||
if [ "$OS_ID" = "ubuntu" ]; then
|
|
||||||
LIST="$MIRROR/ubuntu"
|
|
||||||
elif [ "$OS_ID" = "debian" ]; then
|
|
||||||
LIST="$MIRROR/debian"
|
|
||||||
elif [ "$OS_ID" = "raspbian" ]; then
|
|
||||||
LIST="$MIRROR/raspbian"
|
|
||||||
else
|
|
||||||
LIST="$MIRROR/debian"
|
|
||||||
fi
|
|
||||||
BAK=/etc/apt/sources.list.d/auto-check-mirror.bak
|
|
||||||
if [ ! -f "$BAK" ]; then
|
|
||||||
cp /etc/apt/sources.list "$BAK" 2>/dev/null || true
|
|
||||||
cp -r /etc/apt/sources.list.d "$BAK.d" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
: > /etc/apt/sources.list
|
|
||||||
echo "deb $LIST $OS_VER main contrib non-free" > /etc/apt/sources.list
|
|
||||||
echo "deb $LIST $OS_VER-updates main contrib non-free" >> /etc/apt/sources.list
|
|
||||||
echo "deb $LIST $OS_VER-backports main contrib non-free" >> /etc/apt/sources.list
|
|
||||||
echo "deb $LIST-security $OS_VER-security main contrib non-free" >> /etc/apt/sources.list
|
|
||||||
echo OK`
|
|
||||||
out, err := runPrivileged(client, "sh -c "+shellQuote(script), sudoPwd)
|
|
||||||
if err != nil || !strings.Contains(out, "OK") {
|
|
||||||
fmt.Printf(" [镜像] apt 切换国内源失败,将使用官方源: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" [镜像] apt 已切换至%s\n", label)
|
|
||||||
}
|
|
||||||
case "apk":
|
|
||||||
runPrivileged(client, "sed -i 's#https\\?://[^/]*alpinelinux.org#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories", sudoPwd)
|
|
||||||
fmt.Printf(" [镜像] apk 已切换至%s\n", label)
|
|
||||||
default:
|
|
||||||
// dnf/yum 等无通用一键切源命令,需手动配置 repo 文件,这里仅提示
|
|
||||||
fmt.Printf(" [镜像] %s 无内置切源逻辑,建议使用系统默认源或手动配置%s repo\n", pm, label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================
|
|
||||||
// Docker 容器化压测支持
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
// DockerInfo 远程 Docker 环境信息
|
|
||||||
type DockerInfo struct {
|
type DockerInfo struct {
|
||||||
Available bool // Docker 是否可用(已安装且有权限)
|
Version string // Docker 版本号(如 24.0.7)
|
||||||
Image string // 镜像名(检测时传入,确认存在时回填)
|
Available bool // 是否可用
|
||||||
HasImage bool // 目标镜像是否已存在本地(避免每次 pull)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetectDocker 检测远程主机的 Docker 环境。
|
// IsDockerAvailable 检测远程 Docker 是否可用(飞牛 NAS 默认已安装)。
|
||||||
// 包括:docker 命令是否存在、是否需要 sudo、镜像是否已存在。
|
// 非 root 用户(如 fnOS admin)无 docker.sock 权限,需通过 sudo 执行。
|
||||||
// sudoPwd 为非 root 用户执行时的密码(用于 sudo docker)。
|
func IsDockerAvailable(client *ssh.Client, sudoPwd string) DockerInfo {
|
||||||
func DetectDocker(client *ssh.Client, image string, sudoPwd string) DockerInfo {
|
out, err := runPrivileged(client, "docker version --format '{{.Server.Version}}' 2>/dev/null", sudoPwd)
|
||||||
info := DockerInfo{Image: image}
|
if err != nil || strings.TrimSpace(out) == "" {
|
||||||
|
return DockerInfo{Available: false}
|
||||||
// 1. 检测 docker 命令是否存在(支持 root 和 sudo)
|
|
||||||
if out, err := runPrivileged(client, "command -v docker", sudoPwd); err != nil || strings.TrimSpace(out) == "" {
|
|
||||||
return info
|
|
||||||
}
|
}
|
||||||
info.Available = true
|
return DockerInfo{
|
||||||
|
Version: strings.TrimSpace(out),
|
||||||
// 2. 检测 docker 服务是否运行(docker info 成功即可)
|
Available: true,
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================
|
|
||||||
// 远程工具检测
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
// requiredTools 压测核心依赖工具(缺则按需在线安装)。lm-sensors 依赖较多,
|
|
||||||
// 仅单文件二进制 stress-ng 与 fio 为必需;sensors 缺失仅影响温度曲线,不阻断压测。
|
|
||||||
// 这是工具检测的唯一来源,DetectTools/EnsureTools 都从这里取列表,避免两处不一致。
|
|
||||||
var requiredTools = []string{"stress-ng", "fio"}
|
|
||||||
|
|
||||||
// ToolInfo 远程工具信息
|
|
||||||
type ToolInfo struct {
|
|
||||||
Name string
|
|
||||||
Path string
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolSet 远程可用工具集合
|
|
||||||
type ToolSet struct {
|
|
||||||
Tools map[string]ToolInfo // name → info
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has 工具是否可用
|
|
||||||
func (ts ToolSet) Has(name string) bool {
|
|
||||||
_, ok := ts.Tools[name]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取工具信息
|
|
||||||
func (ts ToolSet) Get(name string) (ToolInfo, bool) {
|
|
||||||
t, ok := ts.Tools[name]
|
|
||||||
return t, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectTools 检测远程所有相关工具是否可用
|
|
||||||
func DetectTools(client *ssh.Client) ToolSet {
|
|
||||||
ts := ToolSet{Tools: make(map[string]ToolInfo)}
|
|
||||||
for _, name := range requiredTools {
|
|
||||||
if info, ok := detectOne(client, name); ok {
|
|
||||||
ts.Tools[name] = info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ts
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnsureTools 检测工具;缺失且 autoInstall=true 时按发行版自动安装。
|
|
||||||
// sudoPwd 为非 root 用户执行安装时提供(为空则尝试免密 sudo)。useMirror=true
|
|
||||||
// 时安装前把包管理器源切换为国内镜像(清华 TUNA)。返回安装后的工具集合,
|
|
||||||
// 以及最终仍缺失的工具名列表。
|
|
||||||
func EnsureTools(client *ssh.Client, names []string, autoInstall bool, sudoPwd string, useMirror bool) (ToolSet, []string) {
|
|
||||||
ts := DetectTools(client)
|
|
||||||
var missing []string
|
|
||||||
for _, n := range names {
|
|
||||||
if !ts.Has(n) {
|
|
||||||
missing = append(missing, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(missing) == 0 {
|
|
||||||
return ts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var failed []string
|
|
||||||
if autoInstall {
|
|
||||||
pm := detectPkgManager(client)
|
|
||||||
if pm == "" {
|
|
||||||
return ts, missing // 无法识别包管理器,跳过安装
|
|
||||||
}
|
|
||||||
// 安装前先把源切到国内镜像(使用官方源在网络差的环境会超时)
|
|
||||||
setupMirror(client, pm, sudoPwd, useMirror)
|
|
||||||
// 仅 apt 系需要先刷新元数据
|
|
||||||
if pm == "apt-get" {
|
|
||||||
if out, err := runPrivileged(client, "timeout 180 apt-get update 2>&1", sudoPwd); err != nil || strings.Contains(out, "Err") || strings.Contains(out, "Failed") {
|
|
||||||
fmt.Printf(" [安装] apt-get update 异常(可能无可用源/无权限): %v\n%s\n", err, truncate(out, 400))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, n := range missing {
|
|
||||||
pkg := pkgName(pm, n)
|
|
||||||
if pkg == "" {
|
|
||||||
failed = append(failed, n)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
installCmd := pkgInstallCmd(pm, pkg)
|
|
||||||
// runPrivileged 统一处理 sudo 与引号,这里直接给裸安装命令(带 timeout 兜底)
|
|
||||||
out, err := runPrivileged(client, fmt.Sprintf("timeout 300 %s 2>&1", installCmd), sudoPwd)
|
|
||||||
if err != nil || !strings.Contains(out, "Setting up "+pkg) && !strings.Contains(out, "已安装") && !strings.Contains(out, "already newest") {
|
|
||||||
fmt.Printf(" [安装] %s 安装失败: %v\n%s\n", pkg, err, truncate(out, 500))
|
|
||||||
}
|
|
||||||
// 安装后重新检测该工具是否到位
|
|
||||||
if info, ok := detectOne(client, n); ok {
|
|
||||||
ts.Tools[n] = info
|
|
||||||
} else {
|
|
||||||
failed = append(failed, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(failed) == 0 {
|
|
||||||
return ts, nil
|
|
||||||
}
|
|
||||||
return ts, failed
|
|
||||||
}
|
|
||||||
return ts, missing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// runPrivileged 以特权执行命令:root 直接执行;非 root 用 sudo,
|
// runPrivileged 以特权执行命令:root 直接执行;非 root 用 sudo,
|
||||||
// 若提供了 sudo 密码则通过 stdin 喂入(无需 NOPASSWD 配置)。
|
// 若提供了 sudo 密码则通过 stdin 喂入(无需 NOPASSWD 配置)。
|
||||||
|
// fnOS 的 admin 用户无 docker.sock 权限,docker 命令均需走此路径。
|
||||||
func runPrivileged(client *ssh.Client, cmd, sudoPwd string) (string, error) {
|
func runPrivileged(client *ssh.Client, cmd, sudoPwd string) (string, error) {
|
||||||
// 已是 root 则直接执行
|
// 已是 root 则直接执行
|
||||||
if out, err := sshclient.RunCommand(client, "id -u"); err == nil && strings.TrimSpace(out) == "0" {
|
if out, err := sshclient.RunCommand(client, "id -u"); err == nil && strings.TrimSpace(out) == "0" {
|
||||||
@@ -211,7 +43,7 @@ func runPrivileged(client *ssh.Client, cmd, sudoPwd string) (string, error) {
|
|||||||
// 非 root:尝试 sudo
|
// 非 root:尝试 sudo
|
||||||
if sudoPwd != "" {
|
if sudoPwd != "" {
|
||||||
// echo 密码 | sudo -S 执行;整条命令用双引号包裹,避免命令内部单引号嵌套冲突。
|
// echo 密码 | sudo -S 执行;整条命令用双引号包裹,避免命令内部单引号嵌套冲突。
|
||||||
// 仅对命令内的双引号转义(安装命令均不含双引号,安全)。
|
// 仅对命令内的双引号转义(docker 命令均不含双引号,安全)。
|
||||||
quoted := strings.ReplaceAll(singleLine(cmd), `"`, `\"`)
|
quoted := strings.ReplaceAll(singleLine(cmd), `"`, `\"`)
|
||||||
wrapped := fmt.Sprintf("echo %s | sudo -S -- sh -c \"%s\"", shellQuote(sudoPwd), quoted)
|
wrapped := fmt.Sprintf("echo %s | sudo -S -- sh -c \"%s\"", shellQuote(sudoPwd), quoted)
|
||||||
return sshclient.RunCommand(client, wrapped)
|
return sshclient.RunCommand(client, wrapped)
|
||||||
@@ -243,77 +75,3 @@ func truncate(s string, n int) string {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectPkgManager 识别远程发行版的包管理器
|
|
||||||
func detectPkgManager(client *ssh.Client) string {
|
|
||||||
for _, pm := range []string{"apt-get", "dnf", "yum", "apk", "zypper"} {
|
|
||||||
if _, err := sshclient.RunCommand(client, fmt.Sprintf("command -v %s", pm)); err == nil {
|
|
||||||
return pm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkgName 工具名 → 对应发行版的包名
|
|
||||||
func pkgName(pm, tool string) string {
|
|
||||||
m := map[string]string{
|
|
||||||
"stress-ng": "stress-ng",
|
|
||||||
"fio": "fio",
|
|
||||||
"lm-sensors": "lm-sensors",
|
|
||||||
"stressapptest": "stressapptest",
|
|
||||||
"iperf3": "iperf3",
|
|
||||||
}
|
|
||||||
if pm == "dnf" || pm == "yum" {
|
|
||||||
// RHEL 系 lm-sensors 包名为 lm_sensors
|
|
||||||
if tool == "lm-sensors" {
|
|
||||||
return "lm_sensors"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pm == "zypper" {
|
|
||||||
if tool == "lm-sensors" {
|
|
||||||
return "sensors"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := m[tool]; ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return tool
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkgInstallCmd 生成安装命令
|
|
||||||
func pkgInstallCmd(pm, pkg string) string {
|
|
||||||
switch pm {
|
|
||||||
case "apt-get":
|
|
||||||
return fmt.Sprintf("apt-get install -y %s", pkg)
|
|
||||||
case "dnf":
|
|
||||||
return fmt.Sprintf("dnf install -y %s", pkg)
|
|
||||||
case "yum":
|
|
||||||
return fmt.Sprintf("yum install -y %s", pkg)
|
|
||||||
case "apk":
|
|
||||||
return fmt.Sprintf("apk add %s", pkg)
|
|
||||||
case "zypper":
|
|
||||||
return fmt.Sprintf("zypper install -y %s", pkg)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// detectOne 检测单个工具(查系统 PATH;缺失由 EnsureTools 在线安装补全)
|
|
||||||
func detectOne(client *ssh.Client, name string) (ToolInfo, bool) {
|
|
||||||
path := ""
|
|
||||||
if out, err := sshclient.RunCommand(client, "which "+name+" 2>/dev/null"); err == nil {
|
|
||||||
path = strings.TrimSpace(out)
|
|
||||||
}
|
|
||||||
if path == "" {
|
|
||||||
return ToolInfo{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取版本:lm-sensors 仅支持 -v,其余工具均支持 --version
|
|
||||||
verFlag := "--version"
|
|
||||||
if name == "lm-sensors" {
|
|
||||||
verFlag = "-v"
|
|
||||||
}
|
|
||||||
v, _ := sshclient.RunCommand(client, path+" "+verFlag+" 2>&1 | head -1")
|
|
||||||
version := strings.TrimSpace(v)
|
|
||||||
|
|
||||||
return ToolInfo{Name: name, Path: path, Version: version}, true
|
|
||||||
}
|
|
||||||
|
|||||||
44
auto-check/verify_dispatch_main.go
Normal file
44
auto-check/verify_dispatch_main.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
// 临时验证程序:验证 镜像上传 + docker load 分发链路(用完即删)
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"auto-check/pkg/sshclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ip := os.Args[1]
|
||||||
|
cfg := sshclient.NewConfig("admin", "admin123", "", 22, 10*time.Second)
|
||||||
|
cli, err := sshclient.Connect(ip, cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("SSH 连接失败:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
sshclient.EnsureHome(cli)
|
||||||
|
|
||||||
|
v, _ := sshclient.RunCommand(cli, "echo 'admin123' | sudo -S -- docker version --format '{{.Server.Version}}' 2>/dev/null")
|
||||||
|
fmt.Printf("Docker 版本: %q\n", strings.TrimSpace(v))
|
||||||
|
|
||||||
|
exists, _ := sshclient.RunCommand(cli, "echo 'admin123' | sudo -S -- docker images stress-test:latest --format '{{.Repository}}:{{.Tag}}' 2>/dev/null")
|
||||||
|
fmt.Printf("镜像状态: %q\n", strings.TrimSpace(exists))
|
||||||
|
|
||||||
|
if strings.TrimSpace(exists) == "" {
|
||||||
|
if err := sshclient.UploadFile(cli, "docker/stress-test/stress-test.tar", "/tmp/auto-check-image.tar"); err != nil {
|
||||||
|
fmt.Println("上传失败:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
out, err := sshclient.RunCommand(cli, "echo 'admin123' | sudo -S -- docker load -i /tmp/auto-check-image.tar 2>&1")
|
||||||
|
sshclient.RunCommand(cli, "rm -f /tmp/auto-check-image.tar")
|
||||||
|
fmt.Printf("load 输出: %s (err=%v)\n", out, err)
|
||||||
|
exists, _ = sshclient.RunCommand(cli, "echo 'admin123' | sudo -S -- docker images stress-test:latest --format '{{.Repository}}:{{.Tag}}' 2>/dev/null")
|
||||||
|
fmt.Printf("load 后镜像状态: %q\n", strings.TrimSpace(exists))
|
||||||
|
}
|
||||||
|
fmt.Println("验证完成")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user