fix(install): 守护配置对齐宿主机 cron 与验收检查
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run

Docker 向导提示 nx cron/宿主机路径;裸机 cron 含 db_backup;验收脚本检查 cron 与 MySQL 备份能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-06 23:11:02 +08:00
parent 89865ec690
commit c9e08799f9
7 changed files with 169 additions and 28 deletions
+1
View File
@@ -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"
+64 -5
View File
@@ -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_monitorLayer 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
+3
View File
@@ -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
+1
View File
@@ -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:-}
@@ -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
```
+61 -22
View File
@@ -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 cronhealth_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:
+4 -1
View File
@@ -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