
本教程深入探讨了使用 go 语言 `ssh` 库连接 cisco 设备时,发送长命令可能遭遇的截断问题。当通过 ssh shell 传输较长命令时,由于伪终端 (pty) 尺寸配置不当,命令可能在设备端被意外分割,导致执行失败。文章详细分析了问题根源,并提供了通过正确设置 `requestpty` 参数来解决命令截断的实用方法,确保与网络设备的顺畅通信。
在网络自动化和监控场景中,Go 语言因其并发特性和丰富的标准库而备受青睐。通过 Go 的 ssh 库与网络设备(如 Cisco 路由器)进行交互是常见的需求。然而,与传统服务器不同,许多网络设备(特别是旧款或特定配置的设备)通常不直接支持通过 ssh.Session.Run() 执行命令,而是需要通过交互式 Shell 会话来发送命令并读取输出。在这种模式下,正确配置 SSH 会话的伪终端(PTY)至关重要,否则可能导致意想不到的问题,例如长命令被截断。
建立 SSH 连接与 Shell 会话
首先,我们需要使用 Go 的 golang.org/x/crypto/ssh 库建立与 Cisco 设备的 SSH 连接,并创建一个交互式 Shell 会话。
package mainimport ( "bytes" "fmt" "io" "log" "strings" "time" "golang.org/x/crypto/ssh")// SSH配置示例func getSSHConfig() *ssh.ClientConfig { return &ssh.ClientConfig{ User: "your_username", Auth: []ssh.AuthMethod{ ssh.Password("your_password"), }, HostKeyCallback: ssh.InsecureHostKeyCallback(), // 生产环境请使用更安全的HostKeyCallback Timeout: 10 * time.Second, }}// 连接并获取Shell会话func setupSSHSession(addr string, config *ssh.ClientConfig) (*ssh.Client, *ssh.Session, io.Writer, io.Reader, error) { client, err := ssh.Dial("tcp", addr, config) if err != nil { return nil, nil, nil, nil, fmt.Errorf("无法连接SSH服务器: %v", err) } session, err := client.NewSession() if err != nil { client.Close() return nil, nil, nil, nil, fmt.Errorf("无法创建SSH会话: %v", err) } // 获取输入输出管道 sshOut, err := session.StdoutPipe() if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法获取StdoutPipe: %v", err) } sshIn, err := session.StdinPipe() if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法获取StdinPipe: %v", err) } // 请求PTY,这里是关键点,稍后会详细讨论 modes := ssh.TerminalModes{ ssh.ECHO: 0, // 禁用回显 ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud } // 初始请求PTY,这里可能存在问题 // session.RequestPty("xterm", 80, 40, modes) // 假设原始代码中的错误配置:高度80,宽度40 // 修正后的配置将在下文给出 err = session.RequestPty("vt100", 200, 80, modes) // 临时使用一个相对合理的默认值 if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法请求PTY: %v", err) } err = session.Shell() if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法启动Shell: %v", err) } return client, session, sshIn, sshOut, nil}
处理初始输出并等待命令提示符
在 Shell 启动后,设备通常会输出一些欢迎信息、上次登录记录等,直到显示命令提示符。我们需要读取并忽略这些信息,直到识别出提示符,才能开始发送命令。
// 读取直到找到特定提示符func readUntilPrompt(reader io.Reader, prompt string) (string, error) { var buffer bytes.Buffer buf := make([]byte, 1024) for { n, err := reader.Read(buf) if err != nil { if err == io.EOF { break // 连接关闭 } return "", fmt.Errorf("读取SSH输出失败: %v", err) } buffer.Write(buf[:n]) output := buffer.String() // 检查是否包含提示符,Cisco设备提示符可能包含设备名、模式等 // 示例中的 "[local]ewag#" 提示符 if strings.Contains(output, prompt) { return output, nil } // 避免无限循环,设置一个超时机制 // 实际应用中需要更复杂的超时和错误处理 if buffer.Len() > 4096 { // 超过一定长度仍未找到提示符,可能出错 return output, fmt.Errorf("未在预期时间内找到提示符 '%s'", prompt) } time.Sleep(100 * time.Millisecond) // 短暂等待,避免CPU空转 } return buffer.String(), nil}
发送短命令与读取响应
对于短命令,通常不会出现问题。以下是发送 show clock 命令并读取其输出的示例:
// 发送命令func sendCommand(writer io.Writer, command string) error { _, err := writer.Write([]byte(command + "r")) // r 表示回车 if err != nil { return fmt.Errorf("发送命令失败: %v", err) } return nil}func main() { addr := "172.16.32.95:22" // 替换为你的Cisco设备IP和端口 prompt := "[local]ewag#" // 替换为你的设备实际提示符 client, session, sshIn, sshOut, err := setupSSHSession(addr, getSSHConfig()) if err != nil { log.Fatalf("设置SSH会话失败: %v", err) } defer session.Close() defer client.Close() fmt.Println("等待初始提示符...") initialOutput, err := readUntilPrompt(sshOut, prompt) if err != nil { log.Fatalf("读取初始提示符失败: %v", err) } fmt.Printf("初始输出:n%sn", initialOutput) fmt.Println("发送短命令 'show clock'...") err = sendCommand(sshIn, "show clock") if err != nil { log.Fatalf("发送 'show clock' 命令失败: %v", err) } clockOutput, err := readUntilPrompt(sshOut, prompt) if err != nil { log.Fatalf("读取 'show clock' 结果失败: %v", err) } fmt.Printf("短命令输出:n%sn", clockOutput) // ... 接下来将讨论长命令的问题}
遇到的问题:长命令被截断
当尝试发送一个较长的命令时,例如 show session progress ipsg-service ipsg-gprs-svc,我们可能会发现命令在设备端被意外地分割,导致设备报告“未知命令”或“无法识别的关键字”。
// ... (main函数中,在发送短命令之后) fmt.Println("发送长命令 'show session progress ipsg-service ipsg-gprs-svc'...") err = sendCommand(sshIn, "show session progress ipsg-service ipsg-gprs-svc") if err != nil { log.Fatalf("发送长命令失败: %v", err) } longCommandOutput, err := readUntilPrompt(sshOut, prompt) if err != nil { log.Fatalf("读取长命令结果失败: %v", err) } fmt.Printf("长命令输出:n%sn", longCommandOutput) // 预期输出可能类似: // show session progress ipsg // -service ipsg-gprs-svc // Unknown command - "ipsg-service", unrecognized keyword // [local]ewag#
从上述输出可以看出,命令 show session progress ipsg-service ipsg-gprs-svc 在 ipsg 之后被截断并换行,导致设备将其识别为两个独立的、不完整的命令部分,从而引发错误。
根源分析:PTY 尺寸配置
这个问题的根源在于 session.RequestPty 函数的参数配置。RequestPty 用于请求一个伪终端 (Pseudo-Terminal),其签名通常为 RequestPty(term string, h, w int, termmodes TerminalModes) error,其中 h 是终端的高度(行数),w 是终端的宽度(列数)。
许多网络设备在处理交互式 Shell 输入时,会根据 PTY 的宽度参数来限制单行输入缓冲的大小。如果 RequestPty 中设置的宽度 w 过小,当发送的命令字符串长度超过这个宽度时,设备可能会将其视为多行输入,或者在达到宽度限制时自动换行,从而导致命令被截断。
在原始问题中,可能存在两种情况导致宽度不足:
session.RequestPty(“xterm”, 80, 40, modes):这里 w 被设置为 40。如果命令长度超过 40 个字符,就会被截断。对 RequestPty 参数的误解:将高度和宽度参数混淆,或者错误地将一个较小的数值赋给了宽度。
解决方案:调整 PTY 宽度
解决此问题的关键是确保在 session.RequestPty 调用中提供一个足够大的宽度值。一个常见的做法是设置一个较大的固定值(例如 200 或 300),或者将宽度设置为 0,让终端自动协商或使用最大允许宽度(这取决于 SSH 服务器的实现)。
将 setupSSHSession 函数中的 PTY 请求行修改为:
// 修正后的PTY请求,确保宽度足够 // height=0, width=200 通常能解决问题,0 表示让终端自动调整或使用默认值 err = session.RequestPty("vt100", 0, 200, modes) if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法请求PTY: %v", err) }
通过将宽度设置为 200(或 0),我们为 Shell 输入提供了更大的缓冲区,从而避免了长命令被意外截断的问题。修改后的代码将能够正确地发送和执行长命令。
完整示例代码(核心部分更新)
以下是 setupSSHSession 函数更新后的核心代码片段:
// 连接并获取Shell会话 (更新PTY请求)func setupSSHSession(addr string, config *ssh.ClientConfig) (*ssh.Client, *ssh.Session, io.Writer, io.Reader, error) { // ... (省略SSH连接和会话创建部分,与之前相同) // 获取输入输出管道 sshOut, err := session.StdoutPipe() if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法获取StdoutPipe: %v", err) } sshIn, err := session.StdinPipe() if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法获取StdinPipe: %v", err) } // 请求PTY,修正宽度参数 modes := ssh.TerminalModes{ ssh.ECHO: 0, // 禁用回显 ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud } // 关键修正:将宽度设置为一个足够大的值 (例如 200) 或 0 (让服务器决定) // term: "vt100" 是一个通用的终端类型 // height: 0 (或任何合理值,如 24, 80) // width: 200 (确保命令不会被截断) err = session.RequestPty("vt100", 0, 200, modes) if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法请求PTY: %v", err) } err = session.Shell() if err != nil { session.Close() client.Close() return nil, nil, nil, nil, fmt.Errorf("无法启动Shell: %v", err) } return client, session, sshIn, sshOut, nil}
注意事项与最佳实践
PTY 尺寸选择:对于高度 (h),通常可以设置为 0 或一个标准值(如 24 或 80),它对命令截断的影响较小。对于宽度 (w),务必确保其值足够大,以容纳你可能发送的最长命令。将 w 设置为 0 在许多 SSH 服务器上会使其自动协商或使用最大宽度,通常是一个安全的选择。如果 0 不起作用,尝试 200、300 或更大的值。终端类型:term 参数(如 xterm 或 vt100)指定了终端仿真类型。vt100 是一个非常通用的选择,兼容性较好。错误处理:示例代码中省略了部分错误处理,但在生产环境中,必须对所有 SSH 操作进行健壮的错误检查和资源清理(如 defer client.Close())。提示符检测:readUntilPrompt 函数中的提示符检测逻辑需要根据实际设备和配置进行调整。Cisco 设备的提示符可能因模式(用户模式、特权模式、配置模式等)和设备名称而异。分页处理:Cisco 设备在显示大量输出时会进行分页(例如 –More– 提示)。在自动化脚本中,你需要检测并处理分页提示,例如发送空格键继续,或在命令前添加 terminal length 0 来禁用分页。并发与资源管理:如果需要同时管理多个 SSH 连接,请注意 Go 的 Goroutine 和 Channel,确保连接和会话的正确创建、使用和关闭,避免资源泄露。
总结
通过 Go 语言的 ssh 库与 Cisco 等网络设备进行交互时,如果遇到长命令被截断的问题,很可能是由于 session.RequestPty 函数中伪终端 (PTY) 的宽度参数配置不当所致。理解 RequestPty 参数的含义,并确保为终端宽度提供一个足够大的值(例如 200 或 0),是解决此类问题的关键。遵循这些最佳实践,可以确保你的 Go 自动化脚本与网络设备之间建立稳定、可靠的交互。
以上就是Go SSH 连接 Cisco 设备时命令被截断的深度解析与修复的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1424964.html
微信扫一扫
支付宝扫一扫