新增NAS一键打包部署脚本及Docker部署方案
This commit is contained in:
33
deploy/deploy-to-nas.bat
Normal file
33
deploy/deploy-to-nas.bat
Normal file
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "SERVER_HOST=116.62.17.81"
|
||||
set "SERVER_PORT=9444"
|
||||
set "SERVER_USERNAME=wkc"
|
||||
set "SERVER_PASSWORD=wkc@0825"
|
||||
set "REMOTE_ROOT=/volume1/webapp/ccdi"
|
||||
set "DRY_RUN="
|
||||
set /a POSITION=0
|
||||
|
||||
:parse_args
|
||||
if "%~1"=="" goto run_script
|
||||
|
||||
if /I "%~1"=="--dry-run" (
|
||||
set "DRY_RUN=-DryRun"
|
||||
) else (
|
||||
set /a POSITION+=1
|
||||
if !POSITION!==1 set "SERVER_HOST=%~1"
|
||||
if !POSITION!==2 set "SERVER_PORT=%~1"
|
||||
if !POSITION!==3 set "SERVER_USERNAME=%~1"
|
||||
if !POSITION!==4 set "SERVER_PASSWORD=%~1"
|
||||
if !POSITION!==5 set "REMOTE_ROOT=%~1"
|
||||
)
|
||||
|
||||
shift
|
||||
goto parse_args
|
||||
|
||||
:run_script
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%deploy.ps1" -ServerHost "%SERVER_HOST%" -Port "%SERVER_PORT%" -Username "%SERVER_USERNAME%" -Password "%SERVER_PASSWORD%" -RemoteRoot "%REMOTE_ROOT%" %DRY_RUN%
|
||||
set "EXIT_CODE=%ERRORLEVEL%"
|
||||
endlocal & exit /b %EXIT_CODE%
|
||||
112
deploy/deploy.ps1
Normal file
112
deploy/deploy.ps1
Normal file
@@ -0,0 +1,112 @@
|
||||
param(
|
||||
[string]$ServerHost = "116.62.17.81",
|
||||
[int]$Port = 9444,
|
||||
[string]$Username = "wkc",
|
||||
[string]$Password = "wkc@0825",
|
||||
[string]$RemoteRoot = "/volume1/webapp/ccdi",
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repoRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path
|
||||
$stageRoot = Join-Path $repoRoot ".deploy\\ccdi-package"
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[DryRun] 一键部署参数预览"
|
||||
Write-Host "Host: $ServerHost"
|
||||
Write-Host "Port: $Port"
|
||||
Write-Host "Username: $Username"
|
||||
Write-Host "RemoteRoot: $RemoteRoot"
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Ensure-Command {
|
||||
param([string]$CommandName)
|
||||
if (-not (Get-Command $CommandName -ErrorAction SilentlyContinue)) {
|
||||
throw "缺少命令: $CommandName"
|
||||
}
|
||||
}
|
||||
|
||||
function Reset-Directory {
|
||||
param([string]$Path)
|
||||
if (Test-Path $Path) {
|
||||
[System.IO.Directory]::Delete($Path, $true)
|
||||
}
|
||||
New-Item -ItemType Directory -Path $Path | Out-Null
|
||||
}
|
||||
|
||||
function Copy-ItemSafe {
|
||||
param(
|
||||
[string]$Source,
|
||||
[string]$Destination
|
||||
)
|
||||
Copy-Item -Path $Source -Destination $Destination -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Host "[1/5] 检查本地环境"
|
||||
Ensure-Command "mvn"
|
||||
Ensure-Command "npm"
|
||||
Ensure-Command "python"
|
||||
|
||||
Write-Host "[2/5] 打包后端"
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
mvn clean package -DskipTests
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "后端打包失败"
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host "[3/5] 打包前端"
|
||||
Push-Location (Join-Path $repoRoot "ruoyi-ui")
|
||||
try {
|
||||
npm run build:prod
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "前端打包失败"
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host "[4/5] 组装部署目录"
|
||||
Reset-Directory $stageRoot
|
||||
New-Item -ItemType Directory -Path (Join-Path $stageRoot "backend") | Out-Null
|
||||
New-Item -ItemType Directory -Path (Join-Path $stageRoot "frontend") | Out-Null
|
||||
|
||||
Copy-ItemSafe (Join-Path $repoRoot "docker") (Join-Path $stageRoot "docker")
|
||||
Copy-ItemSafe (Join-Path $repoRoot "lsfx-mock-server") (Join-Path $stageRoot "lsfx-mock-server")
|
||||
Copy-ItemSafe (Join-Path $repoRoot "ruoyi-ui\\dist") (Join-Path $stageRoot "frontend\\dist")
|
||||
Copy-ItemSafe (Join-Path $repoRoot "docker-compose.yml") (Join-Path $stageRoot "docker-compose.yml")
|
||||
Copy-ItemSafe (Join-Path $repoRoot ".env.example") (Join-Path $stageRoot ".env.example")
|
||||
Copy-ItemSafe (Join-Path $repoRoot "ruoyi-admin\\target\\ruoyi-admin.jar") (Join-Path $stageRoot "backend\\ruoyi-admin.jar")
|
||||
|
||||
Write-Host "[5/5] 上传并远端部署"
|
||||
$paramikoCheck = @'
|
||||
import importlib.util
|
||||
import sys
|
||||
sys.exit(0 if importlib.util.find_spec("paramiko") else 1)
|
||||
'@
|
||||
$paramikoCheck | python -
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
python -m pip install --user paramiko
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "安装 paramiko 失败"
|
||||
}
|
||||
}
|
||||
|
||||
python (Join-Path $scriptDir "remote-deploy.py") `
|
||||
--host $ServerHost `
|
||||
--port $Port `
|
||||
--username $Username `
|
||||
--password $Password `
|
||||
--local-root $stageRoot `
|
||||
--remote-root $RemoteRoot
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "远端部署失败"
|
||||
}
|
||||
177
deploy/remote-deploy.py
Normal file
177
deploy/remote-deploy.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import argparse
|
||||
import os
|
||||
import posixpath
|
||||
import shlex
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
SKIP_DIRS = {"__pycache__", ".pytest_cache", ".git"}
|
||||
SKIP_FILES = {".DS_Store"}
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Upload CCDI deployment package and run docker compose remotely.")
|
||||
parser.add_argument("--host", required=True)
|
||||
parser.add_argument("--port", type=int, required=True)
|
||||
parser.add_argument("--username", required=True)
|
||||
parser.add_argument("--password", required=True)
|
||||
parser.add_argument("--local-root", required=True)
|
||||
parser.add_argument("--remote-root", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def ensure_remote_dir(ssh, remote_path):
|
||||
command = f"mkdir -p {shlex.quote(remote_path)}"
|
||||
exit_code, output, error = run_command(ssh, command)
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(f"Failed to create remote directory {remote_path}:\n{output}\n{error}")
|
||||
|
||||
|
||||
def resolve_sftp_root(sftp, shell_root):
|
||||
parts = [part for part in shell_root.split("/") if part]
|
||||
for index in range(len(parts)):
|
||||
candidate = "/" + "/".join(parts[index:])
|
||||
try:
|
||||
sftp.listdir(candidate)
|
||||
return candidate
|
||||
except OSError:
|
||||
continue
|
||||
raise RuntimeError(f"Unable to resolve SFTP path for remote root: {shell_root}")
|
||||
|
||||
|
||||
def upload_tree(ssh, sftp, local_root, shell_remote_root, sftp_remote_root):
|
||||
for current_root, dirs, files in os.walk(local_root):
|
||||
dirs[:] = [directory for directory in dirs if directory not in SKIP_DIRS]
|
||||
relative = os.path.relpath(current_root, local_root)
|
||||
relative_posix = "" if relative == "." else relative.replace("\\", "/")
|
||||
shell_remote_dir = shell_remote_root if not relative_posix else posixpath.join(shell_remote_root, relative_posix)
|
||||
sftp_remote_dir = sftp_remote_root if not relative_posix else posixpath.join(sftp_remote_root, relative_posix)
|
||||
ensure_remote_dir(ssh, shell_remote_dir)
|
||||
for file_name in files:
|
||||
if file_name in SKIP_FILES:
|
||||
continue
|
||||
local_file = os.path.join(current_root, file_name)
|
||||
remote_file = posixpath.join(sftp_remote_dir, file_name)
|
||||
sftp.put(local_file, remote_file)
|
||||
|
||||
|
||||
def run_command(ssh, command):
|
||||
stdin, stdout, stderr = ssh.exec_command(command)
|
||||
exit_code = stdout.channel.recv_exit_status()
|
||||
output = stdout.read().decode("utf-8", errors="ignore")
|
||||
error = stderr.read().decode("utf-8", errors="ignore")
|
||||
return exit_code, output, error
|
||||
|
||||
|
||||
def sudo_prefix(password):
|
||||
return f"printf '%s\\n' {shlex.quote(password)} | sudo -S -p '' "
|
||||
|
||||
|
||||
def detect_compose_command(ssh, password):
|
||||
daemon_prefix = ""
|
||||
daemon_checks = [
|
||||
("", "docker ps >/dev/null 2>&1"),
|
||||
(sudo_prefix(password), f"{sudo_prefix(password)}docker ps >/dev/null 2>&1"),
|
||||
]
|
||||
for prefix, probe in daemon_checks:
|
||||
exit_code, _, _ = run_command(ssh, probe)
|
||||
if exit_code == 0:
|
||||
daemon_prefix = prefix
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Docker daemon is not accessible on remote host.")
|
||||
|
||||
checks = [
|
||||
(f"{daemon_prefix}docker compose", f"{daemon_prefix}docker compose version"),
|
||||
(f"{daemon_prefix}docker-compose", f"{daemon_prefix}docker-compose --version"),
|
||||
]
|
||||
for compose_cmd, probe in checks:
|
||||
exit_code, _, _ = run_command(ssh, probe)
|
||||
if exit_code == 0:
|
||||
return compose_cmd
|
||||
raise RuntimeError("Docker Compose command not found on remote host.")
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
local_root = Path(args.local_root).resolve()
|
||||
remote_root = args.remote_root.rstrip("/")
|
||||
if not local_root.exists():
|
||||
raise FileNotFoundError(f"Local root does not exist: {local_root}")
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(
|
||||
hostname=args.host,
|
||||
port=args.port,
|
||||
username=args.username,
|
||||
password=args.password,
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
try:
|
||||
pre_clean = (
|
||||
"set -e;"
|
||||
f"mkdir -p {shlex.quote(remote_root)};"
|
||||
f"mkdir -p {shlex.quote(posixpath.join(remote_root, 'runtime/ruoyi'))};"
|
||||
f"mkdir -p {shlex.quote(posixpath.join(remote_root, 'runtime/logs/backend'))};"
|
||||
f"rm -rf {shlex.quote(posixpath.join(remote_root, 'backend'))} "
|
||||
f"{shlex.quote(posixpath.join(remote_root, 'frontend'))} "
|
||||
f"{shlex.quote(posixpath.join(remote_root, 'docker'))} "
|
||||
f"{shlex.quote(posixpath.join(remote_root, 'lsfx-mock-server'))};"
|
||||
f"rm -f {shlex.quote(posixpath.join(remote_root, 'docker-compose.yml'))} "
|
||||
f"{shlex.quote(posixpath.join(remote_root, '.env.example'))};"
|
||||
)
|
||||
exit_code, output, error = run_command(ssh, pre_clean)
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(f"Remote cleanup failed:\n{output}\n{error}")
|
||||
|
||||
sftp_remote_root = resolve_sftp_root(sftp, remote_root)
|
||||
upload_tree(ssh, sftp, str(local_root), remote_root, sftp_remote_root)
|
||||
|
||||
compose_cmd = detect_compose_command(ssh, args.password)
|
||||
|
||||
deploy_command = (
|
||||
"set -e;"
|
||||
f"cd {shlex.quote(remote_root)};"
|
||||
f"{compose_cmd} up -d --build;"
|
||||
f"{compose_cmd} ps;"
|
||||
)
|
||||
exit_code, output, error = run_command(ssh, deploy_command)
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(f"Remote deploy failed:\n{output}\n{error}")
|
||||
|
||||
logs_command = (
|
||||
"set -e;"
|
||||
f"cd {shlex.quote(remote_root)};"
|
||||
f"{compose_cmd} logs backend --tail 120;"
|
||||
)
|
||||
_, logs_output, logs_error = run_command(ssh, logs_command)
|
||||
|
||||
print("=== DEPLOY OUTPUT ===")
|
||||
print(output.strip())
|
||||
if error.strip():
|
||||
print("=== DEPLOY STDERR ===")
|
||||
print(error.strip())
|
||||
|
||||
if logs_output.strip():
|
||||
print("=== BACKEND LOGS ===")
|
||||
print(logs_output.strip())
|
||||
if logs_error.strip():
|
||||
print("=== BACKEND LOG STDERR ===")
|
||||
print(logs_error.strip())
|
||||
finally:
|
||||
sftp.close()
|
||||
ssh.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user