431 lines
15 KiB
Bash
431 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
# ============================================================
|
|
# pccs build.sh — 一键部署脚本
|
|
# 在部署节点 (pccs@pccs.opencomputing.cn) 上直接运行:
|
|
# cd /d/pccs/repos/pccs && bash build.sh
|
|
#
|
|
# 前提:
|
|
# 1. /d 已作为 NFS 共享盘挂载
|
|
# 2. MariaDB 已安装, 数据库 pccs 已创建, 用户 test/test123 已授权
|
|
# 3. 部署节点已安装 git, python3.10+, pip
|
|
# ============================================================
|
|
set -e
|
|
|
|
SERVER_PATH="${SERVER_PATH:-/d/pccs}"
|
|
REPOS_DIR="${SERVER_PATH}/repos"
|
|
VENV_DIR="${SERVER_PATH}/venv"
|
|
CONF_DIR="${SERVER_PATH}/conf"
|
|
LOG_DIR="${SERVER_PATH}/logs"
|
|
RUN_DIR="${SERVER_PATH}/run"
|
|
APP_PORT="${APP_PORT:-9180}"
|
|
|
|
# --------------- 模块列表 ---------------
|
|
# 共享底层库 (pip install)
|
|
SHARED_LIBS=(
|
|
apppublic
|
|
sqlor
|
|
ahserver
|
|
appbase
|
|
rbac
|
|
)
|
|
# pccs 业务模块 (pip install)
|
|
PCCS_MODULES=(
|
|
pcpool
|
|
pcc
|
|
storage_mgr
|
|
image_mgr
|
|
)
|
|
# 前端 bricks (需要构建)
|
|
BRICKS_DIR="${REPOS_DIR}/bricks"
|
|
|
|
# --------------- 颜色输出 ---------------
|
|
red() { echo -e "\033[31m$1\033[0m"; }
|
|
green() { echo -e "\033[32m$1\033[0m"; }
|
|
blue() { echo -e "\033[34m$1\033[0m"; }
|
|
|
|
echo "=========================================="
|
|
blue "PCCS 算力中心集群系统 — 一键部署"
|
|
echo "=========================================="
|
|
echo "部署路径: ${SERVER_PATH}"
|
|
echo "监听端口: ${APP_PORT}"
|
|
echo ""
|
|
|
|
# ============================================================
|
|
# Phase 0: 创建目录结构
|
|
# ============================================================
|
|
blue "[Phase 0] 创建目录结构..."
|
|
mkdir -p "${REPOS_DIR}" "${VENV_DIR}" "${CONF_DIR}" "${LOG_DIR}" "${RUN_DIR}"
|
|
green " 目录结构已创建"
|
|
|
|
# ============================================================
|
|
# Phase 1: 安装系统依赖
|
|
# ============================================================
|
|
blue "[Phase 1] 安装系统依赖..."
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq python3-venv python3-pip python3-dev build-essential nfs-common
|
|
green " 系统依赖已安装"
|
|
|
|
# ============================================================
|
|
# Phase 2: Clone/Pull 所有仓库 (HTTPS, 只读)
|
|
# ============================================================
|
|
blue "[Phase 2] Clone/Pull 代码仓库..."
|
|
|
|
GIT_BASE="https://git.opencomputing.cn/yumoqing"
|
|
ALL_REPOS=("${SHARED_LIBS[@]}" "${PCCS_MODULES[@]}" "bricks" "pccs")
|
|
|
|
for repo in "${ALL_REPOS[@]}"; do
|
|
if [ -d "${REPOS_DIR}/${repo}/.git" ]; then
|
|
echo " [pull] ${repo}..."
|
|
(cd "${REPOS_DIR}/${repo}" && git pull origin main 2>/dev/null) || true
|
|
else
|
|
echo " [clone] ${repo}..."
|
|
git clone "${GIT_BASE}/${repo}.git" "${REPOS_DIR}/${repo}" 2>/dev/null || {
|
|
red " 警告: ${repo} clone 失败, 跳过"
|
|
}
|
|
fi
|
|
done
|
|
green " 代码仓库已同步"
|
|
|
|
# ============================================================
|
|
# Phase 3: 创建 Python 虚拟环境
|
|
# ============================================================
|
|
blue "[Phase 3] 创建 Python 虚拟环境..."
|
|
python3 -m venv "${VENV_DIR}" --clear
|
|
PIP="${VENV_DIR}/bin/pip"
|
|
PYTHON="${VENV_DIR}/bin/python"
|
|
# 升级 pip
|
|
"${PIP}" install --upgrade pip setuptools wheel -q
|
|
green " 虚拟环境已创建: ${VENV_DIR}"
|
|
|
|
# ============================================================
|
|
# Phase 4: 安装共享底层库 (develop 模式)
|
|
# ============================================================
|
|
blue "[Phase 4] 安装共享底层库..."
|
|
for lib in "${SHARED_LIBS[@]}"; do
|
|
lib_path="${REPOS_DIR}/${lib}"
|
|
if [ -f "${lib_path}/setup.py" ] || [ -f "${lib_path}/setup.cfg" ] || [ -f "${lib_path}/pyproject.toml" ]; then
|
|
echo " [install] ${lib}..."
|
|
"${PIP}" install -e "${lib_path}" -q 2>&1 | tail -1
|
|
else
|
|
echo " [skip] ${lib} — 无 setup"
|
|
fi
|
|
done
|
|
green " 共享底层库已安装"
|
|
|
|
# ============================================================
|
|
# Phase 5: 安装 pccs 业务模块 (develop 模式)
|
|
# ============================================================
|
|
blue "[Phase 5] 安装 pccs 业务模块..."
|
|
for mod in "${PCCS_MODULES[@]}"; do
|
|
mod_path="${REPOS_DIR}/${mod}"
|
|
if [ -f "${mod_path}/setup.json" ] || [ -f "${mod_path}/setup.py" ] || [ -f "${mod_path}/setup.cfg" ]; then
|
|
echo " [install] ${mod}..."
|
|
"${PIP}" install -e "${mod_path}" -q 2>&1 | tail -1
|
|
else
|
|
echo " [skip] ${mod} — 无 setup"
|
|
fi
|
|
done
|
|
green " pccs 业务模块已安装"
|
|
|
|
# ============================================================
|
|
# Phase 6: 构建 Bricks 前端
|
|
# ============================================================
|
|
blue "[Phase 6] 构建 Bricks 前端..."
|
|
if [ -f "${BRICKS_DIR}/build.sh" ]; then
|
|
echo " 构建 bricks.js..."
|
|
(cd "${BRICKS_DIR}" && npm install --silent 2>/dev/null && bash build.sh 2>&1 | tail -3) || {
|
|
red " 警告: bricks 构建失败, 前端可能不可用"
|
|
}
|
|
fi
|
|
green " Bricks 前端已构建"
|
|
|
|
# ============================================================
|
|
# Phase 7: 创建配置文件
|
|
# ============================================================
|
|
blue "[Phase 7] 创建配置..."
|
|
cat > "${CONF_DIR}/config.json" <<CONFEOF
|
|
{
|
|
"workdir": "${SERVER_PATH}",
|
|
"host": "0.0.0.0",
|
|
"port": ${APP_PORT},
|
|
"static": "${REPOS_DIR}/pccs/wwwroot",
|
|
"databases": {
|
|
"pccs": {
|
|
"db": "mysql",
|
|
"host": "127.0.0.1",
|
|
"port": 3306,
|
|
"user": "test",
|
|
"password": "test123",
|
|
"database": "pccs",
|
|
"charset": "utf8mb4",
|
|
"minsize": 2,
|
|
"maxsize": 10
|
|
}
|
|
},
|
|
"modules": [
|
|
{"name": "appbase", "path": "${REPOS_DIR}/appbase"},
|
|
{"name": "rbac", "path": "${REPOS_DIR}/rbac"},
|
|
{"name": "pcpool", "path": "${REPOS_DIR}/pcpool"},
|
|
{"name": "pcc", "path": "${REPOS_DIR}/pcc"},
|
|
{"name": "pccs", "path": "${REPOS_DIR}/pccs"},
|
|
{"name": "storage_mgr", "path": "${REPOS_DIR}/storage_mgr"},
|
|
{"name": "image_mgr", "path": "${REPOS_DIR}/image_mgr"}
|
|
],
|
|
"module_dbname": "pccs",
|
|
"hot_reload": true
|
|
}
|
|
CONFEOF
|
|
green " 配置已生成: ${CONF_DIR}/config.json"
|
|
|
|
# ============================================================
|
|
# Phase 8: 初始化数据库表
|
|
# ============================================================
|
|
blue "[Phase 8] 初始化数据库表..."
|
|
|
|
# 收集所有模块的 models/ 目录
|
|
MODEL_DIRS=()
|
|
for mod in "${PCCS_MODULES[@]}" "appbase" "rbac"; do
|
|
mdir="${REPOS_DIR}/${mod}/models"
|
|
if [ -d "$mdir" ]; then
|
|
MODEL_DIRS+=("$mdir")
|
|
fi
|
|
done
|
|
|
|
# 用 Python 脚本创建表
|
|
"${PYTHON}" - "$SERVER_PATH" "${MODEL_DIRS[@]}" <<'PYEOF'
|
|
import sys, os, json, asyncio
|
|
sys.path.insert(0, sys.argv[1] + '/repos/sqlor')
|
|
sys.path.insert(0, sys.argv[1] + '/repos/apppublic')
|
|
|
|
from appPublic.jsonConfig import getConfig
|
|
from sqlor.dbpools import DBPools
|
|
from sqlor.ddl_template_mysql import DDLTemplateMySQL
|
|
|
|
async def create_tables():
|
|
config = getConfig(sys.argv[1])
|
|
dbname = 'pccs'
|
|
db = DBPools(config.databases)
|
|
ddl = DDLTemplateMySQL()
|
|
|
|
async with db.sqlorContext(dbname) as sor:
|
|
# 检查已有表
|
|
existing = await sor.sqlExe("SHOW TABLES", {})
|
|
existing_tables = set(r[0] for r in existing)
|
|
|
|
model_dirs = sys.argv[2:]
|
|
created = 0
|
|
for mdir in model_dirs:
|
|
if not os.path.isdir(mdir):
|
|
continue
|
|
for f in sorted(os.listdir(mdir)):
|
|
if not f.endswith('.json'):
|
|
continue
|
|
fpath = os.path.join(mdir, f)
|
|
try:
|
|
model = json.load(open(fpath))
|
|
except Exception:
|
|
continue
|
|
tblname = model.get('summary', [{}])[0].get('name', '')
|
|
if not tblname:
|
|
continue
|
|
if tblname in existing_tables:
|
|
print(f' [exists] {tblname}')
|
|
continue
|
|
fields = model.get('fields', [])
|
|
sql = ddl.create_table(tblname, fields)
|
|
print(f' [create] {tblname}')
|
|
async with db.sqlorContext(dbname) as sor:
|
|
try:
|
|
await sor.sqlExe(sql, {})
|
|
created += 1
|
|
except Exception as e:
|
|
print(f' ERROR: {e}')
|
|
|
|
print(f' 共创建 {created} 张表')
|
|
|
|
asyncio.run(create_tables())
|
|
PYEOF
|
|
green " 数据库表已初始化"
|
|
|
|
# ============================================================
|
|
# Phase 9: 加载种子数据 (appcode / RBAC)
|
|
# ============================================================
|
|
blue "[Phase 9] 加载种子数据..."
|
|
|
|
"${PYTHON}" - "$SERVER_PATH" <<'PYEOF'
|
|
import sys, os, json
|
|
sys.path.insert(0, sys.argv[1] + '/repos/apppublic')
|
|
sys.path.insert(0, sys.argv[1] + '/repos/sqlor')
|
|
sys.path.insert(0, sys.argv[1] + '/repos/ahserver')
|
|
|
|
from appPublic.jsonConfig import getConfig
|
|
from sqlor.dbpools import DBPools
|
|
from appPublic.uniqueID import getID
|
|
import datetime
|
|
|
|
async def seed_data():
|
|
config = getConfig(sys.argv[1])
|
|
dbname = 'pccs'
|
|
db = DBPools(config.databases)
|
|
now = datetime.datetime.now().isoformat()
|
|
|
|
async with db.sqlorContext(dbname) as sor:
|
|
# 检查 appcodes_kv 是否有 cluster_type 数据
|
|
rows = await sor.R('appcodes_kv', {'parentid': 'cluster_type'})
|
|
if not rows:
|
|
codes = [
|
|
('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', '失败'),
|
|
]
|
|
for parentid, k, v in codes:
|
|
await sor.C('appcodes_kv', {
|
|
'id': getID(),
|
|
'parentid': parentid,
|
|
'k': k,
|
|
'v': v,
|
|
'created_at': now,
|
|
})
|
|
print(f' 已写入 {len(codes)} 条 appcode')
|
|
else:
|
|
print(' appcode 已存在, 跳过')
|
|
|
|
asyncio.run(seed_data())
|
|
PYEOF
|
|
green " 种子数据已加载"
|
|
|
|
# ============================================================
|
|
# Phase 10: 创建启动脚本
|
|
# ============================================================
|
|
blue "[Phase 10] 创建启动脚本..."
|
|
cat > "${RUN_DIR}/start.sh" <<STARTEOF
|
|
#!/usr/bin/env bash
|
|
# pccs 应用启动脚本
|
|
set -e
|
|
|
|
SERVER_PATH="${SERVER_PATH}"
|
|
VENV_DIR="${VENV_DIR}"
|
|
CONF_DIR="${CONF_DIR}"
|
|
LOG_DIR="${LOG_DIR}"
|
|
|
|
cd "\${SERVER_PATH}"
|
|
export PYTHONPATH="\${REPOS_DIR}/ahserver:\${REPOS_DIR}/apppublic:\${REPOS_DIR}/sqlor:\${REPOS_DIR}/appbase:\${REPOS_DIR}/rbac:\${PYTHONPATH}"
|
|
|
|
echo "Starting pccs on port ${APP_PORT}..."
|
|
exec "\${VENV_DIR}/bin/python" -c "
|
|
import sys, os
|
|
sys.path.insert(0, '${REPOS_DIR}/ahserver')
|
|
sys.path.insert(0, '${REPOS_DIR}/apppublic')
|
|
sys.path.insert(0, '${REPOS_DIR}/sqlor')
|
|
sys.path.insert(0, '${REPOS_DIR}/appbase')
|
|
sys.path.insert(0, '${REPOS_DIR}/rbac')
|
|
|
|
from ahserver.configuredServer import ConfiguredServer
|
|
|
|
class PCCSAuthAPI:
|
|
'''pccs 认证 API — 开发阶段允许所有访问'''
|
|
def needAuth(self, path):
|
|
# 开发阶段: 仅登录页面需要认证
|
|
if '/api/' in path:
|
|
return False
|
|
return False
|
|
|
|
async def getPermissionNeed(self, path):
|
|
return 'any'
|
|
|
|
async def checkUserPassword(self, user_id, password):
|
|
return True
|
|
|
|
async def getUserPermissions(self, user):
|
|
return ['any', 'logined', 'reseller.operator']
|
|
|
|
if __name__ == '__main__':
|
|
server = ConfiguredServer(PCCSAuthAPI, workdir='${SERVER_PATH}')
|
|
server.run()
|
|
" 2>&1 | tee -a "\${LOG_DIR}/pccs.log"
|
|
STARTEOF
|
|
chmod +x "${RUN_DIR}/start.sh"
|
|
green " 启动脚本: ${RUN_DIR}/start.sh"
|
|
|
|
# ============================================================
|
|
# Phase 11: 停止旧进程, 启动应用
|
|
# ============================================================
|
|
blue "[Phase 11] 启动应用..."
|
|
# 停止旧进程
|
|
pkill -f "ahserver.*${APP_PORT}" 2>/dev/null || true
|
|
sleep 1
|
|
|
|
# 后台启动
|
|
nohup bash "${RUN_DIR}/start.sh" > "${LOG_DIR}/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\|302\|404'; then
|
|
green " pccs 应用已启动: http://127.0.0.1:${APP_PORT}"
|
|
else
|
|
red " 警告: 应用可能未正常启动, 检查日志: tail -f ${LOG_DIR}/pccs.log"
|
|
fi
|
|
|
|
# ============================================================
|
|
# 完成
|
|
# ============================================================
|
|
echo ""
|
|
green "=========================================="
|
|
green " pccs 部署完成!"
|
|
green " 访问地址: http://$(hostname -I | awk '{print $1}'):${APP_PORT}"
|
|
green " 日志文件: ${LOG_DIR}/pccs.log"
|
|
green " 启动命令: bash ${RUN_DIR}/start.sh"
|
|
green " 停止命令: pkill -f 'ahserver.*${APP_PORT}'"
|
|
green "=========================================="
|