fix(install): 守护配置对齐宿主机 cron 与验收检查
Docker 向导提示 nx cron/宿主机路径;裸机 cron 含 db_backup;验收脚本检查 cron 与 MySQL 备份能力。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user