用 python 寫一個自動化部署工具

大话性能發表於2024-03-14

效果

起因

現在 springboot 專案的自動化部署已經非常普遍,有用 Jenkins 的,有用 git 鉤子函式的,有用 docker 的...等等。這段時間在玩 python,想著用 python 實現自動化部署,即能鍛鍊下編碼能力,又方便運維。於是開始著手寫了一個 exe 程式,可直接在任何 windows 電腦上執行(不具備 python 環境的 windows 電腦也可以執行)。有興趣的小夥伴可以跟著程式碼一起練一練噢,寫的詳細一點,對 python 新手也很友好。

實現步驟

開發準備

  1. 具有 python 基本環境和 ide 的 windows 或 macOS 電腦一臺
  2. 安裝打包工具pip install pyinstaller
  3. 一點小小的 python 基礎

步驟

1. 匯入依賴

新建一個 py 檔案,可以把它命名為 deployment.py(名字隨意哈,什麼名兒都可以),然後把下面的庫匯入語句 copy 到此 py 檔案中

python
複製程式碼
import os #用於-提取檔名  
import re #用於-正規表示式  
import time #用於-執行緒休眠  
import paramiko #用於-遠端執行linux命令  
from alive_progress import alive_bar #用於-進度條工具類  
from cryptography.fernet import Fernet #用於-加解密程式碼  
import base64 #用於-加解密程式碼  
import hashlib #用於-加解密程式碼

在匯入依賴的時候,可能有些依賴咱們的電腦上之前沒下載過,不要緊,只需要在 pycharm 中按 alt+enter就可以自動匯入了,PyCharm 跟 Idea 的快捷鍵一模一樣,可以按 Idea 的習慣使用。而且在 python 中還不用配置 maven 或 pom 檔案,非常方便。

2. 輸入校驗

部署畢竟是件嚴謹的事情,我們增加個部署金鑰校驗,我的這個部署金鑰承擔了以下的功能

  1. 確保部署的安全性,不是誰拿到這個 exe 程式都能執行的(哼~傲嬌)
  2. 金鑰字串用 - 分割開,前面的區分環境,後面的區分專案或模組。
  3. 如果同學們不需要區分專案子模組,就不需要搞這麼複雜,隨便定義一個金鑰就好了
import os #用於-提取檔名  
import re #用於-正規表示式  
import time #用於-執行緒休眠  
import paramiko #用於-遠端執行linux命令  
from alive_progress import alive_bar #用於-進度條工具類  
from cryptography.fernet import Fernet #用於-加解密程式碼  
import base64 #用於-加解密程式碼  
import hashlib #用於-加解密程式碼  

#檢查金鑰格式
def check_deploy_sign(deploy_site):  
    #確保金鑰只能是以下4個之一才能繼續往下操作,否則無限迴圈輸入 或 退出程式
    if deploy_site != 'pro-main' and deploy_site != 'pro-manage' and deploy_site != 'test-main' and deploy_site != 'test-manage': 
        #校驗失敗,一直校驗
        new_deploy_site = input("錯誤:請填寫部署金鑰:")  
        check_deploy_sign(new_deploy_site)  
     #校驗成功,退出
     return deploy_site  


try:  
    deploy_sign = input("提示:請填寫部署金鑰:")  
    deploy_sign = check_deploy_sign(deploy_sign)  

    # 部署環境  pro代表生成環境,test代表測試環境
    deploy_server = deploy_sign.split('-')[0]  
    # 部署模組或專案 manage代表manage模組,main代表main模組, 
    deploy_site = deploy_sign.split('-')[1]  
    # 打包時的包名,三目運算子
    package_name = 'production' if deploy_server == 'pro' else 'staging'  

except Exception as e:  
    print(f"異常: {str(e)}")  

上面的程式碼中 增加了全域性的異常處理,類似 Java 的 try catch,也定義了一些基本的變數。金鑰是一串由短線連線的字串,短線前的程式碼用以區分環境,短線後的程式碼用以區分模組或專案。另外上面程式碼中的 package_name 是打包時的包名(即 profiles.profile.id),一般配置在 springboot 專案 pom 檔案中的編輯模組,類似下面這樣:

3. 連線 linux 伺服器

import os #用於-提取檔名  
import re #用於-正規表示式  
import time #用於-執行緒休眠  
import paramiko #用於-遠端執行linux命令  
from alive_progress import alive_bar #用於-進度條工具類  
from cryptography.fernet import Fernet #用於-加解密程式碼  
import base64 #用於-加解密程式碼  
import hashlib #用於-加解密程式碼  

#檢查金鑰格式
def check_deploy_sign(deploy_site):  
    #確保金鑰只能是以下4個之一才能繼續往下操作,否則無限迴圈輸入 或 退出程式
    if deploy_site != 'pro-main' and deploy_site != 'pro-manage' and deploy_site != 'test-main' and deploy_site != 'test-manage': 
        #校驗失敗,一直校驗
        new_deploy_site = input("錯誤:請填寫部署金鑰:")  
        check_deploy_sign(new_deploy_site)  
     #校驗成功,退出
     return deploy_site  

# 連線伺服器  
def connect_service(deploy_server):
    server_password = ''  
    server_host = ''  
    sign = hashlib.sha256(deploy_server.encode()).digest()  
    sign = base64.urlsafe_b64encode(sign)  
    if deploy_server == 'pro':  
        server_password = decrypt_str(sign, service_password_pro)  
        server_host = decrypt_str(sign, service_host_pro)  
    elif deploy_server == 'test':  
        server_password = decrypt_str(sign, service_password_test)  
        server_host = decrypt_str(sign, service_host_test)  
    else:  
        raise Exception('失敗:部署伺服器標識有誤')  
    # 連線遠端伺服器  
    ssh = paramiko.SSHClient()  
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())  
    ssh.connect(server_host, username='root', password=server_password)  
    return ssh  

# 解密密碼  
def decrypt_str(key, encrypted_password):  
    f = Fernet(key)  
    decrypted_password = f.decrypt(encrypted_password).decode()  
    return decrypted_password

try:  
    # 伺服器環境資訊的加密字串,包含各伺服器的 ip和密碼  
    service_password_pro = 'asdatrgsd=='  
    service_password_test = 'sgherfhdf=='  
    service_host_pro = 'jfhgfvdcfdtr=='  
    service_host_test = 'jutyrbfvret=='


    deploy_sign = input("提示:請填寫部署金鑰:")  
    deploy_sign = check_deploy_sign(deploy_sign)  

    # 部署環境  pro代表生成環境,test代表測試環境
    deploy_server = deploy_sign.split('-')[0]  
    # 部署模組或專案 manage代表manage模組,main代表main模組, 
    deploy_site = deploy_sign.split('-')[1]  
    # 打包時的包名,三目運算子
    package_name = 'production' if deploy_server == 'pro' else 'staging'  
    #進度條
    with alive_bar(7, force_tty=True, title="進度") as bar:  
        # 連線伺服器  
        ssh = connect_service(deploy_server)  
        bar(0.1)  
        print("完成-伺服器連線成功")  
        time.sleep(0.5)
except Exception as e:  
    print(f"異常: {str(e)}")  

在連線伺服器之前,我們加個進度條顯示,方便檢視部署到哪一步了,要點講解:

  1. with alive_bar 中放的事需要進度條顯示的步驟,connect_service 是連線伺服器的方法
  2. 主機的 ip 和密碼我們用加密的密文顯示,解密的金鑰就是 手動輸入的部署金鑰
  3. 當一段邏輯執行完成後,透過 bar(0.1) 來顯示進度條進度,alive_bar 的第一個引數就是步驟總數

4. 部署工具主邏輯

程式碼要點講解: 下面的程式碼是工程的全部程式碼,主要包含了以下邏輯

  1. 連線伺服器
  2. 進入到專案工程目錄,拉取 git 程式碼
  3. 編譯公共依賴的程式碼(有的專案不一定有公共模組,可酌情刪減)
  4. 編譯打包程式程式碼
  5. 殺死舊程序
  6. 尋找編譯好的程式 jar 包並啟動
  7. 檢測啟動結果
import os #用於-提取檔名
import re #用於-正規表示式
import time #用於-執行緒休眠
import paramiko #用於-遠端執行linux命令
from alive_progress import alive_bar #用於-進度條工具類
from cryptography.fernet import Fernet #用於-加解密程式碼
import base64 #用於-加解密程式碼
import hashlib #用於-加解密程式碼

def check_deploy_sign(deploy_site):
    if deploy_site != 'pro-main' and deploy_site != 'pro-manage' and deploy_site != 'test-main' and deploy_site != 'test-manage':
        new_deploy_site = input("錯誤:請填寫部署金鑰:")
        check_deploy_sign(new_deploy_site)
    return deploy_site


# 解密密碼
def decrypt_str(key, encrypted_password):
    f = Fernet(key)
    decrypted_password = f.decrypt(encrypted_password).decode()
    return decrypted_password

# 執行遠端命令
def execute_command(ssh, command):
    stdin, stdout, stderr = ssh.exec_command(command)
    stdout.channel.recv_exit_status()  # 等待命令執行完畢
    output = stdout.read().decode('utf-8')
    time.sleep(0.5)
    return output

# 執行遠端命令
def execute_command_shell(shell, command, endword):
    shell.send(command + '\n')
    output = ''
    while True:
        while shell.recv_ready():
            recv = shell.recv(1024).decode('utf-8', errors='ignore')
            output += recv
        if endword == '# ':
            if output.endswith('$ ') or output.endswith('# '):
                break
        elif endword in output:
            break
    time.sleep(0.5)
    return output

# 連線伺服器
def connect_service(deploy_server):  
    server_password = ''
    server_host = ''
    sign = hashlib.sha256(deploy_server.encode()).digest()
    sign = base64.urlsafe_b64encode(sign)
    if deploy_server == 'pro':
        server_password = decrypt_str(sign, service_password_pro)
        server_host = decrypt_str(sign, service_host_pro)
    elif deploy_server == 'test':
        server_password = decrypt_str(sign, service_password_test)
        server_host = decrypt_str(sign, service_host_test)
    else:
        raise Exception('失敗:部署伺服器標識有誤')
    # 連線遠端伺服器
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(server_host, username='root', password=server_password)
    return ssh

# 查詢程序
def query_process(ssh, process_name):  
    process_id = ''
    command = f"ps -ef | grep {process_name}-system-master. | grep -v grep"
    process_output = execute_command(ssh, command)
    if process_output:
        # 提取程序ID並殺死程序
        process_id = process_output.split("    ")[1]
    return process_id

# 殺掉程序
def kill_process(ssh, process_id):  
    command = f"kill -9 {process_id}"
    output = execute_command(ssh, command)
    return output

# 尋找編譯好的jar包
def find_jarname(output):
    match = re.search(r"Building jar: .+?/(.+?.jar)", output)
    if match:
        jar_filepath = match.group(1)
        jar_filename = os.path.basename(jar_filepath)
        return jar_filename
    else:
        raise Exception('失敗:jar未找到')


try:
    service_password_pro = 'asdatrgsd=='
    service_password_test = 'sgherfhdf=='
    service_host_pro = 'jfhgfvdcfdtr=='
    service_host_test = 'jutyrbfvret=='


    deploy_sign = input("提示:請填寫部署金鑰:")
    deploy_sign = check_deploy_sign(deploy_sign)

    # 部署環境
    deploy_server = deploy_sign.split('-')[0]
    # 部署模組
    deploy_site = deploy_sign.split('-')[1]
    # 部署環境對應服務正式的名字
    package_name = 'production' if deploy_server == 'pro' else 'staging'

    with alive_bar(7, force_tty=True, title="進度") as bar:
        # 連線伺服器
        ssh = connect_service(deploy_server)
        bar(0.1)
        print("完成-伺服器連線成功")
        time.sleep(0.5)

        # 拉取程式碼
        shell = ssh.invoke_shell()
        execute_command_shell(shell, 'cd /root/build/x-system','#')
        execute_command_shell(shell, 'git pull','#')
        bar(0.2)
        print("完成-git程式碼拉取成功")

        # 編譯程式碼
        execute_command_shell(shell, 'cd /root/build/x-system/modules', '#')
        execute_command_shell(shell, 'mvn clean install', 'BUILD SUCCESS')
        bar(0.4)
        print("完成-公共模組編譯成功")

        # 打包程式碼
        execute_command_shell(shell, 'cd /root/build/x-system/webapps/' + deploy_site + '-system ', '#')
        output=execute_command_shell(shell, 'mvn clean package -P ' + package_name, 'BUILD SUCCESS')

        bar(0.6)
        print("完成-" + deploy_site + "模組打包成功")

        # 查詢程序,如果查不到 就不執行kill命令
        pid = query_process(ssh, deploy_site)
        if pid != '':
            kill_process(ssh, pid)
            print("完成-舊程式程序已被殺掉,等待啟動")
        else:
            print("完成-舊程式PID未找到,直接啟動")
        bar(0.7)


        # 啟動jar
        jar_name = find_jarname(output)
        execute_command_shell(shell, 'cd /root/build/x-system/webapps/' + deploy_site + '-system/target', '#')
        execute_command_shell(shell, 'nohup java -jar ' + jar_name + '>log.out  2>&1 & ', '#')
        bar(0.8)
        print("完成-程式正在啟動中...")


        # 檢視日誌確認服務啟動成功
        log_path = '/var/log/x-system/' + deploy_site + '-system' if deploy_server == 'pro' else '/var/log/x-system/' + deploy_site + '-system-staging'
        execute_command_shell(shell, 'cd '+log_path, '#')
        execute_command_shell(shell, 'tail -200f '+deploy_site+'-system-info.log', 'TomcatWebServer:206 - Tomcat started on port(s)')
        bar(1)
        print("完成-程式啟動成功")
except Exception as e:
    print(f"異常: {str(e)}")

finally:
    time.sleep(10)
    # 關閉連線
    shell.close()
    ssh.close()

程式碼用 try catch finally 包裹,如果過程中出現任何異常,都輸出錯誤原因 一些提示:

  1. 每個人的專案伺服器的路徑都不同,我只是提供個例子,不可盲目複製執行
  2. 每個人專案的名字也不同,我在文中出現類似 manage 和 main,是我專案模組中的名字,只是個例子,不可盲目複製

5.打包

打包命令:

pyinstaller --onefile --icon 太空人.ico --add-data ".\grapheme_break_property.json;grapheme\data"  --name 遠端部署 deployment.py

打包命令中的幾個引數解釋一下:

  1. --onefile :將專案工程檔案輸出在同一個可執行檔案中即 exe 中
  2. --icon 太空人.ico :exe 的圖示是一個 ico 的圖片
  3. --add-data ".\grapheme_break_property.json;grapheme\data" : 打包時 grapheme_break_property 這個依賴找不到,導致打包失敗,就手動新增一下
  4. --name 遠端部署 :exe 的名字(注意不需要帶.exe 字尾)
  5. deployment.py :python 工程的檔名

結語

python 很好玩,希望大家玩的開心
更多內容可以學習《測試工程師 Python 工具開發實戰》書籍《大話效能測試 JMeter 實戰》書籍

相關文章