admin_front_deploy.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Vue前端应用自动化部署脚本
  5. 功能:本地压缩Vue构建文件 -> SSH上传 -> 远程执行部署脚本
  6. """
  7. import os
  8. import sys
  9. import time
  10. import zipfile
  11. import paramiko
  12. import argparse
  13. import tempfile
  14. import hashlib
  15. import subprocess
  16. import logging
  17. from pathlib import Path
  18. from typing import Optional, Tuple, List
  19. import getpass
  20. import shutil
  21. logger = logging.getLogger(__name__)
  22. class VueAutoDeployer:
  23. def __init__(self, hostname: str, username: str,
  24. local_source_dir: str, remote_deploy_dir: str,
  25. remote_script_path: str,
  26. frontend_project_dir: Optional[str] = None,
  27. build_command: str = "npm run build:dev",
  28. password: Optional[str] = None,
  29. port: int = 22,
  30. key_filename: Optional[str] = None):
  31. """
  32. 初始化自动部署器
  33. Args:
  34. hostname: 服务器地址
  35. username: 用户名
  36. local_source_dir: 本地Vue构建文件目录(包含assets和index.html)
  37. remote_deploy_dir: 远程服务器部署目录(上传zip文件的目录)
  38. remote_script_path: 远程部署脚本路径
  39. frontend_project_dir: 前端项目根目录(可选,如果提供则会先执行打包)
  40. build_command: 前端打包命令,默认为 npm run build:dev
  41. password: SSH密码(可选,密钥认证不需要)
  42. port: SSH端口,默认22
  43. key_filename: SSH私钥文件路径(可选)
  44. """
  45. self.hostname = hostname
  46. self.username = username
  47. self.local_source_dir = os.path.expanduser(local_source_dir)
  48. self.remote_deploy_dir = remote_deploy_dir
  49. self.remote_script_path = remote_script_path
  50. self.frontend_project_dir = os.path.expanduser(frontend_project_dir) if frontend_project_dir else None
  51. self.build_command = build_command
  52. self.password = password
  53. self.port = port
  54. self.key_filename = key_filename
  55. self.ssh_client = None
  56. self.sftp_client = None
  57. # 压缩文件名
  58. self.zip_filename = "dist-dev.zip"
  59. # 验证目录
  60. self._validate_local_directories()
  61. def build_frontend(self) -> bool:
  62. """
  63. 执行前端打包
  64. Returns:
  65. 是否打包成功
  66. """
  67. if not self.frontend_project_dir:
  68. print("⚠ 未指定前端项目目录,跳过打包步骤")
  69. return True
  70. print(f"\n正在执行前端打包...")
  71. print(f"项目目录: {self.frontend_project_dir}")
  72. print(f"打包命令: {self.build_command}")
  73. print("-" * 50)
  74. try:
  75. # 检查前端项目目录是否存在
  76. if not os.path.exists(self.frontend_project_dir):
  77. raise FileNotFoundError(f"前端项目目录不存在: {self.frontend_project_dir}")
  78. # 检查package.json是否存在
  79. package_json_path = os.path.join(self.frontend_project_dir, 'package.json')
  80. if not os.path.exists(package_json_path):
  81. raise FileNotFoundError(f"package.json不存在: {package_json_path}")
  82. # 检查npm是否可用
  83. print("检查npm环境...")
  84. try:
  85. if os.name == 'nt': # Windows系统
  86. npm_check = subprocess.run(
  87. "npm --version",
  88. capture_output=True,
  89. text=True,
  90. shell=True,
  91. timeout=10
  92. )
  93. else: # Unix/Linux系统
  94. npm_check = subprocess.run(
  95. ["npm", "--version"],
  96. capture_output=True,
  97. text=True,
  98. timeout=10
  99. )
  100. if npm_check.returncode == 0:
  101. print(f"✓ npm版本: {npm_check.stdout.strip()}")
  102. else:
  103. print(f"⚠ npm检查失败: {npm_check.stderr}")
  104. print("请确保npm已正确安装并添加到PATH环境变量中")
  105. return False
  106. except Exception as e:
  107. print(f"✗ npm环境检查失败: {e}")
  108. print("请确保npm已正确安装并添加到PATH环境变量中")
  109. return False
  110. # 检查node_modules是否存在,如果不存在则先安装依赖
  111. node_modules_path = os.path.join(self.frontend_project_dir, 'node_modules')
  112. if not os.path.exists(node_modules_path):
  113. print("⚠ node_modules不存在,正在安装依赖...")
  114. # 在Windows系统上,需要通过shell执行npm命令
  115. if os.name == 'nt': # Windows系统
  116. install_result = subprocess.run(
  117. "npm install",
  118. cwd=self.frontend_project_dir,
  119. capture_output=True,
  120. text=True,
  121. shell=True, # 在Windows上使用shell
  122. timeout=300 # 5分钟超时
  123. )
  124. else: # Unix/Linux系统
  125. install_result = subprocess.run(
  126. ["npm", "install"],
  127. cwd=self.frontend_project_dir,
  128. capture_output=True,
  129. text=True,
  130. timeout=300 # 5分钟超时
  131. )
  132. if install_result.returncode != 0:
  133. print(f"✗ 依赖安装失败:")
  134. print(f"错误输出: {install_result.stderr}")
  135. return False
  136. print("✓ 依赖安装完成")
  137. # 清理之前的构建文件
  138. dist_path = os.path.join(self.frontend_project_dir, 'dist-dev')
  139. if os.path.exists(dist_path):
  140. print(f"清理之前的构建文件: {dist_path}")
  141. shutil.rmtree(dist_path)
  142. # 执行打包命令
  143. print(f"执行打包命令: {self.build_command}")
  144. start_time = time.time()
  145. # 在Windows系统上,需要通过shell执行npm命令
  146. if os.name == 'nt': # Windows系统
  147. build_result = subprocess.run(
  148. self.build_command,
  149. cwd=self.frontend_project_dir,
  150. capture_output=True,
  151. text=True,
  152. shell=True, # 在Windows上使用shell
  153. timeout=600 # 10分钟超时
  154. )
  155. else: # Unix/Linux系统
  156. cmd_parts = self.build_command.split()
  157. build_result = subprocess.run(
  158. cmd_parts,
  159. cwd=self.frontend_project_dir,
  160. capture_output=True,
  161. text=True,
  162. timeout=600 # 10分钟超时
  163. )
  164. elapsed_time = time.time() - start_time
  165. if build_result.returncode != 0:
  166. print(f"✗ 前端打包失败 (耗时: {elapsed_time:.1f}秒)")
  167. print(f"返回码: {build_result.returncode}")
  168. print(f"错误输出:")
  169. print(build_result.stderr)
  170. if build_result.stdout:
  171. print(f"标准输出:")
  172. print(build_result.stdout)
  173. return False
  174. print(f"✓ 前端打包成功 (耗时: {elapsed_time:.1f}秒)")
  175. # 显示打包输出(如果有的话)
  176. if build_result.stdout:
  177. print("打包输出:")
  178. print(build_result.stdout)
  179. # 验证构建结果
  180. if os.path.exists(dist_path):
  181. # 检查构建文件
  182. files_count = len([f for f in os.listdir(dist_path) if os.path.isfile(os.path.join(dist_path, f))])
  183. dirs_count = len([d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))])
  184. print(f"✓ 构建文件验证通过: {files_count} 个文件, {dirs_count} 个目录")
  185. # 显示构建结果
  186. print("构建文件列表:")
  187. for item in os.listdir(dist_path):
  188. item_path = os.path.join(dist_path, item)
  189. if os.path.isdir(item_path):
  190. print(f" 📁 {item}/")
  191. else:
  192. size = os.path.getsize(item_path)
  193. print(f" 📄 {item} ({size/1024:.1f} KB)")
  194. else:
  195. print(f"⚠ 构建目录不存在: {dist_path}")
  196. return False
  197. return True
  198. except subprocess.TimeoutExpired:
  199. print("✗ 前端打包超时")
  200. return False
  201. except FileNotFoundError as e:
  202. print(f"✗ 文件不存在: {e}")
  203. return False
  204. except Exception as e:
  205. print(f"✗ 前端打包失败: {e}")
  206. return False
  207. def _validate_local_directories(self):
  208. """验证本地目录是否存在且包含必要文件"""
  209. print(f"检查前端应用目录: {self.frontend_project_dir}")
  210. if not os.path.exists(self.frontend_project_dir):
  211. raise FileNotFoundError(f"前端应用目录不存在: {self.frontend_project_dir}")
  212. print("✓ 前端应用目录验证通过")
  213. def _validate_all_local_directories(self):
  214. """验证本地目录是否存在且包含必要文件"""
  215. logger.info("检查本地目录: %s", self.local_source_dir)
  216. if not os.path.exists(self.local_source_dir):
  217. raise FileNotFoundError(f"本地目录不存在: {self.local_source_dir}")
  218. # 检查必要文件
  219. required_files = ['index.html']
  220. required_dirs = ['assets']
  221. missing_items = []
  222. for file in required_files:
  223. file_path = os.path.join(self.local_source_dir, file)
  224. if not os.path.exists(file_path):
  225. missing_items.append(file)
  226. for dir_name in required_dirs:
  227. dir_path = os.path.join(self.local_source_dir, dir_name)
  228. if not os.path.exists(dir_path):
  229. missing_items.append(dir_name)
  230. if missing_items:
  231. raise FileNotFoundError(
  232. f"本地目录缺少必要的文件/目录: {', '.join(missing_items)}\n"
  233. f"请确保 {self.local_source_dir} 包含完整的Vue构建文件"
  234. )
  235. # 显示目录内容
  236. logger.info("本地目录内容:")
  237. for item in os.listdir(self.local_source_dir):
  238. item_path = os.path.join(self.local_source_dir, item)
  239. if os.path.isdir(item_path):
  240. logger.info(" 📁 %s/", item)
  241. else:
  242. logger.info(" 📄 %s", item)
  243. print("✓ 本地目录验证通过")
  244. def _create_zip_from_source(self) -> str:
  245. """
  246. 从源目录创建zip压缩包
  247. Returns:
  248. zip文件的临时路径
  249. """
  250. logger.info("正在创建压缩包: %s", self.zip_filename)
  251. # 创建临时文件
  252. temp_dir = tempfile.mkdtemp(prefix="vue_deploy_")
  253. zip_path = os.path.join(temp_dir, self.zip_filename)
  254. try:
  255. with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
  256. # 添加index.html
  257. index_path = os.path.join(self.local_source_dir, 'index.html')
  258. zipf.write(index_path, 'index.html')
  259. logger.info(" ✓ 添加: index.html")
  260. # 添加assets目录
  261. assets_dir = os.path.join(self.local_source_dir, 'assets')
  262. if os.path.exists(assets_dir):
  263. # 遍历assets目录中的所有文件
  264. for root, dirs, files in os.walk(assets_dir):
  265. # 计算相对路径
  266. rel_path = os.path.relpath(root, self.local_source_dir)
  267. for file in files:
  268. file_path = os.path.join(root, file)
  269. arcname = os.path.join(rel_path, file)
  270. zipf.write(file_path, arcname)
  271. logger.info(" ✓ 添加: %s", arcname)
  272. # 添加其他可能的文件(css, js文件)
  273. for item in os.listdir(self.local_source_dir):
  274. if item not in ['index.html', 'assets']:
  275. item_path = os.path.join(self.local_source_dir, item)
  276. if os.path.isfile(item_path) and item.endswith(('.css', '.js')):
  277. zipf.write(item_path, item)
  278. logger.info(" ✓ 添加: %s", item)
  279. # 获取压缩包信息
  280. zip_size = os.path.getsize(zip_path)
  281. file_count = len(zipfile.ZipFile(zip_path, 'r').namelist())
  282. logger.info("压缩包创建完成")
  283. logger.info("文件路径: %s", zip_path)
  284. logger.info("文件大小: %.2f MB", zip_size / 1024 / 1024)
  285. logger.info("包含文件: %s 个", file_count)
  286. return zip_path
  287. except Exception as e:
  288. # 清理临时目录
  289. shutil.rmtree(temp_dir, ignore_errors=True)
  290. raise Exception(f"创建压缩包失败: {e}")
  291. def connect(self) -> bool:
  292. """连接到SSH服务器"""
  293. logger.info("正在连接到服务器 %s:%s...", self.hostname, self.port)
  294. try:
  295. self.ssh_client = paramiko.SSHClient()
  296. self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  297. # 连接参数
  298. connect_params = {
  299. 'hostname': self.hostname,
  300. 'port': self.port,
  301. 'username': self.username,
  302. }
  303. # 认证方式
  304. if self.key_filename:
  305. connect_params['key_filename'] = self.key_filename
  306. auth_method = "密钥认证"
  307. elif self.password:
  308. connect_params['password'] = self.password
  309. auth_method = "密码认证"
  310. else:
  311. # 交互式输入密码
  312. self.password = getpass.getpass(f"请输入用户 {self.username} 的密码: ")
  313. connect_params['password'] = self.password
  314. auth_method = "密码认证"
  315. logger.info("使用认证方式: %s", auth_method)
  316. self.ssh_client.connect(**connect_params, timeout=30)
  317. # 测试连接
  318. stdin, stdout, stderr = self.ssh_client.exec_command('echo "连接成功" && whoami && hostname')
  319. output = stdout.read().decode().strip()
  320. user = output.split('\n')[1] if len(output.split('\n')) > 1 else '未知'
  321. hostname = output.split('\n')[2] if len(output.split('\n')) > 2 else '未知'
  322. logger.info("SSH连接成功")
  323. logger.info("服务器: %s", hostname)
  324. logger.info("用户: %s", user)
  325. # 创建SFTP客户端
  326. self.sftp_client = self.ssh_client.open_sftp()
  327. return True
  328. except paramiko.AuthenticationException:
  329. logger.error("SSH认证失败,请检查用户名/密码/密钥")
  330. return False
  331. except paramiko.SSHException as e:
  332. logger.exception("SSH连接异常: %s", e)
  333. return False
  334. except Exception as e:
  335. logger.exception("连接失败: %s", e)
  336. return False
  337. def disconnect(self):
  338. """断开连接"""
  339. if self.sftp_client:
  340. self.sftp_client.close()
  341. if self.ssh_client:
  342. self.ssh_client.close()
  343. logger.info("已断开SSH连接")
  344. def execute_command(self, command: str, verbose: bool = True) -> Tuple[int, str, str]:
  345. """
  346. 执行远程命令
  347. Args:
  348. command: 要执行的命令
  349. verbose: 是否显示输出
  350. Returns:
  351. (返回码, 标准输出, 标准错误)
  352. """
  353. try:
  354. if verbose:
  355. logger.info("执行命令: %s", command)
  356. stdin, stdout, stderr = self.ssh_client.exec_command(command, timeout=60)
  357. # 读取输出
  358. stdout_str = stdout.read().decode('utf-8', errors='ignore').strip()
  359. stderr_str = stderr.read().decode('utf-8', errors='ignore').strip()
  360. # 等待命令完成并获取返回码
  361. exit_status = stdout.channel.recv_exit_status()
  362. if verbose:
  363. if stdout_str:
  364. logger.info("输出:\n%s", stdout_str)
  365. if stderr_str and exit_status != 0:
  366. logger.warning("错误:\n%s", stderr_str)
  367. logger.info("返回码: %s", exit_status)
  368. return exit_status, stdout_str, stderr_str
  369. except Exception as e:
  370. logger.exception("执行命令失败: %s", e)
  371. return -1, "", str(e)
  372. def upload_file(self, local_path: str, remote_path: str) -> bool:
  373. """
  374. 上传文件到服务器
  375. Args:
  376. local_path: 本地文件路径
  377. remote_path: 远程文件路径
  378. Returns:
  379. 是否成功
  380. """
  381. try:
  382. if not os.path.exists(local_path):
  383. logger.error("本地文件不存在: %s", local_path)
  384. return False
  385. logger.info("本地文件: %s", local_path)
  386. file_size = os.path.getsize(local_path)
  387. logger.info("正在上传文件: %s (%.2f MB)", os.path.basename(local_path), file_size / 1024 / 1024)
  388. # 确保远程目录存在并检查权限
  389. remote_dir = os.path.dirname(remote_path)
  390. logger.info("远程文件目录: %s", remote_dir)
  391. # 创建目录
  392. exit_code, stdout, stderr = self.execute_command(f"mkdir -p {remote_dir}", verbose=False)
  393. if exit_code != 0:
  394. logger.error("创建远程目录失败: %s", stderr)
  395. return False
  396. # 检查目录权限
  397. exit_code, stdout, stderr = self.execute_command(f"ls -ld {remote_dir}", verbose=False)
  398. if exit_code == 0:
  399. logger.info("目录权限: %s", stdout)
  400. # 检查写入权限
  401. test_file = os.path.join(remote_dir, "test_write_permission.tmp")
  402. exit_code, stdout, stderr = self.execute_command(f"touch {test_file} && rm -f {test_file}", verbose=False)
  403. if exit_code != 0:
  404. logger.error("远程目录没有写入权限: %s", remote_dir)
  405. logger.error("错误: %s", stderr)
  406. return False
  407. logger.info("远程目录写入权限检查通过")
  408. # 使用SFTP上传文件(显示进度)
  409. start_time = time.time()
  410. def progress_callback(transferred, total):
  411. elapsed = time.time() - start_time
  412. if elapsed > 0:
  413. speed = transferred / elapsed / 1024 # KB/s
  414. percent = (transferred / total) * 100
  415. sys.stdout.write(f"\r 进度: {percent:.1f}% ({transferred/1024/1024:.2f}/{total/1024/1024:.2f} MB) 速度: {speed:.1f} KB/s")
  416. sys.stdout.flush()
  417. self.sftp_client.put(local_path, remote_path, callback=progress_callback)
  418. # 验证上传
  419. exit_code, stdout, stderr = self.execute_command(f"ls -lh {remote_path}", verbose=False)
  420. if exit_code == 0 and stdout:
  421. logger.info("文件验证成功: %s", stdout)
  422. else:
  423. logger.warning("文件验证失败: 文件可能未正确上传")
  424. if stderr:
  425. logger.warning("错误: %s", stderr)
  426. # 检查目录内容
  427. logger.info("检查目录内容: %s", remote_dir)
  428. self.execute_command(f"ls -la {remote_dir}", verbose=True)
  429. return False
  430. elapsed = time.time() - start_time
  431. logger.info("文件上传成功,耗时: %.1f秒", elapsed)
  432. return True
  433. except paramiko.SFTPError as e:
  434. logger.exception("SFTP上传失败: %s", e)
  435. logger.info("可能的原因:")
  436. logger.info(" 1. 远程目录权限不足")
  437. logger.info(" 2. 磁盘空间不足")
  438. logger.info(" 3. 网络连接中断")
  439. return False
  440. except Exception as e:
  441. logger.exception("文件上传失败: %s", e)
  442. return False
  443. def check_remote_prerequisites(self) -> bool:
  444. """
  445. 检查远程服务器是否满足部署条件
  446. Returns:
  447. 是否满足条件
  448. """
  449. logger.info("检查远程服务器部署条件...")
  450. checks = []
  451. # 检查远程目录是否存在
  452. exit_code, stdout, stderr = self.execute_command(
  453. f"ls -ld {self.remote_deploy_dir} 2>/dev/null || echo '目录不存在'",
  454. verbose=False
  455. )
  456. if "目录不存在" in stdout:
  457. checks.append(("部署目录", "✗", f"{self.remote_deploy_dir} 不存在"))
  458. else:
  459. checks.append(("部署目录", "✓", f"{self.remote_deploy_dir}"))
  460. # 检查部署脚本是否存在且有执行权限
  461. exit_code, stdout, stderr = self.execute_command(
  462. f"ls -la {self.remote_script_path} 2>/dev/null || echo '脚本不存在'",
  463. verbose=False
  464. )
  465. if "脚本不存在" in stdout:
  466. checks.append(("部署脚本", "✗", f"{self.remote_script_path} 不存在"))
  467. else:
  468. # 检查执行权限
  469. exit_code, stdout, stderr = self.execute_command(
  470. f"test -x {self.remote_script_path} && echo '可执行' || echo '不可执行'",
  471. verbose=False
  472. )
  473. if "可执行" in stdout:
  474. checks.append(("部署脚本", "✓", "存在且可执行"))
  475. else:
  476. checks.append(("部署脚本", "⚠", "存在但不可执行"))
  477. # 检查unzip命令
  478. exit_code, stdout, stderr = self.execute_command(
  479. "which unzip 2>/dev/null && unzip -v 2>/dev/null | head -1",
  480. verbose=False
  481. )
  482. if exit_code == 0:
  483. checks.append(("unzip工具", "✓", stdout.strip()))
  484. else:
  485. checks.append(("unzip工具", "✗", "未安装"))
  486. # 检查zip命令
  487. exit_code, stdout, stderr = self.execute_command(
  488. "which zip 2>/dev/null && zip -v 2>/dev/null | head -1",
  489. verbose=False
  490. )
  491. if exit_code == 0:
  492. checks.append(("zip工具", "✓", stdout.strip()))
  493. else:
  494. checks.append(("zip工具", "⚠", "未安装(备份功能可能受影响)"))
  495. # 检查nginx目录(通常的部署目录)
  496. exit_code, stdout, stderr = self.execute_command(
  497. "ls -ld /usr/share/nginx/html 2>/dev/null || ls -ld /var/www/html 2>/dev/null || echo '未找到nginx目录'",
  498. verbose=False
  499. )
  500. if "未找到nginx目录" in stdout:
  501. checks.append(("nginx目录", "⚠", "未找到标准nginx目录"))
  502. else:
  503. checks.append(("nginx目录", "✓", stdout.strip().split()[-1]))
  504. # 显示检查结果
  505. logger.info("=" * 60)
  506. logger.info("服务器环境检查结果:")
  507. logger.info("=" * 60)
  508. all_passed = True
  509. for check_name, status, message in checks:
  510. if status == "✓":
  511. logger.info(" %s %s: %s", status, check_name, message)
  512. elif status == "⚠":
  513. logger.info(" %s %s: %s", status, check_name, message)
  514. else:
  515. logger.info(" %s %s: %s", status, check_name, message)
  516. all_passed = False
  517. logger.info("=" * 60)
  518. if not all_passed:
  519. logger.warning("警告: 部分检查未通过,部署可能会失败")
  520. response = input("是否继续部署?(y/N): ").strip().lower()
  521. return response == 'y'
  522. return True
  523. def deploy(self, cleanup_temp: bool = True) -> bool:
  524. """
  525. 执行完整的部署流程
  526. Args:
  527. cleanup_temp: 是否清理临时文件
  528. Returns:
  529. 是否部署成功
  530. """
  531. logger.info("=" * 70)
  532. logger.info("Vue前端应用自动化部署流程")
  533. logger.info("=" * 70)
  534. temp_zip_path = None
  535. temp_dir = None
  536. try:
  537. # 步骤0: 前端打包(如果指定了前端项目目录)
  538. if self.frontend_project_dir:
  539. print("\n[步骤 0/5] 前端应用打包")
  540. print("-"*40)
  541. if not self.build_frontend():
  542. return False
  543. # 步骤1: 本地压缩文件
  544. step_num = "1/5" if self.frontend_project_dir else "1/4"
  545. print(f"\n[步骤 {step_num}] 本地压缩Vue构建文件")
  546. print("-"*40)
  547. temp_zip_path = self._create_zip_from_source()
  548. temp_dir = os.path.dirname(temp_zip_path)
  549. # 验证所有目录
  550. self._validate_all_local_directories()
  551. # 步骤2: 连接到服务器
  552. step_num = "2/5" if self.frontend_project_dir else "2/4"
  553. print(f"\n[步骤 {step_num}] 连接到远程服务器")
  554. print("-"*40)
  555. if not self.connect():
  556. return False
  557. # 检查服务器环境
  558. if not self.check_remote_prerequisites():
  559. return False
  560. # 步骤3: 上传文件
  561. step_num = "3/5" if self.frontend_project_dir else "3/4"
  562. print(f"\n[步骤 {step_num}] 上传文件到服务器")
  563. print("-"*40)
  564. remote_zip_path = os.path.join(self.remote_deploy_dir, self.zip_filename)
  565. if not self.upload_file(temp_zip_path, remote_zip_path):
  566. return False
  567. # 步骤4: 执行部署脚本
  568. step_num = "4/5" if self.frontend_project_dir else "4/4"
  569. print(f"\n[步骤 {step_num}] 执行远程部署脚本")
  570. # 构建部署命令,传递上传的zip文件路径作为参数 {remote_zip_path}
  571. deploy_command = f"{self.remote_script_path}"
  572. logger.info("执行部署命令: %s", deploy_command)
  573. logger.info("-" * 40)
  574. start_time = time.time()
  575. exit_code, stdout, stderr = self.execute_command(deploy_command, verbose=True)
  576. elapsed_time = time.time() - start_time
  577. logger.info("-" * 40)
  578. logger.info("部署执行完成,耗时: %.1f秒", elapsed_time)
  579. if exit_code != 0:
  580. logger.error("部署失败,返回码: %s", exit_code)
  581. if stderr:
  582. logger.error("错误信息:\n%s", stderr)
  583. return False
  584. logger.info("部署成功完成")
  585. # 可选: 验证部署结果
  586. print("\n验证部署结果...")
  587. self.execute_command("ls -la /home/lq/nginx/html/ 2>/dev/null | head -10", verbose=True)
  588. return True
  589. except Exception as e:
  590. logger.exception("部署过程中发生错误: %s", e)
  591. return False
  592. finally:
  593. # 清理临时文件
  594. if cleanup_temp and temp_dir and os.path.exists(temp_dir):
  595. try:
  596. shutil.rmtree(temp_dir)
  597. logger.info("已清理临时文件: %s", temp_dir)
  598. except:
  599. logger.warning("清理临时文件失败: %s", temp_dir)
  600. # 断开连接
  601. self.disconnect()
  602. def parse_arguments():
  603. """解析命令行参数"""
  604. parser = argparse.ArgumentParser(
  605. description='Vue前端应用自动化部署工具',
  606. formatter_class=argparse.RawDescriptionHelpFormatter,
  607. epilog="""
  608. 使用示例:
  609. # 基本用法(仅上传已构建的文件)
  610. %(prog)s --host 192.168.1.100 --user deploy --source ./dist-dev
  611. # 完整流程(先打包再部署)
  612. %(prog)s --host 192.168.1.100 --user deploy --source ./dist-dev --frontend-dir ./LQAdminFront
  613. # 自定义打包命令
  614. %(prog)s --host 192.168.1.100 --user deploy --source ./dist-dev --frontend-dir ./LQAdminFront --build-cmd "npm run build:prod"
  615. # 使用SSH密钥认证
  616. %(prog)s --host example.com --user ubuntu --key ~/.ssh/id_rsa --source ./build --frontend-dir ./frontend
  617. """
  618. )
  619. # 必需参数
  620. parser.add_argument('--host', required=True, help='服务器地址 (例如: 192.168.1.100 或 example.com)')
  621. parser.add_argument('--user', required=True, help='SSH用户名')
  622. parser.add_argument('--source', required=True, help='本地Vue构建文件目录(包含assets和index.html)')
  623. # 可选参数
  624. parser.add_argument('--frontend-dir', help='前端项目根目录(如果提供则会先执行打包)')
  625. parser.add_argument('--build-cmd', default='npm run build:dev',
  626. help='前端打包命令 (默认: npm run build:dev)')
  627. parser.add_argument('--remote-dir', default='/home/lq/lq_workspace/deploy_workspace/put/',
  628. help='远程服务器上传目录 (默认: /home/lq/lq_workspace/deploy_workspace/put/)')
  629. parser.add_argument('--script', default='/home/lq/lq_workspace/deploy_workspace/admin_front_deploy.sh',
  630. help='远程部署脚本路径 (默认: /home/lq/lq_workspace/deploy_workspace/admin_front_deploy.sh)')
  631. parser.add_argument('--port', type=int, default=22, help='SSH端口 (默认: 22)')
  632. parser.add_argument('--key', help='SSH私钥文件路径(如果使用密钥认证)')
  633. parser.add_argument('--password', help='SSH密码(如果使用密码认证)')
  634. parser.add_argument('--no-cleanup', action='store_true',
  635. help='不清理临时文件(用于调试)')
  636. parser.add_argument('--verbose', '-v', action='store_true',
  637. help='显示详细输出')
  638. return parser.parse_args()
  639. def main():
  640. """主函数"""
  641. logging.basicConfig(level=logging.INFO, format="%(message)s")
  642. args = parse_arguments()
  643. # 显示欢迎信息
  644. logger.info("=" * 70)
  645. logger.info("🚀 Vue前端应用自动化部署工具")
  646. logger.info("=" * 70)
  647. print(f"服务器: {args.host}:{args.port}")
  648. print(f"用户: {args.user}")
  649. print(f"本地源目录: {args.source}")
  650. if args.frontend_dir:
  651. print(f"前端项目目录: {args.frontend_dir}")
  652. print(f"打包命令: {args.build_cmd}")
  653. print(f"远程目录: {args.remote_dir}")
  654. print(f"部署脚本: {args.script}")
  655. print("="*70)
  656. try:
  657. # 创建部署器实例
  658. deployer = VueAutoDeployer(
  659. hostname=args.host,
  660. username=args.user,
  661. local_source_dir=args.source,
  662. remote_deploy_dir=args.remote_dir,
  663. remote_script_path=args.script,
  664. frontend_project_dir=args.frontend_dir,
  665. build_command=args.build_cmd,
  666. password=args.password,
  667. port=args.port,
  668. key_filename=args.key
  669. )
  670. # 执行部署
  671. success = deployer.deploy(cleanup_temp=not args.no_cleanup)
  672. if success:
  673. logger.info("=" * 70)
  674. logger.info("🎉 部署流程全部完成!")
  675. logger.info("=" * 70)
  676. logger.info("建议操作:")
  677. logger.info(" 1. 访问网站检查是否正常显示")
  678. logger.info(" 2. 检查nginx错误日志: sudo tail -f /var/log/nginx/error.log")
  679. logger.info(" 3. 如果需要重启nginx: sudo systemctl restart nginx")
  680. logger.info("=" * 70)
  681. return 0
  682. else:
  683. logger.error("=" * 70)
  684. logger.error("❌ 部署失败!")
  685. logger.error("=" * 70)
  686. return 1
  687. except FileNotFoundError as e:
  688. logger.error("文件错误: %s", e)
  689. return 1
  690. except KeyboardInterrupt:
  691. logger.warning("用户中断操作")
  692. return 130
  693. except Exception as e:
  694. logger.exception("发生未预期的错误: %s", e)
  695. return 1
  696. if __name__ == "__main__":
  697. # 检查必要依赖
  698. try:
  699. import paramiko
  700. except ImportError:
  701. logging.basicConfig(level=logging.INFO, format="%(message)s")
  702. logger.error("错误: 未安装paramiko库")
  703. logger.error("请安装依赖: pip install paramiko")
  704. sys.exit(1)
  705. sys.exit(main())