diff --git a/deploy/nexus-1panel.sh b/deploy/nexus-1panel.sh index 821c8eee..c29e7527 100755 --- a/deploy/nexus-1panel.sh +++ b/deploy/nexus-1panel.sh @@ -1159,6 +1159,7 @@ cmd_upgrade() { fi apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")" + upsert_env_var "$NEXUS_ROOT/$ENV_PROD" "NEXUS_HOST_ROOT" "$NEXUS_ROOT" save_host_profile "$NEXUS_ROOT" purge_legacy_bundled_services "$NEXUS_ROOT" sync_1panel_service_hosts "$NEXUS_ROOT" diff --git a/deploy/verify-1panel-install-wizard.sh b/deploy/verify-1panel-install-wizard.sh index 23607fde..bd204ea3 100644 --- a/deploy/verify-1panel-install-wizard.sh +++ b/deploy/verify-1panel-install-wizard.sh @@ -15,11 +15,13 @@ NC='\033[0m' pass() { echo -e "${GREEN}[PASS]${NC} $*"; } fail() { echo -e "${RED}[FAIL]${NC} $*"; FAILURES=$((FAILURES + 1)); } -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +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 @@ -68,6 +70,9 @@ check_env_prod() { 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 @@ -192,9 +197,56 @@ check_install_mode() { 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:8600/health 2>/dev/null || true)" + health="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/health" 2>/dev/null || true)" if [[ "$health" == "ok" ]]; then pass "/health → ok" else @@ -202,7 +254,7 @@ check_http_local() { info "修复: docker logs --tail=80 $NEXUS_CONTAINER" return 1 fi - code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/install.html 2>/dev/null || echo 000)" + 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" @@ -226,7 +278,7 @@ check_install_api() { return 0 fi local status env_json - status="$(curl -sf http://127.0.0.1:8600/api/install/status 2>/dev/null || true)" + 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 @@ -235,7 +287,7 @@ check_install_api() { else fail "/api/install/status 无响应" fi - env_json="$(curl -sf http://127.0.0.1:8600/api/install/env-check 2>/dev/null || true)" + 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 @@ -286,6 +338,8 @@ print_next_steps() { 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 @@ -320,6 +374,8 @@ main() { check_install_mode || true check_http_local || true check_install_api || true + check_ops_cron || true + check_mysql_backup || true check_openresty_hint || true print_next_steps @@ -331,6 +387,9 @@ main() { 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 diff --git a/docker/.env.prod.example b/docker/.env.prod.example index 7a95cd7a..10339cf7 100644 --- a/docker/.env.prod.example +++ b/docker/.env.prod.example @@ -26,5 +26,8 @@ NEXUS_DB_MAX_OVERFLOW=20 # 1Panel 有密码时: redis://:你的密码@1Panel-redis-xxxx:6379/0 NEXUS_REDIS_URL=redis://host.docker.internal:6379/0 +# 宿主机 git 仓库路径(容器内安装向导用于提示 nx cron / nx update) +NEXUS_HOST_ROOT=/opt/nexus + # Optional # NEXUS_PUBLISH_PORT=8600 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 579e75fd..e821dae8 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -26,6 +26,7 @@ services: NEXUS_HOST: "0.0.0.0" NEXUS_PORT: "8600" NEXUS_DEPLOY_PATH: /app + NEXUS_HOST_ROOT: ${NEXUS_HOST_ROOT:-/opt/nexus} # Redis URL 仅来自 /app/.env(安装向导 + entrypoint export_app_env),勿在此注入以免覆盖密码 NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-} NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-} diff --git a/docs/changelog/2026-06-06-nx-god-menu-audit-round6-guardian-verify-cron.md b/docs/changelog/2026-06-06-nx-god-menu-audit-round6-guardian-verify-cron.md new file mode 100644 index 00000000..80647026 --- /dev/null +++ b/docs/changelog/2026-06-06-nx-god-menu-audit-round6-guardian-verify-cron.md @@ -0,0 +1,35 @@ +# nx 上帝菜单巡检第六轮 — 守护配置对齐 + 验收 cron/备份 + +| 项 | 内容 | +|----|------| +| 日期 | 2026-06-06 | +| 动机 | 安装向导 Docker 守护文案仍指向容器内 `/app`;裸机 cron 格式过时;验收脚本未检查 Layer 3 cron 与 MySQL 备份 | + +## 变更摘要 + +1. **install.py**:`_host_deploy_root` + `_append_ops_cron_results`;Docker 守护提示 `install_ops_cron` / `nx cron`;裸机 cron 与第四轮脚本同格式(含 db_backup)。 +2. **docker-compose.prod.yml**:注入 `NEXUS_HOST_ROOT`(默认 `/opt/nexus`)。 +3. **nexus-1panel.sh upgrade**:同步 `NEXUS_HOST_ROOT` 到 `.env.prod`。 +4. **verify-1panel-install-wizard.sh**:已安装时检查 crontab、备份脚本与备份文件;HTTP 探测改用 `NEXUS_PUBLISH_PORT`。 + +## 涉及文件 + +- `server/api/install.py` +- `docker/docker-compose.prod.yml` +- `docker/.env.prod.example` +- `deploy/nexus-1panel.sh` +- `deploy/verify-1panel-install-wizard.sh` +- `tests/test_security_unit.py` + +## 迁移 / 重启 + +- `git pull` 后 `nx update` 使容器获得 `NEXUS_HOST_ROOT` 环境变量(可选)。 +- 已安装主机若缺 cron:`sudo nx cron`。 + +## 验证 + +```bash +pytest tests/test_security_unit.py::test_configure_guardian_docker_mode_skips_supervisor -q +bash -n deploy/verify-1panel-install-wizard.sh +sudo bash deploy/verify-1panel-install-wizard.sh +``` diff --git a/server/api/install.py b/server/api/install.py index 55231cdb..4313e9e9 100644 --- a/server/api/install.py +++ b/server/api/install.py @@ -656,9 +656,61 @@ def _detect_bt_panel() -> bool: return Path("/www/server/panel/class/common.py").exists() +def _host_deploy_root(install_dir: str) -> str: + """宿主机仓库路径(容器内 /app 与宿主机 /opt/nexus 不同)。""" + host = os.environ.get("NEXUS_HOST_ROOT", "").strip() + if host: + return host + if install_dir != "/app" and Path(install_dir).is_dir(): + return install_dir + return "/opt/nexus" + + +def _append_ops_cron_results(results: list[str], install_dir: str) -> None: + """Crontab 条目与 deploy/install_ops_cron.sh 保持一致。""" + health_marker = "nexus-health-monitor" + backup_marker = "nexus-mysql-backup" + health_line = ( + f"* * * * * NEXUS_DEPLOY_DIR={install_dir} " + f"{install_dir}/deploy/health_monitor.sh " + f">> /var/log/nexus_health.log 2>&1 # {health_marker}" + ) + backup_line = ( + f"0 3 * * * NEXUS_ROOT={install_dir} " + f"{install_dir}/deploy/db_backup.sh " + f">> /var/log/nexus_backup.log 2>&1 # {backup_marker}" + ) + try: + current = subprocess.run( + ["crontab", "-l"], capture_output=True, text=True, timeout=5 + ).stdout or "" + added: list[str] = [] + if health_marker not in current and "health_monitor.sh" not in current: + added.append(health_line) + if backup_marker not in current and "db_backup.sh" not in current: + added.append(backup_line) + if added: + new_cron = current.rstrip() + "\n" + "\n".join(added) + "\n" + proc = subprocess.run( + ["crontab", "-"], input=new_cron, capture_output=True, text=True, timeout=5 + ) + if proc.returncode == 0: + results.append("✓ Crontab 已配置(health_monitor + db_backup)") + else: + results.append("✗ Crontab 配置失败 — 请手动: bash deploy/install_ops_cron.sh") + else: + results.append("✓ Crontab 已存在 health_monitor / db_backup 条目") + except Exception: + results.append( + f"⚠ 宿主机 cron 需在安装完成后执行: " + f"sudo bash {install_dir}/deploy/install_ops_cron.sh" + ) + + def _configure_docker_guardian(install_dir: str, api_port: str) -> list[str]: """1Panel + Docker: Compose restart/healthcheck + in-app self_monitor (no host Supervisor).""" results: list[str] = [] + host_root = _host_deploy_root(install_dir) if Path("/.dockerenv").is_file(): results.append("✓ Layer 1: Docker 容器运行中(Compose restart: unless-stopped)") @@ -703,7 +755,11 @@ def _configure_docker_guardian(install_dir: str, api_port: str) -> list[str]: f"ℹ Layer 3(宿主机): 配置 OpenResty 反代 → 127.0.0.1:{publish_port}(见 deploy/1panel/openresty-nexus.conf.example)" ) - results.append(f"ℹ 日常升级: 宿主机 cd {install_dir} && nx update") + results.append( + f"ℹ Layer 3(宿主机 cron): sudo bash {host_root}/deploy/install_ops_cron.sh" + ) + results.append("ℹ 或: sudo nx cron(health_monitor 每分钟 + MySQL 备份每日 03:00)") + results.append(f"ℹ 日常升级: 宿主机 cd {host_root} && nx update") return results @@ -759,7 +815,7 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]: except OSError: results.append("✗ Supervisor 配置写入失败(需 root 权限)") - # 3. Update health_monitor.sh INSTALL_DIR + # 3. health_monitor 默认路径(cron 行内 NEXUS_DEPLOY_DIR 优先,此处仅兼容旧部署) health_sh = Path(install_dir) / "deploy" / "health_monitor.sh" if health_sh.exists(): try: @@ -772,31 +828,14 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]: ) write_utf8_lf(health_sh, content) health_sh.chmod(0o755) - results.append("✓ health_monitor.sh INSTALL_DIR 已更新") + results.append("✓ health_monitor.sh DEPLOY_DIR 默认值已更新") except OSError: results.append("✗ health_monitor.sh 更新失败") else: results.append("⚠ health_monitor.sh 未找到,跳过") - # 4. Crontab - cron_entry = f"* * * * * {install_dir}/deploy/health_monitor.sh" - try: - current = subprocess.run( - ["crontab", "-l"], capture_output=True, text=True, timeout=5 - ).stdout or "" - if "health_monitor.sh" not in current: - new_cron = current.rstrip() + "\n" + cron_entry + "\n" - proc = subprocess.run( - ["crontab", "-"], input=new_cron, capture_output=True, text=True, timeout=5 - ) - if proc.returncode == 0: - results.append("✓ Crontab 已配置(每分钟健康检查)") - else: - results.append("✗ Crontab 配置失败 — 请手动添加") - else: - results.append("✓ Crontab 已存在 health_monitor 条目") - except Exception: - results.append("⚠ Crontab 配置跳过(权限不足或 cron 不可用)") + # 4. Crontab(与 install_ops_cron.sh 同格式) + _append_ops_cron_results(results, install_dir) # 5. Reload Supervisor try: diff --git a/tests/test_security_unit.py b/tests/test_security_unit.py index 8faccdff..07ee8875 100644 --- a/tests/test_security_unit.py +++ b/tests/test_security_unit.py @@ -286,11 +286,14 @@ def test_configure_guardian_docker_mode_skips_supervisor(monkeypatch): lambda self: str(self) == "/.dockerenv", ) - docker_results = _configure_docker_guardian("/opt/nexus", "8600") + monkeypatch.setenv("NEXUS_HOST_ROOT", "/opt/nexus") + docker_results = _configure_docker_guardian("/app", "8600") docker_text = "\n".join(docker_results) assert "Supervisor" not in docker_text assert "self_monitor" in docker_text assert "OpenResty" in docker_text or "反代" in docker_text + assert "install_ops_cron" in docker_text or "nx cron" in docker_text + assert "/opt/nexus" in docker_text bare_results = _configure_guardian("/opt/nexus", "8600") assert bare_results == docker_results