模拟 saltstack/ansible 系列三(基于短连接

前言

前面讲了基于SSH的ansible实现,今天我来讲讲基于短连接方式的saltstack实现。

为什么要讲基于短连接的saltstack实现呢?

前面不是说saltstack是基于长连接的吗?原因就在于当你掌握了短连接的实现后,长连接就水到渠成了,你也更能理解到短连接和长连接实现的一个区别

Agent设计

不管是短连接实现还是长连接实现,都需要在远程主机起一个服务来承担Agent角色,就如同saltstack的salt-minion一样。

下面我们就基于flask来简单实现一个短连接形式的agent(考虑性能的话可以使用sanic,这里用flask的原因是本人团队内部培训时,我主要培训的是flask

由于flask属于第三方库,所以需要使用如下命令先行安装

1
shell复制代码pip install flask

如果想做成和salt-minion二进制模式启动的话,我们还需要安装一个额外的模块pyinstaller

1
shell复制代码pip install pyinstaller

以下是flask作为一个服务端基本的代码: app.py

1
2
3
4
5
6
7
8
9
10
python复制代码from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
return "Hello, World!"

if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)

当我们执行这个代码时,程序将会监听8080端口,请求http://127.0.0.1:8080将会返回Hello, World!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码$ python3 app.py

* Serving Flask app 'app' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on all addresses.
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 142-934-530

$ http http://127.0.0.1:8080

HTTP/1.0 200 OK
Content-Length: 13
Content-Type: text/html; charset=utf-8
Date: Wed, 24 Nov 2021 14:46:12 GMT
Server: Werkzeug/2.0.2 Python/3.10.0

Hello, World!

此时,我们可以通过pyinstaller将该程序编译成一个二进制程序

1
bash复制代码pyinstaller -F app.py

saltstack功能分析与代码实现

连接远程主机

基于短连接实现的agent,连接远程主机功能只需要判断接口是否能通即可,如上我们请求http://127.0.0.1:8080时返回200状态码,即可认为成功连接远程主机。

test.ping

ping功能的实现,我们可以写个ping接口,然后返回pong来实现,或者根据返回状态码是否为200来判断

我们把上边的app.py修改一下,加上ping接口

1
2
3
4
5
6
7
8
9
10
python复制代码from flask import Flask

app = Flask(__name__)

@app.route("/ping", methods=["GET"])
def ping():
return "pong"

if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)

此时我们请求http://127.0.0.1:8080/ping则会返回pong,此时即可认为远程主机在线

1
2
3
4
5
6
7
8
9
python复制代码$ http http://127.0.0.1:8080/ping

HTTP/1.0 200 OK
Content-Length: 4
Content-Type: text/html; charset=utf-8
Date: Wed, 24 Nov 2021 15:04:58 GMT
Server: Werkzeug/2.0.2 Python/3.10.0

pong

cmd.run

执行命令我们可以再写一个接收POST请求的接口,接收一个参数,用于命令的传入,然后根据传入的命令在本地主机执行命令,获取命令返回的结果后响应给客户端。

我们把上边的app.py增加一个cmd接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码from flask import Flask, request
import subprocess

app = Flask(__name__)

@app.route("/ping", methods=["GET"])
def ping():
return "pong"

@app.route("/cmd", methods=["post"])
def cmd():
body = request.json
cmd = body.get("cmd")
# 基于subprocess.Popen方法执行本地shell命令
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
if proc:
result = proc.stdout.read()
return result
else:
return None

if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)

此时我们请求http://127.0.0.1:8080/cmd,传入命令,则会返回命令结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码$ http http://127.0.0.1:8080/cmd cmd='df -h'

HTTP/1.0 200 OK
Content-Length: 590
Content-Type: text/html; charset=utf-8
Date: Wed, 24 Nov 2021 15:25:24 GMT
Server: Werkzeug/2.0.2 Python/3.8.10

Filesystem Size Used Avail Use% Mounted on
udev 957M 0 957M 0% /dev
tmpfs 198M 1.1M 197M 1% /run
/dev/sda1 20G 1.5G 18G 8% /
tmpfs 990M 0 990M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 990M 0 990M 0% /sys/fs/cgroup
/dev/sda15 98M 290K 98M 1% /boot/efi
/dev/loop0 61M 61M 0 100% /snap/lxd/21843
/dev/loop2 29M 29M 0 100% /snap/snapd/13643
/dev/loop1 58M 58M 0 100% /snap/core20/1244
tmpfs 198M 0 198M 0% /run/user/1000

上传文件

执行命令我们可以再写一个接收文件的接口,将客户端上传过来的文件保存到本地,可以接收一个目标目录的参数,用做存放文件的目录

我们把上边的app.py增加一个upload_file接口

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
python复制代码from flask import Flask, request
import subprocess

app = Flask(__name__)

@app.route("/ping", methods=["GET"])
def ping():
return "pong"

@app.route("/cmd", methods=["post"])
def cmd():
body = request.json
cmd = body.get("cmd")
# 基于subprocess.Popen方法执行本地shell命令
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
if proc:
result = proc.stdout.read()
return result
else:
return None

@app.route("/upload_file", methods=["post"])
def upload_file():
# 从请求中获取文件,如果上传的不是以file为key的文件,则认为未上传文件
if "file" not in request.files:
return "not find file in request.files"
file = request.files["file"]
# 如果请求没有带dest参数,则dest默认为/opt
dest = request.args.get("dest", "/opt")
if file:
file.save(dest)
return "file uploaded successfully"
return "upload error"

if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)

客户端代码如下:

1
2
3
4
5
6
7
8
9
python复制代码def upload_file(src, dest):
resp = requests.post(
"http://127.0.0.1:8080/upload_file?dest=" + dest,
files={"file": open(src, "rb")},
)
if resp.status_code == 200:
return resp.text
else:
return None

此时客户端请求http://127.0.0.1:8080/upload_file,传入参数,则上传文件成功

下载文件

下载文件我们可以写个接口,接收文件所在目录地址和文件名2个参数

我们把上边的app.py增加一个download_file接口

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
python复制代码from flask import Flask, request
import subprocess

app = Flask(__name__)

@app.route("/ping", methods=["GET"])
def ping():
return "pong"

@app.route("/cmd", methods=["post"])
def cmd():
body = request.json
cmd = body.get("cmd")
# 基于subprocess.Popen方法执行本地shell命令
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
if proc:
result = proc.stdout.read()
return result
else:
return None

@app.route("/upload_file", methods=["post"])
def upload_file():
# 从请求中获取文件,如果上传的不是以file为key的文件,则认为未上传文件
if "file" not in request.files:
return "not find file in request.files"
file = request.files["file"]
# 如果请求没有带dest参数,则dest默认为/opt
dest = request.args.get("dest", "/opt")
if file:
file.save(dest)
return "file uploaded successfully"
return "upload error"

@app.route("/download_file", methods=["post"])
def download_file():
body = request.json
directory = body.get("directory")
filename = body.get("filename")
# send_from_directory用于发送本地的文件
return send_from_directory(
directory=directory, filename=filename, as_attachment=True
)


if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)

客户端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码def download_file(src, dest):
filename = src.split("/")[-1]
directory = src.replace(filename, "")
print(directory, filename)
data = {"directory": directory, "filename": filename}
resp = requests.post("http://127.0.0.1:8080/download_file", json=data)
if resp.status_code == 200:
with open(dest, "wb") as f:
f.write(resp.content)
return True
else:
return False

以上接口基本上实现saltstack常用功能。下一文章我们来讲讲长连接实现saltstack基本功能

相关系列文章同步发布于个人博客
Jackless

模拟 saltstack/ansible 系列一(序言)

模拟 saltstack/ansible 系列二(实现 ansible 主要功能)

模拟 saltstack/ansible 系列三(基于短连接实现 saltstack 主要功能)

模拟 saltstack/ansible 系列四(基于长连接实现 saltstack 主要功能)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%