前言

最近遇到一个临时需求,需要将客户环境中一个服务每天的日志进行一系列复杂处理,并生成数据报表。由于数据处理逻辑复杂,且需要存入数据库,在客户环境使用 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 方法执行一个命令。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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 目录下,脚本内容 与 调用方式分别如下:

1
2
3
4
5
# 脚本文件
#!/bin/bash
today=$(date +"%Y-%m-%d")
# 将数据写入文件
$(df -h > $today.log)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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. 打开远程服务器文件并拷贝到本地
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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等方法,像操作本地文件系统一样,非常便利。

简单封装下

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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. 编写程序:

    • 连接客户服务器

    • 执行远程服务器的脚本,生成日志文件

    • 拷贝远程服务器的日志文件到本地

    • 处理日志文件

    • 删除本地文件

  2. 在服务器上启动一个定时任务运行该程序

总结

本篇文章记录了如何使用 Go语言连接远程服务器(ssh),并将远程服务器的文件拷贝至本地(scp)的方法和过程。

更多

微信公众号:CodePlayer