Go 語言實現 ssh - scp
前言
最近遇到一個臨時需求,需要將客戶環境中一個服務每天的日誌進行一系列複雜處理,並生成數據報表。由於數據處理邏輯複雜,且需要存入數據庫,在客戶環境使用 shell
腳本無法處理,因此就需要將日誌先拷貝到本地,再進行處理;同時爲了避免每天人工拷貝日誌,需要實現自動化,整條鏈路自動執行,無需人工干預。平時使用 Go
語言較多,由此就引出了 Go
語言 ssh
連接遠程客戶服務器,並利用 scp
將數據拷貝下來的一系列操作。
說明:本文中的示例,均是基於 Go1.17 64 位機器
連接遠程服務器並執行命令(ssh)
如下給出了使用 用戶名+密碼
的方式連接遠程服務器並執行了 /usr/bin/whoami
命令的示例,步驟如下:
-
生成
ClientConfig
:想要連接遠程服務器,必須要至少指定一種實現了Auth
的AuthMethod
,我們這裏使用密碼的方式;同時需要提供 一種用於安全校驗遠程服務端 key 的方法HostKeyCallback
,我們這裏使用的是不校驗的方式ssh.InsecureIgnoreHostKey()
,生產情況下建議使用ssh.FixedHostKey(key PublicKey)
; -
調用
Dial
:Dial
方法與遠程服務器建立連接,並返回一個client
; -
NewSession
:NewSession
方法開啓一個會話,在一個會話內可以通過Run
方法執行一個命令。
import (
"bytes"
"fmt"
"log"
"golang.org/x/crypto/ssh"
)
func main() {
var (
username = "your username"
password = "your password"
addr = "ip:22"
)
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", addr, config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
defer client.Close()
// 開啓一個session,用於執行一個命令
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
// 執行命令,並將執行的結果寫到 b 中
var b bytes.Buffer
session.Stdout = &b
// 也可以使用 session.CombinedOutput() 整合輸出
if err := session.Run("/usr/bin/whoami"); err != nil {
log.Fatal("Failed to run: " + err.Error())
}
fmt.Println(b.String()) // root
}
上面的例子,我們在 Run
方法裏面傳入了一個命令,然後遠程服務器會將執行結果返回給我們,如果是複雜操作,通過傳入命令的方式就比較麻煩。比如上面提到的需求,需要我從 k8s
容器中拷貝出服務每天的日誌,拆解後的步驟爲:獲取服務的多個 k8s pod
名稱,根據當前日期,從多個容器中分別拷貝日誌文件,然後整合成一個日誌文件。針對複雜操作,我們可以在遠程服務器編寫一個腳本,然後 Run
方法中傳入執行腳本的命令。
簡單舉個示例,我們在遠程服務器編寫了一個腳本 test.sh
,放在了 /opt
目錄下,腳本內容 與 調用方式分別如下:
# 腳本文件
#!/bin/bash
today=$(date +"%Y-%m-%d")
# 將數據寫入文件
$(df -h > $today.log)
package main
import (
"fmt"
"log"
"golang.org/x/crypto/ssh"
)
func main() {
var (
username = "your username"
password = "your password"
addr = "ip:22"
)
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", addr, config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
// 調用遠程服務器腳本腳本
res, err := session.CombinedOutput("sh /opt/test.sh")
if err != nil {
log.Fatal("Failed to run: " + err.Error())
}
fmt.Println(string(res))
/*
Filesystem Size Used Avail Use% Mounted on
devtmpfs 909M 0 909M 0% /dev
tmpfs 919M 24K 919M 1% /dev/shm
tmpfs 919M 540K 919M 1% /run
tmpfs 919M 0 919M 0% /sys/fs/cgroup
/dev/vda1 50G 6.9G 40G 15% /
tmpfs 184M 0 184M 0% /run/user/0
*/
}
拷貝遠程服務器文件到本地(scp)
拷貝文件步驟比較簡單:
-
建立
ssh client
-
基於
ssh client
創建sftp client
-
打開遠程服務器文件並拷貝到本地
package main
import (
"io"
"log"
"os"
"time"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)
func main() {
var (
username = "your username"
password = "your password"
addr = "ip:22"
)
// 1. 建立 ssh client
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", addr, config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
defer client.Close()
// 2. 基於ssh client, 創建 sftp 客戶端
sftpClient, err := sftp.NewClient(client)
if err != nil {
log.Fatal("Failed to init sftp client: ", err)
}
defer sftpClient.Close()
// 3. 打開遠程服務器文件
filename := time.Now().Format("2006-01-02") + ".log"
source, err := sftpClient.Open("/opt/" + filename)
if err != nil {
log.Fatal("Failed to open remote file: ", err)
}
defer source.Close()
// 4. 創建本地文件
target, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatal("Failed to open local file: ", err)
}
defer target.Close()
// 5. 數據複製
n, err := io.Copy(target, source)
if err != nil {
log.Fatal("Failed to copy file: ", err)
}
log.Println("Succeed to copy file: ", n)
}
在 sftp client
中,還有許多方法,例如 Walk
、ReadDir
、Stat
、Mkdir
等,針對文件也有 Read
、Write
、WriteTo
、ReadFrom
等方法,像操作本地文件系統一樣,非常便利。
簡單封裝下
package main
import (
"fmt"
"io"
"log"
"os"
"time"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)
type Cli struct {
user string
pwd string
addr string
client *ssh.Client
}
func NewCli(user, pwd, addr string) Cli {
return Cli{
user: user,
pwd: pwd,
addr: addr,
}
}
// Connect 連接遠程服務器
func (c *Cli) Connect() error {
config := &ssh.ClientConfig{
User: c.user,
Auth: []ssh.AuthMethod{
ssh.Password(c.pwd),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", c.addr, config)
if nil != err {
return fmt.Errorf("connect server error: %w", err)
}
c.client = client
return nil
}
// Run 運行命令
func (c Cli) Run(shell string) (string, error) {
if c.client == nil {
if err := c.Connect(); err != nil {
return "", err
}
}
session, err := c.client.NewSession()
if err != nil {
return "", fmt.Errorf("create new session error: %w", err)
}
defer session.Close()
buf, err := session.CombinedOutput(shell)
return string(buf), err
}
// Scp 複製文件
func (c Cli) Scp(srcFileName, targetFileName string) (int64, error) {
if c.client == nil {
if err := c.Connect(); err != nil {
return 0, err
}
}
sftpClient, err := sftp.NewClient(c.client)
if err != nil {
return 0, fmt.Errorf("new sftp client error: %w", err)
}
defer sftpClient.Close()
source, err := sftpClient.Open(srcFileName)
if err != nil {
return 0, fmt.Errorf("sftp client open file error: %w", err)
}
defer source.Close()
target, err := os.OpenFile(targetFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return 0, fmt.Errorf("open local file error: %w", err)
}
defer target.Close()
n, err := io.Copy(target, source)
if err != nil {
return 0, fmt.Errorf("copy file error: %w", err)
}
return n, nil
}
// 調用測試
func main() {
var (
username = "your username"
password = "your password"
addr = "ip:22"
)
// 初始化
client := NewCli(username, password, addr)
// ssh 並運行腳本
_, err := client.Run("sh /opt/test.sh")
if err != nil {
log.Printf("failed to run shell,err=[%v]\n", err)
return
}
// scp 文件到本地
filename := time.Now().Format("2006-01-02") + ".log"
n, err := client.Scp("/opt/"+filename, filename)
if err != nil {
log.Printf("failed to scp file,err=[%v]\n", err)
return
}
log.Printf("Succeed to scp file, size=[%d]\n", n)
// 處理文件並刪除本地文件......
}
通過上面的一系列操作,就可以實現了我的需求:
- 編寫程序:
-
連接客戶服務器
-
執行遠程服務器的腳本,生成日誌文件
-
拷貝遠程服務器的日誌文件到本地
-
處理日誌文件
-
刪除本地文件
- 在服務器上啓動一個定時任務運行該程序
總結
本篇文章記錄瞭如何使用 Go語言
連接遠程服務器(ssh),並將遠程服務器的文件拷貝至本地(scp)的方法和過程。
更多
個人博客: https://lifelmy.github.io/
微信公衆號:漫漫 Coding 路
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/nVAAMAW0jIeSMx9oJysUjw