文章目录
- 前言
- 一、了解SSH
- 二、重要知识点
- 1.安装ssh库
- 2.ssh库重要知识牢记
- 三、模拟连接远程服务器并执行命令
- 四、SSH与os/exec标准库下执行命令的几种方式对比
- 五、SSH库下三种执行命令方式演示
- 5.1. session.CombinedOutput()示例
- 5.2. session.Run()示例
- 5.3. session.Start()、session.Wait()示例
- 六、两种捕获标准输出和标准错误的方法:`StdoutPipe` / `StderrPipe` 和 `session.Stdout` / `session.Stderr`之间的区别
- 6.1. 使用 `StdoutPipe` 和 `StderrPipe`捕获标准输出和标准错误
- 6.2. 使用 重定向 `session.Stdout` 和 `session.Stderr` 捕获标准输出和标准错误
- 6.3.两种方式的区别
- 七、示例: 连接到多台服务器并执行多个命令返回命令执行结果
- 总结
前言
SSH 全称为 Secure Shell,是一种用于安全地远程登录到网络上的其他计算机的网络协议。相信做运维的同学没有不了解 SSH的,比较常用的登录服务器的 shell 工具例如 Xshell、SecureCRT、iTerm2 等都是基于 SSH 协议实现的。Golang 中的的 crypto/ssh 库提供了实现 SSH 客户端的功能,本文接下来详细讲解下如何使用 Golang 实现操作 SSH 客户端,为后续运维开发的道路上使用golang编写脚本先夯实一下基础
一、了解SSH
在Golang中,有几个常用的SSH库,如golang.org/x/crypto/ssh和github.com/go-ssh/ssh。
本次将重点介绍golang.org/x/crypto/ssh,因为它是由Go官方维护的.SSH库功能分类:SSH客户端: 允许用户通过SSH协议连接到远程服务器。SSH服务器: 允许远程用户通过SSH协议连接到本地服务器。命令执行: 在远程服务器上执行命令。文件传输: 在本地和远程服务器之间传输文件。交会时会话: 类比xshell,当代码执行后,如同在操作真实的xshell一样
二、重要知识点
1.安装ssh库
代码如下(示例):
go get golang.org/x/crypto/ssh
2.ssh库重要知识牢记
结合演示代码一起更好理解
如下(示例):
1、client 对象(SSH 客户端)在整个程序中只创建一次2、可以通过 client.NewSession() 多次创建多个 session 对象.每个 session 是一个独立的会话,每次执行命令时都会创建一个新的会话3、每次 session.Run() 或 session.Start() 执行命令时,都会用新的会话来执行不同的命令这些会话共享底层的 SSH 连接,但是它们独立执行命令4、当某个会话的命令执行完毕,必须调用session.Close() 释放相关资源。5、切记不能在同一个 session 上并行执行多个命令。如果需要并行执行多个命令,应该创建多个 session
演示代码(示例):
package mainimport ("fmt""golang.org/x/crypto/ssh""log"
)func main() {// SSH 配置config := &ssh.ClientConfig{User: "root", // 替换为远程服务器的用户名Auth: []ssh.AuthMethod{ssh.Password("1"), // 替换为远程服务器密码},HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 忽略主机密钥验证}// 连接远程服务器client, err := ssh.Dial("tcp", "192.168.56.160:22", config) // 替换为远程服务器的IP地址if err != nil {log.Fatalf("Failed to dial: %v", err)}defer client.Close()// 创建第一个会话session1, err := client.NewSession()if err != nil {log.Fatalf("Failed to create session 1: %v", err)}defer session1.Close()// 执行第一个命令fmt.Println("Executing command on session 1-1")err = session1.Run("echo Hello from session 1-1")if err != nil {log.Fatalf("Failed to run command on session 1-1: %v", err)}// 演示在第一个会话中执行第二个命令看是否能成功fmt.Println("Executing command on session 1-2")err = session1.Run("echo Hello from session 1-2")if err != nil {log.Fatalf("Failed to run command on session 1-2: %v", err)}// 创建第二个会话session2, err := client.NewSession()if err != nil {log.Fatalf("Failed to create session 2: %v", err)}defer session2.Close()// 执行第二个命令fmt.Println("Executing command on session 2")err = session2.Run("echo Hello from session 2")if err != nil {log.Fatalf("Failed to run command on session 2: %v", err)}// 创建第三个会话session3, err := client.NewSession()if err != nil {log.Fatalf("Failed to create session 3: %v", err)}defer session3.Close()// 执行第三个命令fmt.Println("Executing command on session 3")err = session3.Run("echo Hello from session 3")if err != nil {log.Fatalf("Failed to run command on session 3: %v", err)}fmt.Println("All commands executed successfully")
}
执行这段代码,返回如下所示,在同一个会话下并行的运行两条命令,发现运行失败
当将1-2这段代码注释掉后,再次运行代码可以成功运行,跟上述的描述一致
三、模拟连接远程服务器并执行命令
演示怎么在golang中使用SSH库连接服务器并执行相应的linux命令
package mainimport ("golang.org/x/crypto/ssh""log"
)func main() {// 创建SSH配置--密码认证config := &ssh.ClientConfig{User: "root",Auth: []ssh.AuthMethod{ssh.Password("1"), //密码认证},HostKeyCallback: ssh.InsecureIgnoreHostKey(),}// 创建SSH配置--SSH密钥认证(生产环境下建议采用该方式) 二选一即可//config := &ssh.ClientConfig{//User: "username",//Auth: []ssh.AuthMethod{// ssh.PublicKeysFromFile("path/to/private/key", "path/to/public/key"),//},// HostKeyCallback: ssh.FixedHostKey(hostKey),//}// 连接到远程服务器,并返回一个ssh客户端实例,/*返回值类型:*ssh.Clienterror*/client, err := ssh.Dial("tcp", "192.168.56.160:22", config)if err != nil {log.Fatalf("Failed to dial: %v", err)}defer client.Close()// 使用客户端创建一个ssh会话session, err := client.NewSession()if err != nil {log.Fatalf("Failed to create session: %v", err)}defer session.Close()// 在ssh会话中执行命令并输出命令结果out, err := session.CombinedOutput("ls /export")if err != nil {log.Fatalf("Failed to run: %v", err)}log.Printf("%s", out)
}
四、SSH与os/exec标准库下执行命令的几种方式对比
方法 | 功能描述 | 阻塞/非阻塞 | 输出捕获 | 使用场景 |
---|---|---|---|---|
cmd:=exec.Command(“xx”,“x”) err:=cmd.Run() | 执行本地命令并等待命令完成,返回错误 | 阻塞 | 不捕获输出(需用 Output/CombinedOutput 捕获) | 本地命令执行,等待命令完成 |
err:=newsession.Run("xxx") | 执行远程命令并等待命令完成,返回错误 | 阻塞 | 不捕获输出(需手动捕获) | 远程 SSH 命令执行,等待完成 |
cmd:=exec.Command(“xx”,“xx”) cmd.Start() | 启动本地命令异步执行,不等待命令完成 | 非阻塞,如果要阻塞,使用exec.Command().Wait()实现 | 可通过 Stdout、 Stderr 获取输出 | 本地命令异步执行,非阻塞 |
err:=newsession.Start("xx") | 启动远程命令异步执行,不等待命令完成 | 非阻塞,适用于需要启动后台进程的场景,如果要阻塞使用,newsession.Wait()实现 | 可通过 Stdout、 Stderr 获取输出 | 远程命令异步执行,非阻塞 |
cmd:=exec.Command(“xx”,“x”) out,err:=cmd.CombinedOutput() | 执行本地命令并捕获标准输出和标准错误的合并输出 | 阻塞 | 捕获标准输出和标准错误的合并输出 | 本地命令执行,捕获所有输出 |
out,err:=newsession.CombinedOutput("xx") | 执行远程命令并捕获标准输出和标准错误的合并输出 | 阻塞 | 捕获标准输出和标准错误的合并输出 | 远程命令执行,捕获所有输出 |
五、SSH库下三种执行命令方式演示
5.1. session.CombinedOutput()示例
连接192.168.56.160服务器,并执行ls /var/log/命令查看目录下的文件
注意事项:1、CombinedOutput()函数剖析func (s *ssh.Session) CombinedOutput(cmd string) ([]byte, error)接收参数类型 string返回值类型[]byte,error将[]byte转换为string类型输出的结果为命令的执行结果2、在一个session会话中执行多条命令的操作将多条命令保存在切片中,然后for循环将命令(value)传递给CombinedOutput()函数即可// 示例命令commands := []string{"ls -l /tmp", "uptime", "df -h"} for _, command := range commands {executeCommand(client, command, ip, resultChan, &mu)}out, err := session.CombinedOutput(commands)
package mainimport ("golang.org/x/crypto/ssh""log"
)func main() {// 创建SSH配置--密码认证config := &ssh.ClientConfig{User: "root",Auth: []ssh.AuthMethod{ssh.Password("1"), //密码认证},HostKeyCallback: ssh.InsecureIgnoreHostKey(),}// 连接到远程服务器,并返回一个ssh客户端实例client, err := ssh.Dial("tcp", "192.168.56.160:22", config)if err != nil {log.Fatalf("Failed to dial: %v", err)}defer client.Close()// 使用客户端创建一个ssh会话session, err := client.NewSession()if err != nil {log.Fatalf("Failed to create session: %v", err)}defer session.Close()// 在ssh会话中执行命令并输出命令结果。out, err := session.CombinedOutput("ls /var/log/")if err != nil {log.Fatalf("Failed to run: %v", err)}log.Printf("out:%s\n", out)
}
5.2. session.Run()示例
注意事项:session.Run(cmd string )errorfunc (s *ssh.Session) Run(cmd string) error接收参数类型 string返回类型 error<如果要想获取到执行的结果和错误,即区分标准输出和标准错误,则使用下方的方法>
package mainimport ("bytes""fmt""log""golang.org/x/crypto/ssh"
)// setupSSHClient 配置并返回一个SSH客户端
func setupSSHClient(user, password, host string, port int) (*ssh.Client, error) {config := &ssh.ClientConfig{User: user,Auth: []ssh.AuthMethod{ssh.Password(password),},HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 注意:这里使用了不安全的回调,仅用于示例。在实际应用中,你应该验证主机密钥。}client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), config)if err != nil {return nil, err}return client, nil
}
func main() {user := "root"password := "1"host := "192.168.56.162"port := 22 // 默认SSH端口是22client, err := setupSSHClient(user, password, host, port)if err != nil {log.Fatalf("Failed to setup SSH client: %v", err)}defer client.Close()if host == "192.168.56.162" {newsession, _ := client.NewSession()defer newsession.Close()//Run()// 创建一个缓冲区来捕获命令的输出var outputBuf bytes.Buffer// 将标准输出和标准错误都重定向到同一个缓冲区newsession.Stdout = &outputBufnewsession.Stderr = &outputBuferr := newsession.Run("ls /var/log/audit/")if err != nil {// 输出执行命令时的错误fmt.Printf("Error executing command: %v\n", err)}// 打印命令的输出(包括标准输出和标准错误)fmt.Printf("Command output:\n%s\n", outputBuf.String())}
}
5.3. session.Start()、session.Wait()示例
注意事项:func (s *ssh.Session) Start(cmd string) error接收参数类型 string返回类型 error如果要想获取到执行的结果和错误,即区分标准输出和标准错误,则使用下方的方法func (s *ssh.Session) Wait() error返回类型 error等待session.Start() 单独使用时,命令会在后台执行,程序不会等待命令的完成,立即继续执行后续代码。session.Start() 和 session.Wait() 一起使用时,程序会在 Wait() 处等待命令执行完成,之后才会继续执行后续的代码。
package mainimport ("fmt""golang.org/x/crypto/ssh""log""time"
)func main() {// SSH 配置config := &ssh.ClientConfig{User: "root", // 替换为远程服务器的用户名Auth: []ssh.AuthMethod{ssh.Password("1"), // 替换为远程服务器密码},HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 忽略主机密钥验证}// 连接远程服务器client, err := ssh.Dial("tcp", "192.168.56.160:22", config) // 替换为远程服务器的IP地址if err != nil {log.Fatalf("Failed to dial: %v", err)}defer client.Close()// 创建会话session, err := client.NewSession()if err != nil {log.Fatalf("Failed to create session: %v", err)}defer session.Close()// 示例 1:使用 session.Start() 启动命令,但不等待fmt.Println("=== 示例 1: 使用 session.Start() 启动命令,不等待 ===")err = session.Start("sleep 5") // 启动一个后台命令if err != nil {log.Fatalf("Failed to start command: %v", err)}// 程序不会等待 sleep 5 执行完成,立即继续执行下一行fmt.Println("命令已启动,程序继续执行,不等待命令结束")// 等待一段时间,观察命令是否执行完time.Sleep(2 * time.Second)fmt.Println("程序在等待2秒后继续执行。")// 示例 2:使用 session.Start() 启动命令,并等待命令执行完毕// 创建新的会话用于第二个命令session2, err := client.NewSession()if err != nil {log.Fatalf("Failed to create session for second command: %v", err)}defer session2.Close()fmt.Println("\n=== 示例 2: 使用 session.Start() 启动命令,并调用 session.Wait() 等待 ===")err = session2.Start("sleep 5") // 启动一个后台命令if err != nil {log.Fatalf("Failed to start second command: %v", err)}// 程序会在这里等待命令执行完成err = session2.Wait() // 等待命令完成if err != nil {log.Fatalf("Failed to wait for command to finish: %v", err)}fmt.Println("命令执行完成,程序继续执行")// 结束fmt.Println("\n所有命令已执行完毕")
}
六、两种捕获标准输出和标准错误的方法:StdoutPipe
/ StderrPipe
和 session.Stdout
/ session.Stderr
之间的区别
6.1. 使用 StdoutPipe
和 StderrPipe
捕获标准输出和标准错误
重要代码示例
// 获取 标准输出和标准错误
stdout, _ := session.StdoutPipe()
output := make([]byte, 1024)
for {n, err := stdout.Read(output)if err != nil {break}fmt.Sprintf("STDOUT from %s: %s", ip, string(output[:n]))
}stderr, err := session.StderrPipe()
output := make([]byte, 1024)
for {n, err := stderr.Read(output)if err != nil {break}fmt.Sprintf("STDERR from %s: %s", ip, string(output[:n]))
}
解释
1. 使用 `StdoutPipe` 和 `StderrPipe`:- `StdoutPipe()` 和 `StderrPipe()` 返回一个 `io.Reader`,可以用来读取远程命令的标准输出(stdout)和标准错误输出(stderr)- 可以通过从这些管道中读取数据来获取命令的输出,通常会使用协程来异步读取这些管道中的数据2. 工作原理:- 首先通过 `session.StdoutPipe()` 和 `session.StderrPipe()` 获取输出的管道(`io.Reader`)- 然后在程序中手动读取这些管道的内容,通常通过 `io.Copy` 或者 `bufio.Reader` 来处理流。- 这种方式适用于需要处理较大输出或需要实时读取命令输出的场景。3. 优点:- 可以实时读取输出,因为管道是持续开放的,适合需要处理大量数据或逐行输出的情况。- 可以分别处理标准输出和标准错误,提供更多灵活性。4. 缺点:- 需要异步读取标准输出和标准错误,可能需要更多的代码来确保并发处理和同步。- 适用于需要实时处理输出的场景,不适合简单的命令输出捕获。
6.2. 使用 重定向 session.Stdout
和 session.Stderr
捕获标准输出和标准错误
重要代码示例
....
// 创建一个缓冲区来捕获命令的输出
var outputBuf bytes.Buffer
// 将标准输出和标准错误都重定向到同一个缓冲区
session.Stdout = &outputBuf
session.Stderr = &outputBuf
err := newsession.Run("ls /var/log/audit/")
if err != nil {// 输出执行命令时的错误fmt.Printf("Error executing command: %v\n", err)
}
// 打印命令的输出(包括标准输出和标准错误)
fmt.Printf("Command output:\n%s\n", outputBuf.String())
...
解释
1. 使用 `newsession.Stdout` 和 `newsession.Stderr`:- `session.Stdout` 和 `session.Stderr` 分别是 `io.Writer` 类型,允许将命令的标准输出和标准错误直接写入一个缓冲区(如 `bytes.Buffer`)。- 可以通过 `outputBuf.String()` 获取完整的命令输出。这里,`Stdout` 和 `Stderr` 都被重定向到同一个 `bytes.Buffer`,- 这样就能捕获命令的所有输出(无论是标准输出还是标准错误)。2. 工作原理:- `session.Run()` 会直接执行命令并把标准输出和标准错误都写入到指定的缓冲区。- 不需要异步读取输出,命令执行完成后,只需要读取 `outputBuf` 即可获取所有输出。3. 优点:- 代码简单,易于实现,适合捕获简单的命令输出。- 不需要显式地管理异步读取标准输出和错误流,适用于不需要实时处理输出的场景。- 适合于简单的任务(例如调试、输出日志等)并且输出数据量较小的情况。4. 缺点:- 如果命令输出量大或者需要实时处理输出,可能会遇到缓冲区的限制或延迟。- 不能实时读取输出,必须等命令执行完毕才能获取所有输出。
6.3.两种方式的区别
1. 实时性:- `StdoutPipe` 和 `StderrPipe`:适合实时读取标准输出和标准错误。可以在命令执行的过程中动态处理输出数据。- `Stdout` 和 `Stderr`:适合捕获命令执行后的完整输出,并不实时读取。如果需要完整的命令输出,一次性获取比较简单。2. 使用场景:- `StdoutPipe` 和 `StderrPipe`:适合输出较大、需要流式处理的场景,比如你需要逐行读取或实时处理命令输出的场景。- `Stdout` 和 `Stderr`:适合捕获命令的完整输出并一次性处理,代码简单,适合小规模的输出捕获。3. 复杂性:- `StdoutPipe` 和 `StderrPipe`:稍微复杂,因为需要处理并发读取输出流,可能涉及协程。- `Stdout` 和 `Stderr`:简单易懂,适合不需要实时读取输出的情况。根据实际需求,可以选择适合的方式:如果需要并发处理或实时处理输出流,使用 `StdoutPipe` 和 `StderrPipe`如果需要一次性获取完整输出,使用 `Stdout` 和 `Stderr` 会更加简洁。
七、示例: 连接到多台服务器并执行多个命令返回命令执行结果
先看代码再分析
package mainimport ("bufio""fmt""log""os""strings""sync""golang.org/x/crypto/ssh"
)func executeCommand(client *ssh.Client, command string, ip string, resultChan chan<- string, mu *sync.Mutex) {// 创建一个新的 SSH 会话session, err := client.NewSession()if err != nil {log.Println("Failed to create session:", err)resultChan <- fmt.Sprintf("Error on %s: Failed to create session", ip)return}defer session.Close()// 获取 Stdout 和 Stderr 输出stdout, err := session.StdoutPipe()if err != nil {log.Println("Failed to get StdoutPipe:", err)resultChan <- fmt.Sprintf("Error on %s: Failed to get StdoutPipe", ip)return}stderr, err := session.StderrPipe()if err != nil {log.Println("Failed to get StderrPipe:", err)resultChan <- fmt.Sprintf("Error on %s: Failed to get StderrPipe", ip)return}// 启动命令err = session.Start(command)if err != nil {log.Println("Failed to start command:", err)resultChan <- fmt.Sprintf("Error on %s: Failed to start command", ip)return}// 使用锁来确保对共享资源(如输出的打印)是串行的mu.Lock()defer mu.Unlock()// 读取命令输出并打印到管道go func() {output := make([]byte, 1024)for {n, err := stdout.Read(output)if err != nil {break}resultChan <- fmt.Sprintf("STDOUT from %s: %s", ip, string(output[:n]))}}()go func() {output := make([]byte, 1024)for {n, err := stderr.Read(output)if err != nil {break}resultChan <- fmt.Sprintf("STDERR from %s: %s", ip, string(output[:n]))}}()// 等待命令执行完毕err = session.Wait()if err != nil {log.Println("Error executing command:", err)resultChan <- fmt.Sprintf("Error on %s: %v", ip, err)} else {resultChan <- fmt.Sprintf("Command executed successfully on %s", ip)}
}func main() {// 加载 IP 地址文件file, err := os.Open("/export/test/ips.txt")if err != nil {log.Fatal("Failed to open file:", err)}defer file.Close()// 读取 IP 地址var ips []stringscanner := bufio.NewScanner(file)for scanner.Scan() {ip := strings.TrimSpace(scanner.Text())if ip != "" {ips = append(ips, ip)}}if err := scanner.Err(); err != nil {log.Fatal("Failed to read file:", err)}// 设置 SSH 客户端配置,使用密码认证sshConfig := &ssh.ClientConfig{User: "root", // SSH 用户名Auth: []ssh.AuthMethod{ssh.Password("1"), // 密码认证},HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 注意:生产环境中不建议使用此选项}// 创建一个管道用于接收结果resultChan := make(chan string, len(ips)*3) // 每台机器执行多个命令,调整管道容量var wg sync.WaitGroupvar mu sync.Mutex // 创建锁// 遍历 IP 地址,并为每个 IP 地址启动一个 goroutinefor _, ip := range ips {wg.Add(1)go func(ip string) {defer wg.Done()// 建立 SSH 连接client, err := ssh.Dial("tcp", ip+":22", sshConfig)if err != nil {log.Printf("Failed to connect to %s: %v", ip, err)resultChan <- fmt.Sprintf("Failed to connect to %s", ip)return}defer client.Close()// 对每台机器执行多个命令commands := []string{"ls -l /tmp", "uptime", "df -h"} // 示例命令for _, command := range commands {executeCommand(client, command, ip, resultChan, &mu)}}(ip)}// 在所有任务完成之后关闭 resultChango func() {wg.Wait()close(resultChan)}()// 输出所有结果for result := range resultChan {fmt.Println(result)}
}
涉及到的知识点:1、管道2、互斥锁3、goroutine并发4、SSH5、session.Start/Wait6、分开捕获标准输出和标准错误7、按行读取文件内容上述代码示例演示了如何在多台机器上并发执行多个命令,并使用 sync.Mutex 来保护共享资源(如管道)的访问具体流程:1、从文件中按行读取IP并保存到切片ips中2、设置ssh配置,从管道中读取IP,将每个服务器连接和每个要执行的命令都放在一个 goroutine中。主程序继续启动新的 goroutine 执行任务,而不会因为某一台服务器的命令执行而导致整个程序阻塞3、将连接信息和捕获的标准输出和标准错误信息都写入到管道中4、当服务器连接成功后,调用执行命令函数executeCommand,再该代码中的锁用于保护共享资源(resultChan)的访问因为如果多个 goroutine 同时向通道发送数据(比如日志输出)没有锁会导致输出混乱(多个 goroutine 的日志可能会交错,难以看清)使用 sync.Mutex 来确保每次只有一个 goroutine 向通道发送数据,从而保证输出日志的顺序和一致性保证了多个 goroutine 在写入 resultChan 时不会互相干扰,避免了并发写入导致的数据不一致或错乱5、当所有远程机器的命令执行完成后,关闭会话、关闭通道,最终再打印出通道中所有的日志信息
总结
以上就是SSH标准库自己整理的知识,故不积跬步,无以至千里;不积小流,无以成江海
,慢慢整理golang中运维可以使用到的相关库,向运维逐渐靠拢