diff --git a/src/framework/ssh/ssh_session.go b/src/framework/ssh/ssh_session.go index 1244ac2b..25b6d40c 100644 --- a/src/framework/ssh/ssh_session.go +++ b/src/framework/ssh/ssh_session.go @@ -4,7 +4,9 @@ import ( "bytes" "fmt" "io" + "strings" "sync" + "time" gossh "golang.org/x/crypto/ssh" ) @@ -50,6 +52,57 @@ func (s *SSHClientSession) Read() []byte { return []byte{} } +// RunCMD 执行单次命令 +func (s *SSHClientSession) RunCMD(cmd string) (string, error) { + // 写入命令 + if cmd != "" { + if _, err := s.Write(cmd + "\n"); err != nil { + return "", err + } + time.Sleep(100 * time.Millisecond) + } + + // 超时退出 120s + timeoutTicker := time.NewTicker(120 * time.Second) + defer timeoutTicker.Stop() + // 实时读取SSH消息直接输出 + msTicker := time.NewTicker(100 * time.Millisecond) + defer msTicker.Stop() + // 消息缓冲区 + var buf bytes.Buffer + defer buf.Reset() + for { + select { + case <-timeoutTicker.C: + return "", fmt.Errorf("ssh session timeout") + case <-msTicker.C: + outputByte := s.Read() + if len(outputByte) <= 0 { + continue + } + outputStr := string(outputByte) + buf.WriteString(outputStr) + + // 命令终止符后继续执行命令 + // "~]# ":麒麟, "~]$ ":欧拉, "~# ":NXP, "~$ ":Ubuntu + suffixStr := []string{"~]# ", "~]$ ", "~# ", "~$ "} + suffix := false + for _, v := range suffixStr { + if strings.HasSuffix(outputStr, v) { + suffix = true + break + } + } + if !suffix { + suffix = strings.LastIndex(outputStr, "# ") != -1 // 特殊内容中的终端终止符 + } + if suffix { + return buf.String(), nil + } + } + } +} + // singleWriter SSH客户端会话消息 type singleWriter struct { b bytes.Buffer diff --git a/src/modules/tool/controller/ping.go b/src/modules/tool/controller/ping.go index c8806a53..92c31e61 100644 --- a/src/modules/tool/controller/ping.go +++ b/src/modules/tool/controller/ping.go @@ -223,3 +223,38 @@ func (s *PingController) Run(c *gin.Context) { } } } + +// RTT 时延抖动 +// +// POST /rtt +func (s PingController) RTT(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + NeType string `json:"neType" binding:"required"` // 网元类型 + NeId string `json:"neId" binding:"required"` // 网元ID + Ping model.Ping `json:"ping" binding:"required"` // 参数 + } + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + if body.Ping.DesAddr == "" { + c.JSON(400, result.CodeMsg(400, "ping desAddr is required")) + return + } + + // 网元主机的SSH客户端 + sshClient, err := neService.NewNeInfo.NeRunSSHClient(body.NeType, body.NeId) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sshClient.Close() + + output, err := s.pingService.PingRTT(sshClient, body.Ping) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.OkData(output)) +} diff --git a/src/modules/tool/model/ping.go b/src/modules/tool/model/ping.go index 9a00b1ff..f8889b4b 100644 --- a/src/modules/tool/model/ping.go +++ b/src/modules/tool/model/ping.go @@ -19,7 +19,7 @@ type Ping struct { } // setDefaultValue 设置默认值 -func (p *Ping) setDefaultValue() { +func (p *Ping) SetDefaultValue() { if p.Interval < 1 || p.Interval > 10 { p.Interval = 1 } @@ -39,7 +39,7 @@ func (p *Ping) setDefaultValue() { // NewPinger ping对象 func (p *Ping) NewPinger() (*probing.Pinger, error) { - p.setDefaultValue() + p.SetDefaultValue() pinger, err := probing.NewPinger(p.DesAddr) if err != nil { diff --git a/src/modules/tool/service/ping.go b/src/modules/tool/service/ping.go index 5b888cbf..995ee956 100644 --- a/src/modules/tool/service/ping.go +++ b/src/modules/tool/service/ping.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "io" + "regexp" + "strconv" "strings" "time" @@ -259,3 +261,90 @@ func (s *Ping) parseOptions(reqData any) (string, error) { command = append(command, "\n") return strings.Join(command, " "), nil } + +// PingRTT 时延抖动 +func (s *Ping) PingRTT(sshClient *ssh.ConnSSH, ping model.Ping) (map[string]any, error) { + ping.SetDefaultValue() + // ssh连接会话 + clientSession, err := sshClient.NewClientSession(128, 128) + if err != nil { + return nil, err + } + defer clientSession.Close() + clientSession.RunCMD("") // 排空信息 + + // 命令字符串 + command := []string{"ping"} + // Options + if ping.Interval > 0 { + command = append(command, fmt.Sprintf("-i %d", ping.Interval)) + } + if ping.TTL > 0 { + command = append(command, fmt.Sprintf("-t %d", ping.TTL)) + } + if ping.Count > 0 { + command = append(command, fmt.Sprintf("-c %d", ping.Count)) + } + if ping.Size > 0 { + command = append(command, fmt.Sprintf("-s %d", ping.Size)) + } + if ping.Timeout > 0 { + command = append(command, fmt.Sprintf("-w %d", ping.Timeout)) + } + if ping.SrcAddr != "" { + command = append(command, fmt.Sprintf("-I %s", ping.SrcAddr)) + } + command = append(command, ping.DesAddr) + + // 执行命令 + output, err := clientSession.RunCMD(strings.Join(command, " ")) + if err != nil { + logger.Errorf("NeRunSSHCmd RunCMD %s err => %s", output, err.Error()) + return nil, fmt.Errorf("neinfo ssh run cmd err") + } + + packetsStr := "" + rttStr := "" + arr := strings.Split(output, "\r\n") + for _, v := range arr { + fmt.Println(v) + if strings.Contains(v, "packets transmitted") { + packetsStr = v + } + if strings.Contains(v, "rtt min/avg/max/mdev") { + rttStr = v + } + } + if len(arr) < 6 { + return nil, fmt.Errorf("result error") + } + + // 定义一个 map 来存储结果 + stats := make(map[string]any) + + if packetsStr != "" { + // 正则表达式来提取相关字段 "5 packets transmitted, 5 received, 0% packet loss, time 4101ms" + re := regexp.MustCompile(`(?P\d+) packets transmitted, (?P\d+) received, (?P\d+)% packet loss, time (?P