#!/usr/bin/env bash # Verify 1Panel + Docker install wizard readiness (run on the VPS as root). # See: deploy/README-1panel.md set -euo pipefail NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}" ENV_PROD="${NEXUS_ROOT}/docker/.env.prod" COMPOSE_FILE="${NEXUS_ROOT}/docker/docker-compose.prod.yml" COMPOSE_1PANEL="${NEXUS_ROOT}/docker/docker-compose.1panel.yml" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' pass() { echo -e "${GREEN}[PASS]${NC} $*"; } fail() { echo -e "${RED}[FAIL]${NC} $*"; FAILURES=$((FAILURES + 1)); } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; WARNS=$((WARNS + 1)); } info() { echo -e " $*"; } FAILURES=0 WARNS=0 INSTALL_WIZARD_ARCHIVED=false PUBLISH_PORT=8600 require_root() { if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then fail "请用 root 运行(1Panel 终端默认 root)" exit 1 fi } resolve_nexus_container() { NEXUS_CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)" if [[ -z "$NEXUS_CONTAINER" ]]; then fail "未找到运行中的 Nexus 容器" info "修复: cd $NEXUS_ROOT && nx update --no-cache" return 1 fi pass "Nexus 容器: $NEXUS_CONTAINER" } check_repo() { if [[ ! -d "$NEXUS_ROOT/.git" ]]; then fail "仓库不存在: $NEXUS_ROOT" return 1 fi local sha msg sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" msg="$(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || true)" pass "Git: $msg" if git -C "$NEXUS_ROOT" merge-base --is-ancestor b25d079 HEAD 2>/dev/null; then pass "已包含 b25d079(1Panel 容器名 sync 修复)" else warn "早于 b25d079 — 步骤 3 可能仍预填 host.docker.internal" info "修复: cd $NEXUS_ROOT && nx update --no-cache" fi if git -C "$NEXUS_ROOT" merge-base --is-ancestor 3b2856f HEAD 2>/dev/null; then pass "已包含 3b2856f+(Redis :pass@ 自动解析,无 root)" else warn "早于 3b2856f — Redis 认证可能仍误用 root" info "修复: cd $NEXUS_ROOT && git pull && bash deploy/sync-install-wizard-to-container.sh" fi } check_env_prod() { if [[ ! -f "$ENV_PROD" ]]; then fail "缺少 $ENV_PROD" info "修复: bash $NEXUS_ROOT/deploy/install-nexus-fresh.sh --skip-clone" return 1 fi pass "docker/.env.prod 存在" PUBLISH_PORT="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- | tr -d '\r" ' || true)" PUBLISH_PORT="${PUBLISH_PORT:-8600}" pass "NEXUS_PUBLISH_PORT=$PUBLISH_PORT" if grep -qE '^NEXUS_INSTALL_WIZARD_PENDING=1' "$ENV_PROD"; then pass "NEXUS_INSTALL_WIZARD_PENDING=1(安装模式)" else warn "NEXUS_INSTALL_WIZARD_PENDING≠1 — entrypoint 可能已写 /app/.env" fi local db redis db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)" redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)" if [[ -n "$db" ]]; then pass "NEXUS_1PANEL_DB_HOST=$db" else fail "未设置 NEXUS_1PANEL_DB_HOST" info "修复: bash $NEXUS_ROOT/deploy/detect-1panel-services.sh && nx update" fi if [[ -n "$redis" ]]; then pass "NEXUS_1PANEL_REDIS_HOST=$redis" local rurl rurl="$(grep -E '^NEXUS_REDIS_URL=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)" if [[ "$rurl" == *"$redis"* ]]; then pass "NEXUS_REDIS_URL 已指向 Redis 容器" else warn "NEXUS_REDIS_URL 未含 $redis — nx update 会同步" fi else fail "未设置 NEXUS_1PANEL_REDIS_HOST" info "修复: 在 1Panel 应用商店安装 Redis 后执行 nx update" fi } check_1panel_network() { if ! docker network inspect 1panel-network >/dev/null 2>&1; then fail "1panel-network 不存在" info "修复: 在 1Panel 应用商店安装 MySQL + Redis(会自动创建网络)" return 1 fi pass "1panel-network 存在" if [[ -n "${NEXUS_CONTAINER:-}" ]]; then if docker inspect "$NEXUS_CONTAINER" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' \ | grep -q '1panel-network'; then pass "Nexus 已接入 1panel-network" else fail "Nexus 未接入 1panel-network" info "修复: cd $NEXUS_ROOT && nx update --no-cache" fi fi if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then info "探测结果:" "$NEXUS_ROOT/deploy/detect-1panel-services.sh" | sed 's/^/ /' || true fi } probe_from_container() { [[ -n "${NEXUS_CONTAINER:-}" ]] || return 0 local db redis db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)" redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)" if [[ -n "$db" ]]; then if docker exec "$NEXUS_CONTAINER" python3 -c " import socket s=socket.socket(); s.settimeout(3); s.connect(('$db', 3306)); s.close() " 2>/dev/null; then pass "容器内 TCP → MySQL $db:3306" else fail "容器内无法连接 MySQL $db:3306" info "检查 MySQL 容器是否运行、Nexus 是否在 1panel-network" fi fi if [[ -n "$redis" ]]; then if docker exec "$NEXUS_CONTAINER" python3 -c " import socket s=socket.socket(); s.settimeout(3); s.connect(('$redis', 6379)); s.close() " 2>/dev/null; then pass "容器内 TCP → Redis $redis:6379" else fail "容器内无法连接 Redis $redis:6379" fi fi } check_app_env_redis_url() { [[ -n "${NEXUS_CONTAINER:-}" ]] || return 0 if ! docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then return 0 fi local rurl rurl="$(docker exec "$NEXUS_CONTAINER" grep -E '^NEXUS_REDIS_URL=' /app/.env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)" if [[ -z "$rurl" ]]; then warn "/app/.env 无 NEXUS_REDIS_URL" return 0 fi if [[ "$rurl" == *"://root:"* ]]; then fail "NEXUS_REDIS_URL 含 root 用户(1Panel Redis 无 root 账号)" info "修复: redis://:密码@1Panel-redis-xxxx:6379/0 — 见 deploy/test-1panel-redis.sh" elif [[ "$rurl" == *"1Panel-redis"* ]] || [[ "$rurl" == *"://:"* ]]; then pass "NEXUS_REDIS_URL 格式合理(容器名或 :pass@)" else warn "NEXUS_REDIS_URL 可能未指向 1Panel Redis: $rurl" fi } check_install_mode() { [[ -n "${NEXUS_CONTAINER:-}" ]] || return 0 if docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then warn "/app/.env 已存在 — 安装向导可能已进入步骤 3+ 或误写 env" check_app_env_redis_url info "若需重来: 删卷 nexus-state 中 .env 并设 NEXUS_INSTALL_WIZARD_PENDING=1 后 nx update" else pass "/app/.env 不存在(安装模式 OK)" fi if docker exec "$NEXUS_CONTAINER" test -f /app/.install_locked 2>/dev/null \ || docker exec "$NEXUS_CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \ || docker exec "$NEXUS_CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then pass "安装向导已锁定/已归档 — 应访问 /app/ 登录" INSTALL_WIZARD_ARCHIVED=true fi local hosts_json hosts_json="$(docker exec "$NEXUS_CONTAINER" cat /app/web/data/1panel-hosts.json 2>/dev/null || true)" if [[ -n "$hosts_json" ]]; then pass "1panel-hosts.json: $hosts_json" else warn "无 1panel-hosts.json(依赖容器环境变量,nx update 会写入)" fi } check_ops_cron() { if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" != true ]]; then return 0 fi if ! command -v crontab >/dev/null 2>&1; then warn "未安装 cron/crontab — Layer 3 宿主机巡检不可用" info "修复: sudo bash $NEXUS_ROOT/deploy/install_ops_cron.sh" return 0 fi local cron cron="$(crontab -l 2>/dev/null || true)" if echo "$cron" | grep -q 'nexus-health-monitor\|deploy/health_monitor.sh'; then pass "Crontab 含 health_monitor" else warn "Crontab 未配置 health_monitor(Layer 3 巡检)" info "修复: sudo nx cron" fi if echo "$cron" | grep -q 'nexus-mysql-backup\|deploy/db_backup.sh'; then pass "Crontab 含 db_backup" else warn "Crontab 未配置 db_backup(每日 MySQL 备份)" info "修复: sudo nx cron" fi } check_mysql_backup() { if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" != true ]]; then return 0 fi local dump_sh="${NEXUS_ROOT}/deploy/mysql_dump_to_file.sh" if [[ -f "$dump_sh" ]]; then pass "mysql_dump_to_file.sh 存在" else warn "缺少 mysql_dump_to_file.sh — 升级前无法备份 MySQL" info "修复: cd $NEXUS_ROOT && git pull && nx update" return 0 fi if [[ -d /var/backups/nexus ]] && ls /var/backups/nexus/nexus_*.sql.gz >/dev/null 2>&1; then local latest latest="$(ls -t /var/backups/nexus/nexus_*.sql.gz 2>/dev/null | head -1)" pass "已有 MySQL 备份: $(basename "$latest")" else warn "尚无 MySQL 备份文件(/var/backups/nexus)" info "可手动: sudo bash $NEXUS_ROOT/deploy/db_backup.sh" fi } check_http_local() { local health code health="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/health" 2>/dev/null || true)" if [[ "$health" == "ok" ]]; then pass "/health → ok" else fail "/health 非 ok(当前: ${health:-无响应})" info "修复: docker logs --tail=80 $NEXUS_CONTAINER" return 1 fi code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)" if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then if [[ "$code" == "404" ]]; then pass "/app/install.html 已归档 → HTTP 404" else fail "/app/install.html 应已归档但 HTTP $code" info "修复: nx update(会执行 archive_install_wizard_in_container)" fi return 0 fi if [[ "$code" == "200" ]]; then pass "/app/install.html → HTTP 200" else fail "/app/install.html → HTTP $code" info "修复: bash $NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" fi } check_install_api() { if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then pass "安装 API 已关闭(向导已锁定,/api/install/* 返回 403 为预期)" return 0 fi local status env_json status="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/api/install/status" 2>/dev/null || true)" if [[ -n "$status" ]]; then pass "/api/install/status → $status" if echo "$status" | grep -q '"installed":true'; then warn "installed=true — 向导已完成,请访问 /app/ 登录" fi else fail "/api/install/status 无响应" fi env_json="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/api/install/env-check" 2>/dev/null || true)" if [[ -z "$env_json" ]]; then fail "/api/install/env-check 无响应" return 1 fi if echo "$env_json" | grep -q 'docker_defaults'; then local db_host db_host="$(echo "$env_json" | python3 -c " import json,sys d=json.load(sys.stdin) print((d.get('docker_defaults') or {}).get('db_host','')) " 2>/dev/null || true)" if [[ -n "$db_host" && "$db_host" != "host.docker.internal" ]]; then pass "步骤 3 预填 db_host=$db_host" elif [[ "$db_host" == "host.docker.internal" ]]; then fail "步骤 3 仍预填 host.docker.internal" info "修复: nx update --no-cache(需 b25d079+)" fi else warn "env-check 无 docker_defaults(非 Docker 模式?)" fi if echo "$env_json" | grep -q '1Panel-mysql'; then pass "env-check MySQL 探测含 1Panel 容器名" elif echo "$env_json" | grep -q 'host.docker.internal'; then warn "env-check MySQL 仍指向 host.docker.internal" fi } check_container_image_features() { [[ -n "${NEXUS_CONTAINER:-}" ]] || return 0 local host_sha py_ok html_file html_ok host_root_ok host_sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" pass "宿主机 Git: $host_sha" if docker exec "$NEXUS_CONTAINER" grep -q '_host_deploy_root' /app/server/api/install.py 2>/dev/null; then pass "容器 install.py 含 host_deploy_root(round6+)" py_ok=1 else warn "容器 install.py 较旧 — 需 nx update 重建镜像" fi html_file="/app/web/app/install.html" if docker exec "$NEXUS_CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then html_file="/app/web/app/install.html.bak" fi if docker exec "$NEXUS_CONTAINER" grep -q 'install_ops_cron' "$html_file" 2>/dev/null; then pass "容器 install.html 含 Layer 3 cron 指引(round7+)" html_ok=1 else warn "容器 install.html 较旧 — 需 nx update 重建镜像" fi host_root_ok="$(docker exec "$NEXUS_CONTAINER" printenv NEXUS_HOST_ROOT 2>/dev/null || true)" if [[ -n "$host_root_ok" ]]; then pass "NEXUS_HOST_ROOT=$host_root_ok" else warn "容器未注入 NEXUS_HOST_ROOT — nx update 重建后可修复" fi if [[ -n "${py_ok:-}" && -n "${html_ok:-}" ]]; then info "镜像与宿主机代码特性对齐(重建后验收)" else info "修复: cd $NEXUS_ROOT && sudo nx update" fi } check_openresty_hint() { local domain code domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo api.synaglobal.vip)" code="$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 5 "https://${domain}/health" 2>/dev/null || echo 000)" if [[ "$code" == "200" ]] && curl -sk "https://${domain}/health" 2>/dev/null | grep -q '^ok$'; then pass "HTTPS https://${domain}/health → ok" else warn "HTTPS https://${domain}/health → HTTP $code(外网可能未配 OpenResty 反代)" info "1Panel → 网站 → 反代 http://127.0.0.1:8600" info "参考: $NEXUS_ROOT/deploy/1panel/openresty-nexus.conf.example" fi } print_next_steps() { local domain domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo '你的域名')" echo "" if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then echo "==========================================" echo " 已安装 — 日常运维" echo "==========================================" echo " 登录: https://${domain}/app/" echo " 更新: cd $NEXUS_ROOT && nx update" echo " 巡检 cron: sudo nx cron" echo " 手动备份: sudo bash $NEXUS_ROOT/deploy/db_backup.sh" echo " 验收: bash $NEXUS_ROOT/deploy/verify-1panel-install-wizard.sh" echo "" return 0 fi echo "==========================================" echo " 安装向导后续步骤" echo "==========================================" echo "1. 1Panel 数据库 → 建库 nexus、用户 nexus(仅授权 nexus 库)" echo "2. 浏览器打开: https://${domain}/app/install.html" echo "3. 步骤 2:MySQL/Redis TCP 应显示 ✓(容器名:端口)" echo "4. 步骤 3:确认 db_host/redis_host 为 1Panel-mysql/redis-xxxx,填 MySQL 密码" echo "5. 步骤 4:连接检测全绿 → 创建 admin" echo "6. 步骤 5:锁定安装" echo "" if [[ "$FAILURES" -gt 0 ]]; then echo "先修复上方 [FAIL],再执行:" echo " cd $NEXUS_ROOT && nx update --no-cache" fi } main() { require_root echo "Nexus 1Panel 安装向导验收 — $(date -Iseconds)" echo "NEXUS_ROOT=$NEXUS_ROOT" echo "" check_repo || true check_env_prod || true resolve_nexus_container || true check_1panel_network || true probe_from_container || true check_install_mode || true check_http_local || true check_install_api || true check_ops_cron || true check_mysql_backup || true check_container_image_features || true check_openresty_hint || true print_next_steps if [[ "$FAILURES" -gt 0 ]]; then echo "" fail "合计 $FAILURES 项未通过" exit 1 fi echo "" if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then pass "验收通过 — 安装已完成,请访问 /app/ 登录" if [[ "$WARNS" -gt 0 ]]; then warn "有 ${WARNS} 项 [WARN](cron/备份等建议项,不阻断登录)" fi else pass "验收通过 — 可打开安装向导完成步骤 1–5" fi } main "$@"