換種思路解決Linux -> windows的自動部署
有個項目用到了Windows服務器(運行jar包和.NET代碼),如何集成到現有的自動部署平臺(基於Linux)面臨到兩個問題
- 如何將資源傳從Linux傳輸到Windows上
- 如何在windows上將程作為後臺進程並於終端(cmd or powershell)分離,實現類似Linux下nohup命令達到的效果
對於問題1,一開始想尋找一款“windows版的sshd”程序,但是好像沒找到比較官方的;後來想到powershell也有Linux版,想通過在Linux上安裝powershell通過powershell來在Linux和Windows之間傳輸文件,運行遠程命令。但是Linux版的只是powershell core,只有核心功能,況且powershell網上學習資料太少(吐槽一下:包括windows官網對於powershell的教程也只是在“簡介”的程度,完全不夠學習使用),遂無奈放棄之。
對於問題2,覺得在Windows上,用powershell應該能試下目標效果吧,但是經過一番研究,只找到了在powershell內後臺運行命令的方法,而無法做到脫離該powershell終端。後又想將目標進程“封裝”為windows服務,但是經過一番研究,發現不那麽好做。至此,對Windows深感痛心。。。
無奈之余“突發奇想”,何不用flask來做一個小接口程序,運行在Windows上,自動部署平臺通過http接口上傳jar包到目標Windows(解決問題1),至於問題2,也以http接口形式通過python的subprocess庫啟動一個子進程,這裏要註意的是,subprocess.Popen()方法在有個Windows特有的參數creationflags=subprocess.CREATE_NO_WINDOW
期望效果
- 在CI/CD工具裏,通過http請求來進行代碼更新,如
curl 172.16.1.77:5000/stop?project=we-gateway curl -XPUT 172.16.1.77:5000/update-we-gateway -F "[email protected]`ls build/libs/*.jar`" curl 172.16.1.77:5000/start?project=we-gateway
-
在windows上需要手動啟停應用時,也可以方便的通過命令行工具來操作(包括flask自身)
PS C:\deploy\win_server_manage> python .\wechat_manager.py Usage: wechat_manager.py [OPTIONS] COMMAND [ARGS]... Options: --help Show this message and exit. Commands: start Start the specified APP in background stop Stop the specified APP
其中的
wechat_manager.py
依賴click(依賴)實現友好的命令行支持。
具體方案
flask項目目錄結構如下
依次看代碼吧(不復雜也有必要的註釋)
app.py 提供flask接口
import os
import subprocess
import logging
from flask import Flask, request, Response
from werkzeug.utils import secure_filename
import we_manager
# 將flask日誌輸出至指定文件
logger = logging.getLogger()
file_handler = logging.FileHandler(‘C:/deploy/win_server_manage/out.log‘, encoding=‘UTF-8‘)
logger.addHandler(file_handler)
logger.setLevel(logging.DEBUG)
app = Flask(__name__)
app.config.from_pyfile(‘conf.py‘)
def upload_base(project):
file = request.files.get(‘file‘)
if not file:
return ‘No file had uploaded‘, 400
if file.filename == ‘‘:
return ‘No selected file‘, 400
# return ‘"project" key not in the post data‘, 400
filename = secure_filename(file.filename)
file.save(os.path.join(app.config[‘PROJECTS‘][project], filename))
return ‘successfully upload {}\n‘.format(filename)
# 示例1:通過上傳方式更新目標jar包
# 因為無法同時上傳文件和標識項目,故而各項目需要要給單獨接口
@app.route(‘/update-we-gateway‘, methods=[‘put‘, ‘post‘])
def upload_gateway():
return upload_base(‘we-gateway‘)
# 示例2:更新步驟不需要傳文件,直接在目標目錄git pull代碼即可
@app.route(‘/update-we-server‘, methods=[‘put‘, ‘post‘])
def upload_server():
os.chdir(app.config[‘PROJECTS‘][‘we-server‘])
p = subprocess.Popen([‘git‘, ‘pull‘], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW)
return "STDOUT:{}\n STDERR:{}".format(p.stdout.read().decode(‘gb2312‘) if p.stdout else None, p.stderr.read().decode(‘gb2312‘) if p.stderr else None)
@app.route(‘/stop‘)
def stop():
project = request.values.get(‘project‘)
if not project:
return ‘"project" key is necessary in the args‘, 400
if project not in app.config[‘PROJECTS‘]:
return ‘wrong project name‘, 400
return we_manager.stop_(project)
@app.route(‘/start‘)
def start():
project = request.values.get(‘project‘)
if not project:
return ‘"project" key is necessary in the args‘, 400
if project not in app.config[‘PROJECTS‘]:
return ‘wrong project name‘, 400
return we_manager.start_(project)
conf.py 配置項,主要是項目名及其目錄的對應關系
WIN_IP = ‘172.16.1.7‘
PROJECTS = {‘we-gateway‘: ‘C:/deploy/we-gateway-artifacts‘,
‘flask‘: ‘C:/deploy/win_server_manage‘}
wechat_manager.py 啟動和停止項目的主要邏輯,被app.py引用
import os
import subprocess
import glob
import shlex
import conf
def start_(name):
os.chdir(conf.PROJECTS[name])
if name == ‘flask‘:
# flask 作為常駐進程,不能用(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)以及stdout.read()來嘗試獲取標準輸出,會阻塞
# 在有(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)的時候,必須通過下句獲取輸出才能真正啟動flask,stdout = p.stdout.read().decode(‘gb2312‘) if p.stdout else None
# #這裏有這句,則能啟動,阻塞console,無這句,不阻塞console但實際未啟動
# flask的輸出通過flask內部定義
p = subprocess.Popen(shlex.split("flask run --host localhost"), creationflags=subprocess.CREATE_NO_WINDOW)
else:
try:
jarfile = glob.glob(‘*{}*.jar‘.format(name))[0]
except:
print("Can‘t find a valid jar file")
return
# powershell中的out-file相當於Linux中的>
# 以powershell.exe/cmd.exe開頭的子命令記錄的pid應該是powershell/cmd的,直接kill這樣的pid不能殺掉java進程
# 但是由於jar包內部沒有處理輸出,如果python不捕捉子命令輸出的話,就會導致無法獲得輸出,但是捕捉的話,會阻塞python進程(因為常駐內存進程的輸出"沒有盡頭")
p = subprocess.Popen(shlex.split("java -jar {}".format(jarfile)))
with open(‘{}/pid.txt‘.format(conf.PROJECTS[name]), ‘w‘) as f:
f.write(str(p.pid))
print(‘start {}, pid {} stored in pid.txt.‘.format(name, p.pid))
return ‘start {}, pid {} stored in pid.txt.‘.format(name, p.pid)
def stop_(name):
os.chdir(conf.PROJECTS[name])
res = []
with open(‘{}/pid.txt‘.format(conf.PROJECTS[name])) as f:
pid = f.read()
p = subprocess.Popen([‘powershell.exe‘, ‘kill‘, pid], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
res.append(p.stdout.read().decode(‘gb2312‘) if p.stdout else None)
res.append(p.stderr.read().decode(‘gb2312‘) if p.stderr else None)
p.wait(10)
try:
os.remove(‘{}/pid.txt‘.format(conf.PROJECTS[name]))
except FileNotFoundError:
pass
print(‘Stop {}, pid.txt removed.\n {}‘.format(name, res))
return ‘Stop {}, pid.txt removed.\n {}‘.format(name, res)
if __name__ == ‘__main__‘:
import click
@click.group()
def cli():
pass
@click.command()
@click.argument(‘name‘, required=True)
def start(name):
"""Start the specified APP in background"""
if name not in conf.PROJECTS:
click.echo(‘Wrong name of app‘)
return
# p = Process(target=start_, args=(name,))
# p.start() # 不必要了
start_(name)
@click.command()
@click.argument(‘name‘, required=True)
def stop(name):
"""Stop the specified APP"""
if name not in conf.PROJECTS:
click.echo(‘Wrong name of app‘)
return
stop_(name)
cli.add_command(start)
cli.add_command(stop)
cli()
最後,希望能為備受Windows摧殘的夥伴們,提供一些啟發和幫助。
換種思路解決Linux -> windows的自動部署