314 lines
10 KiB
Python
314 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
CapacityReport 镜像构建和导出脚本 (Windows)
|
|
在有外网的 Windows 机器上运行此脚本,生成离线部署包
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import re
|
|
from pathlib import Path
|
|
|
|
# 脚本所在目录
|
|
SCRIPT_DIR = Path(__file__).parent.absolute()
|
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
DIST_DIR = SCRIPT_DIR
|
|
|
|
# 颜色输出(Windows 支持)
|
|
import platform
|
|
if platform.system() == 'Windows':
|
|
try:
|
|
import ctypes
|
|
kernel32 = ctypes.windll.kernel32
|
|
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
except:
|
|
pass
|
|
|
|
class Colors:
|
|
GREEN = '\033[92m'
|
|
YELLOW = '\033[93m'
|
|
RED = '\033[91m'
|
|
RESET = '\033[0m'
|
|
|
|
def print_step(msg):
|
|
print(f"\n{Colors.GREEN}{msg}{Colors.RESET}")
|
|
|
|
def print_info(msg):
|
|
print(f" {msg}")
|
|
|
|
def print_warning(msg):
|
|
print(f"{Colors.YELLOW}警告: {msg}{Colors.RESET}")
|
|
|
|
def print_error(msg):
|
|
print(f"{Colors.RED}错误: {msg}{Colors.RESET}")
|
|
|
|
def run_cmd(cmd, check=True, capture_output=False):
|
|
"""执行命令"""
|
|
if isinstance(cmd, str):
|
|
cmd = cmd.split()
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
check=check,
|
|
capture_output=capture_output,
|
|
text=True
|
|
)
|
|
return result
|
|
except subprocess.CalledProcessError as e:
|
|
if check:
|
|
print_error(f"命令执行失败: {' '.join(cmd)}")
|
|
sys.exit(1)
|
|
return None
|
|
except FileNotFoundError:
|
|
print_error(f"命令未找到: {cmd[0]}")
|
|
sys.exit(1)
|
|
|
|
def check_command(cmd):
|
|
"""检查命令是否存在"""
|
|
try:
|
|
if cmd == "docker":
|
|
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
|
elif cmd == "docker-compose":
|
|
subprocess.run([cmd, "version"], capture_output=True, check=True)
|
|
return True
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
return False
|
|
|
|
def get_python_tag_from_dockerfile():
|
|
"""从 Dockerfile 中提取 Python 镜像标签"""
|
|
dockerfile_path = PROJECT_ROOT / "Dockerfile"
|
|
if not dockerfile_path.exists():
|
|
print_error(f"Dockerfile 不存在: {dockerfile_path}")
|
|
sys.exit(1)
|
|
|
|
with open(dockerfile_path, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
match = re.match(r'^FROM\s+python:(.+?)(?:\s|$)', line, re.IGNORECASE)
|
|
if match:
|
|
tag = match.group(1).strip()
|
|
return tag
|
|
|
|
print_error("无法从 Dockerfile 中找到 Python 镜像标签")
|
|
print_error("请确保 Dockerfile 中包含 'FROM python:xxx' 行")
|
|
sys.exit(1)
|
|
|
|
def image_exists(image_name):
|
|
"""检查镜像是否已存在"""
|
|
try:
|
|
result = subprocess.run(
|
|
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
return image_name in result.stdout
|
|
except:
|
|
return False
|
|
|
|
def format_size(size_bytes):
|
|
"""格式化文件大小"""
|
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
if size_bytes < 1024.0:
|
|
return f"{size_bytes:.1f} {unit}"
|
|
size_bytes /= 1024.0
|
|
return f"{size_bytes:.1f} TB"
|
|
|
|
def main():
|
|
print("=" * 50)
|
|
print("CapacityReport 离线部署包构建脚本")
|
|
print("=" * 50)
|
|
print()
|
|
|
|
# 清理旧的构建文件
|
|
print_step("清理旧的构建文件...")
|
|
files_to_clean = [
|
|
DIST_DIR / "images" / "capacity-images.tar",
|
|
DIST_DIR / "Configure.json",
|
|
DIST_DIR / "ReportScript.sql",
|
|
DIST_DIR / "mysql" / "conf.d" / "custom.cnf",
|
|
DIST_DIR / "mysql" / "init" / "01-init-db.sql",
|
|
]
|
|
|
|
for file_path in files_to_clean:
|
|
if file_path.exists():
|
|
if file_path.is_file():
|
|
file_path.unlink()
|
|
print_info(f"已删除旧 {file_path.name}")
|
|
elif file_path.is_dir():
|
|
shutil.rmtree(file_path)
|
|
print_info(f"已删除旧 {file_path.name}")
|
|
|
|
print_info("清理完成")
|
|
|
|
# 检查 Docker
|
|
print_step("检查 Docker 环境...")
|
|
if not check_command("docker"):
|
|
print_error("未检测到 Docker,请先安装 Docker Desktop")
|
|
sys.exit(1)
|
|
print_info("Docker: OK")
|
|
|
|
# 检查 Docker Compose
|
|
docker_compose_cmd = None
|
|
try:
|
|
subprocess.run(["docker", "compose", "version"], capture_output=True, check=True)
|
|
docker_compose_cmd = "docker compose"
|
|
print_info("Docker Compose: OK (docker compose)")
|
|
except:
|
|
if check_command("docker-compose"):
|
|
docker_compose_cmd = "docker-compose"
|
|
print_info("Docker Compose: OK (docker-compose)")
|
|
else:
|
|
print_error("未检测到 Docker Compose,请先安装 Docker Compose")
|
|
sys.exit(1)
|
|
|
|
# 步骤 1: 拉取基础镜像
|
|
print_step("步骤 1: 拉取基础镜像...")
|
|
|
|
# 获取 Python 版本
|
|
python_tag = get_python_tag_from_dockerfile()
|
|
python_image = f"python:{python_tag}"
|
|
print_info(f"从 Dockerfile 检测到 Python 镜像标签: {python_tag}")
|
|
|
|
# 检查并拉取 Python 镜像
|
|
if image_exists(python_image):
|
|
print_info(f"Python 镜像 {python_image} 已存在,跳过拉取")
|
|
else:
|
|
print_info(f"正在拉取 {python_image}...")
|
|
run_cmd(["docker", "pull", python_image])
|
|
print_info(f"Python 镜像拉取完成")
|
|
|
|
# 检查并拉取 MySQL 镜像
|
|
mysql_image = "mysql:8.0.44"
|
|
if image_exists(mysql_image):
|
|
print_info(f"MySQL 镜像 {mysql_image} 已存在,跳过拉取")
|
|
else:
|
|
print_info(f"正在拉取 {mysql_image}...")
|
|
run_cmd(["docker", "pull", mysql_image])
|
|
print_info(f"MySQL 镜像拉取完成")
|
|
|
|
# 步骤 2: 构建应用镜像
|
|
print_step("步骤 2: 构建应用镜像...")
|
|
os.chdir(PROJECT_ROOT)
|
|
run_cmd(["docker", "build", "-t", "capacity-report-app:latest", "-f", "Dockerfile", "."])
|
|
print_info("应用镜像构建完成")
|
|
|
|
# 步骤 3: 标记 MySQL 镜像
|
|
print_step("步骤 3: 标记 MySQL 镜像...")
|
|
run_cmd(["docker", "tag", mysql_image, "capacity-mysql:8.0.44"])
|
|
print_info("MySQL 镜像标记完成")
|
|
|
|
# 步骤 4: 导出镜像
|
|
print_step("步骤 4: 导出镜像到 dist 目录...")
|
|
images_dir = DIST_DIR / "images"
|
|
images_dir.mkdir(exist_ok=True)
|
|
|
|
tar_file = images_dir / "capacity-images.tar"
|
|
print_info("正在导出镜像到单个 tar 文件:")
|
|
print_info(" - capacity-report-app:latest (应用镜像)")
|
|
print_info(" - capacity-mysql:8.0.44 (MySQL 镜像)")
|
|
|
|
run_cmd([
|
|
"docker", "save",
|
|
"capacity-report-app:latest",
|
|
"capacity-mysql:8.0.44",
|
|
"-o", str(tar_file)
|
|
])
|
|
|
|
print_info(f"镜像导出完成: {tar_file}")
|
|
print_info("注意: 此文件包含应用镜像和 MySQL 镜像,部署时会自动加载所有镜像")
|
|
|
|
# 步骤 5: 复制必要文件
|
|
print_step("步骤 5: 复制必要文件到 dist 目录...")
|
|
|
|
# 创建必要的目录结构
|
|
(DIST_DIR / "mysql" / "conf.d").mkdir(parents=True, exist_ok=True)
|
|
(DIST_DIR / "mysql" / "init").mkdir(parents=True, exist_ok=True)
|
|
|
|
# 复制配置文件
|
|
print_info("正在复制配置文件...")
|
|
|
|
files_to_copy = [
|
|
(PROJECT_ROOT / "Configure.json", DIST_DIR / "Configure.json"),
|
|
(PROJECT_ROOT / "ReportScript.sql", DIST_DIR / "ReportScript.sql"),
|
|
(PROJECT_ROOT / "mysql" / "conf.d" / "custom.cnf", DIST_DIR / "mysql" / "conf.d" / "custom.cnf"),
|
|
(PROJECT_ROOT / "mysql" / "init" / "01-init-db.sql", DIST_DIR / "mysql" / "init" / "01-init-db.sql"),
|
|
]
|
|
|
|
for src, dst in files_to_copy:
|
|
if src.exists():
|
|
shutil.copy2(src, dst)
|
|
print_info(f"{src.name} 已复制")
|
|
else:
|
|
print_warning(f"{src.name} 不存在: {src}")
|
|
|
|
# 更新 Configure.json 为 Docker 环境配置
|
|
config_file = DIST_DIR / "Configure.json"
|
|
if config_file.exists():
|
|
print_info("正在更新 Configure.json 为 Docker 环境配置...")
|
|
try:
|
|
with open(config_file, 'r', encoding='utf-8') as f:
|
|
config = json.load(f)
|
|
|
|
config['MySQL_DBInfo'] = {
|
|
'host': 'capacity-mysql',
|
|
'port': 3306,
|
|
'user': 'root',
|
|
'passwd': 'gmcc123',
|
|
'dbname': 'CapacityReport'
|
|
}
|
|
|
|
with open(config_file, 'w', encoding='utf-8') as f:
|
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
|
|
print_info("已更新数据库配置: host=capacity-mysql, port=3306, user=root, passwd=gmcc123")
|
|
except Exception as e:
|
|
print_warning(f"无法自动更新 Configure.json: {e}")
|
|
print_warning("请手动修改数据库配置")
|
|
|
|
# 步骤 6: 计算文件大小
|
|
print_step("步骤 6: 计算文件大小...")
|
|
if tar_file.exists():
|
|
size = tar_file.stat().st_size
|
|
print_info(f"镜像文件大小: {format_size(size)}")
|
|
|
|
# 步骤 7: 清理临时目录
|
|
print_step("步骤 7: 清理临时目录...")
|
|
temp_dirs = [DIST_DIR / "cache", DIST_DIR / "logs"]
|
|
for temp_dir in temp_dirs:
|
|
if temp_dir.exists():
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
print_info(f"已删除 {temp_dir.name} 目录")
|
|
|
|
# 完成
|
|
print()
|
|
print("=" * 50)
|
|
print("构建完成!")
|
|
print("=" * 50)
|
|
print()
|
|
print(f"离线部署包位置: {DIST_DIR}")
|
|
print()
|
|
print("部署步骤:")
|
|
print("1. 将整个 dist 目录传输到目标 Linux 机器")
|
|
print("2. 在目标机器上执行: sh dist/deploy.sh")
|
|
print()
|
|
print("端口映射:")
|
|
print(" - 应用端口: 19081 (原 9081)")
|
|
print(" - MySQL 端口: 13306 (原 3306)")
|
|
print()
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
print("\n\n构建已取消")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print_error(f"构建失败: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|