feat: 添加UPF N3 Ping测试功能

This commit is contained in:
TsMask
2025-09-12 15:23:03 +08:00
parent 0ff49d198d
commit ed4ea06e2e
5 changed files with 183 additions and 2 deletions

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -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<transmitted>\d+) packets transmitted, (?P<received>\d+) received, (?P<loss>\d+)% packet loss, time (?P<time>\d+)ms`)
matches := re.FindStringSubmatch(packetsStr)
if len(matches) > 0 {
stats["packets_transmitted"], _ = strconv.Atoi(matches[1])
stats["packets_received"], _ = strconv.Atoi(matches[2])
stats["packet_loss"], _ = strconv.Atoi(matches[3])
stats["time_ms"], _ = strconv.Atoi(matches[4])
}
}
if rttStr != "" {
// 正则表达式来提取 rtt 的数据 "rtt min/avg/max/mdev = 0.324/0.458/0.639/0.112 ms"
re := regexp.MustCompile(`rtt min/avg/max/mdev = (?P<min>\d+\.\d+)/(?P<avg>\d+\.\d+)/(?P<max>\d+\.\d+)/(?P<mdev>\d+\.\d+)`)
matches := re.FindStringSubmatch(rttStr)
if len(matches) > 0 {
stats["min_rtt"], _ = strconv.ParseFloat(matches[1], 64)
stats["avg_rtt"], _ = strconv.ParseFloat(matches[2], 64)
stats["max_rtt"], _ = strconv.ParseFloat(matches[3], 64)
stats["mdev_rtt"], _ = strconv.ParseFloat(matches[4], 64)
}
}
return stats, nil
}

View File

@@ -53,5 +53,9 @@ func Setup(router *gin.Engine) {
collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.ping", collectlogs.BUSINESS_TYPE_OTHER)),
controller.NewPing.Run,
)
pingGroup.POST("/rtt",
middleware.PreAuthorize(nil),
controller.NewPing.RTT,
)
}
}