438 lines
17 KiB
Bash
438 lines
17 KiB
Bash
#!/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 "$@"
|