
本教程深入探讨了使用 go 语言 `ssh` 库连接 cisco 设备时,发送长命令可能遭遇的截断问题。当通过 ssh shell 传输较长命令时,由于伪终端 (pty) 尺寸配置不当,命令可能在设备端被意外分割,导致执行失败。文章详细分析了问题根源,并提供了通过正确设置 `requestpty` 参数来解决命令截断的实用方法,确保与网络设备的顺畅通信。
在网络自动化和监控场景中,Go 语言因其并发特性和丰富的标准库而备受青睐。通过 Go 的 ssh 库与网络设备(如 Cisco 路由器)进行交互是常见的需求。然而,与传统服务器不同,许多网络设备(特别是旧款或特定配置的设备)通常不直接支持通过 ssh.Session.Run() 执行命令,而是需要通过交互式 Shell 会话来发送命令并读取输出。在这种模式下,正确配置 SSH 会话的伪终端(PTY)至关重要,否则可能导致意想不到的问题,例如长命令被截断。
首先,我们需要使用 Go 的 golang.org/x/crypto/ssh 库建立与 Cisco 设备的 SSH 连接,并创建一个交互式 Shell 会话。
package main
import (
"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%s\n", 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%s\n", 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%s\n", 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 之后被截断并换行,导致设备将其识别为两个独立的、不完整的命令部分,从而引发错误。
这个问题的根源在于 session.RequestPty 函数的参数配置。RequestPty 用于请求一个伪终端 (Pseudo-Terminal),其签名通常为 RequestPty(term string, h, w int, termmodes TerminalModes) error,其中 h 是终端的高度(行数),w 是终端的宽度(列数)。
许多网络设备在处理交互式 Shell 输入时,会根据 PTY 的宽度参数来限制单行输入缓冲的大小。如果 RequestPty 中设置的宽度 w 过小,当发送的命令字符串长度超过这个宽度时,设备可能会将其视为多行输入,或者在达到宽度限制时自动换行,从而导致命令被截断。
在原始问题中,可能存在两种情况导致宽度不足:
解决此问题的关键是确保在 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
}通过 Go 语言的 ssh 库与 Cisco 等网络设备进行交互时,如果遇到长命令被截断的问题,很可能是由于 session.RequestPty 函数中伪终端 (PTY) 的宽度参数配置不当所致。理解 RequestPty 参数的含义,并确保为终端宽度提供一个足够大的值(例如 200 或 0),是解决此类问题的关键。遵循这些最佳实践,可以确保你的 Go 自动化脚本与网络设备之间建立稳定、可靠的交互。
以上就是Go SSH 连接 Cisco 设备时命令被截断的深度解析与修复的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号