#!/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)