paramiko遍历嵌套文件夹上传到linux服务器,并执行sh脚本

发布时间 2023-04-28 15:52:47作者: cloudguest

场景:由于工作原因,开发打包后都要上传包到对应linux 服务器,并执行对应shell脚本,替换包内配置文件,启动服务。换包频率过于频繁,因此需要实现一种不用打开xshell、xftp的方法,直接将包放在本地文件,双击exe运行所有操作,以节省时间,想到使用python的paramiko、pyinstaller模块实现。

功能分析:

  • 1、遍历本地嵌套文件夹获取文件
  • 2、上传到远程linux服务器
  • 3、上传文件进度条
  • 4、程序运行日志按时间存放本地
  • 5、打包成exe,双击就可以运行

代码实现:

autoChangePackage.py

import paramiko
import os
import time
import sys, math, select
import log


def get_all_files_in_local_dir(local_dir):
    """递归获取当前目录下所有文件目录"""
    all_files = []
    # 获取当前指定目录下的所有目录及文件,包含属性值
    files = os.listdir(local_dir)
    for x in files:
        # local_dir目录中每一个文件或目录的完整路径
        filename = os.path.join(local_dir, x)
        # 如果是目录,则递归处理该目录
        if os.path.isdir(filename):
            all_files.extend(get_all_files_in_local_dir(filename))
        else:
            all_files.append(filename)
    return all_files


def progressbar(cur, total):
    percent = '{:.2%}'.format(cur / total)
    sys.stdout.write('\r')
    sys.stdout.write('[%-50s] %s' % ('=' * int(math.floor(cur * 50 / total)), percent))
    sys.stdout.flush()
    if cur == total:
        sys.stdout.write('\n')


class Dossh():

    def __init__(self, ip, port, uname, passwd):
        self.ip = ip
        self.port = port
        self.uname = uname
        self.passwd = passwd
        self.sshclt = paramiko.SSHClient()
        self.sshclt.load_system_host_keys()
        self.sshclt.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.sshclt.connect(hostname=self.ip, port=self.port, username=self.uname, password=self.passwd,
                            allow_agent=False, look_for_keys=False)
        self.t = paramiko.Transport((self.ip, self.port))
        self.t.connect(username=self.uname, password=self.passwd)
        self.sftp = paramiko.SFTPClient.from_transport(self.t)

    def getssh(self):
        return self.sshclt

    def close_ssh(self):
        self.sshclt.close()
        self.sftp.close()

    def uploadfile_path(self, local_path, remote_path):
        """
        :param local_path:待上传文件夹路径
        :param remote_path:远程路径
        :return:
        """
        # 待上传目录名
        local_pathname = os.path.split(local_path)[-1]
        # 上传远程后的目录名
        real_remote_Path = remote_path + '/' + local_pathname
        ##判断是否存在,不存在则创建
        try:
            self.sftp.stat(remote_path)
        except Exception as e:
            self.sshclt.exec_command("mkdir -p %s" % remote_path)
        self.sshclt.exec_command("mkdir -p %s" % real_remote_Path)
        # 获取本地文件夹下所有文件路径
        all_files = get_all_files_in_local_dir(local_path)
        # 依次判断远程路径是否存在,不存在则创建,然后上传文件
        for file_path in all_files:
            # 统一win和linux 路径分隔符
            file_path = file_path.replace("\\", "/")
            # 用本地根文件夹名分隔本地文件路径,取得相对的文件路径
            # 必须加变量1,仅切割一次,否则会将二级目录下与一级目录名相同的文件上传错误
            off_path_name = file_path.split(local_pathname, 1)[-1]
            # 取得本地存在的嵌套文件夹层级
            abs_path = os.path.split(off_path_name)[0]
            # 生产期望的远程文件夹路径
            reward_remote_path = real_remote_Path + abs_path
            # 判断期望的远程目录是否存在,不存在则创建
            try:
                self.sftp.stat(reward_remote_path)
            except Exception as e:
                self.sshclt.exec_command("mkdir -p %s" % reward_remote_path)
            # 待上传的文件名
            abs_file = os.path.split(file_path)[1]
            # 上传后的远端路径,文件名不变
            to_remote = reward_remote_path + '/' + abs_file
            time.sleep(0.1)
            #  callback=progressbar,添加文件上传进度条
            self.sftp.put(file_path, to_remote, callback=progressbar)
            print(file_path, to_remote)

    def execute_cmd(self, cmd):
        current_cmd = "bash --login -c '%s'" % cmd
        stdin, stdout, stderr = self.sshclt.exec_command(current_cmd, get_pty=True, bufsize=1024 * 1024 * 100)
        res, err = stdout.read(), stderr.read()

        result = res if res else err
        return result.decode('utf-8')

    def do_tail(self, cmd):
        transport = self.sshclt.get_transport()
        channel = transport.open_session()
        channel.exec_command(cmd)

        while 1:
            try:
                rl, _, _ = select.select([channel], [], [], 0.0)
                if len(rl) > 0:
                    print("----------------do_tail服务开始读取日志-------------")
                    for line in self.linesplit(channel):
                        print(line)
            except (KeyboardInterrupt, SystemExit):
                print('do_tail服务日志读取异常...')
                break

    def linesplit(self, socket):
        buffer_bytes = socket.recv(4048)
        buffer_string = buffer_bytes.decode('utf-8', 'ignore')
        done = False
        while not done:
            if "\n" in buffer_string:
                (line, buffer_string) = buffer_string.split("\n", 1)
                yield line + "\n"
            else:
                more = socket.recv(4048)
                if not more:
                    done = True
                else:
                    buffer_string = buffer_string + more.decode('utf-8', 'ignore')
        if buffer_string:
            yield buffer_string


if __name__ == "__main__":

    # 自定义目录存放日志文件
    log_path = './Logs/'
    if not os.path.exists(log_path):
        os.makedirs(log_path)
    # 日志文件名按照程序运行时间设置
    log_file_name = log_path + 'log-' + time.strftime("%Y%m%d-%H%M%S", time.localtime()) + '.log'
    # 记录正常的 print 信息
    sys.stdout = log.Logger(log_file_name)
    # 记录 traceback 异常信息
    sys.stderr = log.Logger(log_file_name)
    print("==========1. 日志记录文件创建成功,见%s==========" % log_file_name)

    print("==========2. 开始连接服务器==========")
    # 服务器信息
    ma_ip = '*.*.*.*'
    ma_port = 22
    ma_user = ''
    ma_passwd = ''
    sshclent = Dossh(ma_ip, int(ma_port), ma_user, ma_passwd)
    print("==========3. 服务器连接成功==========")
    print("==========4. 开始从本地上传文件到106服务器==========")
    # 上传package包(jar+lib)
    # 本地目录,建议使用相对路径,如:'./test'
    # linux服务器目录,绝对路径,如:'/home/'
    # 会本地test目录下所有文件上传到linux服务器的/home/test
    # a、目录存在则仅覆盖文件
    # b、目录不存在则先创建目录,再上传目录下的文件
    sshclent.uploadfile_path("本地目录", 'linux服务器目录')

    # 查看linux服务器上test目录下的文件
    # cmd_ll = 'ls -l  /home/test'
    print("==========5. 文件上传成功==========")
    print("==========6. 开始执行shell脚本==========")
    cmd_shell = 'sh /home/test/test.sh'
    sshclent.execute_cmd(cmd_shell)
    time.sleep(0.1)
    # 由于换包脚本会产生执行日志,每次需要通过查看日志运行状态,来判断配置文件是否替换,服务是否启动成功
    # 但又因为execute_cmd()方法执行tail命令,控制台没有回显,因此使用do_tail()方法查看log
    print("==========7. 开始打印脚本执行日志==========")
    cmd_tailf = 'tail -F  /home/tets/test.log'
    sshclent.do_tail(cmd_tailf)
    sshclent.close_ssh()

log.py

import sys


# 控制台输出记录到文件
class Logger(object):
    def __init__(self, file_name="Default.log", stream=sys.stdout):
        self.terminal = stream
        self.log = open(file_name, "a", encoding='utf-8')

    def write(self, message):
        self.terminal.write(message)
        self.log.write(message)

    def flush(self):
        pass

打包成exe文件

1、安装pyinstaller 模块

2、pycharm编辑器控制台输入pyinstaller -F autoChangePackage.py

3、打开资源管理器,进入对应工程的dist目录下,会看见生成了autoChangePackage.exe

4、双击运行如果提示缺失配置文件,复制python安装目录下所有dll文件到exe目录,就可以成功,但一般很少遇见

注:涉及到的问题解决方法百度都有

参考文献

1、python之paramiko文件夹远程上传
2、Python 将控制台输出日志文件