通过 Pyro4 命令参数上传文件到远端设备上

需求

需要通过云端向本地设备下发规则,由于不支持文件通道,需要通过命令参数的方式下发规则到本地设备中。

有如下约束条件:

  1. 云端与本地设备之间通过 python Pyro4 框架来进行远程调用
  2. 规则是 ebpf 指令码,大小在 kb 级别
  3. python 版本为 python2

如何实现?

经过一通思考交流后,确认实现方案如下:

在云端将 ebpf 指令码使用 gzip 压缩,然后使用 base64 编码为 ascii 码后通过参数传递到本地设备的脚本中,脚本将参数解码,然后使用 gzip 解压后存储到设备上的指定位置中。

同时为了确保安全性,增加 md5sum 校验,文件的源 md5sum 值也通过命令传递,本地设备的脚本在会校验源文件 md5sum,校验失败则删除文件。

示例 demo

#!/usr/bin/python

import sys
import os
from hashlib import md5
import base64
import subprocess
from subprocess import check_call

def generate_file_md5value(fpath):
    m = md5()
    a_file = open(fpath, 'rb')
    m.update(a_file.read())
    a_file.close()
    return m.hexdigest()

def ebpf_base64_code_save(ebpf_code, file_md5sum):
    if len(file_md5sum) == 0:
        return 'must give ebpfcode md5sum value'

    ebpfcode = base64.b64decode(ebpf_code)
    filepath = "/tmp/ebpf.o.gz"
    f = open(filepath, "wb")
    f.write(ebpfcode)
    f.close()
    try:
        check_call('gzip -d -f /tmp/ebpf.o.gz', shell=True)
    except subprocess.CalledProcessError as exc:
        check_call('rm -rf /tmp/ebpf.o.gz')
        return exc.output

    md5sum = generate_file_md5value("/tmp/ebpf.o")

    if md5sum != file_md5sum:
        os.system('rm -rf /tmp/ebpf.o')
        return 'md5sum failed\\n'

    return 'succeed'

def upload_ebpfcode_test(filename):
    md5sum = generate_file_md5value(filename)

    command = 'gzip  ' + filename
    check_call(command, shell=True)

    gzip_filename = filename + '.gz'

    f = open(gzip_filename, "rb")
    component = f.read();
    f.close();

    ebpf_code = base64.b64encode(component)

    command = 'gzip -d -f ' + filename
    check_call(command, shell=True)

    return ebpf_base64_code_save(ebpf_code, md5sum)

def main():
    if (len(sys.argv) > 1):
        print(upload_ebpfcode_test(sys.argv[1]))
    else:
        print("Please input an filename")

if __name__ == "__main__":
    main()

示例 demo 主要流程

upload_ebpfcode_test 在函数开始计算出源文件的 md5sum 值,然后将指定的文件使用 gzip 压缩后进行 base64 编码,最后使用 base64 编码的内容与 md5sum 值作为参数调用 ebpf_base64_code_save 函数。

ebpf_base64_code_save 函数中首先解码 ebpf_code 参数代表的规则文件,然后使用 gzip 解压文件,解压完成后校验文件 md5sum,校验失败则删除文件。

示例 demo 测试

  1. 生成一个文件

    [longyu@debian:15:03:34] tmp $ dd if=/dev/random  of=./data bs=1M count=12
    12+0 records in
    12+0 records out
    12582912 bytes (13 MB, 12 MiB) copied, 0.0874574 s, 144 MB/s
    
  2. 执行 demo 脚本

    [longyu@debian:15:01:57] tmp $ python ./demo.py data
    succeed
    
  3. 查看结果

    [longyu@debian:15:01:59] tmp $ md5sum ./data /tmp/ebpf.o 
    b06ea768af2f76b468b7e3f0fadcb795  ./data
    b06ea768af2f76b468b7e3f0fadcb795  /tmp/ebpf.o
    

md5sum 值相同,测试通过。

本地搭建 Pyro4 环境模拟

我尝试在 debian11 中使用 python Pyro4 来搭建一个与接近实际执行过程的环境,下面是几个关键的内容。

示例代码参考如下官方文档编写:

Intro and Example - Pyro 4.82 documentation

python 安装 Pyro4

执行如下命令安装之:

[longyu@debian:11:34:20] tmp $ sudo pip install Pyro4

Pyro4 测试脚本

服务端 server.py 源码:

# saved as greeting-server.py

import sys
import os
from hashlib import md5
import base64
import subprocess
from subprocess import check_call
import Pyro4
import serpent

def generate_file_md5value(fpath):
    m = md5()
    a_file = open(fpath, 'rb')
    m.update(a_file.read())
    a_file.close()
    return m.hexdigest()

def ebpf_base64_code_save(ebpf_code, file_md5sum):
    if len(file_md5sum) == 0:
        return 'must give ebpfcode md5sum value'

    ebpfcode = base64.b64decode(ebpf_code)
    filepath = "/tmp/ebpf.o.gz"
    f = open(filepath, "wb")
    f.write(ebpfcode)
    f.close()
    try:
        check_call('gzip -d -f /tmp/ebpf.o.gz', shell=True)
    except subprocess.CalledProcessError as exc:
        check_call('rm -rf /tmp/ebpf.o.gz')
        return exc.output

    md5sum = generate_file_md5value("/tmp/ebpf.o")

    if md5sum != file_md5sum:
        os.system('rm -rf /tmp/ebpf.o')
        return 'md5sum failed\n'

    return 'succeed'

@Pyro4.expose
class GreetingMaker(object):
    def save_ebpfcode(self, argument):
        ebpf_code = argument[0]
        ebpf_code = ebpf_code['data'].strip()
        md5sum = argument[1]
        return ebpf_base64_code_save(ebpf_code, md5sum)
    def hello(self):
        return 'hello world\n'

daemon = Pyro4.Daemon()                # make a Pyro daemon
uri = daemon.register(GreetingMaker)   # register the greeting maker as a Pyro object

print("Ready. Object uri =", uri)      # print the uri so we can use it in the client later
daemon.requestLoop()                   # start the event loop of the server to wait for calls

客户端 client.py 源码:

# saved as greeting-client.py
import sys
import os
from hashlib import md5
import base64
import subprocess
from subprocess import check_call
import Pyro4

def generate_file_md5value(fpath):
    m = md5()
    a_file = open(fpath, 'rb')
    m.update(a_file.read())
    a_file.close()
    return m.hexdigest()

uri = input("What is the Pyro uri of the greeting object? ").strip()
if (len(sys.argv[1]) > 1):
    filename = sys.argv[1]
else:
    print('Please input filename\n')
    os.exit(-1)

greeting_maker = Pyro4.Proxy(uri)         # get a Pyro proxy to the greeting object

md5sum = generate_file_md5value(filename)
command = 'gzip  ' + filename
check_call(command, shell=True) 

gzip_filename = filename + '.gz'
f = open(gzip_filename, "rb")
component = f.read();
f.close();

command = 'gzip -d -f ' + filename
check_call(command, shell=True)

argument = [component, md5sum]
print(greeting_maker.save_ebpfcode(argument))

测试过程

  1. 运行服务端

    [longyu@debian:13:21:03] tmp $ python3 ./server.py 
    Ready. Object uri = PYRO:obj_f51af1f1695f4f47a779aacb4936eb0e@localhost:38207
    

    打印出来的 PYRO:obj_f51af1f1695f4f47a779aacb4936eb0e@localhost:38207 包含连接信息,客户端连接的时候会使用。

  2. 运行客户端

    [longyu@debian:13:22:51] tmp $ python3 ./client.py data
    What is the Pyro uri of the greeting object? PYRO:obj_f51af1f1695f4f47a779aacb4936eb0e@localhost:38207
    succeed
    

    客户端需要输入服务端打印出来的 uri 信息来建立连接。

  3. 文件 md5sum 校验

    [longyu@debian:13:23:03] tmp $ md5sum ./data /tmp/ebpf.o 
    c4fa672da73f38f9441d2c9638848269  ./data
    c4fa672da73f38f9441d2c9638848269  /tmp/ebpf.o
    

测试中发现的一些问题记录

  1. python 版本问题

    使用 pip 安装好 Pyro4 module 后,我尝试使用 python 来 import 这个模块测试,结果发现这个模块并不存在,一通尝试发现我的系统中 pip 命令是 python3 的命令,而默认的 python 链接却指向 python2,造成 Pyro4 模块无法导入。这里我并没有注意到我使用的 python 版本与真实环境的版本不一致。

  2. Pyro4 函数参数传递问题

    最开始我在 server.py 的 save_ebpfcode 函数中定义了三个参数,使用时发现并不支持,需要将多个参数拼接成一个 list 来传递。

  3. 为什么 client.py 中没有对压缩后的文件进行 base64 编码?

    最开始的时候我使用 demo.py 中的代码对压缩后的文件进行 base64 编码,然后传递到远程调用参数中,测试发现在服务端脚本执行时解压文件一直报文件不是 gzip 压缩格式。

    经过一通分析发现 python3 中 base64.b64encode(component) 返回的数据类型为 binary 格式,这种格式在 Pyro4 中会存在如下问题:

    在这里插入图片描述

    简单点说,就是当参数类型为 binary 的时候,Pyro4 会对参数进行 base64 编码并转化为 {'data': 'aXJtZW4gZGUgam9uZw==', 'encoding': 'base64'} 这种格式,表面上看去好像 data key 的 value 就是传递的参数,其实这个参数又被执行了一次 base64 编码。

    于是在 client.py 中直接将压缩后的文件内容作为参数传递,同时将 server.py 中的解析代码修改为如下内容:

        def save_ebpfcode(self, argument):
            ebpf_code = argument[0]
            ebpf_code = ebpf_code['data'].strip()
            md5sum = argument[1]
            return ebpf_base64_code_save(ebpf_code, md5sum)
    

python2 使用 Pyro4 进行测试

debian11 上安装 python2 pip 与 python2 Pyro4 module

我参考 https://blog.emacsos.com/pip2-in-debian-11-bullseye.html 这篇链接的内容安装,使用 wget 下载 get-pip.py 发现速度很慢,尝试配置 socks5 代理发现 wget 并不支持,尝试使用 tsocks 命令时想到可以使用已经配置好代理的浏览器直接下载,果然很快。

安装过程如下:

[longyu@debian:14:25:51] Downloads $ python2 ./get-pip.py 
DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.
Defaulting to user installation because normal site-packages is not writeable
Collecting pip<21.0
  Downloading pip-20.3.4-py2.py3-none-any.whl (1.5 MB)
     |████████████████████████████████| 1.5 MB 11 kB/s 
Collecting setuptools<45
  Using cached setuptools-44.1.1-py2.py3-none-any.whl (583 kB)
Collecting wheel
  Downloading wheel-0.37.1-py2.py3-none-any.whl (35 kB)
Installing collected packages: pip, setuptools, wheel
  WARNING: The scripts pip, pip2 and pip2.7 are installed in '/home/longyu/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
  WARNING: The scripts easy_install and easy_install-2.7 are installed in '/home/longyu/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
  WARNING: The script wheel is installed in '/home/longyu/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed pip-20.3.4 setuptools-44.1.1 wheel-0.37.1

执行如下命令安装 Pyro4 module:

[longyu@debian:14:28:56] Downloads $ python2 -m pip install Pyro4
DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.
Defaulting to user installation because normal site-packages is not writeable
Collecting Pyro4
  Using cached Pyro4-4.82-py2.py3-none-any.whl (89 kB)
Collecting serpent<1.30,>=1.27; python_version < "3.2"
  Downloading serpent-1.28-py2.py3-none-any.whl (11 kB)
Collecting selectors34; python_version < "3.4"
  Downloading selectors34-1.2-py2.py3-none-any.whl (8.2 kB)
Collecting six
  Downloading six-1.16.0-py2.py3-none-any.whl (11 kB)
Installing collected packages: serpent, six, selectors34, Pyro4
  WARNING: The scripts pyro4-check-config, pyro4-flameserver, pyro4-httpgateway, pyro4-ns, pyro4-nsc and pyro4-test-echoserver are installed in '/home/longyu/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed Pyro4-4.82 selectors34-1.2 serpent-1.28 six-1.16.0

测试代码

服务端 server.py 源码:

# saved as greeting-server.py

import sys
import os
from hashlib import md5
import base64
import subprocess
from subprocess import check_call
import Pyro4
import serpent

def generate_file_md5value(fpath):
    m = md5()
    a_file = open(fpath, 'rb')
    m.update(a_file.read())
    a_file.close()
    return m.hexdigest()

def ebpf_base64_code_save(ebpf_code, file_md5sum):
    if len(file_md5sum) == 0:
        return 'must give ebpfcode md5sum value'

    ebpfcode = base64.b64decode(ebpf_code)
    filepath = "/tmp/ebpf.o.gz"
    f = open(filepath, "wb")
    f.write(ebpfcode)
    f.close()
    try:
        check_call('gzip -d -f /tmp/ebpf.o.gz', shell=True)
    except subprocess.CalledProcessError as exc:
        check_call('rm -rf /tmp/ebpf.o.gz')
        return exc.output

    md5sum = generate_file_md5value("/tmp/ebpf.o")

    if md5sum != file_md5sum:
        os.system('rm -rf /tmp/ebpf.o')
        return 'md5sum failed\n'

    return 'succeed'

@Pyro4.expose
class GreetingMaker(object):
    def save_ebpfcode(self, argument):
        ebpf_code = argument[0]
        md5sum = argument[1]
        return ebpf_base64_code_save(ebpf_code, md5sum)
    def hello(self):
        return 'hello world\n'

daemon = Pyro4.Daemon()                # make a Pyro daemon
uri = daemon.register(GreetingMaker)   # register the greeting maker as a Pyro object

print("Ready. Object uri =", uri)      # print the uri so we can use it in the client later
daemon.requestLoop()                   # start the event loop of the server to wait for calls

客户端 client.py 源码:

# saved as greeting-client.py
import sys
import os
from hashlib import md5
import base64
import subprocess
from subprocess import check_call
import Pyro4

def generate_file_md5value(fpath):
    m = md5()
    a_file = open(fpath, 'rb')
    m.update(a_file.read())
    a_file.close()
    return m.hexdigest()

uri = input("What is the Pyro uri of the greeting object? ")
if (len(sys.argv[1]) > 1):
    filename = sys.argv[1]
else:
    print('Please input filename\n')
    os.exit(-1)

greeting_maker = Pyro4.Proxy(uri)         # get a Pyro proxy to the greeting object

md5sum = generate_file_md5value(filename)
command = 'gzip  ' + filename
check_call(command, shell=True) 

gzip_filename = filename + '.gz'
f = open(gzip_filename, "rb")
component = f.read();
f.close();

ebpf_code = base64.b64encode(component)

command = 'gzip -d -f ' + filename
check_call(command, shell=True)

argument = [ebpf_code, md5sum]
print argument
print(greeting_maker.save_ebpfcode(argument))

测试过程

运行服务器端:

[longyu@debian:14:41:29] tmp $ python ./server.py 
('Ready. Object uri =', <Pyro4.core.URI at 0x7f0be2ed2810; PYRO:obj_ae6d77e3e4474bdabb769457c1bcd2e6@localhost:33951>)

运行客户端:

[longyu@debian:14:38:48] tmp $ python ./client.py ./test
What is the Pyro uri of the greeting object? 'PYRO:obj_ae6d77e3e4474bdabb769457c1bcd2e6@localhost:33951'
succeed

文件 md5sum 对比:

[longyu@debian:14:42:28] tmp $ md5sum ./test ./ebpf.o 
d8e8fca2dc0f896fd7cb4cb0031ba249  ./test
d8e8fca2dc0f896fd7cb4cb0031ba249  ./ebpf.o

测试过程中发现的问题记录

  1. python2 中 base64.b64encode 返回值的类型为 u,使用 Pyro4 传递参数的时候不会被再次编码,client.py 这里的逻辑与 demo.py 一致

  2. python2 的 input 输出函数中,要输入特殊字符,需要添加单引号

    将 uri 包含在一对单引号中就能够正常输入,否则 : 等特殊字符无法输入,python 脚本运行报错。

总结

写作本文的时候,由于忽略了 python 的版本多走了许多弯路,最终将 python3 与 python2 的不同测试过程都进行了描述。

将文件转化为参数传递不太符合常规认知,可是在特定的场景中它也有用武之地,能够这样做的基础认知是参数也是数据,在网络通信中,文件也是一种数据,这两者并没有什么区别。

参考链接

Intro and Example - Pyro 4.82 documentation

Why do I need ‘b’ to encode a string with Base64?

Tips & Tricks - Pyro 4.82 documentation

Install pip for Python2.7 in Debian 11 Bullseye

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值