#!/usr/bin/env bash # ============================================================ # pccs build.sh — 一键部署脚本 (符合 web-application-spec) # # 在部署节点上运行: # cd /d/pccs/repos/pccs && bash build.sh # # 前提: # - MariaDB 已安装, pccs 库已创建, test/test123 已授权 # - /d 已作为 NFS 共享盘导出 # - git, python3.10+, pip, npm 已安装 # ============================================================ set -e # --------------- 配置 --------------- WORKDIR="$(cd "$(dirname "$0")/../.." && pwd)" CDIR="${WORKDIR}" # 应用根目录 (/d/pccs) PKGDIR="${CDIR}/repos" # 模块 clone 目录 VENV="${CDIR}/venv" APPNAME="pccs" APP_PORT=9180 # Git 仓库基地址 GIT_BASE="https://git.opencomputing.cn/yumoqing" # --------------- 颜色 --------------- red() { echo -e "\033[31m$1\033[0m"; } green() { echo -e "\033[32m$1\033[0m"; } blue() { echo -e "\033[34m$1\033[0m"; } blue "==========================================" blue " pccs — 算力中心集群系统 部署" blue "==========================================" # ============================================================ # Phase 0: 创建目录 # ============================================================ mkdir -p "${CDIR}/app" "${CDIR}/conf" "${CDIR}/files" "${CDIR}/logs" \ "${CDIR}/wwwroot/imgs" "${PKGDIR}" # ============================================================ # Phase 1: 安装系统依赖 # ============================================================ blue "[1/10] 系统依赖..." sudo apt-get update -qq sudo apt-get install -y -qq python3-venv python3-pip python3-dev \ build-essential nfs-common mysql-client green " OK" # ============================================================ # Phase 2: Python 虚拟环境 # ============================================================ blue "[2/10] Python 虚拟环境..." python3 -m venv "${VENV}" --clear PIP="${VENV}/bin/pip" PYTHON="${VENV}/bin/python" "${PIP}" install --upgrade pip setuptools wheel -q # 先安装 xls2ddl (spec 要求, 用于 DDL 生成和数据导入) "${PIP}" install xls2ddl -q 2>/dev/null || true green " OK" # ============================================================ # Phase 3: Clone/Pull 所有模块 # ============================================================ blue "[3/10] 同步代码仓库..." ALL_REPOS=( # 基础层 apppublic sqlor ahserver # 数据库层 appbase rbac # 前端 bricks bricks_for_python # 业务模块 pcpool pcc storage_mgr image_mgr # 主应用 pccs ) for repo in "${ALL_REPOS[@]}"; do if [ -d "${PKGDIR}/${repo}/.git" ]; then (cd "${PKGDIR}/${repo}" && git pull origin main 2>/dev/null) || true else echo " clone ${repo}..." git clone "${GIT_BASE}/${repo}.git" "${PKGDIR}/${repo}" 2>/dev/null || { red " WARN: ${repo} clone failed" } fi done green " OK" # ============================================================ # Phase 4: pip install 所有模块 # ============================================================ blue "[4/10] 安装 Python 模块..." MODULES=( apppublic sqlor ahserver appbase rbac bricks_for_python pcpool pcc storage_mgr image_mgr ) for m in "${MODULES[@]}"; do mp="${PKGDIR}/${m}" if [ -f "${mp}/setup.py" ] || [ -f "${mp}/setup.cfg" ] || [ -f "${mp}/pyproject.toml" ]; then "${PIP}" install -e "${mp}" -q 2>&1 | tail -1 fi done green " OK" # ============================================================ # Phase 5: 数据库 DDL — json2ddl 生成建表 SQL # ============================================================ blue "[5/10] 数据库 DDL 生成..." # 收集所有有 models/ 的模块 DB_MODULES=(appbase rbac pcpool pcc storage_mgr image_mgr) for m in "${DB_MODULES[@]}"; do mdir="${PKGDIR}/${m}/models" if [ -d "$mdir" ]; then has_json=$(ls "$mdir"/*.json 2>/dev/null | wc -l) has_xlsx=$(ls "$mdir"/*.xlsx 2>/dev/null | wc -l) if [ "$has_json" -gt 0 ]; then echo " [${m}] json2ddl → DDL" (cd "$mdir" && "${VENV}/bin/json2ddl" mysql . > /tmp/pccs_${m}.ddl.sql 2>/dev/null) || true mysql -u test -ptest123 pccs < /tmp/pccs_${m}.ddl.sql 2>/dev/null || true fi if [ "$has_xlsx" -gt 0 ]; then echo " [${m}] xls2ddl → DDL" (cd "$mdir" && xls2ddl mysql . > /tmp/pccs_${m}_xlsx.ddl.sql 2>/dev/null) || true mysql -u test -ptest123 pccs < /tmp/pccs_${m}_xlsx.ddl.sql 2>/dev/null || true fi fi done green " OK" # ============================================================ # Phase 6: 导入初始数据 (data/ 目录) # ============================================================ blue "[6/10] 初始数据导入..." for m in "${DB_MODULES[@]}"; do ddir="${PKGDIR}/${m}/data" if [ -d "$ddir" ]; then for xlsx in "$ddir"/*.xlsx; do [ -f "$xlsx" ] || continue echo " [${m}] dbloader → ${xlsx}" "${VENV}/bin/dbloader" "${CDIR}" pccs "$xlsx" 2>/dev/null || true done fi done green " OK" # ============================================================ # Phase 7: CRUD 生成 — xls2ui # ============================================================ blue "[7/10] CRUD 界面生成..." for m in "${DB_MODULES[@]}"; do jdir="${PKGDIR}/${m}/json" mdir="${PKGDIR}/${m}/models" odir="${PKGDIR}/${m}/wwwroot" if [ -d "$jdir" ] && [ -d "$mdir" ]; then echo " [${m}] xls2ui..." (cd "$jdir" && "${VENV}/bin/xls2ui" -m "$mdir" -o "$odir" "$m" *.json 2>/dev/null) || true fi done green " OK" # ============================================================ # Phase 8: 创建 wwwroot 软链接 # ============================================================ blue "[8/10] 创建 wwwroot 软链接..." for m in "${DB_MODULES[@]}"; do wdir="${PKGDIR}/${m}/wwwroot" if [ -d "$wdir" ]; then ln -sfn "$wdir" "${CDIR}/wwwroot/${m}" 2>/dev/null || true fi done # pccs 自身的 wwwroot ln -sfn "${PKGDIR}/pccs/wwwroot" "${CDIR}/wwwroot/${APPNAME}" 2>/dev/null || true # Bricks 前端 if [ -f "${PKGDIR}/bricks/build.sh" ]; then blue "[bricks] 构建前端..." (cd "${PKGDIR}/bricks" && npm install --silent 2>/dev/null && bash build.sh 2>&1 | tail -3) || true ln -sfn "${PKGDIR}/bricks/dist" "${CDIR}/wwwroot/bricks" 2>/dev/null || true fi green " OK" # ============================================================ # Phase 9: 种子应用数据 (appcode / RBAC) # ============================================================ blue "[9/10] 种子数据..." "${PYTHON}" - "${CDIR}" "${PKGDIR}" <<'PYEOF' import sys, os, json CDIR = sys.argv[1] PKGDIR = sys.argv[2] sys.path.insert(0, os.path.join(PKGDIR, 'apppublic')) sys.path.insert(0, os.path.join(PKGDIR, 'sqlor')) sys.path.insert(0, os.path.join(PKGDIR, 'ahserver')) from appPublic.jsonConfig import getConfig from sqlor.dbpools import DBPools from appPublic.uniqueID import getID import datetime, asyncio APPCODES = [ ('cluster_type', 'k8s', 'Kubernetes集群'), ('cluster_type', 'slurm', 'Slurm集群'), ('cluster_type', 'ray', 'Ray集群'), ('cluster_status', 'deploying', '部署中'), ('cluster_status', 'running', '运行中'), ('cluster_status', 'failed', '失败'), ('cluster_status', 'stopped', '已停止'), ('cluster_status', 'destroyed', '已销毁'), ('node_role', 'control', '控制节点'), ('node_role', 'compute', '算力节点'), ('node_role', 'storage', '存储节点'), ('node_status', 'joining', '加入中'), ('node_status', 'active', '活跃'), ('node_status', 'draining', '排空中'), ('node_status', 'removed', '已移除'), ('node_type', 'control', '控制节点'), ('node_type', 'compute', '算力节点'), ('node_type', 'storage', '存储节点'), ('storage_type', 'nfs', 'NFS'), ('storage_type', 'glusterfs', 'GlusterFS'), ('storage_type', 'cephfs', 'CephFS'), ('storage_status', 'online', '在线'), ('storage_status', 'offline', '离线'), ('storage_status', 'maintenance', '维护中'), ('storage_access_mode', 'rw', '读写'), ('storage_access_mode', 'ro', '只读'), ('export_status', 'active', '活跃'), ('export_status', 'inactive', '停用'), ('mount_status', 'unmounted', '未挂载'), ('mount_status', 'mounted', '已挂载'), ('mount_status', 'failed', '失败'), ('mirror_source_type', 'public_mirror', '公共镜像'), ('mirror_source_type', 'private_registry', '私有仓库'), ('mirror_source_type', 'hub', '官方Hub'), ('mirror_region', 'cn', '中国'), ('mirror_region', 'us', '美国'), ('mirror_region', 'eu', '欧洲'), ('mirror_status', 'active', '活跃'), ('mirror_status', 'inactive', '停用'), ('mirror_status', 'error', '异常'), ('registry_type', 'docker-registry', 'Docker Registry'), ('registry_type', 'harbor', 'Harbor'), ('registry_status', 'deploying', '部署中'), ('registry_status', 'running', '运行中'), ('registry_status', 'failed', '失败'), ('registry_status', 'destroyed', '已销毁'), ('image_type', 'vm', '虚拟机镜像'), ('image_type', 'base', '基础镜像'), ('image_type', 'app', '应用镜像'), ('image_type', 'custom', '自定义镜像'), ('sync_status', 'pending', '待同步'), ('sync_status', 'syncing', '同步中'), ('sync_status', 'synced', '已同步'), ('sync_status', 'failed', '失败'), ] async def seed(): config = getConfig(CDIR) db = DBPools(config.databases) now = datetime.datetime.now().isoformat() async with db.sqlorContext('pccs') as sor: rows = await sor.R('appcodes_kv', {'parentid': 'cluster_type'}) if rows: print(' appcode 已存在, 跳过') return for parentid, k, v in APPCODES: await sor.C('appcodes_kv', { 'id': getID(), 'parentid': parentid, 'k': k, 'v': v, 'created_at': now, }) print(f' 已写入 {len(APPCODES)} 条 appcode') asyncio.run(seed()) PYEOF green " OK" # ============================================================ # Phase 10: 创建 systemd 服务 + 启动脚本 # ============================================================ blue "[10/10] 服务配置..." # start.sh cat > "${CDIR}/start.sh" </dev/null || true export PYTHONPATH="${PKGDIR}/ahserver:${PKGDIR}/apppublic:${PKGDIR}/sqlor:${PKGDIR}/appbase:${PKGDIR}/rbac:${PKGDIR}/bricks_for_python:\$PYTHONPATH" exec "${VENV}/bin/python" "${CDIR}/app/${APPNAME}.py" 2>&1 | tee -a "${CDIR}/logs/${APPNAME}.log" STARTEOF chmod +x "${CDIR}/start.sh" # stop.sh cat > "${CDIR}/stop.sh" </dev/null && echo "pccs stopped" || echo "pccs not running" STOPEOF chmod +x "${CDIR}/stop.sh" # systemd service sudo tee /etc/systemd/system/pccs.service > /dev/null </dev/null || true green " OK" # ============================================================ # 启动应用 # ============================================================ blue "启动应用..." "${CDIR}/stop.sh" 2>/dev/null || true sleep 1 nohup bash "${CDIR}/start.sh" > "${CDIR}/logs/nohup.log" 2>&1 & sleep 3 if curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${APP_PORT}/" 2>/dev/null | grep -q '200\|404'; then green " pccs 已启动: http://127.0.0.1:${APP_PORT}" else red " WARN: 无响应, 检查日志: tail -f ${CDIR}/logs/pccs.log" fi echo "" green "==========================================" green " 部署完成!" green " 访问: http://$(hostname -I | awk '{print $1}'):${APP_PORT}" green " 启动: sudo systemctl start pccs" green " 停止: sudo systemctl stop pccs" green " 日志: tail -f ${CDIR}/logs/pccs.log" green "=========================================="