admin_front_deploy.py 33 KB

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