首先,这里面包含两个模块metaupdater

meta负责接收updater上传上来的设备软件运行信息,以及下发运行软件的地址等相关参数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
{
"name": "falcon-agent",
"version": "1.0.0",
"tarball": "http://11.11.11.11:8888/falcon",
"md5": "http://11.11.11.11:8888/falcon",
"cmd": "start" // 运行参数
},
{
"name": "dinp-agent",
"version": "2.0.0",
"tarball": "http://11.11.11.11:8888/dinp",
"md5": "",
"cmd": "stop"
}
]

updater则被用来下载软件,以及上报软件运行状态信息。

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"name": "falcon-agent",
"version": "1.0.0",
"status": "started"
},
{
"name": "dinp-agent",
"version": "2.0.0",
"status": "stoped"
}
]

流程简述

两个模块都比较简单,不过也是由于简单它们也就被我看上了,在我的项目中,meta是需要和服务器通信的,但服务器不需要meta将数据发送给服务器进行保存(可以指定其监听相应的hostname),updater基本是4min向meta心跳一次。

信息流动

服务器在得到更新或添加软件的信后,主动向meta下发下载软件的地址指令,meta附加上用户信息、PC信息等其他信息进行下载请求(这里可以交由其他模块进行下载操作,下载完成后通知到meta模块即可)。

新增功能

bash指令控制

下发的指令多添加一些bash指令,主要是为了给出一些自定义的操作,对于这个操作一般也只会针对一两个设备先进行测试,测试没发现问题后,再逐步将这些指令更新入control文件,当然,这是长期的过程。

防止cmd出问题,协程无法退出的问题,当然也是可以利用contorl文件中的方法的,利用linux命令来kill掉这个卡住的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   ctx, cancelFunc := context.WithCancel(context.TODO())

go func() { // 会阻塞,开个协程

cmd := exec.CommandContext(ctx, `sh`, "-c", "sleep 2;echo hello;")
// 执行任务, 捕获输出
output, err := cmd.CombinedOutput()

chan <- output // 抛出结果
}

// select等待时间,等不到就调用cancelFunc
// 取消上下文
cancelFunc()

跳板机的操作

在实际的应用中,设备都处于内网环境下,当我们需要对设备进行操作时,就需要跳板机了,这里不需要太过于复杂的跳板机操作,不过这里需要对接自家的后台用户的验证,刚好网上有一些简单的跳板机server,这里的实现,多是参考网上代码。

跳板机不仅仅是需要使用到密码进行验证,这里,拉出一个动态验证密码(手机、邮箱、站内信),设定一个超时时间。

可以通过control脚本来控制跳板机服务的起停,跳板机的状态和重启操作这里是直接塞给了meta,毕竟算起来,没有违背meta设计简单的初衷。

参考:https://blog.yumaojun.net/2017/02/23/go-ssh-server/ 服务端

参考:https://mritd.me/2018/11/09/go-interactive-shell/ 客户端

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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package main

import (
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os/exec"
"sync"
"syscall"
"unsafe"

"github.com/kr/pty"
"golang.org/x/crypto/ssh"
)

func main() {
// In the latest version of crypto/ssh (after Go 1.3), the SSH server type has been removed
// in favour of an SSH connection type. A ssh.ServerConn is created by passing an existing
// net.Conn and a ssh.ServerConfig to ssh.NewServerConn, in effect, upgrading the net.Conn
// into an ssh.ServerConn
config := &ssh.ServerConfig{
//Define a function to run when a client attempts a password login
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
// Should use constant-time compare (or better, salt+hash) in a production setting.
if c.User() == "foo" && string(pass) == "bar" {
return nil, nil
}
return nil, fmt.Errorf("password rejected for %q", c.User())
},
// You may also explicitly allow anonymous client authentication, though anon bash
// sessions may not be a wise idea
// NoClientAuth: true,
}
// You can generate a keypair with 'ssh-keygen -t rsa'
privateBytes, err := ioutil.ReadFile("id_rsa")
if err != nil {
log.Fatal("Failed to load private key (./id_rsa)")
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
log.Fatal("Failed to parse private key")
}
config.AddHostKey(private)
// Once a ServerConfig has been configured, connections can be accepted.
listener, err := net.Listen("tcp", "0.0.0.0:2200")
if err != nil {
log.Fatalf("Failed to listen on 2200 (%s)", err)
}
// Accept all connections
log.Print("Listening on 2200...")
for {
tcpConn, err := listener.Accept()
if err != nil {
log.Printf("Failed to accept incoming connection (%s)", err)
continue
}
// Before use, a handshake must be performed on the incoming net.Conn.
sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config)
if err != nil {
log.Printf("Failed to handshake (%s)", err)
continue
}
log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
// Discard all global out-of-band Requests
go ssh.DiscardRequests(reqs)
// Accept all channels
go handleChannels(chans)
}
}
func handleChannels(chans <-chan ssh.NewChannel) {
// Service the incoming Channel channel in go routine
for newChannel := range chans {
go handleChannel(newChannel)
}
}
func handleChannel(newChannel ssh.NewChannel) {
// Since we're handling a shell, we expect a
// channel type of "session". The also describes
// "x11", "direct-tcpip" and "forwarded-tcpip"
// channel types.
if t := newChannel.ChannelType(); t != "session" {
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t))
return
}
// At this point, we have the opportunity to reject the client's
// request for another logical connection
connection, requests, err := newChannel.Accept()
if err != nil {
log.Printf("Could not accept channel (%s)", err)
return
}
// Fire up bash for this session
bash := exec.Command("bash")
// Prepare teardown function
close := func() {
connection.Close()
_, err := bash.Process.Wait()
if err != nil {
log.Printf("Failed to exit bash (%s)", err)
}
log.Printf("Session closed")
}
// Allocate a terminal for this channel
log.Print("Creating pty...")
bashf, err := pty.Start(bash)
if err != nil {
log.Printf("Could not start pty (%s)", err)
close()
return
}
//pipe session to bash and visa-versa
var once sync.Once
go func() {
io.Copy(connection, bashf)
once.Do(close)
}()
go func() {
io.Copy(bashf, connection)
once.Do(close)
}()
// Sessions have out-of-band requests such as "shell", "pty-req" and "env"
go func() {
for req := range requests {
switch req.Type {
case "shell":
// We only accept the default shell
// (i.e. no command in the Payload)
if len(req.Payload) == 0 {
req.Reply(true, nil)
}
case "pty-req":
termLen := req.Payload[3]
w, h := parseDims(req.Payload[termLen+4:])
SetWinsize(bashf.Fd(), w, h)
// Responding true (OK) here will let the client
// know we have a pty ready for input
req.Reply(true, nil)
case "window-change":
w, h := parseDims(req.Payload)
SetWinsize(bashf.Fd(), w, h)
}
}
}()
}

// parseDims extracts terminal dimensions (width x height) from the provided buffer.
func parseDims(b []byte) (uint32, uint32) {
w := binary.BigEndian.Uint32(b)
h := binary.BigEndian.Uint32(b[4:])
return w, h
}

// Winsize stores the Height and Width of a terminal.
type Winsize struct {
Height uint16
Width uint16
x uint16 // unused
y uint16 // unused
}

// SetWinsize sets the size of the given pty.
func SetWinsize(fd uintptr, w, h uint32) {
ws := &Winsize{Width: uint16(w), Height: uint16(h)}
syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws)))
}