|
@@ -13,6 +13,7 @@ import paramiko
|
|
|
import argparse
|
|
import argparse
|
|
|
import tempfile
|
|
import tempfile
|
|
|
import hashlib
|
|
import hashlib
|
|
|
|
|
+import subprocess
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from typing import Optional, Tuple, List
|
|
from typing import Optional, Tuple, List
|
|
|
import getpass
|
|
import getpass
|
|
@@ -22,6 +23,8 @@ class VueAutoDeployer:
|
|
|
def __init__(self, hostname: str, username: str,
|
|
def __init__(self, hostname: str, username: str,
|
|
|
local_source_dir: str, remote_deploy_dir: str,
|
|
local_source_dir: str, remote_deploy_dir: str,
|
|
|
remote_script_path: str,
|
|
remote_script_path: str,
|
|
|
|
|
+ frontend_project_dir: Optional[str] = None,
|
|
|
|
|
+ build_command: str = "npm run build:dev",
|
|
|
password: Optional[str] = None,
|
|
password: Optional[str] = None,
|
|
|
port: int = 22,
|
|
port: int = 22,
|
|
|
key_filename: Optional[str] = None):
|
|
key_filename: Optional[str] = None):
|
|
@@ -34,6 +37,8 @@ class VueAutoDeployer:
|
|
|
local_source_dir: 本地Vue构建文件目录(包含assets和index.html)
|
|
local_source_dir: 本地Vue构建文件目录(包含assets和index.html)
|
|
|
remote_deploy_dir: 远程服务器部署目录(上传zip文件的目录)
|
|
remote_deploy_dir: 远程服务器部署目录(上传zip文件的目录)
|
|
|
remote_script_path: 远程部署脚本路径
|
|
remote_script_path: 远程部署脚本路径
|
|
|
|
|
+ frontend_project_dir: 前端项目根目录(可选,如果提供则会先执行打包)
|
|
|
|
|
+ build_command: 前端打包命令,默认为 npm run build:dev
|
|
|
password: SSH密码(可选,密钥认证不需要)
|
|
password: SSH密码(可选,密钥认证不需要)
|
|
|
port: SSH端口,默认22
|
|
port: SSH端口,默认22
|
|
|
key_filename: SSH私钥文件路径(可选)
|
|
key_filename: SSH私钥文件路径(可选)
|
|
@@ -43,6 +48,8 @@ class VueAutoDeployer:
|
|
|
self.local_source_dir = os.path.expanduser(local_source_dir)
|
|
self.local_source_dir = os.path.expanduser(local_source_dir)
|
|
|
self.remote_deploy_dir = remote_deploy_dir
|
|
self.remote_deploy_dir = remote_deploy_dir
|
|
|
self.remote_script_path = remote_script_path
|
|
self.remote_script_path = remote_script_path
|
|
|
|
|
+ self.frontend_project_dir = os.path.expanduser(frontend_project_dir) if frontend_project_dir else None
|
|
|
|
|
+ self.build_command = build_command
|
|
|
self.password = password
|
|
self.password = password
|
|
|
self.port = port
|
|
self.port = port
|
|
|
self.key_filename = key_filename
|
|
self.key_filename = key_filename
|
|
@@ -53,9 +60,189 @@ class VueAutoDeployer:
|
|
|
self.zip_filename = "dist-dev.zip"
|
|
self.zip_filename = "dist-dev.zip"
|
|
|
|
|
|
|
|
# 验证目录
|
|
# 验证目录
|
|
|
- self._validate_directories()
|
|
|
|
|
|
|
+ self._validate_local_directories()
|
|
|
|
|
+
|
|
|
|
|
+ def build_frontend(self) -> bool:
|
|
|
|
|
+ """
|
|
|
|
|
+ 执行前端打包
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 是否打包成功
|
|
|
|
|
+ """
|
|
|
|
|
+ if not self.frontend_project_dir:
|
|
|
|
|
+ print("⚠ 未指定前端项目目录,跳过打包步骤")
|
|
|
|
|
+ return True
|
|
|
|
|
|
|
|
- def _validate_directories(self):
|
|
|
|
|
|
|
+ print(f"\n正在执行前端打包...")
|
|
|
|
|
+ print(f"项目目录: {self.frontend_project_dir}")
|
|
|
|
|
+ print(f"打包命令: {self.build_command}")
|
|
|
|
|
+ print("-" * 50)
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # 检查前端项目目录是否存在
|
|
|
|
|
+ if not os.path.exists(self.frontend_project_dir):
|
|
|
|
|
+ raise FileNotFoundError(f"前端项目目录不存在: {self.frontend_project_dir}")
|
|
|
|
|
+
|
|
|
|
|
+ # 检查package.json是否存在
|
|
|
|
|
+ package_json_path = os.path.join(self.frontend_project_dir, 'package.json')
|
|
|
|
|
+ if not os.path.exists(package_json_path):
|
|
|
|
|
+ raise FileNotFoundError(f"package.json不存在: {package_json_path}")
|
|
|
|
|
+
|
|
|
|
|
+ # 检查npm是否可用
|
|
|
|
|
+ print("检查npm环境...")
|
|
|
|
|
+ try:
|
|
|
|
|
+ if os.name == 'nt': # Windows系统
|
|
|
|
|
+ npm_check = subprocess.run(
|
|
|
|
|
+ "npm --version",
|
|
|
|
|
+ capture_output=True,
|
|
|
|
|
+ text=True,
|
|
|
|
|
+ shell=True,
|
|
|
|
|
+ timeout=10
|
|
|
|
|
+ )
|
|
|
|
|
+ else: # Unix/Linux系统
|
|
|
|
|
+ npm_check = subprocess.run(
|
|
|
|
|
+ ["npm", "--version"],
|
|
|
|
|
+ capture_output=True,
|
|
|
|
|
+ text=True,
|
|
|
|
|
+ timeout=10
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if npm_check.returncode == 0:
|
|
|
|
|
+ print(f"✓ npm版本: {npm_check.stdout.strip()}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"⚠ npm检查失败: {npm_check.stderr}")
|
|
|
|
|
+ print("请确保npm已正确安装并添加到PATH环境变量中")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"✗ npm环境检查失败: {e}")
|
|
|
|
|
+ print("请确保npm已正确安装并添加到PATH环境变量中")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # 检查node_modules是否存在,如果不存在则先安装依赖
|
|
|
|
|
+ node_modules_path = os.path.join(self.frontend_project_dir, 'node_modules')
|
|
|
|
|
+ if not os.path.exists(node_modules_path):
|
|
|
|
|
+ print("⚠ node_modules不存在,正在安装依赖...")
|
|
|
|
|
+
|
|
|
|
|
+ # 在Windows系统上,需要通过shell执行npm命令
|
|
|
|
|
+ if os.name == 'nt': # Windows系统
|
|
|
|
|
+ install_result = subprocess.run(
|
|
|
|
|
+ "npm install",
|
|
|
|
|
+ cwd=self.frontend_project_dir,
|
|
|
|
|
+ capture_output=True,
|
|
|
|
|
+ text=True,
|
|
|
|
|
+ shell=True, # 在Windows上使用shell
|
|
|
|
|
+ timeout=300 # 5分钟超时
|
|
|
|
|
+ )
|
|
|
|
|
+ else: # Unix/Linux系统
|
|
|
|
|
+ install_result = subprocess.run(
|
|
|
|
|
+ ["npm", "install"],
|
|
|
|
|
+ cwd=self.frontend_project_dir,
|
|
|
|
|
+ capture_output=True,
|
|
|
|
|
+ text=True,
|
|
|
|
|
+ timeout=300 # 5分钟超时
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if install_result.returncode != 0:
|
|
|
|
|
+ print(f"✗ 依赖安装失败:")
|
|
|
|
|
+ print(f"错误输出: {install_result.stderr}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ print("✓ 依赖安装完成")
|
|
|
|
|
+
|
|
|
|
|
+ # 清理之前的构建文件
|
|
|
|
|
+ dist_path = os.path.join(self.frontend_project_dir, 'dist-dev')
|
|
|
|
|
+ if os.path.exists(dist_path):
|
|
|
|
|
+ print(f"清理之前的构建文件: {dist_path}")
|
|
|
|
|
+ shutil.rmtree(dist_path)
|
|
|
|
|
+
|
|
|
|
|
+ # 执行打包命令
|
|
|
|
|
+ print(f"执行打包命令: {self.build_command}")
|
|
|
|
|
+ start_time = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ # 在Windows系统上,需要通过shell执行npm命令
|
|
|
|
|
+ if os.name == 'nt': # Windows系统
|
|
|
|
|
+ build_result = subprocess.run(
|
|
|
|
|
+ self.build_command,
|
|
|
|
|
+ cwd=self.frontend_project_dir,
|
|
|
|
|
+ capture_output=True,
|
|
|
|
|
+ text=True,
|
|
|
|
|
+ shell=True, # 在Windows上使用shell
|
|
|
|
|
+ timeout=600 # 10分钟超时
|
|
|
|
|
+ )
|
|
|
|
|
+ else: # Unix/Linux系统
|
|
|
|
|
+ cmd_parts = self.build_command.split()
|
|
|
|
|
+ build_result = subprocess.run(
|
|
|
|
|
+ cmd_parts,
|
|
|
|
|
+ cwd=self.frontend_project_dir,
|
|
|
|
|
+ capture_output=True,
|
|
|
|
|
+ text=True,
|
|
|
|
|
+ timeout=600 # 10分钟超时
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ elapsed_time = time.time() - start_time
|
|
|
|
|
+
|
|
|
|
|
+ if build_result.returncode != 0:
|
|
|
|
|
+ print(f"✗ 前端打包失败 (耗时: {elapsed_time:.1f}秒)")
|
|
|
|
|
+ print(f"返回码: {build_result.returncode}")
|
|
|
|
|
+ print(f"错误输出:")
|
|
|
|
|
+ print(build_result.stderr)
|
|
|
|
|
+ if build_result.stdout:
|
|
|
|
|
+ print(f"标准输出:")
|
|
|
|
|
+ print(build_result.stdout)
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ print(f"✓ 前端打包成功 (耗时: {elapsed_time:.1f}秒)")
|
|
|
|
|
+
|
|
|
|
|
+ # 显示打包输出(如果有的话)
|
|
|
|
|
+ if build_result.stdout:
|
|
|
|
|
+ print("打包输出:")
|
|
|
|
|
+ print(build_result.stdout)
|
|
|
|
|
+
|
|
|
|
|
+ # 验证构建结果
|
|
|
|
|
+ if os.path.exists(dist_path):
|
|
|
|
|
+ # 检查构建文件
|
|
|
|
|
+ files_count = len([f for f in os.listdir(dist_path) if os.path.isfile(os.path.join(dist_path, f))])
|
|
|
|
|
+ dirs_count = len([d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))])
|
|
|
|
|
+ print(f"✓ 构建文件验证通过: {files_count} 个文件, {dirs_count} 个目录")
|
|
|
|
|
+
|
|
|
|
|
+ # 显示构建结果
|
|
|
|
|
+ print("构建文件列表:")
|
|
|
|
|
+ for item in os.listdir(dist_path):
|
|
|
|
|
+ item_path = os.path.join(dist_path, item)
|
|
|
|
|
+ if os.path.isdir(item_path):
|
|
|
|
|
+ print(f" 📁 {item}/")
|
|
|
|
|
+ else:
|
|
|
|
|
+ size = os.path.getsize(item_path)
|
|
|
|
|
+ print(f" 📄 {item} ({size/1024:.1f} KB)")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"⚠ 构建目录不存在: {dist_path}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ except subprocess.TimeoutExpired:
|
|
|
|
|
+ print("✗ 前端打包超时")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except FileNotFoundError as e:
|
|
|
|
|
+ print(f"✗ 文件不存在: {e}")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"✗ 前端打包失败: {e}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _validate_local_directories(self):
|
|
|
|
|
+ """验证本地目录是否存在且包含必要文件"""
|
|
|
|
|
+ print(f"检查前端应用目录: {self.frontend_project_dir}")
|
|
|
|
|
+
|
|
|
|
|
+ if not os.path.exists(self.frontend_project_dir):
|
|
|
|
|
+ raise FileNotFoundError(f"前端应用目录不存在: {self.frontend_project_dir}")
|
|
|
|
|
+ print("✓ 前端应用目录验证通过")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ def _validate_all_local_directories(self):
|
|
|
"""验证本地目录是否存在且包含必要文件"""
|
|
"""验证本地目录是否存在且包含必要文件"""
|
|
|
print(f"检查本地目录: {self.local_source_dir}")
|
|
print(f"检查本地目录: {self.local_source_dir}")
|
|
|
|
|
|
|
@@ -94,6 +281,8 @@ class VueAutoDeployer:
|
|
|
print(f" 📄 {item}")
|
|
print(f" 📄 {item}")
|
|
|
|
|
|
|
|
print("✓ 本地目录验证通过")
|
|
print("✓ 本地目录验证通过")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def _create_zip_from_source(self) -> str:
|
|
def _create_zip_from_source(self) -> str:
|
|
|
"""
|
|
"""
|
|
@@ -450,14 +639,27 @@ class VueAutoDeployer:
|
|
|
temp_dir = None
|
|
temp_dir = None
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
|
|
+ # 步骤0: 前端打包(如果指定了前端项目目录)
|
|
|
|
|
+ if self.frontend_project_dir:
|
|
|
|
|
+ print("\n[步骤 0/5] 前端应用打包")
|
|
|
|
|
+ print("-"*40)
|
|
|
|
|
+ if not self.build_frontend():
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
# 步骤1: 本地压缩文件
|
|
# 步骤1: 本地压缩文件
|
|
|
- print("\n[步骤 1/4] 本地压缩Vue构建文件")
|
|
|
|
|
|
|
+ step_num = "1/5" if self.frontend_project_dir else "1/4"
|
|
|
|
|
+ print(f"\n[步骤 {step_num}] 本地压缩Vue构建文件")
|
|
|
print("-"*40)
|
|
print("-"*40)
|
|
|
temp_zip_path = self._create_zip_from_source()
|
|
temp_zip_path = self._create_zip_from_source()
|
|
|
temp_dir = os.path.dirname(temp_zip_path)
|
|
temp_dir = os.path.dirname(temp_zip_path)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ # 验证所有目录
|
|
|
|
|
+ self._validate_all_local_directories()
|
|
|
|
|
|
|
|
# 步骤2: 连接到服务器
|
|
# 步骤2: 连接到服务器
|
|
|
- print("\n[步骤 2/4] 连接到远程服务器")
|
|
|
|
|
|
|
+ step_num = "2/5" if self.frontend_project_dir else "2/4"
|
|
|
|
|
+ print(f"\n[步骤 {step_num}] 连接到远程服务器")
|
|
|
print("-"*40)
|
|
print("-"*40)
|
|
|
if not self.connect():
|
|
if not self.connect():
|
|
|
return False
|
|
return False
|
|
@@ -467,7 +669,8 @@ class VueAutoDeployer:
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
# 步骤3: 上传文件
|
|
# 步骤3: 上传文件
|
|
|
- print("\n[步骤 3/4] 上传文件到服务器")
|
|
|
|
|
|
|
+ step_num = "3/5" if self.frontend_project_dir else "3/4"
|
|
|
|
|
+ print(f"\n[步骤 {step_num}] 上传文件到服务器")
|
|
|
print("-"*40)
|
|
print("-"*40)
|
|
|
remote_zip_path = os.path.join(self.remote_deploy_dir, self.zip_filename)
|
|
remote_zip_path = os.path.join(self.remote_deploy_dir, self.zip_filename)
|
|
|
|
|
|
|
@@ -475,11 +678,12 @@ class VueAutoDeployer:
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
# 步骤4: 执行部署脚本
|
|
# 步骤4: 执行部署脚本
|
|
|
- print("\n[步骤 4/4] 执行远程部署脚本")
|
|
|
|
|
|
|
+ step_num = "4/5" if self.frontend_project_dir else "4/4"
|
|
|
|
|
+ print(f"\n[步骤 {step_num}] 执行远程部署脚本")
|
|
|
print("-"*40)
|
|
print("-"*40)
|
|
|
|
|
|
|
|
- # 构建部署命令,传递上传的zip文件路径作为参数 {remote_zip_path}
|
|
|
|
|
- deploy_command = f"{self.remote_script_path} "
|
|
|
|
|
|
|
+ # 构建部署命令,传递上传的zip文件路径作为参数 {remote_zip_path}
|
|
|
|
|
+ deploy_command = f"{self.remote_script_path}"
|
|
|
|
|
|
|
|
print(f"执行部署命令: {deploy_command}")
|
|
print(f"执行部署命令: {deploy_command}")
|
|
|
print("-"*40)
|
|
print("-"*40)
|
|
@@ -501,7 +705,7 @@ class VueAutoDeployer:
|
|
|
|
|
|
|
|
# 可选: 验证部署结果
|
|
# 可选: 验证部署结果
|
|
|
print("\n验证部署结果...")
|
|
print("\n验证部署结果...")
|
|
|
- self.execute_command("ls -la /usr/share/nginx/html/ 2>/dev/null | head -10", verbose=True)
|
|
|
|
|
|
|
+ self.execute_command("ls -la /home/lq/nginx/html/ 2>/dev/null | head -10", verbose=True)
|
|
|
|
|
|
|
|
return True
|
|
return True
|
|
|
|
|
|
|
@@ -530,8 +734,17 @@ def parse_arguments():
|
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
|
epilog="""
|
|
epilog="""
|
|
|
使用示例:
|
|
使用示例:
|
|
|
- %(prog)s --host 192.168.1.100 --user deploy --source ./dist --remote-dir /home/deploy --script /home/deploy/deploy.sh
|
|
|
|
|
- %(prog)s --host example.com --user ubuntu --key ~/.ssh/id_rsa --source ./build --remote-dir /tmp
|
|
|
|
|
|
|
+ # 基本用法(仅上传已构建的文件)
|
|
|
|
|
+ %(prog)s --host 192.168.1.100 --user deploy --source ./dist-dev
|
|
|
|
|
+
|
|
|
|
|
+ # 完整流程(先打包再部署)
|
|
|
|
|
+ %(prog)s --host 192.168.1.100 --user deploy --source ./dist-dev --frontend-dir ./LQAdminFront
|
|
|
|
|
+
|
|
|
|
|
+ # 自定义打包命令
|
|
|
|
|
+ %(prog)s --host 192.168.1.100 --user deploy --source ./dist-dev --frontend-dir ./LQAdminFront --build-cmd "npm run build:prod"
|
|
|
|
|
+
|
|
|
|
|
+ # 使用SSH密钥认证
|
|
|
|
|
+ %(prog)s --host example.com --user ubuntu --key ~/.ssh/id_rsa --source ./build --frontend-dir ./frontend
|
|
|
"""
|
|
"""
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -541,6 +754,9 @@ def parse_arguments():
|
|
|
parser.add_argument('--source', required=True, help='本地Vue构建文件目录(包含assets和index.html)')
|
|
parser.add_argument('--source', required=True, help='本地Vue构建文件目录(包含assets和index.html)')
|
|
|
|
|
|
|
|
# 可选参数
|
|
# 可选参数
|
|
|
|
|
+ parser.add_argument('--frontend-dir', help='前端项目根目录(如果提供则会先执行打包)')
|
|
|
|
|
+ parser.add_argument('--build-cmd', default='npm run build:dev',
|
|
|
|
|
+ help='前端打包命令 (默认: npm run build:dev)')
|
|
|
parser.add_argument('--remote-dir', default='/home/lq/lq_workspace/deploy_workspace/put/',
|
|
parser.add_argument('--remote-dir', default='/home/lq/lq_workspace/deploy_workspace/put/',
|
|
|
help='远程服务器上传目录 (默认: /home/lq/lq_workspace/deploy_workspace/put/)')
|
|
help='远程服务器上传目录 (默认: /home/lq/lq_workspace/deploy_workspace/put/)')
|
|
|
parser.add_argument('--script', default='/home/lq/lq_workspace/deploy_workspace/admin_front_deploy.sh',
|
|
parser.add_argument('--script', default='/home/lq/lq_workspace/deploy_workspace/admin_front_deploy.sh',
|
|
@@ -567,6 +783,9 @@ def main():
|
|
|
print(f"服务器: {args.host}:{args.port}")
|
|
print(f"服务器: {args.host}:{args.port}")
|
|
|
print(f"用户: {args.user}")
|
|
print(f"用户: {args.user}")
|
|
|
print(f"本地源目录: {args.source}")
|
|
print(f"本地源目录: {args.source}")
|
|
|
|
|
+ if args.frontend_dir:
|
|
|
|
|
+ print(f"前端项目目录: {args.frontend_dir}")
|
|
|
|
|
+ print(f"打包命令: {args.build_cmd}")
|
|
|
print(f"远程目录: {args.remote_dir}")
|
|
print(f"远程目录: {args.remote_dir}")
|
|
|
print(f"部署脚本: {args.script}")
|
|
print(f"部署脚本: {args.script}")
|
|
|
print("="*70)
|
|
print("="*70)
|
|
@@ -579,6 +798,8 @@ def main():
|
|
|
local_source_dir=args.source,
|
|
local_source_dir=args.source,
|
|
|
remote_deploy_dir=args.remote_dir,
|
|
remote_deploy_dir=args.remote_dir,
|
|
|
remote_script_path=args.script,
|
|
remote_script_path=args.script,
|
|
|
|
|
+ frontend_project_dir=args.frontend_dir,
|
|
|
|
|
+ build_command=args.build_cmd,
|
|
|
password=args.password,
|
|
password=args.password,
|
|
|
port=args.port,
|
|
port=args.port,
|
|
|
key_filename=args.key
|
|
key_filename=args.key
|