Files
FlowStack/backend/internal/sshexec/client.go
T
Logan.Li 555934cc64 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)
2026-10-27 12:21:39 +08:00

77 lines
1.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}
}