release: nexus btpanel session fix and app-v2
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
|
||||
# Domain: api.synaglobal.vip
|
||||
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
|
||||
#
|
||||
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
|
||||
|
||||
upstream nexus_api {
|
||||
server 127.0.0.1:8600;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# WebSocket log without query string (JWT must not hit access_log)
|
||||
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
|
||||
|
||||
client_max_body_size 500m;
|
||||
client_body_timeout 600s;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_connect_timeout 10s;
|
||||
}
|
||||
|
||||
location /ws/ {
|
||||
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /app/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /agent/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Fallback: install mode or misc routes served by FastAPI
|
||||
location / {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
# 1Panel + Docker 一键部署
|
||||
|
||||
> 完整知识文档:[docs/project/nexus-1panel-operations-knowledge.md](../docs/project/nexus-1panel-operations-knowledge.md)
|
||||
> 巡检归档(2026-06-06):[docs/project/nexus-god-menu-audit-summary.md](../docs/project/nexus-god-menu-audit-summary.md)
|
||||
|
||||
## 脚本速查(公共仓库 · 无需令牌)
|
||||
|
||||
| 脚本 | 用途 | 一条命令 |
|
||||
|------|------|----------|
|
||||
| `quick-install.sh` | 仅装 Nexus(1Panel 已就绪) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
|
||||
| `install-1panel-docker.sh` | 重装服务器全链 | `curl -fsSL ".../install-1panel-docker.sh" \| bash` |
|
||||
| `install-nexus-fresh.sh` | 全新空库安装 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
|
||||
| **`update.sh`** | **一键更新** | `curl -fsSL ".../update.sh" \| bash` |
|
||||
| `nexus-1panel.sh` | 底层 install / upgrade | `bash deploy/nexus-1panel.sh upgrade` |
|
||||
| `fix-1panel-mysql-grant.sh` | 修复 1045 nexus@Docker | `bash deploy/fix-1panel-mysql-grant.sh` |
|
||||
| `test-1panel-redis.sh` | Redis 认证诊断 | `REDIS_PASS='...' bash deploy/test-1panel-redis.sh` |
|
||||
| `verify-1panel-install-wizard.sh` | 一键验收(网络/cron/镜像) | `bash deploy/verify-1panel-install-wizard.sh` |
|
||||
| `install_ops_cron.sh` | 宿主机 cron(巡检+备份) | `sudo bash deploy/install_ops_cron.sh` |
|
||||
| `mysql_dump_to_file.sh` | MySQL 备份(Docker exec) | 见 `db_backup.sh` / `nx` 菜单 `[9]` |
|
||||
| `upgrade.sh` | 自动识别 Docker/Supervisor | `bash deploy/upgrade.sh` |
|
||||
| **`nx`** | **统一运维菜单** | `nx` |
|
||||
|
||||
档位:`--profile 1c4g` · `2c8g`(默认)· `4c16g`
|
||||
|
||||
---
|
||||
|
||||
## 重装服务器(1Panel + Docker + Nexus)
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
```
|
||||
|
||||
4 核 16G:
|
||||
|
||||
```bash
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g
|
||||
```
|
||||
|
||||
已装 1Panel,只装 Nexus:
|
||||
|
||||
```bash
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
```
|
||||
|
||||
## 仅 Nexus Docker
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
```
|
||||
|
||||
安装完成后:`https://api.synaglobal.vip/app/install.html`
|
||||
|
||||
---
|
||||
|
||||
## 一键更新(日常)
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/update.sh
|
||||
# 或
|
||||
nx update
|
||||
# 或菜单
|
||||
nx # → [4] 一键更新
|
||||
```
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `--check` | 仅查 Git,不重建 |
|
||||
| `--no-cache` | 无缓存重建镜像(entrypoint 变更后必用) |
|
||||
| `--no-backup` | 跳过 MySQL 备份 |
|
||||
| `--prune` | 清理悬空镜像 |
|
||||
|
||||
远程:
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
|
||||
curl -fsSL ".../update.sh" | bash -s -- --no-cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NX 全局命令
|
||||
|
||||
安装 / 升级 / 首次运行 `nx` 会自动注册到 `/usr/local/bin`:
|
||||
|
||||
```bash
|
||||
# 手动注册(任意目录可用 nx)
|
||||
NEXUS_ROOT=/opt/nexus bash /opt/nexus/deploy/install-nx-cli.sh
|
||||
|
||||
nx # 统一运维菜单
|
||||
nexus-update # 等同 update.sh
|
||||
nexus-fresh # 等同 install-nexus-fresh.sh
|
||||
nexus-install # 等同 quick-install.sh
|
||||
```
|
||||
|
||||
| 菜单 / 子命令 | 功能 |
|
||||
|---------------|------|
|
||||
| `[1]` | 安装向导(档位 / 迁机 / skip-clone) |
|
||||
| `[2]` | 全新安装 → `/app/install.html` |
|
||||
| `[3]` | 状态 + `/health` + install.html 归档检查 |
|
||||
| `[4]` / `nx update` | **一键更新**(备份 + 重建镜像 + 升级后验收) |
|
||||
| `[5]` | 重启 Nexus 容器 |
|
||||
| `[6]` | 日志 |
|
||||
| `[7]` | 启动 / 停止 Compose 栈 |
|
||||
| `[8]` | 无缓存重建镜像 |
|
||||
| `[9]` | 立即备份 MySQL |
|
||||
| `[e]` / `nx cron` | 安装 health_monitor + db_backup crontab |
|
||||
| `[v]` / `nx verify` | 运行 `verify-1panel-install-wizard.sh` |
|
||||
|
||||
---
|
||||
|
||||
## 三层守护(1Panel Docker)
|
||||
|
||||
| 层 | 机制 | 说明 |
|
||||
|----|------|------|
|
||||
| Layer 1 | Compose `restart` + 容器 `healthcheck` | 容器崩溃自动拉起 |
|
||||
| Layer 2 | Python `self_monitor`(30s) | Redis/MySQL/WebSocket 自检 |
|
||||
| Layer 3 | 宿主机 `health_monitor.sh`(cron) | `/health` 连续失败 → `docker restart` + 可选 Telegram |
|
||||
|
||||
Layer 3 需**在宿主机**安装(容器内无法写 crontab):
|
||||
|
||||
```bash
|
||||
sudo nx cron
|
||||
# 或
|
||||
sudo bash /opt/nexus/deploy/install_ops_cron.sh
|
||||
```
|
||||
|
||||
MySQL 定时备份:每日 03:00 → `/var/backups/nexus/`(`docker exec` 进 1Panel MySQL 容器 dump,**无需**宿主机 `mysqldump`)。
|
||||
|
||||
---
|
||||
|
||||
## 安装完成后
|
||||
|
||||
- 向导锁定后 `install.html` → **`install.html.bak`**,外网 `/app/install.html` 返回 **404**(正常)
|
||||
- 登录:`https://你的域名/app/`
|
||||
- 验收:`sudo nx verify`
|
||||
|
||||
---
|
||||
|
||||
## MySQL / Redis(1Panel 应用商店)
|
||||
|
||||
Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安装。
|
||||
|
||||
### 1Panel 官方互联方式(必读)
|
||||
|
||||
[1Panel 维护者说明](https://github.com/1Panel-dev/1Panel/issues/11676):应用商店应用使用 **bridge + `1panel-network`**;**容器之间用容器名通信**(Docker 内置 DNS),不要用 `localhost` / `127.0.0.1` / `host.docker.internal`。
|
||||
|
||||
| 场景 | 正确主机 |
|
||||
|------|----------|
|
||||
| Nexus → 1Panel MySQL | `1Panel-mysql-xxxx`(容器列表中的名称) |
|
||||
| Nexus → 1Panel Redis | `1Panel-redis-xxxx` |
|
||||
| OpenResty → 应用 | `127.0.0.1:端口`(OpenResty 为 host 网络) |
|
||||
|
||||
`nx update` / `nexus-1panel.sh` 会:
|
||||
|
||||
1. 将 Nexus 加入 **`1panel-network`**(`docker/docker-compose.1panel.yml`)
|
||||
2. 自动探测 MySQL/Redis 容器名写入 `docker/.env.prod`(含 `NEXUS_REDIS_URL=redis://1Panel-redis-xxxx:6379/0`)
|
||||
3. 安装向导步骤 3 **预填 MySQL/Redis 容器名**
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **应用商店** → 安装 **MySQL**、**Redis**
|
||||
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
|
||||
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`)
|
||||
4. 填写 MySQL 密码;Redis:**用户名通常留空** + 应用参数密码(自动尝试 `:pass@` / `default`)
|
||||
|
||||
### 常见错误 1045(Access denied for 'nexus'@'172.18.x.x')
|
||||
|
||||
TCP 已通,但 MySQL 用户多为 **`nexus@localhost`**,Docker 内 Nexus 从 **172.x** 连接会被拒绝。
|
||||
|
||||
在服务器执行(按提示输入 root 密码与 nexus 密码):
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/fix-1panel-mysql-grant.sh
|
||||
```
|
||||
|
||||
或一次性:
|
||||
|
||||
```bash
|
||||
MYSQL_ROOT_PASSWORD='1Panel里MySQL的root密码' \
|
||||
NEXUS_DB_PASSWORD='向导里要填的nexus密码' \
|
||||
bash /opt/nexus/deploy/fix-1panel-mysql-grant.sh
|
||||
```
|
||||
|
||||
然后安装向导步骤 3:**库名/用户 `nexus`**,主机为 `1Panel-mysql-xxxx`,密码与上面一致。
|
||||
|
||||
若仍报 `Can't connect to MySQL/Redis on 'host.docker.internal'`:说明 Nexus **未接入 1panel-network** 或未探测到容器名,在服务器执行:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
步骤 2 会 TCP 检测 MySQL/Redis;步骤 4 验证账号密码。
|
||||
|
||||
卸载旧 Compose 容器:
|
||||
|
||||
```bash
|
||||
bash deploy/uninstall-mysql-compose.sh
|
||||
bash deploy/uninstall-redis-compose.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1Panel 反代
|
||||
|
||||
**禁止**脚本改 `/opt/1panel/apps/openresty/`。
|
||||
|
||||
1. 应用商店 → **OpenResty**
|
||||
2. 网站 → 反代 `http://127.0.0.1:8600`
|
||||
3. 参考:`deploy/1panel/openresty-nexus.conf.example`
|
||||
|
||||
---
|
||||
|
||||
## 密钥
|
||||
|
||||
新机自动生成 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY` → `docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`。
|
||||
|
||||
MySQL 密码在 1Panel 自建库时设定,向导步骤 3 填写。
|
||||
|
||||
| 场景 | 命令 |
|
||||
|------|------|
|
||||
| 新机 | `curl \| quick-install.sh` |
|
||||
| 迁机 | `nexus-1panel.sh install --from-env /path/to/.env` |
|
||||
| 更新 | `update.sh` 或 `nx update` |
|
||||
|
||||
**Git**:公开仓库 `clone` / `pull` / `curl | bash` 无需令牌。
|
||||
|
||||
---
|
||||
|
||||
## 安装向导验收(服务器执行)
|
||||
|
||||
在 **1Panel 终端**(root)一键检查网络、容器名预填、cron、镜像版本、本地 `/health`:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && sudo nx verify
|
||||
```
|
||||
|
||||
**未安装**:通过后再打开 `https://你的域名/app/install.html`。
|
||||
**已安装**:`install.html` 应 HTTP 404;验收检查 crontab、`mysql_dump_to_file.sh`、容器内 round6/7 代码特性。
|
||||
|
||||
常见修复:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
外网 HTTPS 仍失败但脚本本地检查全绿 → 在 1Panel **网站**配置反代 `http://127.0.0.1:8600`(见 `deploy/1panel/openresty-nexus.conf.example`)。
|
||||
|
||||
## 安装向导热修复
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/update.sh --no-cache
|
||||
# 或仅同步静态:
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apply a Nexus release tarball on the production host.
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash deploy/apply-release-bundle.sh \
|
||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
||||
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1 \
|
||||
# --deploy-path /opt/nexus
|
||||
#
|
||||
# Validate only (no backup, no rsync, no rebuild):
|
||||
# bash deploy/apply-release-bundle.sh --validate-only \
|
||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
||||
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1
|
||||
#
|
||||
# This script intentionally preserves runtime secrets and data:
|
||||
# - .env
|
||||
# - docker/.env.prod
|
||||
# - web/data
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARBALL=""
|
||||
EXPECTED_SHA256=""
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
||||
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/opt/nexus-backups}"
|
||||
SKIP_REBUILD=0
|
||||
DRY_RUN=0
|
||||
VALIDATE_ONLY=0
|
||||
APPLY_TMP=""
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
usage() {
|
||||
sed -n '1,26p' "$0"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--tarball)
|
||||
TARBALL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--sha256)
|
||||
EXPECTED_SHA256="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--deploy-path)
|
||||
DEPLOY_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--backup-dir)
|
||||
BACKUP_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--skip-rebuild)
|
||||
SKIP_REBUILD=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--validate-only)
|
||||
VALIDATE_ONLY=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "Unknown argument: $1"
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
error "Missing required command: $1"
|
||||
exit 127
|
||||
}
|
||||
}
|
||||
|
||||
require_runtime_cmds() {
|
||||
require_cmd tar
|
||||
require_cmd sha256sum
|
||||
if [[ "$VALIDATE_ONLY" == 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
require_cmd rsync
|
||||
require_cmd curl
|
||||
}
|
||||
|
||||
run() {
|
||||
echo "+ $*"
|
||||
if [[ "$DRY_RUN" == 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${APPLY_TMP:-}" && -d "$APPLY_TMP" ]]; then
|
||||
rm -rf "$APPLY_TMP"
|
||||
fi
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
|
||||
echo docker
|
||||
return
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo supervisor
|
||||
return
|
||||
fi
|
||||
echo unknown
|
||||
}
|
||||
|
||||
verify_sha256() {
|
||||
if [[ -z "$EXPECTED_SHA256" ]]; then
|
||||
warn "No --sha256 provided; skipping checksum verification"
|
||||
return 0
|
||||
fi
|
||||
local actual
|
||||
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
|
||||
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
|
||||
error "SHA256 mismatch"
|
||||
error "expected: $EXPECTED_SHA256"
|
||||
error "actual: $actual"
|
||||
exit 1
|
||||
fi
|
||||
info "SHA256 OK: $actual"
|
||||
}
|
||||
|
||||
validate_extracted_tree() {
|
||||
local src="$1"
|
||||
for required in server deploy Dockerfile.prod frontend-v2 web/app-v2; do
|
||||
if [[ ! -e "${src}/${required}" ]]; then
|
||||
error "Release bundle missing required path: ${required}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
for forbidden in .env docker/.env.prod web/data .venv-py312-codex frontend-v2/node_modules 2025.2 =2025.2; do
|
||||
if [[ -e "${src}/${forbidden}" ]]; then
|
||||
error "Release bundle contains forbidden path: ${forbidden}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
info "Extracted release tree validation OK"
|
||||
}
|
||||
|
||||
backup_current() {
|
||||
if [[ ! -d "$DEPLOY_PATH" ]]; then
|
||||
warn "Deploy path does not exist yet: $DEPLOY_PATH"
|
||||
return 0
|
||||
fi
|
||||
local stamp backup
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
backup="${BACKUP_DIR}/nexus-before-${stamp}.tar.gz"
|
||||
run mkdir -p "$BACKUP_DIR"
|
||||
info "Backup current deploy path -> $backup"
|
||||
run tar czf "$backup" \
|
||||
--exclude='./web/data' \
|
||||
--exclude='./.venv*' \
|
||||
--exclude='./venv' \
|
||||
--exclude='./frontend/node_modules' \
|
||||
--exclude='./frontend-v2/node_modules' \
|
||||
-C "$(dirname "$DEPLOY_PATH")" "$(basename "$DEPLOY_PATH")"
|
||||
}
|
||||
|
||||
sync_release() {
|
||||
local src="$1"
|
||||
run mkdir -p "$DEPLOY_PATH"
|
||||
info "Sync release tree -> $DEPLOY_PATH"
|
||||
run rsync -a --delete \
|
||||
--exclude='.env' \
|
||||
--exclude='docker/.env.prod' \
|
||||
--exclude='web/data' \
|
||||
"${src}/" "${DEPLOY_PATH}/"
|
||||
}
|
||||
|
||||
rebuild_or_restart() {
|
||||
if [[ "$SKIP_REBUILD" == 1 ]]; then
|
||||
warn "--skip-rebuild set; not rebuilding/restarting Nexus"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local runtime
|
||||
runtime="$(detect_runtime)"
|
||||
info "Runtime: $runtime"
|
||||
case "$runtime" in
|
||||
docker)
|
||||
if [[ -x "${DEPLOY_PATH}/deploy/nexus-1panel.sh" || -f "${DEPLOY_PATH}/deploy/nexus-1panel.sh" ]]; then
|
||||
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/nexus-1panel.sh" upgrade --skip-git
|
||||
else
|
||||
error "Docker runtime detected but deploy/nexus-1panel.sh missing"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -f "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" ]]; then
|
||||
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh"
|
||||
fi
|
||||
;;
|
||||
supervisor)
|
||||
run supervisorctl restart nexus
|
||||
;;
|
||||
*)
|
||||
error "Unknown runtime. Rebuild/restart manually."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
verify_local_endpoints() {
|
||||
local port health app app_v2
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
port="${port:-8600}"
|
||||
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
||||
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
|
||||
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
|
||||
echo " /health -> ${health}"
|
||||
echo " /app/ -> ${app}"
|
||||
echo " /app-v2/ -> ${app_v2}"
|
||||
if [[ "$health" != "ok" || "$app" != "200" || "$app_v2" != "200" ]]; then
|
||||
error "Local endpoint verification failed"
|
||||
exit 1
|
||||
fi
|
||||
info "Local endpoint verification OK"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ -z "$TARBALL" ]]; then
|
||||
error "--tarball is required"
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
[[ -f "$TARBALL" ]] || {
|
||||
error "Tarball not found: $TARBALL"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_runtime_cmds
|
||||
|
||||
info "Tarball: $TARBALL"
|
||||
info "Deploy path: $DEPLOY_PATH"
|
||||
info "Backup dir: $BACKUP_DIR"
|
||||
verify_sha256
|
||||
|
||||
local extracted
|
||||
APPLY_TMP="$(mktemp -d /tmp/nexus-release-apply.XXXXXX)"
|
||||
trap cleanup EXIT
|
||||
run tar xzf "$TARBALL" -C "$APPLY_TMP"
|
||||
extracted="${APPLY_TMP}/nexus-release"
|
||||
validate_extracted_tree "$extracted"
|
||||
if [[ "$VALIDATE_ONLY" == 1 ]]; then
|
||||
info "Validate-only mode: tarball checksum and content validation passed"
|
||||
return 0
|
||||
fi
|
||||
backup_current
|
||||
sync_release "$extracted"
|
||||
rebuild_or_restart
|
||||
if [[ "$DRY_RUN" != 1 && "$SKIP_REBUILD" != 1 ]]; then
|
||||
verify_local_endpoints
|
||||
fi
|
||||
info "Release applied successfully"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Fail if tracked shell scripts contain CR (CRLF) — breaks bash on Ubuntu.
|
||||
# Usage: bash deploy/check_shell_eol.sh
|
||||
# Run from repo root (or set NEXUS_DEPLOY_DIR).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_REPO_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)"
|
||||
ROOT="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "check_shell_eol: not a git repo at ${ROOT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BAD=0
|
||||
while IFS= read -r -d '' f; do
|
||||
if grep -q $'\r' "${f}" 2>/dev/null; then
|
||||
echo "CRLF/CR detected: ${f}" >&2
|
||||
BAD=1
|
||||
fi
|
||||
done < <(git ls-files -z '*.sh')
|
||||
|
||||
if [ "${BAD}" -ne 0 ]; then
|
||||
echo "Fix: git add --renormalize '*.sh' or run dos2unix on listed files." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "check_shell_eol: OK (all tracked *.sh are LF-only)"
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — MySQL 定时备份(Docker 1Panel / 宿主机)
|
||||
#
|
||||
# 0 3 * * * NEXUS_ROOT=/opt/nexus /opt/nexus/deploy/db_backup.sh >> /var/log/nexus_backup.log 2>&1
|
||||
#
|
||||
# 亦可通过: sudo bash deploy/install_ops_cron.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-$(dirname "$SCRIPT_DIR")}"
|
||||
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
|
||||
RETENTION_DAYS="${NEXUS_BACKUP_RETENTION_DAYS:-30}"
|
||||
DUMP_SH="${SCRIPT_DIR}/mysql_dump_to_file.sh"
|
||||
|
||||
if [[ ! -f "$DUMP_SH" ]]; then
|
||||
echo "[$(date)] ERROR: missing $DUMP_SH"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$DUMP_SH" 2>/dev/null || true
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
RAW_FILE="${BACKUP_DIR}/nexus_daily_${TIMESTAMP}.sql"
|
||||
BACKUP_FILE="${RAW_FILE}.gz"
|
||||
|
||||
echo "[$(date)] Starting backup → $BACKUP_FILE"
|
||||
if ! NEXUS_ROOT="$NEXUS_ROOT" bash "$DUMP_SH" --output "$RAW_FILE" --root "$NEXUS_ROOT"; then
|
||||
echo "[$(date)] ERROR: mysqldump failed"
|
||||
rm -f "$RAW_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gzip -f "$RAW_FILE"
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
echo "[$(date)] ERROR: backup file not created"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SIZE="$(du -h "$BACKUP_FILE" | cut -f1)"
|
||||
echo "[$(date)] Backup completed: $BACKUP_FILE ($SIZE)"
|
||||
|
||||
DELETED="$(find "$BACKUP_DIR" -name 'nexus_*.sql.gz' -mtime +"${RETENTION_DAYS}" -delete -print 2>/dev/null | wc -l)"
|
||||
if [[ "$DELETED" -gt 0 ]]; then
|
||||
echo "[$(date)] Cleaned up $DELETED old backup(s) (>${RETENTION_DAYS} days)"
|
||||
fi
|
||||
|
||||
echo "[$(date)] Backup process complete"
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy-frontend.sh — Build Vuetify frontend and deploy to server
|
||||
#
|
||||
# Docker 生产:前端在镜像内构建,远程执行 nexus-1panel.sh upgrade
|
||||
# Supervisor:tar 上传到 web/app + supervisorctl restart
|
||||
#
|
||||
# Usage: bash deploy/deploy-frontend.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
||||
fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
|
||||
echo "═══ Nexus Frontend Deploy ═══"
|
||||
|
||||
REMOTE_RUNTIME=$(ssh_cmd 'bash -s' <<'REMOTE'
|
||||
set -euo pipefail
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
compose_cmd() {
|
||||
if docker compose "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker compose "$@" 2>/dev/null
|
||||
}
|
||||
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
||||
[[ -d "$d/server" ]] || continue
|
||||
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
|
||||
prof="$d/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
args=(-f "$d/docker/docker-compose.prod.yml")
|
||||
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
|
||||
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
|
||||
args+=(-f "$d/docker/docker-compose.1panel.yml")
|
||||
q=$(cd "$d" && compose_cmd "${args[@]}" \
|
||||
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
|
||||
[[ -n "$q" ]] && echo "docker $d" && exit 0
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$d/deploy/nexus-1panel.sh" ]]; then
|
||||
q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true)
|
||||
[[ -n "$q" ]] && echo "docker $d" && exit 0
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo "supervisor $d"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
echo "unknown"
|
||||
REMOTE
|
||||
)
|
||||
|
||||
RUNTIME="${REMOTE_RUNTIME%% *}"
|
||||
DEPLOY_PATH="${REMOTE_RUNTIME#* }"
|
||||
[[ "$DEPLOY_PATH" == "$REMOTE_RUNTIME" ]] && DEPLOY_PATH="/www/wwwroot/api.synaglobal.vip"
|
||||
|
||||
echo "Remote runtime: ${RUNTIME} @ ${DEPLOY_PATH}"
|
||||
|
||||
echo "▶ Building frontend (local verify)..."
|
||||
cd frontend && npx vite build && cd ..
|
||||
echo "✓ Build complete"
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker 生产:重建镜像以包含新前端(非 tar 热更新)..."
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
|
||||
else
|
||||
echo "▶ Packaging..."
|
||||
tar czf /tmp/nexus-frontend.tar.gz -C web/app index.html assets/
|
||||
echo "✓ Package ready ($(du -h /tmp/nexus-frontend.tar.gz | cut -f1))"
|
||||
|
||||
echo "▶ Uploading to server..."
|
||||
scp "${SSH_OPTS[@]}" /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
|
||||
echo "✓ Upload complete"
|
||||
|
||||
echo "▶ Deploying on server..."
|
||||
ssh_cmd "cd ${DEPLOY_PATH}/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz"
|
||||
echo "▶ Pruning orphaned assets..."
|
||||
if ! ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py --dry-run"; then
|
||||
echo "✗ Prune dry-run failed"
|
||||
exit 1
|
||||
fi
|
||||
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"
|
||||
echo "✓ Extracted and pruned"
|
||||
|
||||
echo "▶ Restarting backend..."
|
||||
ssh_cmd "supervisorctl restart nexus"
|
||||
echo "✓ Restarted"
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
HEALTH=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/health" 2>/dev/null || echo "000")
|
||||
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
|
||||
|
||||
echo ""
|
||||
echo "═══ Verification ═══"
|
||||
echo " /health → $HEALTH"
|
||||
echo " /app/ → $SPA"
|
||||
|
||||
if [[ "$HEALTH" == "200" || "$HEALTH" == "ok" ]] && [[ "$SPA" == "200" ]]; then
|
||||
echo "✓ Deploy successful!"
|
||||
else
|
||||
echo "✗ Verification failed — check server logs"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run ON production server — 自动识别 Docker / Supervisor
|
||||
#
|
||||
# Usage (on server):
|
||||
# cd /opt/nexus && sudo bash deploy/deploy-on-server.sh
|
||||
# cd /www/wwwroot/api.synaglobal.vip && bash deploy/deploy-on-server.sh
|
||||
#
|
||||
# Optional env:
|
||||
# NEXUS_GITEA_TOKEN — if git pull needs auth
|
||||
# SKIP_FRONTEND=1 — Supervisor 模式跳过热更新前端 tar
|
||||
# NEXUS_DEPLOY_PATH — 覆盖自动检测路径
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./detect_deploy_runtime.sh
|
||||
source "${SCRIPT_DIR}/detect_deploy_runtime.sh"
|
||||
|
||||
DEPLOY_PATH="$(resolve_nexus_root "${NEXUS_DEPLOY_PATH:-}" 2>/dev/null || true)"
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
echo "ERROR: cannot resolve Nexus root" >&2
|
||||
exit 1
|
||||
fi
|
||||
cd "${DEPLOY_PATH}"
|
||||
|
||||
RUNTIME="$(detect_nexus_runtime "$DEPLOY_PATH")"
|
||||
PORT="$(nexus_publish_port_for "$DEPLOY_PATH")"
|
||||
|
||||
echo "═══ Nexus server-side deploy ═══"
|
||||
echo "Path: ${DEPLOY_PATH}"
|
||||
echo "Runtime: ${RUNTIME}"
|
||||
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
git remote set-url origin "http://admin:${NEXUS_GITEA_TOKEN}@66.154.115.8:3000/admin/Nexus.git"
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker upgrade (pull + rebuild)..."
|
||||
exec env NEXUS_ROOT="$DEPLOY_PATH" bash "$DEPLOY_PATH/deploy/nexus-1panel.sh" upgrade
|
||||
fi
|
||||
|
||||
echo "▶ git fetch + reset --hard origin/main..."
|
||||
git fetch --all
|
||||
git reset --hard origin/main
|
||||
echo " HEAD: $(git log -1 --oneline)"
|
||||
|
||||
echo "▶ supervisorctl restart nexus..."
|
||||
supervisorctl restart nexus
|
||||
|
||||
if [[ -f /tmp/nexus-frontend.tar.gz && "${SKIP_FRONTEND:-0}" != "1" ]]; then
|
||||
echo "▶ extract /tmp/nexus-frontend.tar.gz..."
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web/app
|
||||
rm -f /tmp/nexus-frontend.tar.gz
|
||||
if [[ -f deploy/prune_frontend_assets.py ]]; then
|
||||
python3 deploy/prune_frontend_assets.py --dry-run
|
||||
python3 deploy/prune_frontend_assets.py
|
||||
fi
|
||||
supervisorctl restart nexus
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
HEALTH=$(curl -s "http://127.0.0.1:${PORT}/health" || true)
|
||||
SPA=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/app/" || echo "000")
|
||||
echo " /health → ${HEALTH}"
|
||||
echo " /app/ → ${SPA}"
|
||||
|
||||
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" ]]; then
|
||||
echo "✓ Server deploy OK"
|
||||
suggest_ops_cron "$DEPLOY_PATH"
|
||||
else
|
||||
echo "✗ Verify failed — check: supervisorctl status nexus" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
# Full production deploy: local rsync + backend upgrade + frontend + health
|
||||
#
|
||||
# 默认从本机工作区 rsync 到生产机,不 push Gitea、不远程 git pull。
|
||||
#
|
||||
# Prerequisites on YOUR machine:
|
||||
# - rsync, SSH Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
|
||||
# - Key: export NEXUS_SSH_KEY=~/.ssh/id_rsa_nexus
|
||||
#
|
||||
# Usage:
|
||||
# cd "$NEXUS_ROOT"
|
||||
# bash deploy/pre_deploy_check.sh
|
||||
# bash deploy/deploy-production.sh
|
||||
#
|
||||
# 仍走 Gitea(仅当显式设置):
|
||||
# NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT}"
|
||||
|
||||
SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh"
|
||||
if [[ -f "$SECRETS" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$SECRETS"
|
||||
fi
|
||||
|
||||
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
||||
fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
scp_cmd() { scp "${SSH_OPTS[@]}" "$@"; }
|
||||
|
||||
DEPLOY_VIA_GIT="${NEXUS_DEPLOY_VIA_GIT:-0}"
|
||||
case "${DEPLOY_VIA_GIT}" in
|
||||
1|true|yes|on) DEPLOY_VIA_GIT=1 ;;
|
||||
*) DEPLOY_VIA_GIT=0 ;;
|
||||
esac
|
||||
|
||||
echo "═══ Nexus production deploy ═══"
|
||||
echo "SSH target: ${NEXUS_SSH_HOST}"
|
||||
echo "Mode: $([[ "$DEPLOY_VIA_GIT" == 1 ]] && echo 'Gitea git pull' || echo 'local rsync')"
|
||||
|
||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
||||
echo " Configure ~/.ssh/config (Host nexus) or set NEXUS_SSH / NEXUS_SSH_KEY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REMOTE_DETECT=$(ssh_cmd 'bash -s' <<'REMOTE'
|
||||
set -euo pipefail
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
compose_cmd() {
|
||||
if docker compose "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker compose "$@" 2>/dev/null
|
||||
}
|
||||
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
||||
[[ -d "$d/server" ]] || continue
|
||||
echo "ROOT=$d"
|
||||
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
|
||||
prof="$d/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
args=(-f "$d/docker/docker-compose.prod.yml")
|
||||
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
|
||||
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
|
||||
args+=(-f "$d/docker/docker-compose.1panel.yml")
|
||||
q=$(cd "$d" && compose_cmd "${args[@]}" \
|
||||
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
|
||||
[[ -n "$q" ]] && echo RUNTIME=docker && exit 0
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$d/deploy/nexus-1panel.sh" ]] && command -v nx >/dev/null 2>&1; then
|
||||
q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true)
|
||||
[[ -n "$q" ]] && echo RUNTIME=docker && exit 0
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo RUNTIME=supervisor
|
||||
exit 0
|
||||
fi
|
||||
echo RUNTIME=unknown
|
||||
exit 0
|
||||
done
|
||||
echo RUNTIME=none
|
||||
REMOTE
|
||||
)
|
||||
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-$(echo "$REMOTE_DETECT" | sed -n 's/^ROOT=//p' | head -1)}"
|
||||
RUNTIME="$(echo "$REMOTE_DETECT" | sed -n 's/^RUNTIME=//p' | head -1)"
|
||||
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
||||
fi
|
||||
if [[ -z "$RUNTIME" || "$RUNTIME" == "none" ]]; then
|
||||
RUNTIME="unknown"
|
||||
fi
|
||||
|
||||
echo "Deploy path: ${DEPLOY_PATH}"
|
||||
echo "Runtime: ${RUNTIME}"
|
||||
|
||||
resolve_deploy_chown() {
|
||||
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
|
||||
echo "${NEXUS_DEPLOY_CHOWN}"
|
||||
return
|
||||
fi
|
||||
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
|
||||
echo "www:www"
|
||||
return
|
||||
fi
|
||||
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
|
||||
}
|
||||
DEPLOY_CHOWN="$(resolve_deploy_chown)"
|
||||
echo "File owner: ${DEPLOY_CHOWN}"
|
||||
|
||||
if [[ "$DEPLOY_VIA_GIT" == 1 ]]; then
|
||||
echo "▶ Git mode: remote git pull..."
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && sudo git fetch --all && sudo git reset --hard origin/main && \
|
||||
sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
|
||||
elif [[ "$RUNTIME" == "supervisor" ]]; then
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
|
||||
else
|
||||
echo "ERROR: 无法识别远程运行时" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "▶ Local sync → ${DEPLOY_PATH}..."
|
||||
NEXUS_DEPLOY_PATH="${DEPLOY_PATH}" bash "${ROOT}/deploy/rsync-local-to-server.sh"
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker: 重建镜像(--skip-git)..."
|
||||
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade --skip-git"
|
||||
elif [[ "$RUNTIME" == "supervisor" ]]; then
|
||||
echo "▶ Supervisor: restart..."
|
||||
ssh_cmd "supervisorctl restart nexus"
|
||||
else
|
||||
echo "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor)" >&2
|
||||
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME" == "docker" || "$RUNTIME" == "supervisor" ]]; then
|
||||
echo "▶ Frontend: local vite build + upload to host web/app..."
|
||||
cd frontend && npx vite build && cd ..
|
||||
tar czf /tmp/nexus-frontend.tar.gz -C web/app index.html assets/
|
||||
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
|
||||
ssh_cmd "cd ${DEPLOY_PATH}/web/app && sudo tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz && sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app"
|
||||
|
||||
echo "▶ Frontend V2: local vite build:safe + upload to host web/app-v2..."
|
||||
cd frontend-v2 && npm run build:safe && cd ..
|
||||
tar czf /tmp/nexus-frontend-v2.tar.gz -C web/app-v2 index.html assets/
|
||||
scp_cmd /tmp/nexus-frontend-v2.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend-v2.tar.gz"
|
||||
ssh_cmd "sudo mkdir -p ${DEPLOY_PATH}/web/app-v2 && cd ${DEPLOY_PATH}/web/app-v2 && sudo tar xzf /tmp/nexus-frontend-v2.tar.gz && rm /tmp/nexus-frontend-v2.tar.gz && sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app-v2"
|
||||
|
||||
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
|
||||
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"
|
||||
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Sync host web/app and web/app-v2 into container..."
|
||||
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} NEXUS_PUBLISH_PORT=\$(grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"' || echo 8600) bash ${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" || {
|
||||
echo "WARN: web/app sync failed — run sync_webapp_to_container.sh on server manually" >&2
|
||||
}
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
PORT="$(ssh_cmd "grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"'" 2>/dev/null || true)"
|
||||
PORT="${PORT:-8600}"
|
||||
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:${PORT}/health" 2>/dev/null || true)
|
||||
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app/" 2>/dev/null || echo "000")
|
||||
SPA_V2=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app-v2/" 2>/dev/null || echo "000")
|
||||
echo " /health → ${HEALTH}"
|
||||
echo " /app/ → ${SPA}"
|
||||
echo " /app-v2/ → ${SPA_V2}"
|
||||
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
|
||||
echo "✓ Deploy successful"
|
||||
if ssh_cmd "command -v crontab >/dev/null && crontab -l 2>/dev/null | grep -q nexus-health-monitor" 2>/dev/null; then
|
||||
echo "✓ Ops cron (health_monitor) already installed"
|
||||
else
|
||||
echo "▶ Installing ops cron (health_monitor + db_backup)…"
|
||||
if ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/install_ops_cron.sh" 2>/dev/null; then
|
||||
echo "✓ Ops cron installed"
|
||||
else
|
||||
echo " 提示: 远程 cron 安装失败,可在服务器执行: sudo nx cron"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "✗ Verification failed — check logs on server" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Detect 1Panel App Store MySQL/Redis container names on 1panel-network.
|
||||
# See: https://github.com/1Panel-dev/1Panel/issues/11676
|
||||
# https://github.com/1Panel-dev/appstore (mysql/redis → networks: 1panel-network)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
network_container_names() {
|
||||
docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null \
|
||||
| tr ' ' '\n' | sed '/^$/d'
|
||||
}
|
||||
|
||||
pick_service_container() {
|
||||
local kind="$1"
|
||||
local names line
|
||||
names="$(network_container_names)"
|
||||
if [[ -z "$names" ]]; then
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "(^1Panel-${kind}-|${kind})" | head -1 || true
|
||||
return
|
||||
fi
|
||||
line="$(echo "$names" | grep -iE "^1Panel-${kind}-" | head -1 || true)"
|
||||
if [[ -n "$line" ]]; then
|
||||
echo "$line"
|
||||
return
|
||||
fi
|
||||
echo "$names" | grep -i "$kind" | head -1 || true
|
||||
}
|
||||
|
||||
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
echo "1panel-network not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MYSQL_HOST="$(pick_service_container mysql)"
|
||||
REDIS_HOST="$(pick_service_container redis)"
|
||||
|
||||
if [[ -z "$MYSQL_HOST" && -z "$REDIS_HOST" ]]; then
|
||||
echo "No MySQL/Redis containers on 1panel-network" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[[ -n "$MYSQL_HOST" ]] && echo "NEXUS_1PANEL_DB_HOST=${MYSQL_HOST}"
|
||||
[[ -n "$REDIS_HOST" ]] && echo "NEXUS_1PANEL_REDIS_HOST=${REDIS_HOST}"
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — 部署运行时检测(Docker 1Panel / Supervisor 裸机)
|
||||
#
|
||||
# source deploy/detect_deploy_runtime.sh
|
||||
# ROOT="$(resolve_nexus_root)"
|
||||
# RUNTIME="$(detect_nexus_runtime "$ROOT")" # docker | supervisor | unknown
|
||||
set -euo pipefail
|
||||
|
||||
_COMPOSE_FILE="docker/docker-compose.prod.yml"
|
||||
_COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
|
||||
_ENV_PROD="docker/.env.prod"
|
||||
|
||||
resolve_nexus_root() {
|
||||
local hint="${1:-}"
|
||||
if [[ -n "$hint" && -d "$hint/server" ]]; then
|
||||
echo "$hint"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${NEXUS_ROOT:-}" && -d "${NEXUS_ROOT}/server" ]]; then
|
||||
echo "$NEXUS_ROOT"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${NEXUS_DEPLOY_PATH:-}" && -d "${NEXUS_DEPLOY_PATH}/server" ]]; then
|
||||
echo "$NEXUS_DEPLOY_PATH"
|
||||
return 0
|
||||
fi
|
||||
local d
|
||||
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
||||
if [[ -d "$d/server" ]]; then
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
for d in /www/wwwroot/*/; do
|
||||
[[ -d "${d}server" ]] || continue
|
||||
echo "${d%/}"
|
||||
return 0
|
||||
done
|
||||
if command -v supervisorctl >/dev/null 2>&1; then
|
||||
local cwd
|
||||
cwd="$(supervisorctl status nexus 2>/dev/null | grep -oP 'cwd=\K[^ ,]+' || true)"
|
||||
if [[ -n "$cwd" && -d "$cwd/server" ]]; then
|
||||
echo "$cwd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
_has_1panel_network() {
|
||||
docker network inspect 1panel-network >/dev/null 2>&1
|
||||
}
|
||||
|
||||
detect_nexus_runtime() {
|
||||
local root="$1"
|
||||
if [[ -f "$root/$_ENV_PROD" ]] && command -v docker >/dev/null 2>&1 \
|
||||
&& docker compose version >/dev/null 2>&1; then
|
||||
local -a args=(-f "$root/$_COMPOSE_FILE")
|
||||
if _has_1panel_network && [[ -f "$root/$_COMPOSE_1PANEL" ]]; then
|
||||
args+=(-f "$root/$_COMPOSE_1PANEL")
|
||||
fi
|
||||
local prof q
|
||||
prof="$root/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$root/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
q="$(cd "$root" && docker compose "${args[@]}" \
|
||||
--env-file "$root/$_ENV_PROD" --env-file "$prof" \
|
||||
ps -q nexus 2>/dev/null || true)"
|
||||
if [[ -n "$q" ]]; then
|
||||
echo docker
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 \
|
||||
&& supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo supervisor
|
||||
return 0
|
||||
fi
|
||||
echo unknown
|
||||
}
|
||||
|
||||
nexus_publish_port_for() {
|
||||
local root="$1"
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$root/$_ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
if [[ -z "$port" && -f "$root/.env" ]]; then
|
||||
port="$(grep -E '^NEXUS_PORT=' "$root/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
fi
|
||||
echo "${port:-8600}"
|
||||
}
|
||||
|
||||
nexus_restart_service() {
|
||||
local root="$1"
|
||||
local runtime
|
||||
runtime="$(detect_nexus_runtime "$root")"
|
||||
case "$runtime" in
|
||||
docker)
|
||||
local cname
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]]; then
|
||||
docker restart "$cname"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
;;
|
||||
supervisor)
|
||||
supervisorctl restart nexus
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
nexus_has_ops_cron() {
|
||||
command -v crontab >/dev/null 2>&1 \
|
||||
&& crontab -l 2>/dev/null | grep -q 'nexus-health-monitor'
|
||||
}
|
||||
|
||||
suggest_ops_cron() {
|
||||
local root="$1"
|
||||
if nexus_has_ops_cron; then
|
||||
return 0
|
||||
fi
|
||||
echo ""
|
||||
echo "[WARN] 未检测到 health_monitor / db_backup cron"
|
||||
echo " 建议: sudo bash ${root}/deploy/install_ops_cron.sh"
|
||||
echo " 或: sudo nx cron"
|
||||
}
|
||||
|
||||
prompt_ops_cron_if_missing() {
|
||||
local root="$1"
|
||||
if nexus_has_ops_cron; then
|
||||
echo "[INFO] Crontab 已含 health_monitor / db_backup"
|
||||
return 0
|
||||
fi
|
||||
suggest_ops_cron "$root"
|
||||
if [[ ! -t 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
local ans
|
||||
read -r -p "现在安装巡检/备份 cron? [Y/n]: " ans
|
||||
if [[ "$ans" == "n" || "$ans" == "N" ]]; then
|
||||
echo "[WARN] 已跳过 cron 安装"
|
||||
return 0
|
||||
fi
|
||||
bash "${root}/deploy/install_ops_cron.sh"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# 下载并执行 install-nexus-fresh.sh(公共仓库无需令牌)
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/download-install-fresh.sh" | bash
|
||||
#
|
||||
# 推荐直接使用:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
OUT="${OUT:-/tmp/install-nexus-fresh.sh}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
|
||||
download() {
|
||||
local url base
|
||||
for base in "${NEXUS_RAW_BASE}" "http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/install-nexus-fresh.sh"
|
||||
if curl -fsSL "$url" -o "$OUT" 2>/dev/null && head -1 "$OUT" | grep -q '^#!/'; then
|
||||
ok "已下载 install-nexus-fresh.sh"
|
||||
chmod +x "$OUT"
|
||||
exec bash "$OUT" "$@"
|
||||
fi
|
||||
done
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
url="http://${NEXUS_GITEA_HOST}/api/v1/repos/admin/Nexus/raw/deploy/install-nexus-fresh.sh?ref=${NEXUS_GIT_BRANCH}"
|
||||
if curl -fsSL -H "Authorization: token ${NEXUS_GITEA_TOKEN}" "$url" -o "$OUT" 2>/dev/null \
|
||||
&& head -1 "$OUT" | grep -q '^#!/'; then
|
||||
ok "已下载(API + 令牌)"
|
||||
chmod +x "$OUT"
|
||||
exec bash "$OUT" "$@"
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! download "$@"; then
|
||||
err "下载失败,请改用公共仓库一键入口:"
|
||||
err " curl -fsSL \"${NEXUS_RAW_BASE}/deploy/quick-install.sh\" | bash"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Grant nexus@'%' on 1Panel App Store MySQL for Docker (1panel-network) install wizard.
|
||||
# Fixes: Access denied for user 'nexus'@'172.18.x.x' (1045)
|
||||
#
|
||||
# Usage:
|
||||
# MYSQL_ROOT_PASSWORD='root密码' NEXUS_DB_PASSWORD='nexus密码' bash deploy/fix-1panel-mysql-grant.sh
|
||||
# bash deploy/fix-1panel-mysql-grant.sh # prompts
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_DB_NAME="${NEXUS_DB_NAME:-nexus}"
|
||||
NEXUS_DB_USER="${NEXUS_DB_USER:-nexus}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; exit 1; }
|
||||
|
||||
pick_mysql_container() {
|
||||
local name
|
||||
if [[ -n "${MYSQL_CONTAINER:-}" ]]; then
|
||||
echo "$MYSQL_CONTAINER"
|
||||
return 0
|
||||
fi
|
||||
if docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
name="$(docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' \
|
||||
| tr ' ' '\n' | grep -iE '^1Panel-mysql-' | head -1 || true)"
|
||||
[[ -n "$name" ]] && echo "$name" && return 0
|
||||
fi
|
||||
name="$(docker ps --format '{{.Names}}' | grep -iE '^1Panel-mysql-' | head -1 || true)"
|
||||
[[ -n "$name" ]] && echo "$name" && return 0
|
||||
error "未找到 1Panel MySQL 容器(可设 MYSQL_CONTAINER=名称)"
|
||||
}
|
||||
|
||||
prompt_secret() {
|
||||
local var="$1" prompt="$2"
|
||||
if [[ -n "${!var:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
read -r -s -p "$prompt: " "$var"
|
||||
echo ""
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
error "$var 不能为空"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
[[ "${EUID:-$(id -u)}" -eq 0 ]] || error "请用 root 运行"
|
||||
|
||||
local mysql_c
|
||||
mysql_c="$(pick_mysql_container)"
|
||||
info "MySQL 容器: $mysql_c"
|
||||
|
||||
prompt_secret MYSQL_ROOT_PASSWORD "1Panel MySQL root 密码"
|
||||
prompt_secret NEXUS_DB_PASSWORD "nexus 用户密码(与安装向导步骤 3 一致)"
|
||||
|
||||
info "创建库/用户并授权 ${NEXUS_DB_USER}@'%' → ${NEXUS_DB_NAME}.* ..."
|
||||
|
||||
docker exec -i "$mysql_c" mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" <<SQL
|
||||
CREATE DATABASE IF NOT EXISTS \`${NEXUS_DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER IF NOT EXISTS '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
|
||||
ALTER USER '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
|
||||
GRANT ALL PRIVILEGES ON \`${NEXUS_DB_NAME}\`.* TO '${NEXUS_DB_USER}'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
SELECT user, host FROM mysql.user WHERE user = '${NEXUS_DB_USER}';
|
||||
SQL
|
||||
|
||||
info "完成。请在安装向导步骤 3 使用: 库/用户=${NEXUS_DB_NAME}/${NEXUS_DB_USER},密码与上面一致"
|
||||
info "若已改 install.py,执行: bash deploy/sync-install-wizard-to-container.sh"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gate Log — 门控执行日志记录器
|
||||
|
||||
每次门控检查的结果都追加到 deploy/gate_log.jsonl (JSON Lines)
|
||||
每行一条记录,包含:时间戳、门控名、结果、详情
|
||||
|
||||
这个文件是用户审查AI是否偷懒的关键证据:
|
||||
- 查看最近一次: tail -1 deploy/gate_log.jsonl | python -m json.tool
|
||||
- 查看所有BLOCK: grep '"BLOCK"' deploy/gate_log.jsonl
|
||||
- 查看某天: grep '2026-05-26' deploy/gate_log.jsonl
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
GATE_LOG = os.path.join(
|
||||
os.environ.get("NEXUS_DEPLOY_DIR", "/opt/nexus"),
|
||||
"deploy", "gate_log.jsonl"
|
||||
)
|
||||
|
||||
def log_gate(gate_name: str, result: str, detail: str = "", files_checked: list = None):
|
||||
"""Record a gate check result.
|
||||
|
||||
Args:
|
||||
gate_name: e.g. "Changelog", "Audit", "Test", "Lint", "Import", "Security", "Review"
|
||||
result: "PASS", "BLOCK", "SKIP", "WARN"
|
||||
detail: Human-readable detail
|
||||
files_checked: List of files involved in this gate check
|
||||
"""
|
||||
entry = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"gate": gate_name,
|
||||
"result": result,
|
||||
"detail": detail,
|
||||
}
|
||||
if files_checked:
|
||||
entry["files"] = files_checked
|
||||
|
||||
os.makedirs(os.path.dirname(GATE_LOG), exist_ok=True)
|
||||
with open(GATE_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) >= 3:
|
||||
log_gate(sys.argv[1], sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else "")
|
||||
else:
|
||||
# Print recent log
|
||||
if os.path.exists(GATE_LOG):
|
||||
with open(GATE_LOG, encoding="utf-8") as f:
|
||||
lines = f.readlines()[-20:]
|
||||
for line in lines:
|
||||
d = json.loads(line)
|
||||
icon = {"PASS": "✅", "BLOCK": "🚫", "SKIP": "⏭️", "WARN": "⚠️"}.get(d["result"], "?")
|
||||
print(f"{d['ts'][:19]} {icon} {d['gate']}: {d.get('detail','')}")
|
||||
else:
|
||||
print("No gate log found")
|
||||
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — External Health Monitor (Layer 3 of 3-tier guardian)
|
||||
# Runs via crontab every minute. Works on Docker (1Panel) and Supervisor bare-metal.
|
||||
#
|
||||
# Layer 1: Docker restart policy / Supervisor autorestart
|
||||
# Layer 2: Python self_monitor (30s)
|
||||
# Layer 3: This script — HTTP /health, 3 failures → restart + optional Telegram
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
|
||||
NEXUS_SERVICE="${NEXUS_SERVICE:-nexus}"
|
||||
STATE_DIR=""
|
||||
FAIL_FILE=""
|
||||
LAST_RESTART_FILE=""
|
||||
# Legacy paths (cron user may have created root-owned files under /tmp)
|
||||
LEGACY_FAIL_FILE="/tmp/nexus_health_fail_count"
|
||||
LEGACY_LAST_RESTART_FILE="/tmp/nexus_health_last_restart"
|
||||
MAX_FAIL=3
|
||||
RESTART_COOLDOWN_SEC="${NEXUS_HEALTH_RESTART_COOLDOWN:-300}"
|
||||
ENV_PROD="${DEPLOY_DIR}/docker/.env.prod"
|
||||
|
||||
ensure_state_paths() {
|
||||
if [[ -n "$STATE_DIR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local preferred="${DEPLOY_DIR}/var/layer3-health"
|
||||
local fallback="${HOME:-/tmp}/.nexus/layer3-health"
|
||||
if mkdir -p "$preferred" 2>/dev/null && [[ -w "$preferred" ]]; then
|
||||
STATE_DIR="$preferred"
|
||||
elif command -v sudo >/dev/null 2>&1 && sudo mkdir -p "$preferred" 2>/dev/null \
|
||||
&& sudo chown -R "$(id -un):$(id -gn)" "$preferred" 2>/dev/null \
|
||||
&& [[ -w "$preferred" ]]; then
|
||||
STATE_DIR="$preferred"
|
||||
else
|
||||
mkdir -p "$fallback"
|
||||
STATE_DIR="$fallback"
|
||||
fi
|
||||
FAIL_FILE="${STATE_DIR}/fail_count"
|
||||
LAST_RESTART_FILE="${STATE_DIR}/last_restart"
|
||||
}
|
||||
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo docker "$@" 2>/dev/null
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
init_layer3_state() {
|
||||
ensure_state_paths
|
||||
chmod 755 "$STATE_DIR" 2>/dev/null || true
|
||||
if [[ ! -f "$FAIL_FILE" && -f "$LEGACY_FAIL_FILE" ]]; then
|
||||
cp "$LEGACY_FAIL_FILE" "$FAIL_FILE" 2>/dev/null || true
|
||||
fi
|
||||
if [[ ! -f "$LAST_RESTART_FILE" && -f "$LEGACY_LAST_RESTART_FILE" ]]; then
|
||||
cp "$LEGACY_LAST_RESTART_FILE" "$LAST_RESTART_FILE" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
write_state_file() {
|
||||
local path="$1" value="$2"
|
||||
init_layer3_state
|
||||
printf '%s\n' "$value" >"$path" 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
resolve_publish_port() {
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
echo "${port:-8600}"
|
||||
}
|
||||
|
||||
PUBLISH_PORT="$(resolve_publish_port)"
|
||||
HEALTH_URL="http://127.0.0.1:${PUBLISH_PORT}/health"
|
||||
|
||||
resolve_nexus_container() {
|
||||
docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||
}
|
||||
|
||||
read_env_var() {
|
||||
local key="$1" file="$2"
|
||||
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' || true
|
||||
}
|
||||
|
||||
normalize_env_value() {
|
||||
local v="${1:-}"
|
||||
v="${v//$'\r'/}"
|
||||
if [[ ${#v} -ge 2 && ${v:0:1} == '"' && ${v: -1} == '"' ]]; then
|
||||
v="${v:1:-1}"
|
||||
v="${v//\\n/$'\n'}"
|
||||
v="${v//\\\"/\"}"
|
||||
v="${v//\\\\/\\}"
|
||||
fi
|
||||
echo "$v"
|
||||
}
|
||||
|
||||
load_telegram_from_db() {
|
||||
local cname="$1"
|
||||
[[ -n "$cname" ]] || return 1
|
||||
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] && return 0
|
||||
local out token chat
|
||||
out="$(docker_cmd exec "$cname" python3 -c "
|
||||
import asyncio
|
||||
from server.config import settings as s
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
|
||||
async def main():
|
||||
async with AsyncSessionLocal() as db:
|
||||
await s.load_settings_from_db(db)
|
||||
t = (s.TELEGRAM_BOT_TOKEN or '').strip()
|
||||
c = (s.TELEGRAM_CHAT_ID or '').strip()
|
||||
if t and c:
|
||||
print(t)
|
||||
print(c)
|
||||
|
||||
asyncio.run(main())
|
||||
" 2>/dev/null)" || true
|
||||
[[ -n "$out" ]] || return 1
|
||||
token="$(printf '%s\n' "$out" | sed -n '1p')"
|
||||
chat="$(printf '%s\n' "$out" | sed -n '2p')"
|
||||
[[ -n "$token" && -n "$chat" ]] || return 1
|
||||
BOT_TOKEN="$token"
|
||||
CHAT_ID="$chat"
|
||||
}
|
||||
|
||||
load_telegram_config() {
|
||||
local cname="$1" dotenv
|
||||
BOT_TOKEN=""
|
||||
CHAT_ID=""
|
||||
for dotenv in \
|
||||
"${DEPLOY_DIR}/.env" \
|
||||
"$ENV_PROD" \
|
||||
; do
|
||||
[[ -f "$dotenv" ]] || continue
|
||||
BOT_TOKEN="$(read_env_var NEXUS_TELEGRAM_BOT_TOKEN "$dotenv")"
|
||||
CHAT_ID="$(read_env_var NEXUS_TELEGRAM_CHAT_ID "$dotenv")"
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
||||
done
|
||||
if [[ -n "$cname" ]]; then
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
NEXUS_TELEGRAM_BOT_TOKEN=*) BOT_TOKEN="${line#NEXUS_TELEGRAM_BOT_TOKEN=}" ;;
|
||||
NEXUS_TELEGRAM_CHAT_ID=*) CHAT_ID="${line#NEXUS_TELEGRAM_CHAT_ID=}" ;;
|
||||
esac
|
||||
done < <(docker_cmd exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
||||
load_telegram_from_db "$cname" || true
|
||||
fi
|
||||
BOT_TOKEN="$(normalize_env_value "$BOT_TOKEN")"
|
||||
CHAT_ID="$(normalize_env_value "$CHAT_ID")"
|
||||
}
|
||||
|
||||
send_telegram() {
|
||||
local msg="${1}" resp http log="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
BOT_TOKEN="$(normalize_env_value "${BOT_TOKEN:-}")"
|
||||
CHAT_ID="$(normalize_env_value "${CHAT_ID:-}")"
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] || return 1
|
||||
resp="$(curl -s --max-time 15 -w '\n%{http_code}' \
|
||||
"https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CHAT_ID}" \
|
||||
--data-urlencode "text=${msg}" \
|
||||
-d "parse_mode=HTML" 2>/dev/null)" || return 1
|
||||
http="${resp##*$'\n'}"
|
||||
if [[ "$http" != "200" ]]; then
|
||||
echo "$(date -Is 2>/dev/null || date) telegram send failed http=${http}" >> "$log" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
bump_fail_count() {
|
||||
local fail=0
|
||||
init_layer3_state
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
(
|
||||
flock -x 9
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
fail=$((fail + 1))
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
) 9>"${FAIL_FILE}.lock"
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
else
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
fail=$((fail + 1))
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
fi
|
||||
echo "$fail"
|
||||
}
|
||||
|
||||
reset_fail_count() {
|
||||
write_state_file "$FAIL_FILE" "0" || true
|
||||
}
|
||||
|
||||
restart_cooldown_active() {
|
||||
local now last elapsed
|
||||
[[ -f "$LAST_RESTART_FILE" ]] || return 1
|
||||
now="$(date +%s)"
|
||||
last="$(cat "$LAST_RESTART_FILE" 2>/dev/null || echo 0)"
|
||||
elapsed=$((now - last))
|
||||
[[ "$elapsed" -lt "$RESTART_COOLDOWN_SEC" ]]
|
||||
}
|
||||
|
||||
mark_restart_attempt() {
|
||||
write_state_file "$LAST_RESTART_FILE" "$(date +%s)" || true
|
||||
}
|
||||
|
||||
restart_nexus() {
|
||||
local cname="$1" via=""
|
||||
if [[ -n "$cname" ]]; then
|
||||
docker_cmd restart "$cname" >/dev/null 2>&1 && via="Docker 容器 ${cname}"
|
||||
elif command -v supervisorctl >/dev/null 2>&1 && supervisorctl status "$NEXUS_SERVICE" >/dev/null 2>&1; then
|
||||
supervisorctl restart "$NEXUS_SERVICE" >/dev/null 2>&1 && via="Supervisor ${NEXUS_SERVICE}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
echo "$via"
|
||||
}
|
||||
|
||||
telegram_health_fail_cooldown() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】健康检查连续失败(冷却期内)</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
连续失败:已达 ${MAX_FAIL} 次
|
||||
Docker 容器:${NEXUS_CONTAINER:-未识别}
|
||||
具体情况:/health 仍不可达,但距上次自动重启不足 ${RESTART_COOLDOWN_SEC} 秒,本次跳过重启以免频繁抖动
|
||||
建议操作:查看 docker logs ${NEXUS_CONTAINER:-nexus} 与 ${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_fail_restart() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】Nexus 连续 ${MAX_FAIL} 次无响应</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
连续失败:${FAIL} 次(阈值 ${MAX_FAIL} 次)
|
||||
Docker 容器:${NEXUS_CONTAINER:-未识别}
|
||||
具体情况:宿主机 cron 无法访问 API 健康端点,平台可能已宕机或端口异常
|
||||
正在执行:自动重启 Nexus 服务…
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_recovered() {
|
||||
local via="$1" body
|
||||
body="$(cat <<EOF
|
||||
🟢 <b>【Layer 3 宿主机巡检】Nexus 已自动恢复</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
恢复方式:${via}
|
||||
具体情况:自动重启后 /health 检测通过,服务已恢复在线
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_still_down() {
|
||||
local via="$1" body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】自动重启后仍未恢复</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
已尝试:${via}
|
||||
具体情况:重启完成后 /health 仍失败,可能存在数据库/Redis 故障或配置错误
|
||||
建议操作:立即 SSH 登录排查 docker logs、MySQL、Redis,需人工介入
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_restart_failed() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】无法执行自动重启</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
具体情况:未找到可重启的 Docker 容器或 Supervisor 服务(${NEXUS_SERVICE})
|
||||
建议操作:确认 nexus 容器名称与 docker ps 输出,手动执行 docker restart 或 nx update
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
NEXUS_CONTAINER="$(resolve_nexus_container)"
|
||||
load_telegram_config "$NEXUS_CONTAINER"
|
||||
init_layer3_state
|
||||
HOST_LABEL="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown)"
|
||||
TS_LABEL="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || date)"
|
||||
|
||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
reset_fail_count
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FAIL="$(bump_fail_count)"
|
||||
if [[ "$FAIL" -lt "$MAX_FAIL" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if restart_cooldown_active; then
|
||||
telegram_health_fail_cooldown
|
||||
exit 0
|
||||
fi
|
||||
|
||||
telegram_health_fail_restart
|
||||
mark_restart_attempt
|
||||
|
||||
if VIA="$(restart_nexus "$NEXUS_CONTAINER")"; then
|
||||
sleep 5
|
||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
telegram_health_recovered "$VIA"
|
||||
reset_fail_count
|
||||
else
|
||||
telegram_health_still_down "$VIA"
|
||||
fi
|
||||
else
|
||||
telegram_health_restart_failed
|
||||
fi
|
||||
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 重装服务器一键入口(1Panel + Docker + Nexus)
|
||||
#
|
||||
# 适用:全新 VPS / 重装系统后,root 一条命令装完整栈。
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
#
|
||||
# 4 核 16G + 自定义域名:
|
||||
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g --domain api.example.com
|
||||
#
|
||||
# 已装 1Panel,只装 Nexus Docker:
|
||||
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
#
|
||||
# 流程:
|
||||
# ① 可选:官方 1Panel 安装(含 Docker,交互式)
|
||||
# ② 检测 / 安装 Docker Compose
|
||||
# ③ install-nexus-fresh.sh → MySQL + Nexus 容器(Redis 自行安装)
|
||||
#
|
||||
# 反代:必须在 1Panel 面板 UI 配置,禁止脚本直接改 /opt/1panel/apps/
|
||||
# 示例: deploy/1panel/openresty-nexus.conf.example
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
|
||||
PANEL_INSTALL_URL="${PANEL_INSTALL_URL:-https://resource.1panel.pro/v2/quick_start.sh}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
WITH_1PANEL=auto
|
||||
SKIP_1PANEL=false
|
||||
SKIP_DOCKER_INSTALL=false
|
||||
EXTRA_ARGS=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
重装服务器 — 1Panel + Docker + Nexus 一键安装
|
||||
|
||||
curl -fsSL "${NEXUS_RAW_BASE}/deploy/install-1panel-docker.sh" | bash
|
||||
|
||||
选项(传给 bash -s --):
|
||||
--with-1panel 未装 1Panel 时先跑官方安装脚本(交互式)
|
||||
--skip-1panel 跳过 1Panel,只装 Nexus Docker 栈
|
||||
--skip-docker-install 不自动装 Docker(已在 1Panel 装好)
|
||||
--profile 2c8g|4c16g|1c4g
|
||||
--domain NAME
|
||||
--root PATH
|
||||
-h, --help
|
||||
|
||||
说明:
|
||||
- 1Panel 官方安装为交互式(端口/用户/密码),装完记下面板地址
|
||||
- Nexus 密钥按本机自动生成,备份在 /root/.nexus-install-secrets-*.env
|
||||
- 网站反代请在 1Panel UI 操作,勿手工改 /opt/1panel/apps/
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--with-1panel) WITH_1PANEL=true; shift ;;
|
||||
--skip-1panel) SKIP_1PANEL=true; WITH_1PANEL=false; shift ;;
|
||||
--skip-docker-install) SKIP_DOCKER_INSTALL=true; shift ;;
|
||||
--profile|--domain|--root)
|
||||
EXTRA_ARGS+=("$1" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--skip-clone|--skip-1panel-check)
|
||||
EXTRA_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
*) EXTRA_ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
banner() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus — 1Panel + Docker + Nexus 一键安装 ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 域名: ${NEXUS_DOMAIN} 档位: ${NEXUS_PROFILE} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行(su - 后直接运行)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
load_deploy_env() {
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
}
|
||||
|
||||
has_1panel() {
|
||||
command -v 1pctl >/dev/null 2>&1
|
||||
}
|
||||
|
||||
has_docker_compose() {
|
||||
docker compose version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
phase_1panel() {
|
||||
if [[ "$SKIP_1PANEL" == true ]]; then
|
||||
info "跳过 1Panel 安装(--skip-1panel)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if has_1panel; then
|
||||
info "已安装 1Panel"
|
||||
if 1pctl user-info >/dev/null 2>&1; then
|
||||
1pctl user-info 2>/dev/null | grep -E '面板地址|面板用户' || true
|
||||
else
|
||||
warn "1pctl user-info 失败,面板可能需修复;继续安装 Nexus…"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$WITH_1PANEL" == auto ]]; then
|
||||
echo ""
|
||||
warn "未检测到 1Panel(1pctl 不存在)"
|
||||
echo " ① 先装 1Panel(推荐,含 Docker)再装 Nexus"
|
||||
echo " ② 跳过 1Panel,仅装 Nexus Docker(需本机已有 Docker)"
|
||||
echo ""
|
||||
read -r -p "是否现在安装 1Panel? [Y/n]: " ans
|
||||
case "${ans:-Y}" in
|
||||
n|N) WITH_1PANEL=false ;;
|
||||
*) WITH_1PANEL=true ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ "$WITH_1PANEL" != true ]]; then
|
||||
warn "未安装 1Panel,将仅安装 Nexus Docker 栈"
|
||||
return 0
|
||||
fi
|
||||
|
||||
step "安装 1Panel(官方脚本,交互式)..."
|
||||
info "下载: $PANEL_INSTALL_URL"
|
||||
local tmp
|
||||
tmp="$(mktemp /tmp/1panel-quick-start.XXXXXX.sh)"
|
||||
if ! curl -fsSL "$PANEL_INSTALL_URL" -o "$tmp"; then
|
||||
error "下载 1Panel 安装脚本失败"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$tmp"
|
||||
echo ""
|
||||
warn "接下来为 1Panel 官方安装向导,请按提示设置端口、安全入口、用户名、密码"
|
||||
warn "安装目录建议默认 /opt"
|
||||
echo ""
|
||||
bash "$tmp"
|
||||
rm -f "$tmp"
|
||||
|
||||
if ! has_1panel; then
|
||||
error "1Panel 安装后仍未找到 1pctl,请检查安装日志"
|
||||
exit 1
|
||||
fi
|
||||
info "1Panel 安装完成"
|
||||
1pctl user-info 2>/dev/null || true
|
||||
echo ""
|
||||
warn "请在 1Panel → 应用商店 安装 OpenResty(用于 HTTPS 反代)"
|
||||
read -r -p "OpenResty 已装好或稍后自行安装,按 Enter 继续 Nexus 安装..."
|
||||
}
|
||||
|
||||
install_docker_engine() {
|
||||
step "安装 Docker Engine + Compose 插件..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates curl >/dev/null
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable --now docker 2>/dev/null || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q docker docker-compose-plugin >/dev/null 2>&1 || \
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable --now docker 2>/dev/null || true
|
||||
else
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
fi
|
||||
}
|
||||
|
||||
phase_docker() {
|
||||
step "检测 Docker Compose..."
|
||||
if has_docker_compose; then
|
||||
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version | head -1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_DOCKER_INSTALL" == true ]]; then
|
||||
error "未检测到 docker compose,且已指定 --skip-docker-install"
|
||||
error "请在 1Panel → 容器 安装 Docker,或去掉该参数"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
warn "未检测到 docker compose"
|
||||
if has_1panel; then
|
||||
warn "建议在 1Panel → 容器 安装 Docker 后重新运行本脚本"
|
||||
read -r -p "是否尝试用 get.docker.com 自动安装? [y/N]: " ans
|
||||
[[ "${ans:-N}" == y || "${ans:-N}" == Y ]] || exit 1
|
||||
else
|
||||
info "自动安装 Docker(get.docker.com)..."
|
||||
fi
|
||||
|
||||
install_docker_engine
|
||||
|
||||
if ! has_docker_compose; then
|
||||
error "Docker 安装后仍无 compose 插件,请手动安装 docker-compose-plugin"
|
||||
exit 1
|
||||
fi
|
||||
info "Docker Compose 就绪"
|
||||
}
|
||||
|
||||
download_fresh_installer() {
|
||||
local name="install-nexus-fresh.sh" dest
|
||||
dest="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
|
||||
local url base
|
||||
for base in "${NEXUS_RAW_BASE}" "http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/${name}"
|
||||
if curl -fsSL "$url" -o "$dest" 2>/dev/null && head -1 "$dest" | grep -q '^#!/'; then
|
||||
chmod +x "$dest"
|
||||
echo "$dest"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
phase_nexus() {
|
||||
step "安装 Nexus Docker 栈(MySQL + Nexus;Redis 请自行安装)..."
|
||||
local installer
|
||||
if [[ -x "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" ]]; then
|
||||
installer="${NEXUS_ROOT}/deploy/install-nexus-fresh.sh"
|
||||
info "使用本地: $installer"
|
||||
exec bash "$installer" --skip-1panel-check "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
if installer="$(download_fresh_installer)"; then
|
||||
info "已下载 install-nexus-fresh.sh"
|
||||
exec bash "$installer" --skip-1panel-check "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
error "无法获取 install-nexus-fresh.sh,请检查 Gitea 可达性"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main() {
|
||||
banner
|
||||
require_root
|
||||
load_deploy_env
|
||||
phase_1panel
|
||||
phase_docker
|
||||
phase_nexus
|
||||
}
|
||||
|
||||
# curl | bash 时 stdin 脚本无 BASH_SOURCE[0],不可配合 set -u 做入口判断
|
||||
main "$@"
|
||||
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 全新安装脚本(1Panel + Docker Compose)
|
||||
#
|
||||
# 适用:新机已装 1Panel / Docker,空库,走 /app/install.html 安装向导
|
||||
# 风格参考 1Panel quick_start.sh:分阶段检查 + 结束输出访问地址
|
||||
#
|
||||
# 用法(公共仓库,root 下一条命令,参考 1Panel):
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
#
|
||||
# 已 clone 到 /opt/nexus:
|
||||
# cd /opt/nexus && bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
#
|
||||
# bash deploy/install-nexus-fresh.sh --profile 4c16g
|
||||
#
|
||||
# 公共仓库匿名 clone,无需 Gitea 令牌。NEXUS 密钥由安装脚本按机自动生成。
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_install_self="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
if ! head -1 "$_install_self" | grep -q '^#!/'; then
|
||||
echo "[ERROR] 脚本文件无效。若用 curl 下载,请改用: bash deploy/download-install-fresh.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -qiE '^(Not found|404)' "$_install_self" 2>/dev/null; then
|
||||
echo "[ERROR] 下载到的是 Gitea 404 页面,不是安装脚本。" >&2
|
||||
echo " 请: cd /opt/nexus && git pull && sudo bash deploy/install-nexus-fresh.sh --skip-clone" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── 默认配置(与 SECRETS.md / nexus-1panel 一致)──
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://66.154.115.8:3000/admin/Nexus/raw/branch/main}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Nexus 6.0 全新安装(1Panel + Docker)
|
||||
|
||||
sudo bash install-nexus-fresh.sh [选项]
|
||||
|
||||
选项:
|
||||
--domain NAME 公网域名(默认: ${NEXUS_DOMAIN})
|
||||
--profile NAME 资源档位 1c4g|2c8g|4c16g(默认: 2c8g)
|
||||
--root PATH 安装目录(默认: /opt/nexus)
|
||||
--skip-clone 已在 /opt/nexus,不重新 clone
|
||||
--skip-1panel-check 不检测 1Panel 是否已安装
|
||||
-h, --help 显示帮助
|
||||
|
||||
安装后请用浏览器打开: https://域名/app/install.html
|
||||
root 下全局命令: nexus-fresh | nexus-update | nx
|
||||
EOF
|
||||
}
|
||||
|
||||
SKIP_1PANEL_CHECK=false
|
||||
SKIP_CLONE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--domain) NEXUS_DOMAIN="$2"; shift 2 ;;
|
||||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--skip-clone) SKIP_CLONE=true; shift ;;
|
||||
--skip-1panel-check) SKIP_1PANEL_CHECK=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
banner() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus 6.0 — 全新安装(1Panel + Docker Compose) ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 域名: ${NEXUS_DOMAIN} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 档位: ${NEXUS_PROFILE} 目录: ${NEXUS_ROOT} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
load_credentials() {
|
||||
step "加载凭据..."
|
||||
local f
|
||||
for f in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
if [[ -f "$f" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "$f"
|
||||
set +a
|
||||
info "已加载 $f"
|
||||
fi
|
||||
done
|
||||
if [[ -f "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh"
|
||||
set +a
|
||||
info "已加载 ${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh(可选)"
|
||||
fi
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
info "全新安装:NEXUS 密钥将在 docker/.env.prod 中按本机自动生成"
|
||||
}
|
||||
|
||||
install_root_commands() {
|
||||
[[ -d "${NEXUS_ROOT}/deploy" ]] || return 0
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "${NEXUS_ROOT}/deploy/install-nx-cli.sh" || true
|
||||
info "全局命令: nx · nexus-update · nexus-fresh · nexus-install · nexus-1panel"
|
||||
}
|
||||
|
||||
phase_preflight() {
|
||||
step "阶段 1/6 — 环境检查"
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请切换到 root 再执行(不要用 sudo 包一层):"
|
||||
error " su -"
|
||||
error " bash $0 ..."
|
||||
exit 1
|
||||
fi
|
||||
info "运行用户: root(无需 sudo)"
|
||||
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
error "无法识别操作系统"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
info "系统: ${NAME:-Linux} ${VERSION_ID:-}"
|
||||
case "${ID:-}" in
|
||||
ubuntu|debian) info "受支持的分支: Debian/Ubuntu ✓" ;;
|
||||
centos|rhel|rocky|almalinux) info "受支持的分支: RHEL 系 ✓" ;;
|
||||
*) warn "未在官方列表中测试,可能仍可运行" ;;
|
||||
esac
|
||||
|
||||
local mem_kb
|
||||
mem_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||
if [[ "$mem_kb" -lt 3500000 ]]; then
|
||||
warn "内存 < 4GB,建议档位 1c4g: --profile 1c4g"
|
||||
else
|
||||
info "内存: $((mem_kb / 1024 / 1024)) GiB 量级"
|
||||
fi
|
||||
}
|
||||
|
||||
phase_1panel() {
|
||||
step "阶段 2/6 — 1Panel / Docker"
|
||||
if [[ "$SKIP_1PANEL_CHECK" != true ]]; then
|
||||
if command -v 1pctl >/dev/null 2>&1; then
|
||||
info "检测到 1Panel (1pctl)"
|
||||
if 1pctl version >/dev/null 2>&1; then
|
||||
info "1Panel: $(1pctl version 2>/dev/null | head -1 || true)"
|
||||
fi
|
||||
else
|
||||
warn "未检测到 1pctl — 若尚未安装 1Panel,可先执行:"
|
||||
warn " curl -sSL https://resource.1panel.pro/v2/quick_start.sh -o quick_start.sh"
|
||||
warn " bash quick_start.sh"
|
||||
read -r -p "继续安装 Nexus? [y/N]: " cont
|
||||
[[ "$cont" == "y" || "$cont" == "Y" ]] || exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
error "未检测到 docker compose"
|
||||
error "请在 1Panel → 容器 中安装 Docker,或: apt install -y docker.io docker-compose-v2"
|
||||
exit 1
|
||||
fi
|
||||
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version | head -1)"
|
||||
}
|
||||
|
||||
phase_packages() {
|
||||
step "阶段 3/6 — 依赖包"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl openssl ca-certificates iproute2 >/dev/null 2>&1 || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q git curl openssl ca-certificates iproute >/dev/null 2>&1 || true
|
||||
fi
|
||||
info "git / curl 就绪"
|
||||
}
|
||||
|
||||
git_clone_url() {
|
||||
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
else
|
||||
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
}
|
||||
|
||||
phase_clone() {
|
||||
step "阶段 4/6 — 获取 Nexus 代码"
|
||||
if [[ "$SKIP_CLONE" == true ]]; then
|
||||
if [[ ! -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
error "--skip-clone 但 ${NEXUS_ROOT} 不是 git 仓库"
|
||||
exit 1
|
||||
fi
|
||||
load_credentials
|
||||
info "跳过 clone,使用现有 ${NEXUS_ROOT}"
|
||||
info "版本: $(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || echo '?')"
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
load_credentials
|
||||
local url
|
||||
url="$(git_clone_url)"
|
||||
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
info "公共仓库:匿名 git clone(无需令牌)"
|
||||
fi
|
||||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||||
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
info "仓库已存在,同步 ${NEXUS_GIT_BRANCH}..."
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --prune
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${NEXUS_GIT_BRANCH}"
|
||||
else
|
||||
if [[ -d "$NEXUS_ROOT" ]] && [[ -n "$(ls -A "$NEXUS_ROOT" 2>/dev/null)" ]]; then
|
||||
error "目录非空且不是 git 仓库: $NEXUS_ROOT"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$NEXUS_ROOT"
|
||||
info "克隆 → $NEXUS_ROOT"
|
||||
git clone "$url" "$NEXUS_ROOT"
|
||||
git -C "$NEXUS_ROOT" checkout "$NEXUS_GIT_BRANCH" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
load_credentials
|
||||
info "版本: $(git -C "$NEXUS_ROOT" log -1 --oneline)"
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
}
|
||||
|
||||
phase_install_stack() {
|
||||
step "阶段 5/6 — Docker 构建与启动(全新库,约 10~20 分钟)"
|
||||
export NEXUS_ROOT NEXUS_DOMAIN NEXUS_PROFILE
|
||||
bash "${NEXUS_ROOT}/deploy/nexus-1panel.sh" install \
|
||||
--fresh \
|
||||
--skip-clone \
|
||||
--profile "$NEXUS_PROFILE" \
|
||||
--domain "$NEXUS_DOMAIN"
|
||||
}
|
||||
|
||||
phase_post() {
|
||||
step "阶段 6/6 — 收尾"
|
||||
install_root_commands
|
||||
|
||||
local port health
|
||||
port="8600"
|
||||
health="$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "pending")"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}Nexus 全新安装完成${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════════╣${NC}"
|
||||
echo -e "${GREEN}║${NC} 本地健康: http://127.0.0.1:${port}/health → ${health} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}① 浏览器完成安装向导(必做)${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} https://${NEXUS_DOMAIN}/app/install.html ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}② 1Panel 创建网站并反代${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} 目标: http://127.0.0.1:${port} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} 示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}③ 日常运维${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nx # 统一菜单(一键更新 / 重启 / 日志) ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nexus-update # 等同 deploy/update.sh ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
warn_if_already_installed() {
|
||||
local env_prod="${NEXUS_ROOT}/docker/.env.prod" cname
|
||||
[[ -f "$env_prod" ]] || return 0
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]] && docker exec "$cname" test -f /var/lib/nexus/.install_locked 2>/dev/null; then
|
||||
warn "检测到已安装并锁定的 Nexus(将清除 nexus-state 卷内配置并重建)"
|
||||
read -r -p "确认继续全新安装? [y/N]: " ok
|
||||
[[ "$ok" == "y" || "$ok" == "Y" ]] || exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
banner
|
||||
if [[ "$SKIP_CLONE" == true && -d "${NEXUS_ROOT}/deploy" ]]; then
|
||||
install_root_commands
|
||||
fi
|
||||
warn_if_already_installed
|
||||
phase_preflight
|
||||
phase_1panel
|
||||
phase_packages
|
||||
phase_clone
|
||||
phase_install_stack
|
||||
phase_post
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# 注册 Nexus 全局 CLI(/usr/local/bin)— install / upgrade / nx 均会调用
|
||||
#
|
||||
# NEXUS_ROOT=/opt/nexus bash deploy/install-nx-cli.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
BIN_DIR="${NEXUS_BIN_DIR:-/usr/local/bin}"
|
||||
DEPLOY="${NEXUS_ROOT}/deploy"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
if [[ ! -d "$DEPLOY" ]]; then
|
||||
error "未找到 $DEPLOY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
warn "非 root:无法写入 $BIN_DIR,请使用 root 执行或: sudo bash $DEPLOY/install-nx-cli.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
chmod +x \
|
||||
"$DEPLOY/nx" \
|
||||
"$DEPLOY/update.sh" \
|
||||
"$DEPLOY/nexus-1panel.sh" \
|
||||
"$DEPLOY/install-nexus-fresh.sh" \
|
||||
"$DEPLOY/quick-install.sh" \
|
||||
"$DEPLOY/install-1panel-docker.sh" \
|
||||
"$DEPLOY/upgrade-1panel-docker.sh" \
|
||||
"$DEPLOY/sync-install-wizard-to-container.sh" \
|
||||
"$DEPLOY/uninstall-mysql-compose.sh" \
|
||||
"$DEPLOY/uninstall-redis-compose.sh" \
|
||||
"$DEPLOY/install-nx-cli.sh" \
|
||||
2>/dev/null || true
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
link_cli() {
|
||||
local src="$1" name="$2"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
warn "跳过 $name(缺少 $src)"
|
||||
return 0
|
||||
fi
|
||||
ln -sf "$src" "${BIN_DIR}/${name}"
|
||||
}
|
||||
|
||||
link_cli "$DEPLOY/nx" nx
|
||||
link_cli "$DEPLOY/update.sh" nexus-update
|
||||
link_cli "$DEPLOY/install-nexus-fresh.sh" nexus-fresh
|
||||
link_cli "$DEPLOY/quick-install.sh" nexus-install
|
||||
link_cli "$DEPLOY/nexus-1panel.sh" nexus-1panel
|
||||
link_cli "$DEPLOY/install-1panel-docker.sh" nexus-1panel-docker
|
||||
|
||||
if command -v nx >/dev/null 2>&1; then
|
||||
info "全局命令已就绪: nx → $(readlink -f "$(command -v nx)" 2>/dev/null || command -v nx)"
|
||||
else
|
||||
error "nx 未出现在 PATH 中,请确认 $BIN_DIR 在 PATH 内"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,526 @@
|
||||
#!/bin/bash
|
||||
# ================================================================
|
||||
# Nexus 6.0 — One-Click Install Script
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://YOUR_DOMAIN/deploy/install.sh | sudo bash -s -- --domain nexus.example.com
|
||||
# sudo bash install.sh --domain nexus.example.com
|
||||
#
|
||||
# Supports: Ubuntu 20.04+, Debian 11+, CentOS 7/8, Rocky 8/9, AlmaLinux 8/9
|
||||
# Auto-detects BT Panel (宝塔面板) and adapts paths accordingly.
|
||||
# ================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors ──
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
# ── Helper: Idempotent config write ──
|
||||
write_config() {
|
||||
local target="$1" content="$2"
|
||||
if [ -f "$target" ]; then
|
||||
local existing
|
||||
existing=$(cat "$target" 2>/dev/null || true)
|
||||
if [ "$existing" = "$content" ]; then
|
||||
info " [skip] $target (unchanged)"
|
||||
return 0
|
||||
fi
|
||||
info " [update] $target (content changed)"
|
||||
else
|
||||
info " [create] $target"
|
||||
fi
|
||||
echo "$content" > "$target"
|
||||
}
|
||||
|
||||
# ── Defaults ──
|
||||
DOMAIN=""
|
||||
DEPLOY_DIR=""
|
||||
PORT=8600
|
||||
SKIP_NGINX=false
|
||||
SKIP_SUPERVISOR=false
|
||||
BT_MODE=false
|
||||
NEXUS_REPO=""
|
||||
|
||||
# ── Parse Arguments ──
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--domain) DOMAIN="$2"; shift 2 ;;
|
||||
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
|
||||
--port) PORT="$2"; shift 2 ;;
|
||||
--skip-nginx) SKIP_NGINX=true; shift ;;
|
||||
--skip-supervisor) SKIP_SUPERVISOR=true; shift ;;
|
||||
--repo) NEXUS_REPO="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: sudo bash install.sh --domain DOMAIN [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --domain DOMAIN Required. Domain or IP for Nginx server_name"
|
||||
echo " --deploy-dir PATH Installation directory (auto-detected)"
|
||||
echo " --port PORT uvicorn port (default: 8600)"
|
||||
echo " --skip-nginx Skip Nginx configuration"
|
||||
echo " --skip-supervisor Skip Supervisor configuration"
|
||||
echo " --repo URL Git repository URL (optional, for cloning)"
|
||||
exit 0 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Banner ──
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 6.0 Install Script"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# ================================================================
|
||||
# Phase 1: Pre-flight Checks
|
||||
# ================================================================
|
||||
|
||||
info "Phase 1/7: Pre-flight checks..."
|
||||
|
||||
# 1.1 Must run as root
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
error "This script must be run as root. Use: sudo bash install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1.2 Domain is required
|
||||
if [ -z "$DOMAIN" ]; then
|
||||
error "--domain is required. Usage: sudo bash install.sh --domain nexus.example.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1.3 OS detection
|
||||
if [ ! -f /etc/os-release ]; then
|
||||
error "Cannot detect OS. /etc/os-release not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OS_ID=$(grep '^ID=' /etc/os-release | head -1 | cut -d'=' -f2 | tr -d '"' | cut -d'-' -f1)
|
||||
OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | head -1 | cut -d'=' -f2 | tr -d '"')
|
||||
|
||||
case "$OS_ID" in
|
||||
ubuntu|debian) PKG_MANAGER="apt" ;;
|
||||
centos|rocky|almalinux|rhel) PKG_MANAGER="yum" ;;
|
||||
*)
|
||||
warn "Unsupported OS: $OS_ID. Script may still work but is untested."
|
||||
PKG_MANAGER="apt"
|
||||
;;
|
||||
esac
|
||||
info " OS: $OS_ID $OS_VERSION (pkg: $PKG_MANAGER)"
|
||||
|
||||
# 1.4 BT Panel detection
|
||||
if [ -d "/www/server/panel" ] && [ -f "/www/server/panel/class/common.py" ]; then
|
||||
BT_MODE=true
|
||||
info " BT Panel: detected"
|
||||
else
|
||||
info " BT Panel: not detected (standard mode)"
|
||||
fi
|
||||
|
||||
# 1.5 Set deploy directory
|
||||
if [ -z "$DEPLOY_DIR" ]; then
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
DEPLOY_DIR="/www/wwwroot/$DOMAIN"
|
||||
else
|
||||
DEPLOY_DIR="/opt/nexus"
|
||||
fi
|
||||
fi
|
||||
VENV_DIR="$DEPLOY_DIR/venv"
|
||||
|
||||
# Set log paths early (used in multiple phases)
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
LOG_DIR="/www/wwwlogs"
|
||||
else
|
||||
LOG_DIR="/var/log/nexus"
|
||||
fi
|
||||
|
||||
info " Deploy dir: $DEPLOY_DIR"
|
||||
info " Domain: $DOMAIN"
|
||||
info " Port: $PORT"
|
||||
|
||||
# ================================================================
|
||||
# Phase 2: System Dependencies
|
||||
# ================================================================
|
||||
|
||||
info "Phase 2/7: Installing system dependencies..."
|
||||
|
||||
if [ "$PKG_MANAGER" = "apt" ]; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq \
|
||||
python3 python3-venv python3-pip python3-dev \
|
||||
build-essential libssl-dev libffi-dev default-libmysqlclient-dev \
|
||||
git curl >/dev/null 2>&1 || warn "Some apt packages failed to install (may already exist)"
|
||||
elif [ "$PKG_MANAGER" = "yum" ]; then
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q \
|
||||
python3 python3-devel python3-pip \
|
||||
gcc openssl-devel libffi-devel mysql-devel \
|
||||
git curl >/dev/null 2>&1 || warn "Some dnf packages failed to install"
|
||||
else
|
||||
yum install -y -q \
|
||||
python3 python3-devel python3-pip \
|
||||
gcc openssl-devel libffi-devel mysql-devel \
|
||||
git curl >/dev/null 2>&1 || warn "Some yum packages failed to install"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# Phase 3: Python 3.10+ Resolution
|
||||
# ================================================================
|
||||
|
||||
info "Phase 3/7: Resolving Python 3.10+..."
|
||||
|
||||
PYTHON=""
|
||||
CANDIDATES="python3.12 python3.11 python3.10 /usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10 python3"
|
||||
|
||||
# Add BT Panel Python paths
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
for p in /www/server/python-manager/versions/*/bin/python3.*; do
|
||||
[ -x "$p" ] && CANDIDATES="$CANDIDATES $p"
|
||||
done
|
||||
fi
|
||||
|
||||
for candidate in $CANDIDATES; do
|
||||
if command -v "$candidate" >/dev/null 2>&1 || [ -x "$candidate" ]; then
|
||||
ver=$("$candidate" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)
|
||||
if [ -n "$ver" ]; then
|
||||
major=$(echo "$ver" | cut -d'.' -f1)
|
||||
minor=$(echo "$ver" | cut -d'.' -f2)
|
||||
if [ "$major" -eq 3 ] && [ "${minor:-0}" -ge 10 ] 2>/dev/null; then
|
||||
PYTHON="$candidate"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Fallback: install Python 3.12 on Ubuntu via deadsnakes PPA
|
||||
if [ -z "$PYTHON" ] && [ "$OS_ID" = "ubuntu" ]; then
|
||||
warn " Python 3.10+ not found. Adding deadsnakes PPA..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get install -y -qq software-properties-common >/dev/null 2>&1
|
||||
add-apt-repository -y ppa:deadsnakes/ppa >/dev/null 2>&1
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq python3.12 python3.12-venv python3.12-dev >/dev/null 2>&1
|
||||
if command -v python3.12 >/dev/null 2>&1; then
|
||||
PYTHON="python3.12"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PYTHON" ]; then
|
||||
error "Python 3.10+ is required but not found."
|
||||
error "Install Python 3.10+ manually and re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYTHON_VERSION=$("$PYTHON" --version 2>&1)
|
||||
info " Using: $PYTHON ($PYTHON_VERSION)"
|
||||
|
||||
# ================================================================
|
||||
# Phase 4: Application Deployment
|
||||
# ================================================================
|
||||
|
||||
info "Phase 4/7: Deploying application..."
|
||||
|
||||
# 4.1 Create deploy directory
|
||||
mkdir -p "$DEPLOY_DIR"
|
||||
|
||||
# 4.2 Download / Clone Nexus
|
||||
if [ -d "$DEPLOY_DIR/server" ] && [ -d "$DEPLOY_DIR/web" ]; then
|
||||
info " Application files already exist in $DEPLOY_DIR (skipping download)"
|
||||
if [ -d "$DEPLOY_DIR/.git" ]; then
|
||||
info " Git repo detected, pulling latest..."
|
||||
(cd "$DEPLOY_DIR" && git pull --ff-only 2>/dev/null || warn " git pull failed (may have local changes)")
|
||||
fi
|
||||
elif [ -n "$NEXUS_REPO" ]; then
|
||||
info " Cloning from $NEXUS_REPO..."
|
||||
git clone "$NEXUS_REPO" "$DEPLOY_DIR" 2>/dev/null || {
|
||||
error "git clone failed. Check the repository URL."
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
error "No application files found in $DEPLOY_DIR and no --repo URL provided."
|
||||
error "Either:"
|
||||
error " 1. Copy Nexus files to $DEPLOY_DIR manually and re-run"
|
||||
error " 2. Use --repo https://your-gitea-url/Nexus.git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4.3 Create Python venv
|
||||
if [ -d "$VENV_DIR/bin" ] && "$VENV_DIR/bin/python" -c 'import sys; v=sys.version_info; assert v.major==3 and v.minor>=10' 2>/dev/null; then
|
||||
info " venv already exists with Python 3.10+ (skipping)"
|
||||
else
|
||||
info " Creating Python venv..."
|
||||
rm -rf "$VENV_DIR"
|
||||
"$PYTHON" -m venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# 4.4 Install pip dependencies
|
||||
info " Installing Python dependencies (this may take a few minutes)..."
|
||||
"$VENV_DIR/bin/pip" install -q --upgrade pip
|
||||
"$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || {
|
||||
warn " pip install had some warnings, but continuing..."
|
||||
}
|
||||
|
||||
# 4.5 Create log directory
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# 4.6 Create backup directory
|
||||
mkdir -p "$DEPLOY_DIR/backups"
|
||||
|
||||
# ================================================================
|
||||
# Phase 5: Supervisor Configuration
|
||||
# ================================================================
|
||||
|
||||
if [ "$SKIP_SUPERVISOR" = true ]; then
|
||||
warn "Phase 5/7: Skipping Supervisor (per --skip-supervisor)"
|
||||
else
|
||||
info "Phase 5/7: Configuring Supervisor..."
|
||||
|
||||
# Calculate worker count
|
||||
WORKERS=$(nproc 2>/dev/null || echo 2)
|
||||
[ "$WORKERS" -gt 4 ] && WORKERS=4
|
||||
[ "$WORKERS" -lt 1 ] && WORKERS=1
|
||||
|
||||
# Set Supervisor config path
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
SUPERVISOR_CONF_DIR="/www/server/panel/plugin/supervisor/profile"
|
||||
CONF_FILE="$SUPERVISOR_CONF_DIR/nexus.ini"
|
||||
else
|
||||
SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d"
|
||||
CONF_FILE="$SUPERVISOR_CONF_DIR/nexus.conf"
|
||||
fi
|
||||
|
||||
# Generate Supervisor config
|
||||
SUPERVISOR_CONF="[program:nexus]
|
||||
command=$VENV_DIR/bin/uvicorn server.main:app --host 0.0.0.0 --port $PORT --workers $WORKERS --loop uvloop --http httptools --log-level info
|
||||
directory=$DEPLOY_DIR
|
||||
user=root
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=10
|
||||
startsecs=3
|
||||
stopwaitsecs=10
|
||||
stopsignal=INT
|
||||
environment=PATH=\"$VENV_DIR/bin:%(ENV_PATH)s\",HOME=\"/root\",NEXUS_WORKERS=\"$WORKERS\"
|
||||
stderr_logfile=$LOG_DIR/nexus_error.log
|
||||
stdout_logfile=$LOG_DIR/nexus_access.log
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stderr_logfile_backups=5
|
||||
stdout_logfile_backups=5"
|
||||
|
||||
# Write config (idempotent)
|
||||
mkdir -p "$SUPERVISOR_CONF_DIR"
|
||||
write_config "$CONF_FILE" "$SUPERVISOR_CONF"
|
||||
|
||||
# Ensure Supervisor is running
|
||||
if ! supervisorctl status >/dev/null 2>&1; then
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
if [ -f /etc/init.d/supervisor ]; then
|
||||
/etc/init.d/supervisor start >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
systemctl start supervisor >/dev/null 2>&1 || true
|
||||
fi
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Reload Supervisor config
|
||||
supervisorctl reread >/dev/null 2>&1 || true
|
||||
supervisorctl update >/dev/null 2>&1 || true
|
||||
info " Supervisor config written to $CONF_FILE (workers=$WORKERS)"
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# Phase 6: Nginx Configuration
|
||||
# ================================================================
|
||||
|
||||
if [ "$SKIP_NGINX" = true ]; then
|
||||
warn "Phase 6/7: Skipping Nginx (per --skip-nginx)"
|
||||
else
|
||||
info "Phase 6/7: Configuring Nginx..."
|
||||
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
NGINX_CONF_FILE="/www/server/panel/vhost/nginx/$DOMAIN.conf"
|
||||
NGINX_RELOAD_CMD="/www/server/nginx/sbin/nginx -s reload"
|
||||
NGINX_TEST_CMD="/www/server/nginx/sbin/nginx -t"
|
||||
NGINX_ACCESS_LOG="/www/wwwlogs/$DOMAIN.log"
|
||||
NGINX_ERROR_LOG="/www/wwwlogs/$DOMAIN.error.log"
|
||||
else
|
||||
NGINX_CONF_FILE="/etc/nginx/sites-available/nexus"
|
||||
NGINX_RELOAD_CMD="systemctl reload nginx"
|
||||
NGINX_TEST_CMD="nginx -t"
|
||||
NGINX_ACCESS_LOG="/var/log/nginx/nexus_access.log"
|
||||
NGINX_ERROR_LOG="/var/log/nginx/nexus_error.log"
|
||||
fi
|
||||
|
||||
NGINX_CONF="# Nexus 6.0 — Nginx Reverse Proxy
|
||||
# Auto-generated by install.sh
|
||||
upstream nexus_api {
|
||||
server 127.0.0.1:$PORT;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name $DOMAIN;
|
||||
|
||||
# Reverse proxy to Nexus (uvicorn)
|
||||
location / {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
|
||||
# WebSocket support (terminal, alerts)
|
||||
location /ws/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection \"upgrade\";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
# Static assets caching
|
||||
location /app/vendor/ {
|
||||
proxy_pass http://nexus_api;
|
||||
expires 7d;
|
||||
add_header Cache-Control \"public, immutable\";
|
||||
}
|
||||
|
||||
# Block sensitive files
|
||||
location ~ ^/(\\.env|\\.git|\\.svn) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Block config.json access
|
||||
location ~ /data/config\\.json$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Upload limit
|
||||
client_max_body_size 500m;
|
||||
|
||||
# Well-known for SSL cert validation
|
||||
location ~ \\.well-known {
|
||||
allow all;
|
||||
}
|
||||
|
||||
access_log $NGINX_ACCESS_LOG;
|
||||
error_log $NGINX_ERROR_LOG;
|
||||
}"
|
||||
|
||||
# Write Nginx config
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
write_config "$NGINX_CONF_FILE" "$NGINX_CONF"
|
||||
else
|
||||
mkdir -p /etc/nginx/sites-available
|
||||
write_config "$NGINX_CONF_FILE" "$NGINX_CONF"
|
||||
if [ ! -L /etc/nginx/sites-enabled/nexus ]; then
|
||||
ln -sf "$NGINX_CONF_FILE" /etc/nginx/sites-enabled/nexus
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test and reload Nginx
|
||||
if $NGINX_TEST_CMD >/dev/null 2>&1; then
|
||||
$NGINX_RELOAD_CMD >/dev/null 2>&1 || warn " Nginx reload failed"
|
||||
info " Nginx config written to $NGINX_CONF_FILE"
|
||||
else
|
||||
warn " Nginx config test failed. Please check $NGINX_CONF_FILE manually"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# Phase 7: Start Application + Verify
|
||||
# ================================================================
|
||||
|
||||
info "Phase 7/7: Starting application..."
|
||||
|
||||
# Start via Supervisor
|
||||
if [ "$SKIP_SUPERVISOR" = false ]; then
|
||||
supervisorctl start nexus >/dev/null 2>&1 || supervisorctl restart nexus >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Wait for health check
|
||||
HEALTH_OK=false
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
||||
HEALTH_OK=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# ── Print Results ──
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
|
||||
if [ "$HEALTH_OK" = true ]; then
|
||||
echo -e " ${GREEN}Nexus installed successfully!${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}Nexus process started but health check pending.${NC}"
|
||||
echo -e " ${YELLOW}It may need a few more seconds to start up.${NC}"
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " Deploy: $DEPLOY_DIR"
|
||||
echo " Domain: $DOMAIN"
|
||||
echo " Port: $PORT"
|
||||
echo " Workers: ${WORKERS:-N/A}"
|
||||
echo " BT Panel: $BT_MODE"
|
||||
echo ""
|
||||
|
||||
if [ "$HEALTH_OK" = true ]; then
|
||||
echo -e " ${GREEN}Next step:${NC} Open your browser and complete the web install wizard:"
|
||||
echo ""
|
||||
echo " http://$DOMAIN/app/install.html"
|
||||
echo ""
|
||||
echo " The wizard will guide you through:"
|
||||
echo " Step 1: Welcome & prerequisites"
|
||||
echo " Step 2: Environment check"
|
||||
echo " Step 3: MySQL + Redis configuration"
|
||||
echo " Step 4: Admin account + brand"
|
||||
echo " Step 5: Complete"
|
||||
else
|
||||
echo " Troubleshooting:"
|
||||
echo " supervisorctl status nexus"
|
||||
echo " tail -f $LOG_DIR/nexus_error.log"
|
||||
echo " curl http://127.0.0.1:$PORT/health"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Post-install:"
|
||||
echo " 1. Configure SSL (HTTPS)"
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
echo " BT Panel -> Website -> $DOMAIN -> SSL"
|
||||
else
|
||||
echo " certbot --nginx -d $DOMAIN"
|
||||
fi
|
||||
echo " 2. Set up health monitor crontab:"
|
||||
echo " * * * * * $DEPLOY_DIR/deploy/health_monitor.sh"
|
||||
echo " 3. Set up database backup crontab:"
|
||||
echo " 0 3 * * * $DEPLOY_DIR/deploy/db_backup.sh"
|
||||
echo ""
|
||||
echo " Management:"
|
||||
echo " supervisorctl status nexus"
|
||||
echo " supervisorctl restart nexus"
|
||||
echo " supervisorctl stop nexus"
|
||||
echo ""
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — 安装/更新 host crontab:health_monitor + db_backup
|
||||
#
|
||||
# sudo bash deploy/install_ops_cron.sh
|
||||
# NEXUS_ROOT=/opt/nexus sudo bash deploy/install_ops_cron.sh
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
DEPLOY_DIR="${NEXUS_ROOT}/deploy"
|
||||
HEALTH_MARKER="nexus-health-monitor"
|
||||
BACKUP_MARKER="nexus-mysql-backup"
|
||||
HEALTH_LOG="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
BACKUP_LOG="${NEXUS_BACKUP_LOG:-/var/log/nexus_backup.log}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 运行: sudo bash $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_crontab() {
|
||||
if command -v crontab >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
warn "未检测到 crontab,尝试安装 cron 包…"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq cron
|
||||
systemctl enable --now cron 2>/dev/null || systemctl enable --now crond 2>/dev/null || true
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y cronie
|
||||
systemctl enable --now crond 2>/dev/null || true
|
||||
fi
|
||||
if ! command -v crontab >/dev/null 2>&1; then
|
||||
error "无法安装 crontab(请手动: apt install cron)"
|
||||
exit 1
|
||||
fi
|
||||
info "cron 已安装"
|
||||
}
|
||||
|
||||
ensure_crontab
|
||||
|
||||
for f in \
|
||||
"${DEPLOY_DIR}/health_monitor.sh" \
|
||||
"${DEPLOY_DIR}/db_backup.sh" \
|
||||
"${DEPLOY_DIR}/mysql_dump_to_file.sh" \
|
||||
; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
error "缺少脚本: $f"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$f" 2>/dev/null || true
|
||||
done
|
||||
|
||||
touch "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
|
||||
chmod 644 "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
|
||||
|
||||
HEALTH_LINE="* * * * * NEXUS_DEPLOY_DIR=${NEXUS_ROOT} bash ${DEPLOY_DIR}/health_monitor.sh >> ${HEALTH_LOG} 2>&1 # ${HEALTH_MARKER}"
|
||||
BACKUP_LINE="0 3 * * * NEXUS_ROOT=${NEXUS_ROOT} bash ${DEPLOY_DIR}/db_backup.sh >> ${BACKUP_LOG} 2>&1 # ${BACKUP_MARKER}"
|
||||
|
||||
existing="$(crontab -l 2>/dev/null || true)"
|
||||
filtered="$(printf '%s\n' "$existing" | grep -v "$HEALTH_MARKER" | grep -v "$BACKUP_MARKER" | grep -v 'deploy/health_monitor.sh' | grep -v 'deploy/db_backup.sh' || true)"
|
||||
{
|
||||
printf '%s\n' "$filtered" | sed '/^[[:space:]]*$/d'
|
||||
echo "$HEALTH_LINE"
|
||||
echo "$BACKUP_LINE"
|
||||
} | crontab -
|
||||
|
||||
info "已写入 crontab:"
|
||||
crontab -l | grep -E "$HEALTH_MARKER|$BACKUP_MARKER" || true
|
||||
info "健康巡检日志: $HEALTH_LOG"
|
||||
info "MySQL 备份日志: $BACKUP_LOG"
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Nexus 6.0 — Database Index Migration
|
||||
-- Run this manually if create_all() didn't create the indexes.
|
||||
-- Safe to run even if indexes already exist (will skip with "Duplicate key name" error).
|
||||
|
||||
-- Server list queries filter by is_online frequently (Dashboard, API list)
|
||||
CREATE INDEX IF NOT EXISTS idx_servers_is_online ON servers (is_online);
|
||||
|
||||
-- Server list queries filter by category (sidebar tabs)
|
||||
CREATE INDEX IF NOT EXISTS idx_servers_category ON servers (category);
|
||||
|
||||
-- Sync log queries filter by server_id + date range (logs page, server detail)
|
||||
CREATE INDEX IF NOT EXISTS idx_sync_logs_srv_start ON sync_logs (server_id, started_at);
|
||||
|
||||
-- Login attempt queries filter by username + time (brute-force detection)
|
||||
CREATE INDEX IF NOT EXISTS idx_login_attempts_username_time ON login_attempts (username, attempted_at);
|
||||
|
||||
-- Audit log queries filter by action + time (audit page)
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_action_time ON audit_logs (action, created_at);
|
||||
|
||||
-- Push schedule queries filter by enabled (cron scheduler)
|
||||
CREATE INDEX IF NOT EXISTS idx_push_schedules_enabled ON push_schedules (enabled);
|
||||
|
||||
-- Push retry queries filter by status + next_retry_at (retry engine)
|
||||
CREATE INDEX IF NOT EXISTS idx_push_retry_pending ON push_retry_jobs (status, next_retry_at);
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — MySQL dump(Docker 1Panel / 宿主机 mysqldump 通用)
|
||||
#
|
||||
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql [--root /opt/nexus]
|
||||
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql --db-url 'mysql+aiomysql://...'
|
||||
#
|
||||
# 策略(按序):
|
||||
# 1. DB host 为运行中容器名 → docker exec 该容器内 mysqldump(连 127.0.0.1)
|
||||
# 2. 宿主机有 mysqldump 且 host 可解析 → 本机 mysqldump
|
||||
# 3. 1panel-network 存在 → docker run --rm mysql:8 客户端
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
OUTPUT=""
|
||||
DB_URL=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: mysql_dump_to_file.sh --output FILE.sql [--root /opt/nexus] [--db-url URL]
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output|-o) OUTPUT="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--db-url) DB_URL="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "未知参数: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
echo "缺少 --output" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolve_db_url() {
|
||||
if [[ -n "$DB_URL" ]]; then
|
||||
echo "$DB_URL"
|
||||
return 0
|
||||
fi
|
||||
local cname url
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]]; then
|
||||
url="$(docker exec "$cname" grep -E '^NEXUS_DATABASE_URL=' /app/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||
[[ -n "$url" ]] && echo "$url" && return 0
|
||||
fi
|
||||
for f in \
|
||||
"${NEXUS_ROOT}/docker/.env.prod" \
|
||||
"${NEXUS_ROOT}/.env" \
|
||||
; do
|
||||
[[ -f "$f" ]] || continue
|
||||
url="$(grep -E '^NEXUS_DATABASE_URL=' "$f" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||
[[ -n "$url" ]] && echo "$url" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
DB_URL="$(resolve_db_url || true)"
|
||||
if [[ -z "$DB_URL" ]]; then
|
||||
echo "未找到 NEXUS_DATABASE_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$DB_URL" "$OUTPUT" <<'PY'
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def docker_names() -> set[str]:
|
||||
r = subprocess.run(
|
||||
["docker", "ps", "--format", "{{.Names}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return {ln.strip() for ln in (r.stdout or "").splitlines() if ln.strip()}
|
||||
|
||||
|
||||
def network_exists(name: str) -> bool:
|
||||
return (
|
||||
subprocess.run(
|
||||
["docker", "network", "inspect", name],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
def write_dump(cmd: list[str], env: dict[str, str], out: str) -> int:
|
||||
r = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
sys.stderr.write(r.stderr or r.stdout or "mysqldump failed\n")
|
||||
return r.returncode
|
||||
with open(out, "w", encoding="utf-8") as fh:
|
||||
fh.write(r.stdout)
|
||||
return 0
|
||||
|
||||
|
||||
def dump_flags(db: str) -> list[str]:
|
||||
return [
|
||||
"--single-transaction",
|
||||
"--routines",
|
||||
"--triggers",
|
||||
"--set-gtid-purged=OFF",
|
||||
db,
|
||||
]
|
||||
|
||||
|
||||
url = sys.argv[1].replace("mysql+aiomysql://", "mysql://", 1)
|
||||
url = url.replace("mysql+asyncmy://", "mysql://", 1)
|
||||
out = sys.argv[2]
|
||||
p = urllib.parse.urlparse(url)
|
||||
user = urllib.parse.unquote(p.username or "")
|
||||
password = urllib.parse.unquote(p.password or "")
|
||||
host = p.hostname or "127.0.0.1"
|
||||
if host == "host.docker.internal":
|
||||
host = "127.0.0.1"
|
||||
port = str(p.port or 3306)
|
||||
db = (p.path or "/nexus").lstrip("/").split("?")[0]
|
||||
env = {**__import__("os").environ, "MYSQL_PWD": password}
|
||||
names = docker_names()
|
||||
|
||||
# 1) MySQL 跑在 Docker 容器内(1Panel 常见:URL host = 容器名)
|
||||
if host in names:
|
||||
cmd = [
|
||||
"docker",
|
||||
"exec",
|
||||
"-e",
|
||||
f"MYSQL_PWD={password}",
|
||||
host,
|
||||
"mysqldump",
|
||||
"-h",
|
||||
"127.0.0.1",
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
sys.stderr.write(f"docker exec {host} mysqldump 失败,尝试其它方式…\n")
|
||||
|
||||
# 2) 宿主机 mysqldump
|
||||
if shutil.which("mysqldump"):
|
||||
cmd = [
|
||||
"mysqldump",
|
||||
"-h",
|
||||
host,
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
|
||||
# 3) 临时 mysql 客户端容器(1panel-network)
|
||||
for net in ("1panel-network", "bridge"):
|
||||
if not network_exists(net):
|
||||
continue
|
||||
cmd = [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--network",
|
||||
net,
|
||||
"-e",
|
||||
f"MYSQL_PWD={password}",
|
||||
"mysql:8.0",
|
||||
"mysqldump",
|
||||
"-h",
|
||||
host,
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
|
||||
sys.stderr.write("所有 mysqldump 策略均失败(无宿主机客户端且 Docker 回退不可用)\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
# Nexus — Supervisor Process Guardian
|
||||
# - --workers N for multi-core concurrency
|
||||
# - --loop uvloop for 40-50% QPS improvement
|
||||
# - --http httptools for fast HTTP parsing
|
||||
#
|
||||
# Install: sudo cp nexus.conf /etc/supervisor/conf.d/nexus.conf
|
||||
# Start: sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start nexus
|
||||
# Set workers: export NEXUS_WORKERS=$(nproc)
|
||||
#
|
||||
# *** Before deploying, replace ALL {{PLACEHOLDER}} values with your actual paths ***
|
||||
# {{DEPLOY_DIR}} = installation directory (e.g. /opt/nexus or /www/wwwroot/your.domain)
|
||||
# {{VENV_DIR}} = Python venv directory (e.g. /opt/nexus/venv)
|
||||
|
||||
[program:nexus]
|
||||
command={{DEPLOY_DIR}}/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port 8600 --workers %(ENV_NEXUS_WORKERS)s --loop uvloop --http httptools --log-level info
|
||||
directory={{DEPLOY_DIR}}
|
||||
user=root
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=10
|
||||
startsecs=5
|
||||
stopwaitsecs=15
|
||||
stopsignal=INT
|
||||
environment=
|
||||
NEXUS_WORKERS="%(ENV_NEXUS_WORKERS)s",
|
||||
PATH="{{VENV_DIR}}/bin:%(ENV_PATH)s"
|
||||
stderr_logfile=/var/log/nexus/error.log
|
||||
stdout_logfile=/var/log/nexus/access.log
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stderr_logfile_backups=5
|
||||
stdout_logfile_backups=5
|
||||
@@ -0,0 +1,79 @@
|
||||
# Nexus v6.0 — Nginx Reverse Proxy Config (Template)
|
||||
# *** Copy this file and replace ALL {{PLACEHOLDER}} values before deploying ***
|
||||
#
|
||||
# {{SERVER_NAME}} = your domain or IP (e.g. nexus.example.com or 192.168.1.100)
|
||||
# {{DEPLOY_DIR}} = installation root (e.g. /opt/nexus or /www/wwwroot/nexus.example.com)
|
||||
# {{LOG_DIR}} = nginx log directory (e.g. /var/log/nginx or /www/wwwlogs)
|
||||
|
||||
# ── Upstream: Python FastAPI backend ──
|
||||
upstream nexus_api {
|
||||
server 127.0.0.1:8600;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{SERVER_NAME}};
|
||||
|
||||
# Document root = frontend static files
|
||||
root {{DEPLOY_DIR}}/web/app;
|
||||
index index.html;
|
||||
|
||||
# ── Python API proxy ──
|
||||
location /api/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_connect_timeout 10s;
|
||||
}
|
||||
|
||||
# ── WebSocket proxy (alerts + WebSSH terminal) ──
|
||||
location /ws/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
# ── Health endpoint ──
|
||||
location /health {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# ── Static assets ──
|
||||
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# ── Deny sensitive files ──
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
location ~ /data/config\.json$ {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# ── Static HTML pages (no SPA — each page is standalone) ──
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# ── Logging ──
|
||||
access_log {{LOG_DIR}}/{{SERVER_NAME}}_nexus.log;
|
||||
error_log {{LOG_DIR}}/{{SERVER_NAME}}_nexus.error.log;
|
||||
|
||||
# ── Upload limit ──
|
||||
client_max_body_size 500m;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
# Nexus v6.0 — Nginx Production Config (HTTPS Template)
|
||||
# *** Copy this file and replace ALL {{PLACEHOLDER}} values before deploying ***
|
||||
#
|
||||
# {{SERVER_NAME}} = your domain (e.g. nexus.example.com)
|
||||
# {{DEPLOY_DIR}} = installation root (e.g. /opt/nexus or /www/wwwroot/nexus.example.com)
|
||||
# {{LOG_DIR}} = nginx log directory (e.g. /var/log/nginx or /www/wwwlogs)
|
||||
# {{SSL_CERT}} = SSL certificate fullchain path
|
||||
# {{SSL_KEY}} = SSL private key path
|
||||
|
||||
# ── Upstream: Python FastAPI backend ──
|
||||
upstream nexus_api {
|
||||
server 127.0.0.1:8600;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# ── HTTP → HTTPS redirect ──
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{SERVER_NAME}};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# ── HTTPS ──
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name {{SERVER_NAME}};
|
||||
|
||||
# SSL
|
||||
ssl_certificate {{SSL_CERT}};
|
||||
ssl_certificate_key {{SSL_KEY}};
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# HSTS
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# Document root = frontend static files
|
||||
root {{DEPLOY_DIR}}/web/app;
|
||||
index index.html;
|
||||
|
||||
# ── Python API proxy ──
|
||||
location /api/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_connect_timeout 10s;
|
||||
}
|
||||
|
||||
# WebSocket access log: omit query string so JWT is not written to disk
|
||||
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
|
||||
|
||||
# ── WebSocket proxy (alerts + WebSSH terminal) ──
|
||||
location /ws/ {
|
||||
access_log {{LOG_DIR}}/{{SERVER_NAME}}.ws.log nexus_ws;
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
# ── Health endpoint ──
|
||||
location /health {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# ── Static assets (cache aggressively) ──
|
||||
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
# Must re-declare HSTS — nginx location-level add_header replaces ALL parent add_header directives
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# ── Deny sensitive files ──
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
location ~ /data/config\.json$ {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# ── Redirect bare page paths to /app/ (for reverse-proxy deployments) ──
|
||||
# If you use proxy_pass instead of root+try_files, uncomment this block:
|
||||
# location ~ ^/(login|index|servers|files|push|scripts|script-executions|credentials|schedules|retries|commands|alerts|audit|settings|install|terminal|assets)\.html$ {
|
||||
# return 301 /app/$1.html;
|
||||
# }
|
||||
|
||||
# ── Static HTML pages (no SPA — each page is standalone) ──
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# ── Logging ──
|
||||
access_log {{LOG_DIR}}/{{SERVER_NAME}}.log;
|
||||
error_log {{LOG_DIR}}/{{SERVER_NAME}}.error.log;
|
||||
|
||||
# ── Upload limit ──
|
||||
client_max_body_size 500m;
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus NX — 统一运维菜单(安装 + 升级 + 日常运维)
|
||||
#
|
||||
# nx 交互菜单
|
||||
# nx update 一键更新(等同 deploy/update.sh)
|
||||
# nx update --no-cache 无缓存重建镜像
|
||||
# nx install-fresh 全新安装
|
||||
# nx health 健康检查
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
NX_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
NX_DIR="$(cd "$(dirname "$NX_SELF")" && pwd)"
|
||||
# shellcheck source=./nexus-1panel.sh
|
||||
source "${NX_DIR}/nexus-1panel.sh"
|
||||
|
||||
pause_enter() {
|
||||
echo ""
|
||||
read -r -p "按 Enter 继续..."
|
||||
}
|
||||
|
||||
clear_screen() {
|
||||
if [[ -t 1 ]]; then
|
||||
clear 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
is_stack_installed() {
|
||||
[[ -d "${NEXUS_ROOT}/.git" && -f "${NEXUS_ROOT}/${ENV_PROD}" ]]
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
if is_stack_installed && docker compose version >/dev/null 2>&1; then
|
||||
local prof q
|
||||
prof="$(profile_env_file "$NEXUS_ROOT")"
|
||||
if [[ -f "$prof" && -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||||
local -a det_args=(-f "$COMPOSE_FILE")
|
||||
if has_1panel_network && [[ -f "$NEXUS_ROOT/$COMPOSE_1PANEL" ]]; then
|
||||
det_args+=(-f "$COMPOSE_1PANEL")
|
||||
fi
|
||||
q="$(cd "$NEXUS_ROOT" && docker compose "${det_args[@]}" \
|
||||
--env-file "$ENV_PROD" --env-file "$prof" ps -q nexus 2>/dev/null || true)"
|
||||
if [[ -n "$q" ]]; then
|
||||
NX_RUNTIME="docker"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
NX_RUNTIME="supervisor"
|
||||
return 0
|
||||
fi
|
||||
NX_RUNTIME="none"
|
||||
}
|
||||
|
||||
install_nx_cli() {
|
||||
NEXUS_ROOT="$(cd "${NX_DIR}/.." && pwd)" \
|
||||
bash "${NX_DIR}/install-nx-cli.sh" 2>/dev/null || true
|
||||
}
|
||||
|
||||
menu_header() {
|
||||
local title="$1"
|
||||
clear_screen
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
printf "║ Nexus NX — %-44s║\n" "$title"
|
||||
echo "╠══════════════════════════════════════════════════════════╣"
|
||||
echo "║ 域名: ${NEXUS_DOMAIN} 目录: ${NEXUS_ROOT}"
|
||||
if is_stack_installed; then
|
||||
detect_runtime
|
||||
echo "║ 运行: ${NX_RUNTIME} 档位: $(cat "${NEXUS_ROOT}/${HOST_PROFILE_FILE}" 2>/dev/null || echo "${NEXUS_PROFILE}")"
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
echo "║ 版本: $(git -C "${NEXUS_ROOT}" log -1 --oneline 2>/dev/null || echo '?')"
|
||||
fi
|
||||
else
|
||||
echo "║ 状态: 未安装"
|
||||
fi
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
}
|
||||
|
||||
run_install() {
|
||||
local profile="${1:-2c8g}" extra="${2:-}"
|
||||
NEXUS_PROFILE="$profile"
|
||||
SKIP_CLONE=false
|
||||
FRESH_INSTALL=false
|
||||
NO_BACKUP=false
|
||||
PRUNE_IMAGES=false
|
||||
FROM_ENV="${NEXUS_FROM_ENV}"
|
||||
case "$extra" in
|
||||
skip-clone) SKIP_CLONE=true ;;
|
||||
fresh) FRESH_INSTALL=true ;;
|
||||
from-env) FROM_ENV="${NEXUS_FROM_ENV}" ;;
|
||||
esac
|
||||
load_secrets
|
||||
cmd_install
|
||||
install_nx_cli
|
||||
pause_enter
|
||||
}
|
||||
|
||||
menu_install() {
|
||||
while true; do
|
||||
menu_header "安装"
|
||||
cat <<'EOF'
|
||||
[1] 一键安装(默认 2核8G / 2c8g)
|
||||
[2] 安装 — 4核16G(4c16g)
|
||||
[3] 安装 — 1核4G(1c4g)
|
||||
[4] 自定义规格(输入 CPU 与内存 GB)
|
||||
[5] 已 clone 到 /opt/nexus,仅 pull + 启动(--skip-clone)
|
||||
[6] 全新安装(install-nexus-fresh.sh → /app/install.html)
|
||||
[7] 检查端口 / 防火墙(不安装)
|
||||
[0] 返回
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) run_install 2c8g "" ;;
|
||||
2) run_install 4c16g "" ;;
|
||||
3) run_install 1c4g "" ;;
|
||||
4)
|
||||
read -r -p "CPU 核数: " cpus
|
||||
read -r -p "内存 GB: " mem
|
||||
CUSTOM_CPUS="$cpus"
|
||||
CUSTOM_MEM_GB="$mem"
|
||||
run_install 2c8g ""
|
||||
;;
|
||||
5) run_install 2c8g skip-clone ;;
|
||||
6)
|
||||
if [[ -x "${NX_DIR}/install-nexus-fresh.sh" ]]; then
|
||||
bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
|
||||
else
|
||||
run_install 2c8g fresh
|
||||
fi
|
||||
;;
|
||||
7)
|
||||
require_root
|
||||
load_secrets
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
if is_stack_installed; then
|
||||
check_ports_post "$NEXUS_ROOT"
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
0) return 0 ;;
|
||||
*) warn "无效选项" ; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
ops_restart_nexus() {
|
||||
step "重启 Nexus(Python)..."
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" restart nexus
|
||||
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
|
||||
supervisorctl restart nexus
|
||||
else
|
||||
error "未检测到运行中的 Nexus"
|
||||
return 1
|
||||
fi
|
||||
sleep 3
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
ops_restart_stack() {
|
||||
step "重启 Compose 栈..."
|
||||
compose_cmd "$NEXUS_ROOT" restart
|
||||
sleep 5
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
ops_stop_stack() {
|
||||
warn "将停止 Nexus 容器(MySQL/Redis 为外置服务,不受影响)"
|
||||
read -r -p "确认? [y/N]: " c
|
||||
[[ "$c" == "y" || "$c" == "Y" ]] || return 0
|
||||
compose_cmd "$NEXUS_ROOT" stop
|
||||
info "已停止"
|
||||
}
|
||||
|
||||
ops_start_stack() {
|
||||
compose_cmd "$NEXUS_ROOT" up -d --remove-orphans
|
||||
verify_nexus_1panel_network "$NEXUS_ROOT" || return 1
|
||||
verify_health "$NEXUS_ROOT" "启动" || true
|
||||
}
|
||||
|
||||
ops_logs() {
|
||||
local svc="${1:-nexus}"
|
||||
echo ""
|
||||
read -r -p "跟踪日志行数 [80]: " n
|
||||
n="${n:-80}"
|
||||
compose_cmd "$NEXUS_ROOT" logs --tail="$n" -f "$svc"
|
||||
}
|
||||
|
||||
ops_status() {
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" ps
|
||||
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
|
||||
supervisorctl status nexus
|
||||
fi
|
||||
echo ""
|
||||
local port h code
|
||||
port="$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
h=$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "fail")
|
||||
info "/health → ${h}"
|
||||
local cname
|
||||
cname="$(nexus_container_name 2>/dev/null || true)"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||||
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
|
||||
if [[ "$code" == "404" ]]; then
|
||||
info "/app/install.html → 已归档 (HTTP 404)"
|
||||
else
|
||||
warn "/app/install.html → 应已归档但 HTTP ${code}"
|
||||
fi
|
||||
else
|
||||
info "/app/install.html → HTTP ${code}"
|
||||
fi
|
||||
}
|
||||
|
||||
ops_backup_now() {
|
||||
NO_BACKUP=false
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
}
|
||||
|
||||
ops_install_cron() {
|
||||
bash "${NX_DIR}/install_ops_cron.sh"
|
||||
}
|
||||
|
||||
ops_update() {
|
||||
local args=()
|
||||
PRUNE_IMAGES=false
|
||||
NO_BACKUP=false
|
||||
NO_CACHE=false
|
||||
read -r -p "跳过 MySQL 备份? [y/N]: " nb
|
||||
[[ "$nb" == "y" || "$nb" == "Y" ]] && args+=(--no-backup)
|
||||
read -r -p "无缓存重建镜像? [y/N]: " nc
|
||||
[[ "$nc" == "y" || "$nc" == "Y" ]] && args+=(--no-cache)
|
||||
read -r -p "升级后清理悬空镜像? [y/N]: " pr
|
||||
[[ "$pr" == "y" || "$pr" == "Y" ]] && args+=(--prune)
|
||||
bash "${NX_DIR}/update.sh" "${args[@]}"
|
||||
pause_enter
|
||||
}
|
||||
|
||||
require_installed() {
|
||||
if ! is_stack_installed; then
|
||||
warn "尚未安装,请先使用菜单 [1] 安装"
|
||||
pause_enter
|
||||
return 1
|
||||
fi
|
||||
require_root
|
||||
load_secrets
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
return 0
|
||||
}
|
||||
|
||||
menu_main() {
|
||||
load_secrets
|
||||
while true; do
|
||||
menu_header "运维菜单"
|
||||
cat <<'EOF'
|
||||
── 安装 ──
|
||||
[1] 安装向导(新机 / 迁机 / 档位)
|
||||
[2] 全新安装 → /app/install.html
|
||||
|
||||
── 日常运维(需已安装)──
|
||||
[3] 状态 + 健康检查
|
||||
[4] 一键更新(pull + 重建镜像)★
|
||||
[5] 重启 Nexus(Python)
|
||||
[6] 查看 Nexus 日志
|
||||
[7] 启动 / 停止 Compose 栈
|
||||
[8] 重建镜像并启动(--build nexus)
|
||||
[9] 备份 MySQL
|
||||
[e] 安装巡检/备份 cron(health_monitor + db_backup)
|
||||
[v] 1Panel 安装/运维验收脚本
|
||||
|
||||
── 其它 ──
|
||||
[a] 检查 Git 是否有更新
|
||||
[b] 端口 / 防火墙检查
|
||||
[c] 进入 Nexus 容器 shell
|
||||
[d] 1Panel 反代说明
|
||||
[0] 退出
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) menu_install ;;
|
||||
2)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
warn "将执行全新安装(清除 nexus-state 卷内旧配置并重建容器)"
|
||||
read -r -p "确认继续? [y/N]: " fresh_ok
|
||||
[[ "$fresh_ok" == "y" || "$fresh_ok" == "Y" ]] || continue
|
||||
fi
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
|
||||
;;
|
||||
3)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
ops_status
|
||||
else
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
4)
|
||||
require_installed || continue
|
||||
ops_update
|
||||
;;
|
||||
5)
|
||||
require_installed || continue
|
||||
ops_restart_nexus
|
||||
pause_enter
|
||||
;;
|
||||
6)
|
||||
require_installed || continue
|
||||
ops_logs nexus
|
||||
;;
|
||||
7)
|
||||
require_installed || continue
|
||||
echo " [1] 启动 [2] 停止 [3] 重启栈"
|
||||
read -r -p "选: " s
|
||||
case "$s" in
|
||||
1) ops_start_stack ;;
|
||||
2) ops_stop_stack ;;
|
||||
3) ops_restart_stack ;;
|
||||
*) warn "无效" ;;
|
||||
esac
|
||||
pause_enter
|
||||
;;
|
||||
8)
|
||||
require_installed || continue
|
||||
read -r -p "无缓存重建? [y/N]: " nc
|
||||
if [[ "$nc" == "y" || "$nc" == "Y" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
|
||||
fi
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build --remove-orphans nexus
|
||||
verify_nexus_1panel_network "$NEXUS_ROOT" || true
|
||||
verify_health "$NEXUS_ROOT" "重建" || true
|
||||
pause_enter
|
||||
;;
|
||||
9)
|
||||
require_installed || continue
|
||||
ops_backup_now
|
||||
pause_enter
|
||||
;;
|
||||
e|E)
|
||||
require_root
|
||||
ops_install_cron
|
||||
pause_enter
|
||||
;;
|
||||
v|V)
|
||||
require_root
|
||||
bash "${NX_DIR}/verify-1panel-install-wizard.sh"
|
||||
pause_enter
|
||||
;;
|
||||
a|A)
|
||||
require_root
|
||||
cmd_check
|
||||
pause_enter
|
||||
;;
|
||||
b|B)
|
||||
require_root
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
is_stack_installed && check_ports_post "$NEXUS_ROOT" || true
|
||||
pause_enter
|
||||
;;
|
||||
c|C)
|
||||
require_installed || continue
|
||||
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
|
||||
;;
|
||||
d|D)
|
||||
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
|
||||
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
info "示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
|
||||
pause_enter
|
||||
;;
|
||||
0) exit 0 ;;
|
||||
*) warn "无效选项"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
nx_main() {
|
||||
local sub="${1:-}"
|
||||
install_nx_cli
|
||||
load_secrets
|
||||
case "$sub" in
|
||||
""|menu|god|ops|admin)
|
||||
# god/ops 已合并进主菜单,保留别名兼容
|
||||
[[ "$sub" == "god" || "$sub" == "ops" || "$sub" == "admin" ]] && \
|
||||
warn "「上帝菜单」已合并进 nx 主菜单,直接选对应项即可"
|
||||
menu_main
|
||||
;;
|
||||
install) menu_install ;;
|
||||
update|upgrade)
|
||||
require_root
|
||||
shift || true
|
||||
exec bash "${NX_DIR}/update.sh" "$@"
|
||||
;;
|
||||
install-fresh|fresh)
|
||||
require_root
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" "$@"
|
||||
;;
|
||||
install-auto|auto)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
warn "已安装,install-auto 将执行升级而非重复安装"
|
||||
exec bash "${NX_DIR}/update.sh"
|
||||
else
|
||||
run_install "${NEXUS_PROFILE:-2c8g}" ""
|
||||
fi
|
||||
;;
|
||||
health)
|
||||
require_root
|
||||
is_stack_installed && ops_status || check_ports_preflight "$NEXUS_ROOT"
|
||||
;;
|
||||
cron)
|
||||
require_root
|
||||
ops_install_cron
|
||||
;;
|
||||
verify)
|
||||
require_root
|
||||
bash "${NX_DIR}/verify-1panel-install-wizard.sh"
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
用法: nx [子命令]
|
||||
|
||||
(无) 统一运维菜单(安装 + 升级 + 重启 + 日志)
|
||||
update 一键更新(等同 bash deploy/update.sh)
|
||||
install-fresh 全新安装(/app/install.html)
|
||||
install-auto 非交互安装(默认 2c8g)
|
||||
health 健康检查
|
||||
cron 安装 health_monitor + db_backup crontab
|
||||
verify 运行 1Panel 验收脚本
|
||||
|
||||
god / ops 已合并进主菜单(兼容别名)
|
||||
|
||||
curl 入口:
|
||||
安装 curl -fsSL .../deploy/quick-install.sh | bash
|
||||
更新 curl -fsSL .../deploy/update.sh | bash
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
main "$@"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
chmod +x "${NX_DIR}/nx" 2>/dev/null || true
|
||||
nx_main "$@"
|
||||
@@ -0,0 +1,362 @@
|
||||
#!/bin/bash
|
||||
# Nexus Pre-deploy Gate Check (v3 — 8 gates)
|
||||
# 在部署前强制检查 8 道门控,任何一道不过则阻止部署
|
||||
#
|
||||
# 门控1: CHANGELOG — docs/changelog/ 下必须有今天的 .md 文件(且行数≥10)
|
||||
# 门控2: AUDIT — docs/audit/ 下必须有今天的审计记录(且包含关键段落)
|
||||
# 门控3: TEST — tests/test_api.py 必须存在且全通过
|
||||
# 门控4: LINT — ruff check server/ 零错误
|
||||
# 门控5: IMPORT — python -c "import server.main" 成功
|
||||
# 门控6: SECURITY — bandit -r server/ 无 HIGH/MEDIUM
|
||||
# 门控7: REVIEW — 审计文件包含 Closure表+文件清单+DoD,且文件清单与 git diff 交叉验证
|
||||
# 门控8: AI_REVIEW — cursor-agent 审查 HEAD~1..HEAD;缓存 docs/reviews/<head_sha>.json
|
||||
#
|
||||
# 用法: bash deploy/pre_deploy_check.sh
|
||||
# 退出码: 0=全部通过, 1=至少一道门未过
|
||||
# 日志: deploy/gate_log.jsonl(每次检查自动追加记录)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_REPO_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)"
|
||||
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
TIMESTAMP=$(date -Iseconds)
|
||||
GATES_PASSED=0
|
||||
GATES_TOTAL=8
|
||||
BLOCKED=0
|
||||
GATE_RESULTS=""
|
||||
|
||||
# Resolve tool from .venv / venv / PATH
|
||||
_pick_bin() {
|
||||
local name="$1"
|
||||
for p in "${DEPLOY_DIR}/.venv/bin/${name}" "${DEPLOY_DIR}/venv/bin/${name}"; do
|
||||
if [ -x "${p}" ]; then
|
||||
echo "${p}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
command -v "${name}" 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ── 日志函数 ──
|
||||
gate_log() {
|
||||
local gate="$1" result="$2" detail="${3:-}"
|
||||
local entry="{\"ts\":\"${TIMESTAMP}\",\"date\":\"${TODAY}\",\"gate\":\"${gate}\",\"result\":\"${result}\",\"detail\":\"${detail}\"}"
|
||||
GATE_RESULTS="${GATE_RESULTS}${entry},"
|
||||
# 追加到 JSONL 日志文件
|
||||
echo "${entry}" >> "${DEPLOY_DIR}/deploy/gate_log.jsonl"
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo " Nexus Pre-deploy Gate Check (v3)"
|
||||
echo " Date: ${TODAY}"
|
||||
echo " Gates: ${GATES_TOTAL}"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# ── Gate 1: Changelog (存在 + 行数≥10) ──
|
||||
echo -n "Gate 1/8: Changelog ... "
|
||||
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
|
||||
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${CHANGELOG_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
LINES=$(wc -l < "${FILE}")
|
||||
if [ "${LINES}" -ge 10 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ Found: $(basename "${FILE}") (${LINES} lines)"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "changelog" "PASS" "$(basename "${FILE}") ${LINES}lines"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ File exists but only ${LINES} lines (need ≥10)"
|
||||
echo " └─ Changelog must have substance, not just a placeholder"
|
||||
BLOCKED=1
|
||||
gate_log "changelog" "BLOCK" "only ${LINES} lines"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ No changelog file found: ${CHANGELOG_DIR}/${TODAY}-*.md"
|
||||
echo " └─ Create changelog before deploying"
|
||||
BLOCKED=1
|
||||
gate_log "changelog" "BLOCK" "file not found"
|
||||
fi
|
||||
|
||||
# ── Gate 2: Audit (存在 + 关键段落) ──
|
||||
echo -n "Gate 2/8: Audit ... "
|
||||
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
|
||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
# 检查关键段落是否存在
|
||||
HAS_STEP3=$(grep -c "Step 3" "${FILE}" 2>/dev/null || echo 0)
|
||||
HAS_CLOSURE=$(grep -c "Closure" "${FILE}" 2>/dev/null || echo 0)
|
||||
HAS_DOD=$(grep -c "DoD" "${FILE}" 2>/dev/null || echo 0)
|
||||
if [ "${HAS_STEP3}" -ge 1 ] && [ "${HAS_CLOSURE}" -ge 1 ] && [ "${HAS_DOD}" -ge 1 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ Found: $(basename "${FILE}") (Step3✓ Closure✓ DoD✓)"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "audit" "PASS" "$(basename "${FILE}")"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Audit file exists but missing required sections:"
|
||||
[ "${HAS_STEP3}" -eq 0 ] && echo " └─ Missing: Step 3 (规则扫描)"
|
||||
[ "${HAS_CLOSURE}" -eq 0 ] && echo " └─ Missing: Closure表"
|
||||
[ "${HAS_DOD}" -eq 0 ] && echo " └─ Missing: DoD (Definition of Done)"
|
||||
BLOCKED=1
|
||||
gate_log "audit" "BLOCK" "missing sections"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ No audit file found: ${AUDIT_DIR}/${TODAY}-*.md"
|
||||
echo " └─ Run line-walk audit (8 steps) and save report before deploying"
|
||||
BLOCKED=1
|
||||
gate_log "audit" "BLOCK" "file not found"
|
||||
fi
|
||||
|
||||
# ── Gate 3: Test (必须存在 + 必须通过) ──
|
||||
echo -n "Gate 3/8: Test ... "
|
||||
TEST_SCRIPT="${DEPLOY_DIR}/tests/test_api.py"
|
||||
if [ ! -f "${TEST_SCRIPT}" ]; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Test script not found: ${TEST_SCRIPT}"
|
||||
echo " └─ Tests are MANDATORY — cannot deploy without test coverage"
|
||||
BLOCKED=1
|
||||
gate_log "test" "BLOCK" "script not found"
|
||||
else
|
||||
# Always hit local API for gate 3 (ignore shell NEXUS_TEST_BASE used by import/e2e scripts)
|
||||
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && env -u NEXUS_TEST_BASE NEXUS_TEST_BASE=http://127.0.0.1:8600 python3 "${TEST_SCRIPT}" 2>&1) || true
|
||||
FAIL_COUNT=$(echo "${TEST_OUTPUT}" | grep -cE "^\s+\[FAIL\]" 2>/dev/null || true)
|
||||
FAIL_COUNT=${FAIL_COUNT:-0}
|
||||
GATE3_PASSED=0
|
||||
if [ "${FAIL_COUNT}" -gt 0 ] || echo "${TEST_OUTPUT}" | grep -qE "[0-9]+ test(s) failed"; then
|
||||
# 本机未起 :8600 时,若失败均为 Connection refused,回退 pytest 专项
|
||||
NON_CONN_FAILS=$(echo "${TEST_OUTPUT}" | grep -E "^\s+\[FAIL\]" | grep -vcE "Connection refused|Errno 111" 2>/dev/null || true)
|
||||
NON_CONN_FAILS=${NON_CONN_FAILS:-0}
|
||||
if [ "${FAIL_COUNT}" -gt 0 ] && [ "${NON_CONN_FAILS}" -eq 0 ]; then
|
||||
PYTEST_BIN=$(_pick_bin pytest || true)
|
||||
FALLBACK_TESTS="tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py tests/test_gate_ai_review.py"
|
||||
if [ -n "${PYTEST_BIN}" ]; then
|
||||
echo -e "${YELLOW}fallback${NC}"
|
||||
echo " └─ Local :8600 unreachable — running pytest gate batch"
|
||||
FALLBACK_OUTPUT=$(cd "${DEPLOY_DIR}" && "${PYTEST_BIN}" ${FALLBACK_TESTS} -q 2>&1) || FALLBACK_RC=$?
|
||||
FALLBACK_RC=${FALLBACK_RC:-0}
|
||||
if [ "${FALLBACK_RC}" -eq 0 ]; then
|
||||
echo -e " └─ ${GREEN}PASS${NC} (pytest fallback)"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "test" "PASS" "pytest fallback :8600 down"
|
||||
GATE3_PASSED=1
|
||||
else
|
||||
echo -e " └─ ${RED}BLOCK${NC} pytest fallback failed:"
|
||||
echo "${FALLBACK_OUTPUT}" | tail -15 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "test" "BLOCK" "pytest fallback failed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "${GATE3_PASSED}" -eq 0 ]; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Tests failed. Output:"
|
||||
echo "${TEST_OUTPUT}" | head -20 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "test" "BLOCK" "tests failed"
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ All tests passed"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "test" "PASS" "all passed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
|
||||
echo -n "Gate 4/8: Lint ... "
|
||||
RUFF_BIN=$(_pick_bin ruff || true)
|
||||
if [ -n "${RUFF_BIN}" ]; then
|
||||
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${RUFF_BIN}" check server/ --select F 2>&1) || true
|
||||
LINT_COUNT=$(echo "${LINT_OUTPUT}" | grep -cE "^server/" 2>/dev/null || true)
|
||||
LINT_COUNT=$(echo "${LINT_COUNT}" | head -1 | tr -d '[:space:]')
|
||||
LINT_COUNT=${LINT_COUNT:-0}
|
||||
if [ "${LINT_COUNT}" -eq 0 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ ruff check: 0 violations"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "lint" "PASS" "0 violations"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ ruff found ${LINT_COUNT} violations:"
|
||||
echo "${LINT_OUTPUT}" | head -15 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "lint" "BLOCK" "${LINT_COUNT} violations"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ ruff not found (.venv/bin/ruff or pip install ruff)"
|
||||
BLOCKED=1
|
||||
gate_log "lint" "BLOCK" "ruff not found"
|
||||
fi
|
||||
|
||||
# ── Gate 5: Import (python -c "import server.main") ──
|
||||
echo -n "Gate 5/8: Import ... "
|
||||
IMPORT_PYTHON=""
|
||||
for p in "${DEPLOY_DIR}/.venv/bin/python3" "${DEPLOY_DIR}/venv/bin/python3"; do
|
||||
if [ -x "${p}" ]; then
|
||||
IMPORT_PYTHON="${p}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "${IMPORT_PYTHON}" ]; then
|
||||
IMPORT_PYTHON="python3"
|
||||
fi
|
||||
IMPORT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${IMPORT_PYTHON}" -c "import server.main" 2>&1) && IMPORT_OK=1 || IMPORT_OK=0
|
||||
if [ "${IMPORT_OK}" -eq 1 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ server.main imports successfully"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "import" "PASS" "ok"
|
||||
else
|
||||
# 检查是否有真正的导入错误(不是缺少运行时依赖如数据库)
|
||||
if echo "${IMPORT_OUTPUT}" | grep -qiE "ImportError|ModuleNotFoundError|SyntaxError"; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Import failed:"
|
||||
echo "${IMPORT_OUTPUT}" | head -5 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "import" "BLOCK" "import/syntax error"
|
||||
else
|
||||
# 运行时依赖缺失(如数据库连接)不算BLOCK,但警告
|
||||
echo -e "${YELLOW}WARN${NC}"
|
||||
echo " └─ Import has runtime dependency issues (not code errors)"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "import" "WARN" "runtime deps only"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Gate 6: Security (bandit — HIGH only) ──
|
||||
echo -n "Gate 6/8: Security ... "
|
||||
BANDIT_BIN=$(_pick_bin bandit || true)
|
||||
if [ -n "${BANDIT_BIN}" ]; then
|
||||
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && "${BANDIT_BIN}" -r server/ -f txt -ll 2>&1) || true
|
||||
HIGH_COUNT=$(echo "${SEC_OUTPUT}" | grep -cE "Severity: High" 2>/dev/null || true)
|
||||
HIGH_COUNT=$(echo "${HIGH_COUNT}" | head -1 | tr -d '[:space:]')
|
||||
HIGH_COUNT=${HIGH_COUNT:-0}
|
||||
if [ "${HIGH_COUNT}" -eq 0 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ bandit: 0 HIGH findings"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "security" "PASS" "0 HIGH"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ bandit found ${HIGH_COUNT} HIGH severity issues:"
|
||||
echo "${SEC_OUTPUT}" | grep -A3 "Severity: High" | head -15 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "security" "BLOCK" "${HIGH_COUNT} HIGH findings"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ bandit not found (.venv/bin/bandit or pip install bandit)"
|
||||
BLOCKED=1
|
||||
gate_log "security" "BLOCK" "bandit not found"
|
||||
fi
|
||||
|
||||
# ── Gate 7: Review (审计文件交叉验证) ──
|
||||
echo -n "Gate 7/8: Review ... "
|
||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
# 检查审计文件中是否列出了实际改动的文件
|
||||
REVIEW_OK=1
|
||||
# 如果是git仓库,交叉验证改动文件
|
||||
if command -v git &>/dev/null && [ -d "${DEPLOY_DIR}/.git" ]; then
|
||||
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1..HEAD 2>/dev/null || true)
|
||||
for CF in ${CHANGED_FILES}; do
|
||||
# 跳过非代码文件与门控运行日志
|
||||
case "${CF}" in
|
||||
docs/*|CLAUDE.md|*.md|deploy/gate_log.jsonl|web/app/assets/*) continue ;;
|
||||
esac
|
||||
# 检查审计文件是否提到了这个文件
|
||||
if ! grep -q "$(basename "${CF}")" "${FILE}" 2>/dev/null; then
|
||||
if [ "${REVIEW_OK}" -eq 1 ]; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
REVIEW_OK=0
|
||||
fi
|
||||
echo " └─ Changed file not in audit: ${CF}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ "${REVIEW_OK}" -eq 1 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ Audit covers all changed files"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "review" "PASS" "audit covers changes"
|
||||
else
|
||||
BLOCKED=1
|
||||
gate_log "review" "BLOCK" "audit missing changed files"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ No audit file to validate against"
|
||||
BLOCKED=1
|
||||
gate_log "review" "BLOCK" "no audit file"
|
||||
fi
|
||||
|
||||
# ── Gate 8: AI Review (cursor-agent + docs/reviews cache) ──
|
||||
echo -n "Gate 8/8: AI Review ... "
|
||||
AI_REVIEW_OUT=$(cd "${DEPLOY_DIR}" && python3 "${DEPLOY_DIR}/scripts/gate_ai_review.py" 2>&1) || AI_REVIEW_RC=$?
|
||||
AI_REVIEW_RC=${AI_REVIEW_RC:-0}
|
||||
if [ "${AI_REVIEW_RC}" -eq 0 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo "${AI_REVIEW_OUT}" | sed 's/^/ └─ /' | head -5
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
if echo "${AI_REVIEW_OUT}" | grep -qi "skip"; then
|
||||
gate_log "ai_review" "PASS" "skipped no code"
|
||||
else
|
||||
gate_log "ai_review" "PASS" "ok"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo "${AI_REVIEW_OUT}" | sed 's/^/ │ /' | head -12
|
||||
BLOCKED=1
|
||||
gate_log "ai_review" "BLOCK" "see gate_ai_review.py output"
|
||||
fi
|
||||
|
||||
# ── Pre-flight: shell scripts LF ──
|
||||
echo -n "Pre-flight: Shell EOL (LF) ... "
|
||||
if bash "${DEPLOY_DIR}/deploy/check_shell_eol.sh" >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
gate_log "shell_eol" "PASS" "all tracked sh LF"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Run: bash deploy/check_shell_eol.sh"
|
||||
echo " └─ Fix: git add --renormalize '*.sh'"
|
||||
BLOCKED=1
|
||||
gate_log "shell_eol" "BLOCK" "CR in tracked sh"
|
||||
fi
|
||||
|
||||
# ── Pre-flight: deploy-critical paths LF in git index ──
|
||||
echo -n "Pre-flight: Text EOL (git index) ... "
|
||||
if python3 "${DEPLOY_DIR}/scripts/check_text_eol.py" >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
gate_log "text_eol" "PASS" "git index LF"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Run: python3 scripts/check_text_eol.py --fix-hint"
|
||||
BLOCKED=1
|
||||
gate_log "text_eol" "BLOCK" "CR in git index"
|
||||
fi
|
||||
|
||||
# ── Summary ──
|
||||
echo ""
|
||||
echo "========================================"
|
||||
if [ "${BLOCKED}" -eq 0 ]; then
|
||||
echo -e " Result: ${GREEN}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy allowed"
|
||||
echo "========================================"
|
||||
gate_log "summary" "PASS" "${GATES_PASSED}/${GATES_TOTAL}"
|
||||
exit 0
|
||||
else
|
||||
echo -e " Result: ${RED}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy BLOCKED"
|
||||
echo "========================================"
|
||||
gate_log "summary" "BLOCK" "${GATES_PASSED}/${GATES_TOTAL}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus production host preflight before applying a release tarball.
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/preflight-release-host.sh \
|
||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
||||
# --sha256 a4eb8f35f0b98d40c1ad83117bfc652e7686ffc7d55f6bf90fcfd433a2ad6701 \
|
||||
# --deploy-path /opt/nexus
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARBALL=""
|
||||
EXPECTED_SHA256=""
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
||||
MIN_FREE_MB="${NEXUS_RELEASE_MIN_FREE_MB:-1024}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
usage() {
|
||||
sed -n '1,12p' "$0"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--tarball)
|
||||
TARBALL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--sha256)
|
||||
EXPECTED_SHA256="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--deploy-path)
|
||||
DEPLOY_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--min-free-mb)
|
||||
MIN_FREE_MB="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "Unknown argument: $1"
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
check_cmd() {
|
||||
local cmd="$1"
|
||||
if command -v "$cmd" >/dev/null 2>&1; then
|
||||
info "command OK: $cmd"
|
||||
return 0
|
||||
fi
|
||||
error "missing command: $cmd"
|
||||
return 1
|
||||
}
|
||||
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
|
||||
echo docker
|
||||
return
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo supervisor
|
||||
return
|
||||
fi
|
||||
echo unknown
|
||||
}
|
||||
|
||||
verify_tarball() {
|
||||
if [[ -z "$TARBALL" ]]; then
|
||||
warn "--tarball not provided; skipping tarball checksum/content checks"
|
||||
return 0
|
||||
fi
|
||||
[[ -f "$TARBALL" ]] || {
|
||||
error "tarball not found: $TARBALL"
|
||||
return 1
|
||||
}
|
||||
info "tarball exists: $TARBALL"
|
||||
if [[ -n "$EXPECTED_SHA256" ]]; then
|
||||
local actual
|
||||
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
|
||||
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
|
||||
error "SHA256 mismatch: expected=$EXPECTED_SHA256 actual=$actual"
|
||||
return 1
|
||||
fi
|
||||
info "SHA256 OK: $actual"
|
||||
else
|
||||
warn "--sha256 not provided; checksum not verified"
|
||||
fi
|
||||
tar tzf "$TARBALL" nexus-release/server nexus-release/deploy nexus-release/web/app-v2 >/dev/null
|
||||
info "tarball required paths OK"
|
||||
}
|
||||
|
||||
check_deploy_path() {
|
||||
if [[ -d "$DEPLOY_PATH" ]]; then
|
||||
info "deploy path exists: $DEPLOY_PATH"
|
||||
else
|
||||
warn "deploy path does not exist yet: $DEPLOY_PATH"
|
||||
fi
|
||||
if [[ -f "${DEPLOY_PATH}/.env" ]]; then
|
||||
info "runtime .env preserved path exists"
|
||||
else
|
||||
warn "${DEPLOY_PATH}/.env not found"
|
||||
fi
|
||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]]; then
|
||||
info "docker/.env.prod exists"
|
||||
else
|
||||
warn "${DEPLOY_PATH}/docker/.env.prod not found"
|
||||
fi
|
||||
if [[ -d "${DEPLOY_PATH}/web/data" ]]; then
|
||||
info "web/data exists"
|
||||
else
|
||||
warn "${DEPLOY_PATH}/web/data not found"
|
||||
fi
|
||||
}
|
||||
|
||||
check_disk_space() {
|
||||
local target free_mb
|
||||
target="$DEPLOY_PATH"
|
||||
[[ -e "$target" ]] || target="$(dirname "$DEPLOY_PATH")"
|
||||
free_mb="$(df -Pm "$target" | awk 'NR==2 {print $4}')"
|
||||
if [[ -z "$free_mb" ]]; then
|
||||
warn "unable to determine free disk space for $target"
|
||||
return 0
|
||||
fi
|
||||
if (( free_mb < MIN_FREE_MB )); then
|
||||
error "low disk space on $target: ${free_mb}MB < ${MIN_FREE_MB}MB"
|
||||
return 1
|
||||
fi
|
||||
info "disk free OK: ${free_mb}MB >= ${MIN_FREE_MB}MB"
|
||||
}
|
||||
|
||||
check_runtime() {
|
||||
local runtime
|
||||
runtime="$(detect_runtime)"
|
||||
info "runtime detected: $runtime"
|
||||
case "$runtime" in
|
||||
docker)
|
||||
docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' >/dev/null \
|
||||
&& info "Nexus docker container found" \
|
||||
|| warn "Nexus docker container not found by known name pattern"
|
||||
;;
|
||||
supervisor)
|
||||
supervisorctl status nexus || true
|
||||
;;
|
||||
*)
|
||||
warn "runtime unknown; apply script may require manual restart"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_local_endpoint() {
|
||||
local port health app app_v2
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
port="${port:-8600}"
|
||||
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
||||
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
|
||||
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
|
||||
echo " current /health -> ${health:-<empty>}"
|
||||
echo " current /app/ -> ${app}"
|
||||
echo " current /app-v2/ -> ${app_v2}"
|
||||
}
|
||||
|
||||
main() {
|
||||
local failures=0
|
||||
for cmd in tar sha256sum df awk grep sed curl; do
|
||||
check_cmd "$cmd" || failures=$((failures + 1))
|
||||
done
|
||||
check_cmd rsync || failures=$((failures + 1))
|
||||
|
||||
verify_tarball || failures=$((failures + 1))
|
||||
check_deploy_path
|
||||
check_disk_space || failures=$((failures + 1))
|
||||
check_runtime
|
||||
check_local_endpoint
|
||||
|
||||
if (( failures > 0 )); then
|
||||
error "preflight failed: ${failures} blocking issue(s)"
|
||||
return 1
|
||||
fi
|
||||
info "preflight OK"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove orphaned files under web/app/assets/ not referenced by current Vite build.
|
||||
|
||||
Vite/Rolldown minified bundles reference chunks by bare filename (e.g. ServersPage-xxx.js)
|
||||
without import() paths — scan file contents for known asset basenames.
|
||||
|
||||
Orphans newer than --retention-days are kept so long-lived browser tabs can still load
|
||||
their lazy chunks after a deploy (see docs/design/specs/2026-06-02-keep-session-no-refresh-design.md).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# Leading `_` required for Vite chunks like `_plugin-vue_export-helper-*.js`
|
||||
ASSET_NAME_RE = re.compile(
|
||||
r"[_A-Za-z0-9][A-Za-z0-9_.-]*\.(?:js|css|woff2?|ttf|eot)"
|
||||
)
|
||||
URL_RE = re.compile(r"""url\(["']?([^"')]+)["']?\)""")
|
||||
DEFAULT_RETENTION_DAYS = 14
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> tuple[bool, int, Path]:
|
||||
dry_run = "--dry-run" in argv
|
||||
retention_days = DEFAULT_RETENTION_DAYS
|
||||
positional: list[str] = []
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
if arg == "--dry-run":
|
||||
i += 1
|
||||
continue
|
||||
if arg.startswith("--retention-days="):
|
||||
retention_days = int(arg.split("=", 1)[1])
|
||||
i += 1
|
||||
continue
|
||||
if arg == "--retention-days":
|
||||
if i + 1 >= len(argv):
|
||||
raise SystemExit("--retention-days requires a value")
|
||||
retention_days = int(argv[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
positional.append(arg)
|
||||
i += 1
|
||||
if retention_days < 0:
|
||||
raise SystemExit("--retention-days must be >= 0")
|
||||
app_dir = Path(positional[0] if positional else "/www/wwwroot/api.synaglobal.vip/web/app")
|
||||
return dry_run, retention_days, app_dir
|
||||
|
||||
|
||||
def _referenced_basenames(text: str, known: set[str]) -> set[str]:
|
||||
found: set[str] = set()
|
||||
for m in ASSET_NAME_RE.finditer(text):
|
||||
name = m.group(0)
|
||||
if name in known:
|
||||
found.add(name)
|
||||
for raw in URL_RE.findall(text):
|
||||
name = Path(raw).name
|
||||
if name in known:
|
||||
found.add(name)
|
||||
return found
|
||||
|
||||
|
||||
def collect(app_dir: Path) -> set[Path]:
|
||||
assets = app_dir / "assets"
|
||||
index = app_dir / "index.html"
|
||||
if not index.is_file():
|
||||
raise SystemExit(f"missing {index}")
|
||||
|
||||
all_names = {p.name for p in assets.iterdir() if p.is_file()}
|
||||
keep_names: set[str] = set()
|
||||
|
||||
for m in re.finditer(r"/app/assets/([^\s\"']+)", index.read_text(encoding="utf-8")):
|
||||
name = m.group(1).split("/")[-1]
|
||||
if name in all_names:
|
||||
keep_names.add(name)
|
||||
|
||||
if not keep_names:
|
||||
raise SystemExit("no entry assets found in index.html")
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for name in list(keep_names):
|
||||
path = assets / name
|
||||
if not path.is_file():
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
for ref in _referenced_basenames(text, all_names):
|
||||
if ref not in keep_names:
|
||||
keep_names.add(ref)
|
||||
changed = True
|
||||
|
||||
return {assets / n for n in keep_names}
|
||||
|
||||
|
||||
def _partition_orphans(
|
||||
orphan: list[Path],
|
||||
retention_days: int,
|
||||
) -> tuple[list[Path], list[Path]]:
|
||||
"""Split orphans into delete-now vs keep-for-open-tabs (mtime within window)."""
|
||||
if retention_days <= 0:
|
||||
return orphan, []
|
||||
cutoff = time.time() - retention_days * 86400
|
||||
to_remove: list[Path] = []
|
||||
retained: list[Path] = []
|
||||
for p in orphan:
|
||||
if p.stat().st_mtime >= cutoff:
|
||||
retained.append(p)
|
||||
else:
|
||||
to_remove.append(p)
|
||||
return to_remove, retained
|
||||
|
||||
|
||||
def main() -> int:
|
||||
dry_run, retention_days, app_dir = _parse_args(sys.argv[1:])
|
||||
assets = app_dir / "assets"
|
||||
if not assets.is_dir():
|
||||
raise SystemExit(f"not a directory: {assets}")
|
||||
|
||||
keep = collect(app_dir)
|
||||
all_files = {p for p in assets.iterdir() if p.is_file()}
|
||||
orphan = sorted(all_files - keep, key=lambda p: p.name)
|
||||
to_remove, retained = _partition_orphans(orphan, retention_days)
|
||||
|
||||
if dry_run:
|
||||
print(
|
||||
f"dry-run retention_days={retention_days} "
|
||||
f"referenced_kept={len(keep)} "
|
||||
f"would_remove={len(to_remove)} "
|
||||
f"retained_orphans={len(retained)} "
|
||||
f"total={len(all_files)}"
|
||||
)
|
||||
return 0
|
||||
|
||||
for p in to_remove:
|
||||
p.unlink()
|
||||
print(
|
||||
f"kept={len(keep)} removed={len(to_remove)} "
|
||||
f"retained_orphans={len(retained)} retention_days={retention_days} "
|
||||
f"total_before={len(all_files)}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 公共仓库一键安装入口(参考 1Panel quick_start.sh)
|
||||
#
|
||||
# root 登录后一条命令:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
#
|
||||
# 或保存后执行:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" -o /tmp/quick-install.sh
|
||||
# bash /tmp/quick-install.sh
|
||||
#
|
||||
# 可选参数会传给 install-nexus-fresh.sh,例如:
|
||||
# bash /tmp/quick-install.sh --profile 4c16g
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Nexus 公共仓库一键安装
|
||||
|
||||
curl -fsSL "${NEXUS_RAW_BASE}/deploy/quick-install.sh" | bash
|
||||
|
||||
可选参数: --profile 4c16g --domain 你的域名 --skip-1panel-check
|
||||
EOF
|
||||
}
|
||||
|
||||
validate_script_file() {
|
||||
local f="$1"
|
||||
if [[ ! -f "$f" ]] || ! head -1 "$f" | grep -q '^#!/'; then
|
||||
return 1
|
||||
fi
|
||||
if grep -qiE '^(Not found|404)' "$f" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
download_script() {
|
||||
local name="$1" dest="$2"
|
||||
local url base
|
||||
for base in \
|
||||
"${NEXUS_RAW_BASE}" \
|
||||
"http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/${name}"
|
||||
if curl -fsSL "$url" -o "$dest" 2>/dev/null && validate_script_file "$dest"; then
|
||||
info "已下载 ${name}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_via_git() {
|
||||
local url="http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
url="http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
step "改用 git clone..."
|
||||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --depth 1 2>/dev/null || \
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --prune
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${NEXUS_GIT_BRANCH}" 2>/dev/null || \
|
||||
git -C "$NEXUS_ROOT" pull
|
||||
else
|
||||
rm -rf "$NEXUS_ROOT"
|
||||
git clone --depth 1 -b "$NEXUS_GIT_BRANCH" "$url" "$NEXUS_ROOT"
|
||||
fi
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
exec bash "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" --skip-clone "$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行(su - 后直接运行,不要用 sudo 包一层)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus 6.0 — 公共仓库一键安装 ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
esac
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
|
||||
if download_script "install-nexus-fresh.sh" "$tmp"; then
|
||||
chmod +x "$tmp"
|
||||
exec bash "$tmp" "$@"
|
||||
fi
|
||||
|
||||
step "raw 下载失败,尝试 git clone..."
|
||||
run_via_git "$@"
|
||||
}
|
||||
|
||||
# curl | bash 时 stdin 脚本无 BASH_SOURCE[0],不可配合 set -u 做入口判断
|
||||
main "$@"
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync local Nexus working tree to production host (no Gitea push/pull).
|
||||
# Transfer via tar|ssh; remote ownership auto-detected (www:www on BT, else deploy path owner).
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/rsync-local-to-server.sh
|
||||
# NEXUS_DEPLOY_PATH=/opt/nexus bash deploy/rsync-local-to-server.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT}"
|
||||
|
||||
SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh"
|
||||
if [[ -f "$SECRETS" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$SECRETS"
|
||||
fi
|
||||
|
||||
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
||||
fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
|
||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-}"
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
DEPLOY_PATH="$(ssh_cmd 'for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do [[ -d "$d/server" ]] && echo "$d" && exit 0; done; echo /opt/nexus')"
|
||||
fi
|
||||
|
||||
resolve_deploy_chown() {
|
||||
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
|
||||
echo "${NEXUS_DEPLOY_CHOWN}"
|
||||
return
|
||||
fi
|
||||
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
|
||||
echo "www:www"
|
||||
return
|
||||
fi
|
||||
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
|
||||
}
|
||||
DEPLOY_CHOWN="$(resolve_deploy_chown)"
|
||||
|
||||
echo "═══ Nexus local → server sync ═══"
|
||||
echo "SSH target: ${NEXUS_SSH_HOST}"
|
||||
echo "Remote path: ${DEPLOY_PATH}"
|
||||
echo "Owner: ${DEPLOY_CHOWN}"
|
||||
|
||||
TAR_EXCLUDES=(
|
||||
--exclude='.git'
|
||||
--exclude='.env'
|
||||
--exclude='.env.*'
|
||||
--exclude='SECRETS.md'
|
||||
--exclude='deploy/nexus-1panel.secrets.sh'
|
||||
--exclude='docker/.env.prod'
|
||||
--exclude='web/data'
|
||||
--exclude='frontend/node_modules'
|
||||
--exclude='frontend-v2/node_modules'
|
||||
--exclude='node_modules'
|
||||
--exclude='.venv'
|
||||
--exclude='.venv-*'
|
||||
--exclude='venv'
|
||||
--exclude='__pycache__'
|
||||
--exclude='.pytest_cache'
|
||||
--exclude='.ruff_cache'
|
||||
--exclude='.cursor'
|
||||
--exclude='.playwright-mcp'
|
||||
--exclude='.install_locked'
|
||||
--exclude='.install_state.json'
|
||||
--exclude='web/app/assets'
|
||||
--exclude='tmp'
|
||||
--exclude='2025.2'
|
||||
--exclude='=2025.2'
|
||||
--exclude='*.bak-'
|
||||
--exclude='frontend/test-results'
|
||||
--exclude='frontend/e2e/.auth'
|
||||
)
|
||||
|
||||
echo "▶ tar stream → ${DEPLOY_PATH} ..."
|
||||
tar -C "${ROOT}" -czf - "${TAR_EXCLUDES[@]}" . | \
|
||||
ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "sudo tar -xzf - -C '${DEPLOY_PATH}'"
|
||||
|
||||
echo "▶ chown ${DEPLOY_CHOWN} (skip docker/.env.prod, web/data, .env) ..."
|
||||
ssh_cmd "sudo bash -s" <<REMOTE
|
||||
set -euo pipefail
|
||||
root='${DEPLOY_PATH}'
|
||||
own='${DEPLOY_CHOWN}'
|
||||
chown_tree() {
|
||||
local p="\$1"
|
||||
[[ -e "\$p" ]] || return 0
|
||||
sudo chown -R "\$own" "\$p"
|
||||
}
|
||||
for top in server deploy scripts tests standards docs frontend web; do
|
||||
chown_tree "\$root/\$top"
|
||||
done
|
||||
if [[ -d "\$root/docker" ]]; then
|
||||
while IFS= read -r -d '' p; do
|
||||
sudo chown -R "\$own" "\$p"
|
||||
done < <(find "\$root/docker" \( -path "\$root/docker/.env.prod" \) -prune -o -print0)
|
||||
fi
|
||||
for f in pyproject.toml requirements.txt requirements-dev.txt AGENTS.md CLAUDE.md; do
|
||||
[[ -f "\$root/\$f" ]] && sudo chown "\$own" "\$root/\$f"
|
||||
done
|
||||
REMOTE
|
||||
|
||||
echo "✓ sync complete (owner ${DEPLOY_CHOWN})"
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Run tests/test_api.py on the deploy host (reads NEXUS_TEST_* from .env).
|
||||
set -euo pipefail
|
||||
cd /www/wwwroot/api.synaglobal.vip
|
||||
export NEXUS_TEST_BASE="${NEXUS_TEST_BASE:-http://127.0.0.1:8600}"
|
||||
python3 tests/test_api.py
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync install wizard static files into the running Nexus prod container.
|
||||
# Primary fix: Dockerfile.prod bakes these into the image on build.
|
||||
# This script is a safety net after upgrade if an old image is still running.
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
CONTAINER="${NEXUS_CONTAINER:-}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
resolve_container() {
|
||||
if [[ -n "$CONTAINER" ]]; then
|
||||
return 0
|
||||
fi
|
||||
CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$CONTAINER" ]]; then
|
||||
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
|
||||
exit 1
|
||||
fi
|
||||
info "容器: $CONTAINER"
|
||||
}
|
||||
|
||||
sync_file() {
|
||||
local src="$1" dest="$2"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
error "缺少源文件: $src"
|
||||
exit 1
|
||||
fi
|
||||
docker exec "$CONTAINER" mkdir -p "$(dirname "$dest")"
|
||||
docker cp "$src" "${CONTAINER}:${dest}"
|
||||
}
|
||||
|
||||
wizard_archived_in_container() {
|
||||
docker exec "$CONTAINER" test -f /app/.install_locked 2>/dev/null \
|
||||
|| docker exec "$CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \
|
||||
|| docker exec "$CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null
|
||||
}
|
||||
|
||||
main() {
|
||||
local app="$NEXUS_ROOT/web/app"
|
||||
local wizard_archived=false
|
||||
if [[ ! -f "$app/install.html" ]]; then
|
||||
error "仓库中无 $app/install.html,请先 git pull"
|
||||
exit 1
|
||||
fi
|
||||
resolve_container
|
||||
if wizard_archived_in_container; then
|
||||
wizard_archived=true
|
||||
info "安装已锁定或向导已归档,跳过静态/Python 热同步"
|
||||
local code
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "404" ]]; then
|
||||
info "/app/install.html 已归档 → HTTP 404"
|
||||
exit 0
|
||||
fi
|
||||
warn "/app/install.html 应已归档但返回 HTTP $code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "同步安装向导静态文件..."
|
||||
sync_file "$app/install.html" /app/web/app/install.html
|
||||
sync_file "$app/vendor/tailwindcss-browser.js" /app/web/app/vendor/tailwindcss-browser.js
|
||||
sync_file "$app/vendor/theme-init.js" /app/web/app/vendor/theme-init.js
|
||||
sync_file "$app/vendor/theme.css" /app/web/app/vendor/theme.css
|
||||
sync_file "$app/vendor/alpinejs.min.js" /app/web/app/vendor/alpinejs.min.js
|
||||
|
||||
local install_py="$NEXUS_ROOT/server/api/install.py" code
|
||||
if [[ -f "$install_py" ]]; then
|
||||
info "同步 install.py(Docker Redis env-check 修复)..."
|
||||
sync_file "$install_py" /app/server/api/install.py
|
||||
info "重启 Nexus 容器以加载 Python 变更..."
|
||||
docker restart "$CONTAINER" >/dev/null
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/health" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
else
|
||||
warn "未找到 $install_py,跳过 Python 热同步"
|
||||
fi
|
||||
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "200" ]]; then
|
||||
info "/app/install.html → HTTP 200"
|
||||
else
|
||||
warn "/app/install.html → HTTP $code(请检查 Nexus 是否在 ${NEXUS_PUBLISH_PORT} 监听)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync Git-tracked web/app and web/app-v2 into the running Nexus Docker container.
|
||||
#
|
||||
# Root cause: Dockerfile.prod runs vite build inside the image; Docker layer cache
|
||||
# can produce asset hashes that differ from committed web/app on the host.
|
||||
#
|
||||
# Usage (on production host after git pull / upgrade):
|
||||
# NEXUS_ROOT=/opt/nexus bash deploy/sync_webapp_to_container.sh
|
||||
#
|
||||
# Called automatically from deploy/nexus-1panel.sh cmd_upgrade and deploy-production.sh.
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
CONTAINER="${NEXUS_CONTAINER:-}"
|
||||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
resolve_container() {
|
||||
if [[ -n "$CONTAINER" ]]; then
|
||||
return 0
|
||||
fi
|
||||
CONTAINER="$(docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$CONTAINER" ]]; then
|
||||
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
|
||||
exit 1
|
||||
fi
|
||||
info "容器: $CONTAINER"
|
||||
}
|
||||
|
||||
verify_entry_bundle() {
|
||||
local html bundle_path code
|
||||
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/" 2>/dev/null || true)"
|
||||
bundle_path="$(printf '%s' "$html" | sed -n 's/.*src="\(\/app\/assets\/index-[^"]*\.js\)".*/\1/p' | head -1)"
|
||||
if [[ -z "$bundle_path" ]]; then
|
||||
error "/app/ index.html 未解析到主 bundle"
|
||||
exit 1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
error "主 bundle ${bundle_path} → HTTP ${code}"
|
||||
exit 1
|
||||
fi
|
||||
info "入口校验 OK: ${bundle_path} → HTTP 200"
|
||||
}
|
||||
|
||||
verify_app_v2_entry_bundle() {
|
||||
local html bundle_path code
|
||||
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app-v2/" 2>/dev/null || true)"
|
||||
bundle_path="$(printf '%s' "$html" | sed -n 's/.*src="\(\/app-v2\/assets\/[^"]*\.js\)".*/\1/p' | head -1)"
|
||||
if [[ -z "$bundle_path" ]]; then
|
||||
error "/app-v2/ index.html 未解析到主 bundle"
|
||||
exit 1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
error "V2 主 bundle ${bundle_path} → HTTP ${code}"
|
||||
exit 1
|
||||
fi
|
||||
info "V2 入口校验 OK: ${bundle_path} → HTTP 200"
|
||||
}
|
||||
|
||||
main() {
|
||||
local app="${NEXUS_ROOT}/web/app"
|
||||
local app_v2="${NEXUS_ROOT}/web/app-v2"
|
||||
if [[ ! -f "${app}/index.html" ]]; then
|
||||
error "缺少 ${app}/index.html,请先 git pull"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${app}/assets" ]]; then
|
||||
error "缺少 ${app}/assets"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${app_v2}/index.html" ]]; then
|
||||
error "缺少 ${app_v2}/index.html,请先构建 frontend-v2"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${app_v2}/assets" ]]; then
|
||||
error "缺少 ${app_v2}/assets"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolve_container
|
||||
|
||||
info "同步 web/app → 容器 /app/web/app ..."
|
||||
docker_cmd exec "$CONTAINER" rm -rf /app/web/app/assets
|
||||
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app
|
||||
docker_cmd cp "${app}/index.html" "${CONTAINER}:/app/web/app/index.html"
|
||||
docker_cmd cp "${app}/assets" "${CONTAINER}:/app/web/app/assets"
|
||||
|
||||
if [[ -f "${app}/favicon.ico" ]]; then
|
||||
docker_cmd cp "${app}/favicon.ico" "${CONTAINER}:/app/web/app/favicon.ico" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
info "同步 web/app-v2 → 容器 /app/web/app-v2 ..."
|
||||
docker_cmd exec "$CONTAINER" rm -rf /app/web/app-v2/assets
|
||||
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app-v2
|
||||
docker_cmd cp "${app_v2}/index.html" "${CONTAINER}:/app/web/app-v2/index.html"
|
||||
docker_cmd cp "${app_v2}/assets" "${CONTAINER}:/app/web/app-v2/assets"
|
||||
|
||||
if [[ -x "${NEXUS_ROOT}/deploy/prune_frontend_assets.py" ]]; then
|
||||
info "容器内 prune 未引用 assets(可选)..."
|
||||
docker_cmd exec "$CONTAINER" python3 /app/deploy/prune_frontend_assets.py --dry-run 2>/dev/null || \
|
||||
warn "容器内 prune 跳过(脚本或路径不可用)"
|
||||
fi
|
||||
|
||||
verify_entry_bundle
|
||||
verify_app_v2_entry_bundle
|
||||
info "web/app 与 web/app-v2 同步完成"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Diagnose 1Panel Redis auth from Nexus container network perspective.
|
||||
# Usage:
|
||||
# REDIS_PASS='redis_WP3NyN' bash deploy/test-1panel-redis.sh
|
||||
# bash deploy/test-1panel-redis.sh # prompts for password
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
ok() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
fail() { echo -e "\033[0;31m[FAIL]\033[0m $*"; }
|
||||
|
||||
pick_redis_container() {
|
||||
if [[ -n "${REDIS_CONTAINER:-}" ]]; then
|
||||
echo "$REDIS_CONTAINER"
|
||||
return 0
|
||||
fi
|
||||
local h
|
||||
h="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$h" ]]; then
|
||||
echo "$h"
|
||||
return 0
|
||||
fi
|
||||
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
|
||||
h="$("$NEXUS_ROOT/deploy/detect-1panel-services.sh" 2>/dev/null | sed -n 's/^NEXUS_1PANEL_REDIS_HOST=//p' | head -1)"
|
||||
[[ -n "$h" ]] && echo "$h" && return 0
|
||||
fi
|
||||
docker ps --format '{{.Names}}' | grep -iE '^1Panel-redis-' | head -1 || true
|
||||
}
|
||||
|
||||
try_ping() {
|
||||
local label="$1" url="$2"
|
||||
local nexus
|
||||
nexus="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$nexus" ]]; then
|
||||
fail "未找到 Nexus 容器"
|
||||
return 1
|
||||
fi
|
||||
if docker exec "$nexus" python3 -c "
|
||||
import redis
|
||||
r = redis.Redis.from_url('${url}', socket_timeout=3)
|
||||
print(r.ping())
|
||||
" 2>/dev/null | grep -q True; then
|
||||
ok "$label → $url"
|
||||
return 0
|
||||
fi
|
||||
fail "$label"
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
local redis_host pass
|
||||
redis_host="$(pick_redis_container)"
|
||||
[[ -n "$redis_host" ]] || { fail "未找到 Redis 容器/主机名"; exit 1; }
|
||||
info "Redis 主机: $redis_host"
|
||||
|
||||
if [[ -z "${REDIS_PASS:-}" ]]; then
|
||||
read -r -s -p "1Panel Redis 密码(应用参数): " pass
|
||||
echo ""
|
||||
else
|
||||
pass="$REDIS_PASS"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "从 Nexus 容器内尝试多种 URL 格式(807f4c2 修复后)..."
|
||||
local any=0
|
||||
try_ping "无认证" "redis://${redis_host}:6379/0" && any=1 || true
|
||||
if [[ -n "$pass" ]]; then
|
||||
try_ping "仅密码 (:pass@)" "redis://:${pass}@${redis_host}:6379/0" && any=1 || true
|
||||
try_ping "default 用户" "redis://default:${pass}@${redis_host}:6379/0" && any=1 || true
|
||||
info "跳过 root 用户探测 — 1Panel Redis 无 root 账号"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$any" -eq 1 ]]; then
|
||||
ok "至少一种格式可用"
|
||||
echo ""
|
||||
info "安装向导步骤 3:1Panel Redis 无账号,只填密码(后端自动用 :pass@ 或 default)"
|
||||
info "已有 .env 时写入 NEXUS_REDIS_URL,例如:"
|
||||
info " redis://:${pass}@${redis_host}:6379/0"
|
||||
info " 或 redis://default:${pass}@${redis_host}:6379/0"
|
||||
exit 0
|
||||
fi
|
||||
fail "全部失败。请在 1Panel → Redis → 参数 核对密码,或检查 Nexus 是否在 1panel-network"
|
||||
info "本机: docker exec -it ${redis_host} redis-cli -u 'redis://:密码@127.0.0.1:6379' ping"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 卸载 Nexus 旧版 Compose 内置 MySQL 容器
|
||||
#
|
||||
# bash deploy/uninstall-mysql-compose.sh
|
||||
# bash deploy/uninstall-mysql-compose.sh --purge-volume
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
|
||||
PURGE_VOLUME=false
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
卸载 Nexus Compose 遗留 MySQL 容器
|
||||
|
||||
bash deploy/uninstall-mysql-compose.sh [--purge-volume]
|
||||
|
||||
说明:
|
||||
- 不卸载宿主机/1Panel 自建的 MySQL
|
||||
- 卸载后请自建库 nexus + 用户,安装向导步骤 3 填写连接信息
|
||||
EOF
|
||||
}
|
||||
|
||||
compose_env_args() {
|
||||
local_env=()
|
||||
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
|
||||
local prof="${NEXUS_ROOT}/docker/.host-profile"
|
||||
if [[ -f "$prof" ]]; then
|
||||
local p
|
||||
p="$(tr -d '\r\n' <"$prof")"
|
||||
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--purge-volume) PURGE_VOLUME=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "$(id -u)" -eq 0 ]] || { error "请使用 root 执行"; exit 1; }
|
||||
|
||||
removed=0
|
||||
|
||||
stop_rm_container() {
|
||||
local name="$1"
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
|
||||
info "停止并删除容器: $name"
|
||||
docker stop "$name" >/dev/null 2>&1 || true
|
||||
docker rm -f "$name" >/dev/null 2>&1 || true
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
for pattern in nexus-prod-mysql-1 nexus-prod-mysql nexus-mysql-1; do
|
||||
stop_rm_container "$pattern"
|
||||
done
|
||||
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
|
||||
compose_env_args
|
||||
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]] && grep -q '^ mysql:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
|
||||
warn "compose 仍含 mysql 服务,尝试 compose rm..."
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop mysql 2>/dev/null || true)
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f mysql 2>/dev/null || true)
|
||||
removed=$((removed + 1))
|
||||
elif [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" && -f "${NEXUS_ROOT}/docker/.env.prod" ]]; then
|
||||
info "Compose --remove-orphans(从 nexus-prod 项目移除 mysql 孤儿容器)..."
|
||||
compose_files=(-f "${NEXUS_ROOT}/${COMPOSE_FILE}")
|
||||
if docker network inspect 1panel-network >/dev/null 2>&1 \
|
||||
&& [[ -f "${NEXUS_ROOT}/docker/docker-compose.1panel.yml" ]]; then
|
||||
compose_files+=(-f "${NEXUS_ROOT}/docker/docker-compose.1panel.yml")
|
||||
fi
|
||||
(cd "$NEXUS_ROOT" && docker compose "${compose_files[@]}" "${local_env[@]}" up -d --remove-orphans 2>/dev/null || true)
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_VOLUME" == true ]]; then
|
||||
while IFS= read -r vol; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
info "删除数据卷: $vol"
|
||||
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol"
|
||||
done < <(docker volume ls -q | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
fi
|
||||
|
||||
leftover="$(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)"
|
||||
if [[ -n "$leftover" ]]; then
|
||||
warn "仍有 MySQL 相关容器:"
|
||||
echo "$leftover" | sed 's/^/ /'
|
||||
warn "若确认为 Compose 遗留,可: docker rm -f <容器名>"
|
||||
elif [[ "$removed" -gt 0 ]]; then
|
||||
info "Compose MySQL 容器已卸载。"
|
||||
else
|
||||
info "未发现 Nexus Compose MySQL 容器。"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "当前 Nexus 栈:"
|
||||
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | grep -i nexus || docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
echo ""
|
||||
info "下一步:1Panel 应用商店安装 MySQL → nx update 探测容器名 → 向导步骤 3 使用 1Panel-mysql-xxxx"
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 卸载 Nexus 旧版 Compose 内置 Redis 容器
|
||||
#
|
||||
# 适用:已从 docker-compose.prod.yml 移除 redis 服务,改用宿主机/1Panel 自建 Redis。
|
||||
# 不会卸载 apt/1Panel 安装的 Redis,仅删除 Docker 容器与(可选)数据卷。
|
||||
#
|
||||
# bash deploy/uninstall-redis-compose.sh
|
||||
# bash deploy/uninstall-redis-compose.sh --purge-volume # 同时删 redis-data 卷
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
|
||||
PURGE_VOLUME=false
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
卸载 Nexus Compose 遗留 Redis 容器
|
||||
|
||||
bash deploy/uninstall-redis-compose.sh [--purge-volume]
|
||||
|
||||
选项:
|
||||
--purge-volume 删除 nexus-prod_redis-data 数据卷(不可恢复)
|
||||
-h, --help 显示帮助
|
||||
|
||||
说明:
|
||||
- 不卸载宿主机 redis-server / 1Panel 应用商店 Redis
|
||||
- 卸载后请在宿主机安装 Redis,安装向导步骤 2 须能 ping 通
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--purge-volume) PURGE_VOLUME=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
removed=0
|
||||
|
||||
stop_rm_container() {
|
||||
local name="$1"
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
|
||||
info "停止并删除容器: $name"
|
||||
docker stop "$name" >/dev/null 2>&1 || true
|
||||
docker rm "$name" >/dev/null 2>&1 || true
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# 常见 Compose 命名
|
||||
for pattern in nexus-prod-redis-1 nexus-prod-redis nexus-redis-1; do
|
||||
stop_rm_container "$pattern"
|
||||
done
|
||||
|
||||
# 名称含 nexus 且含 redis 的容器
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*redis|redis.*nexus' || true)
|
||||
|
||||
# 若旧 compose 仍定义 redis 服务
|
||||
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]]; then
|
||||
if grep -q '^ redis:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
|
||||
warn "compose 仍含 redis 服务,尝试 compose rm..."
|
||||
local_env=()
|
||||
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
|
||||
prof="${NEXUS_ROOT}/docker/.host-profile"
|
||||
if [[ -f "$prof" ]]; then
|
||||
p="$(tr -d '\r\n' <"$prof")"
|
||||
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
|
||||
fi
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop redis 2>/dev/null || true)
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f redis 2>/dev/null || true)
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_VOLUME" == true ]]; then
|
||||
while IFS= read -r vol; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
info "删除数据卷: $vol"
|
||||
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol(可能被占用)"
|
||||
done < <(docker volume ls -q | grep -iE 'nexus.*redis|redis.*nexus' || true)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$removed" -gt 0 ]]; then
|
||||
info "Compose Redis 容器已卸载。"
|
||||
else
|
||||
info "未发现 Nexus Compose Redis 容器(可能已删除)。"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "下一步:1Panel 应用商店安装 Redis → nx update 写入 NEXUS_1PANEL_REDIS_HOST"
|
||||
echo " 向导 Redis URL:redis://:密码@1Panel-redis-xxxx:6379/0(无 root 用户)"
|
||||
echo " 诊断:bash deploy/test-1panel-redis.sh"
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
# ================================================================
|
||||
# Nexus 6.0 — Uninstall Script
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash uninstall.sh # interactive
|
||||
# sudo bash uninstall.sh --deploy-dir /opt/nexus # specify deploy dir
|
||||
# sudo bash uninstall.sh --purge # also remove deploy dir
|
||||
#
|
||||
# Safe: stops services, removes configs. Database is NEVER deleted automatically.
|
||||
# ================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
DEPLOY_DIR=""
|
||||
PURGE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
|
||||
--purge) PURGE=true; shift ;;
|
||||
-h|--help)
|
||||
echo "Usage: sudo bash uninstall.sh [--deploy-dir PATH] [--purge]"
|
||||
echo ""
|
||||
echo " --deploy-dir PATH Specify Nexus installation directory"
|
||||
echo " --purge Also remove the deploy directory and venv"
|
||||
echo ""
|
||||
echo " Note: MySQL database is NEVER deleted automatically."
|
||||
exit 0 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Auto-detect deploy directory
|
||||
if [ -z "$DEPLOY_DIR" ]; then
|
||||
for d in /opt/nexus /www/wwwroot/*/; do
|
||||
if [ -d "$d/server" ] && [ -d "$d/web" ]; then
|
||||
DEPLOY_DIR="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_DIR" ] || [ ! -d "$DEPLOY_DIR/server" ]; then
|
||||
error "Cannot find Nexus installation. Use --deploy-dir to specify."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect BT Panel
|
||||
BT_MODE=false
|
||||
if [ -d "/www/server/panel" ] && [ -f "/www/server/panel/class/common.py" ]; then
|
||||
BT_MODE=true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 6.0 Uninstall"
|
||||
echo "=========================================="
|
||||
echo " Deploy: $DEPLOY_DIR"
|
||||
echo " BT Panel: $BT_MODE"
|
||||
echo " Purge files: $PURGE"
|
||||
echo ""
|
||||
|
||||
# Confirm
|
||||
read -rp "Are you sure you want to uninstall Nexus? [y/N] " confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. Stop Supervisor process
|
||||
info "Step 1/5: Stopping Nexus process..."
|
||||
supervisorctl stop nexus >/dev/null 2>&1 || true
|
||||
|
||||
# 2. Remove Supervisor config
|
||||
info "Step 2/5: Removing Supervisor config..."
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
SUPERVISOR_CONF="/www/server/panel/plugin/supervisor/profile/nexus.ini"
|
||||
else
|
||||
SUPERVISOR_CONF="/etc/supervisor/conf.d/nexus.conf"
|
||||
fi
|
||||
if [ -f "$SUPERVISOR_CONF" ]; then
|
||||
rm -f "$SUPERVISOR_CONF"
|
||||
supervisorctl reread >/dev/null 2>&1 || true
|
||||
supervisorctl update >/dev/null 2>&1 || true
|
||||
info " Removed $SUPERVISOR_CONF"
|
||||
else
|
||||
info " Supervisor config not found (already removed)"
|
||||
fi
|
||||
|
||||
# 3. Remove Nginx config
|
||||
info "Step 3/5: Removing Nginx config..."
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
# Find the domain from deploy dir
|
||||
DOMAIN=$(basename "$DEPLOY_DIR")
|
||||
NGINX_CONF="/www/server/panel/vhost/nginx/$DOMAIN.conf"
|
||||
else
|
||||
NGINX_CONF="/etc/nginx/sites-available/nexus"
|
||||
NGINX_SYMLINK="/etc/nginx/sites-enabled/nexus"
|
||||
fi
|
||||
|
||||
if [ -f "$NGINX_CONF" ]; then
|
||||
rm -f "$NGINX_CONF"
|
||||
[ -L "${NGINX_SYMLINK:-}" ] && rm -f "$NGINX_SYMLINK"
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
/www/server/nginx/sbin/nginx -s reload >/dev/null 2>&1 || true
|
||||
else
|
||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
|
||||
fi
|
||||
info " Removed $NGINX_CONF"
|
||||
else
|
||||
info " Nginx config not found (already removed)"
|
||||
fi
|
||||
|
||||
# 4. Remove crontab entries
|
||||
info "Step 4/5: Removing crontab entries..."
|
||||
crontab -l 2>/dev/null | grep -v "health_monitor.sh" | grep -v "db_backup.sh" | crontab - 2>/dev/null || true
|
||||
info " Crontab entries removed"
|
||||
|
||||
# 5. Optionally remove deploy directory
|
||||
if [ "$PURGE" = true ]; then
|
||||
info "Step 5/5: Removing deploy directory (--purge)..."
|
||||
rm -rf "$DEPLOY_DIR"
|
||||
info " Removed $DEPLOY_DIR"
|
||||
else
|
||||
info "Step 5/5: Keeping deploy directory (use --purge to remove)"
|
||||
info " To remove manually: rm -rf $DEPLOY_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${GREEN}Nexus uninstalled successfully.${NC}"
|
||||
echo ""
|
||||
echo " Note: MySQL database was NOT deleted."
|
||||
echo " To drop the database manually:"
|
||||
echo " mysql -u root -e 'DROP DATABASE IF EXISTS nexus;'"
|
||||
echo ""
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus 6.0 — 一键更新(拉代码 + 重建镜像 + 重启)
|
||||
#
|
||||
# 已在 /opt/nexus:
|
||||
# bash deploy/update.sh
|
||||
# bash deploy/update.sh --no-cache # 强制无缓存重建(entrypoint/Dockerfile 变更后)
|
||||
#
|
||||
# 远程一条命令(推荐在仓库目录执行,管道模式会回退 /opt/nexus):
|
||||
# cd /opt/nexus && bash deploy/update.sh
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
_resolve_nexus_root() {
|
||||
local src="${BASH_SOURCE[0]:-}"
|
||||
if [[ -n "$src" && "$src" != /dev/fd/* && "$src" != "-" && -f "$src" ]]; then
|
||||
cd "$(dirname "$(readlink -f "$src")")/.." && pwd
|
||||
return
|
||||
fi
|
||||
if [[ -d /opt/nexus ]]; then
|
||||
echo "警告: 通过管道执行无法解析脚本路径,已回退 /opt/nexus;推荐: cd /opt/nexus && bash deploy/update.sh" >&2
|
||||
echo /opt/nexus
|
||||
return
|
||||
fi
|
||||
echo "错误: 无法定位 Nexus 根目录;请 cd /opt/nexus && bash deploy/update.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ROOT="$(_resolve_nexus_root)"
|
||||
|
||||
NEXUS_ROOT="$ROOT" bash "$ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
exec bash "$ROOT/deploy/nexus-1panel.sh" upgrade "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
# 兼容入口 — 请优先使用 deploy/nexus-1panel.sh
|
||||
exec "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/nexus-1panel.sh" upgrade "$@"
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus 6.0 — Upgrade Script(自动识别 Docker / Supervisor)
|
||||
#
|
||||
# sudo bash deploy/upgrade.sh
|
||||
# sudo bash deploy/upgrade.sh --deploy-dir /opt/nexus
|
||||
#
|
||||
# Docker 1Panel:委托 deploy/nexus-1panel.sh upgrade(拉代码 + 备份 + 重建镜像)
|
||||
# Supervisor 裸机:git pull + venv pip + supervisorctl restart
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./detect_deploy_runtime.sh
|
||||
source "${SCRIPT_DIR}/detect_deploy_runtime.sh"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
DEPLOY_DIR=""
|
||||
EXTRA_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
|
||||
--no-backup|--require-backup|--no-cache|--prune|--check|--dry-run)
|
||||
EXTRA_ARGS+=("$1"); shift ;;
|
||||
-h|--help)
|
||||
cat <<'EOF'
|
||||
用法: sudo bash deploy/upgrade.sh [--deploy-dir PATH] [nexus-1panel 升级选项]
|
||||
|
||||
Docker 环境自动走 nexus-1panel.sh upgrade;Supervisor 环境走 venv + supervisorctl。
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*) EXTRA_ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$DEPLOY_DIR" ]]; then
|
||||
DEPLOY_DIR="$(resolve_nexus_root 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$DEPLOY_DIR" || ! -d "$DEPLOY_DIR/server" ]]; then
|
||||
error "找不到 Nexus 安装目录,请使用 --deploy-dir 指定"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUNTIME="$(detect_nexus_runtime "$DEPLOY_DIR")"
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
info "检测到 Docker Compose 运行时,委托 nexus-1panel.sh upgrade"
|
||||
exec env NEXUS_ROOT="$DEPLOY_DIR" bash "$DEPLOY_DIR/deploy/nexus-1panel.sh" upgrade "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
VENV_DIR="$DEPLOY_DIR/venv"
|
||||
ENV_FILE="$DEPLOY_DIR/.env"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 6.0 Upgrade (Supervisor)"
|
||||
echo "=========================================="
|
||||
echo " Deploy: $DEPLOY_DIR"
|
||||
echo ""
|
||||
|
||||
info "Step 1/4: Updating source code..."
|
||||
if [[ -d "$DEPLOY_DIR/.git" ]]; then
|
||||
cd "$DEPLOY_DIR"
|
||||
OLD_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
git pull --ff-only 2>/dev/null || {
|
||||
error "git pull failed. Resolve conflicts: cd $DEPLOY_DIR && git status"
|
||||
exit 1
|
||||
}
|
||||
NEW_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
if [[ "$OLD_REV" == "$NEW_REV" ]]; then
|
||||
info " Already up to date (no new commits)"
|
||||
else
|
||||
info " Updated: ${OLD_REV:0:8}..${NEW_REV:0:8}"
|
||||
fi
|
||||
else
|
||||
warn " Not a git repo. Skip git pull"
|
||||
fi
|
||||
|
||||
info "Step 2/4: Updating Python dependencies..."
|
||||
if [[ -d "$VENV_DIR/bin" ]]; then
|
||||
"$VENV_DIR/bin/pip" install -q --upgrade pip
|
||||
"$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || {
|
||||
warn " pip install had warnings, continuing..."
|
||||
}
|
||||
else
|
||||
error " venv not found at $VENV_DIR. Run install.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Step 3/4: Restarting Nexus..."
|
||||
supervisorctl restart nexus >/dev/null 2>&1 || {
|
||||
warn " supervisorctl restart failed. Try: supervisorctl restart nexus"
|
||||
}
|
||||
|
||||
info "Step 4/4: Verifying health..."
|
||||
PORT="$(nexus_publish_port_for "$DEPLOY_DIR")"
|
||||
HEALTH_OK=false
|
||||
for _ in $(seq 1 15); do
|
||||
if curl -sf "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
|
||||
HEALTH_OK=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [[ "$HEALTH_OK" == true ]]; then
|
||||
echo -e " ${GREEN}Upgrade complete! Nexus is healthy.${NC}"
|
||||
suggest_ops_cron "$DEPLOY_DIR"
|
||||
else
|
||||
echo -e " ${YELLOW}Health check pending. Verify manually:${NC}"
|
||||
echo " curl http://127.0.0.1:${PORT}/health"
|
||||
echo " supervisorctl status nexus"
|
||||
fi
|
||||
echo ""
|
||||
@@ -0,0 +1,437 @@
|
||||
#!/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 "$@"
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post-deploy smoke: login + optional batch endpoint (run on server)."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600")
|
||||
USER = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin")
|
||||
PASSWORD = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "")
|
||||
|
||||
|
||||
def post(path: str, body: dict, headers: dict | None = None) -> dict:
|
||||
h = {"Content-Type": "application/json", **(headers or {})}
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}{path}",
|
||||
data=json.dumps(body).encode(),
|
||||
headers=h,
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.load(resp)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not PASSWORD:
|
||||
print("SKIP: set NEXUS_TEST_ADMIN_PASSWORD", file=sys.stderr)
|
||||
return 2
|
||||
login = post("/api/auth/login", {"username": USER, "password": PASSWORD})
|
||||
token = login.get("access_token")
|
||||
exp = login.get("expires_in")
|
||||
print(f"login ok expires_in={exp}")
|
||||
if exp != 3600:
|
||||
print(f"WARN: expected expires_in=3600, got {exp}")
|
||||
if not token:
|
||||
print("FAIL: no access_token")
|
||||
return 1
|
||||
# Minimal authenticated call
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/api/servers/stats",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
stats = json.load(resp)
|
||||
print(f"servers/stats ok keys={list(stats.keys())[:5]}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"HTTP {e.code}: {e.read().decode()[:500]}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user