feat: 构建节点磁盘性能测试(dd 实测读写)
- 新增 internal/sshexec(x/crypto/ssh):远程命令通道,凭证来自 Credential(type=ssh) 解密(私钥/密码/端口/用户) - POST /api/build-nodes/:id/disk-test:写 dd(256MB, oflag=direct, 不支持自动降级含缓存并标注) + 读 dd + 清理;IP 为本机时直接本地 exec;输出解析兼容 Linux/Mac 两种 dd 格式;60s 超时保护 - BuildNode 新增 credentialId 关联 SSH 凭证 + 磁盘测速结果持久字段 (显式 column 标签修正 GORM MBps 命名) - 节点卡片新增仪表盘按钮触发测试;磁盘行显示 ↑写 ↓读 MB/s (>200 绿 SSD 级 / 50-200 黄 / <50 红),hover 看测试模式与时间 - 编辑弹窗新增 SSH 凭证下拉 - 本机实测:local-mac 节点 写 2336 MB/s · 读 1793 MB/s(direct)
This commit is contained in:
1 parent
6841c1104a
commit
555934cc64
8 files changed
+314
-2
No files matched your search
@@ -0,0 +1,76 @@
|
||||
// Package sshexec 远程命令执行通道(构建节点 SSH 操作)。
|
||||
package sshexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Config SSH 连接参数(来自 SSH 凭证内容 JSON)
|
||||
type Config struct {
|
||||
Host string // IP
|
||||
Port int // 默认 22
|
||||
User string
|
||||
Password string
|
||||
PrivateKey string // PEM 私钥(与 Password 二选一)
|
||||
}
|
||||
|
||||
// Run 执行单条远程命令,返回合并输出;timeout 为整条命令的硬上限
|
||||
func Run(cfg Config, cmd string, timeout time.Duration) (string, error) {
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 22
|
||||
}
|
||||
if cfg.User == "" {
|
||||
cfg.User = "root"
|
||||
}
|
||||
|
||||
var authMethods []ssh.AuthMethod
|
||||
if cfg.PrivateKey != "" {
|
||||
signer, err := ssh.ParsePrivateKey([]byte(cfg.PrivateKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("私钥解析失败: %v", err)
|
||||
}
|
||||
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
||||
}
|
||||
if cfg.Password != "" {
|
||||
authMethods = append(authMethods, ssh.Password(cfg.Password))
|
||||
}
|
||||
if len(authMethods) == 0 {
|
||||
return "", fmt.Errorf("未提供密码或私钥")
|
||||
}
|
||||
|
||||
client, err := ssh.Dial("tcp", net.JoinHostPort(cfg.Host, fmt.Sprint(cfg.Port)), &ssh.ClientConfig{
|
||||
User: cfg.User,
|
||||
Auth: authMethods,
|
||||
Timeout: 10 * time.Second,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 内网构建节点场景
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("SSH 连接失败: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
var output []byte
|
||||
var runErr error
|
||||
go func() {
|
||||
defer close(done)
|
||||
output, runErr = session.CombinedOutput(cmd)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
return string(output), runErr
|
||||
case <-time.After(timeout):
|
||||
_ = session.Close()
|
||||
return string(output), fmt.Errorf("命令执行超时(%s)", timeout)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user