Go 語言實現 ssh - scp

前言


最近遇到一個臨時需求,需要將客戶環境中一個服務每天的日誌進行一系列複雜處理,並生成數據報表。由於數據處理邏輯複雜,且需要存入數據庫,在客戶環境使用 shell 腳本無法處理,因此就需要將日誌先拷貝到本地,再進行處理;同時爲了避免每天人工拷貝日誌,需要實現自動化,整條鏈路自動執行,無需人工干預。平時使用 Go 語言較多,由此就引出了 Go 語言 ssh 連接遠程客戶服務器,並利用 scp 將數據拷貝下來的一系列操作。

說明:本文中的示例,均是基於 Go1.17 64 位機器

連接遠程服務器並執行命令(ssh)

如下給出了使用 用戶名+密碼 的方式連接遠程服務器並執行了 /usr/bin/whoami 命令的示例,步驟如下:

  1. 生成 ClientConfig:想要連接遠程服務器,必須要至少指定一種實現了 AuthAuthMethod,我們這裏使用密碼的方式;同時需要提供 一種用於安全校驗遠程服務端 key 的方法 HostKeyCallback,我們這裏使用的是不校驗的方式 ssh.InsecureIgnoreHostKey(),生產情況下建議使用 ssh.FixedHostKey(key PublicKey)

  2. 調用 DialDial 方法與遠程服務器建立連接,並返回一個 client

  3. NewSessionNewSession方法開啓一個會話,在一個會話內可以通過 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)

拷貝文件步驟比較簡單:

  1. 建立 ssh client

  2. 基於 ssh client 創建 sftp client

  3. 打開遠程服務器文件並拷貝到本地

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 中,還有許多方法,例如 WalkReadDirStatMkdir等,針對文件也有 ReadWriteWriteToReadFrom等方法,像操作本地文件系統一樣,非常便利。

簡單封裝下

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)

 // 處理文件並刪除本地文件......
}

通過上面的一系列操作,就可以實現了我的需求:

  1. 編寫程序:
  1. 在服務器上啓動一個定時任務運行該程序

總結

本篇文章記錄瞭如何使用 Go語言連接遠程服務器(ssh),並將遠程服務器的文件拷貝至本地(scp)的方法和過程。

更多

個人博客: https://lifelmy.github.io/

微信公衆號:漫漫 Coding 路

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/nVAAMAW0jIeSMx9oJysUjw