Files
HyperNode-AI/deploy/manage.sh
T

378 lines
10 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# HyperNode-AI 部署管理器 — 一键部署 / 卸载 / 更新
#
# 新服务器:
# curl -fsSL https://git.bz121.com/dekun/HyperNode-AI/raw/branch/main/deploy/manage.sh | bash
#
# 已安装:
# bash /opt/hypernode/deploy/manage.sh
#
set -e
if [[ -n "${BASH_VERSION:-}" ]]; then
set -o pipefail
fi
INSTALL_ROOT="${INSTALL_ROOT:-/opt/hypernode}"
DATA_DIR="${DATA_DIR:-/var/lib/hypernode}"
GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/HyperNode-AI.git}"
GIT_BRANCH="${GIT_BRANCH:-main}"
APP_USER="hypernode"
SERVICE_NAME="hypernode"
BIND_HOST="0.0.0.0"
BIND_PORT="33333"
ENV_FILE="${INSTALL_ROOT}/.env"
GENERATED_ADMIN_PASSWORD=""
_script_src="${BASH_SOURCE[0]:-}"
if [[ -n "${_script_src}" && -f "${_script_src}" ]]; then
REPO_ROOT="$(cd "$(dirname "${_script_src}")/.." && pwd)"
else
REPO_ROOT=""
fi
unset _script_src
set -u
log() {
printf '\n==> %s\n' "$*"
}
ask() {
local value=""
if [[ ! -r /dev/tty ]]; then
echo "请在终端中运行。通过 curl 管道执行时,也需要当前会话有终端。" >&2
exit 1
fi
if ! read -r -p "$1" value </dev/tty; then
exit 0
fi
printf '%s' "${value}"
}
repo_ready() {
[[ -f "${1}/app/main.py" && -f "${1}/requirements.txt" && -f "${1}/deploy/manage.sh" ]]
}
git_in_install() {
git -c safe.directory="${INSTALL_ROOT}" -C "${INSTALL_ROOT}" "$@"
}
require_root() {
if [[ "$(id -u)" -ne 0 ]]; then
echo "请使用 root 执行。可以先 sudo -i,再运行安装命令。" >&2
echo "或:curl -fsSL https://git.bz121.com/dekun/HyperNode-AI/raw/branch/main/deploy/manage.sh | sudo bash" >&2
exit 1
fi
}
ensure_git() {
if command -v git >/dev/null 2>&1; then
return 0
fi
if ! command -v apt-get >/dev/null 2>&1; then
echo "未找到 git,且当前系统没有 apt-get。" >&2
exit 1
fi
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y git ca-certificates curl
}
bootstrap_repo() {
if repo_ready "${INSTALL_ROOT}"; then
REPO_ROOT="${INSTALL_ROOT}"
return 0
fi
if [[ -n "${REPO_ROOT}" ]] && repo_ready "${REPO_ROOT}"; then
return 0
fi
echo "HyperNode-AI 部署管理器 — 首次自举"
echo "将克隆到: ${INSTALL_ROOT}"
require_root
ensure_git
if [[ -e "${INSTALL_ROOT}" ]]; then
echo "错误: ${INSTALL_ROOT} 已存在,但不是有效的 HyperNode-AI 目录。" >&2
echo "请先处理该目录,或设置 INSTALL_ROOT 指向其他路径。" >&2
exit 1
fi
git clone --branch "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}"
exec bash "${INSTALL_ROOT}/deploy/manage.sh" "$@" </dev/tty
}
install_packages() {
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y python3 python3-venv python3-pip git ca-certificates curl rsync
python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)'
}
ensure_user() {
if ! id -u "${APP_USER}" >/dev/null 2>&1; then
useradd --system --home-dir "${INSTALL_ROOT}" --shell /usr/sbin/nologin "${APP_USER}"
fi
}
ensure_install_tree() {
if repo_ready "${INSTALL_ROOT}"; then
return 0
fi
if [[ -n "${REPO_ROOT}" && "${REPO_ROOT}" != "${INSTALL_ROOT}" ]] && repo_ready "${REPO_ROOT}"; then
mkdir -p "${INSTALL_ROOT}"
rsync -a --delete \
--exclude '.venv/' \
--exclude '.env' \
--exclude '__pycache__/' \
--exclude '*.db' \
"${REPO_ROOT}/" "${INSTALL_ROOT}/"
return 0
fi
ensure_git
git clone --branch "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}"
}
write_env() {
mkdir -p "${DATA_DIR}"
if [[ -f "${ENV_FILE}" ]]; then
log "保留已有配置 ${ENV_FILE}"
return 0
fi
local jwt_secret admin_password
jwt_secret="$(openssl rand -hex 32)"
admin_password="$(openssl rand -hex 12)"
GENERATED_ADMIN_PASSWORD="${admin_password}"
log "写入 ${ENV_FILE}"
cat >"${ENV_FILE}" <<EOF
DATABASE_URL=sqlite:////${DATA_DIR}/hypernode.db
YIELD_TIMEZONE=Asia/Shanghai
YIELD_HOUR=0
YIELD_MINUTE=5
DISABLE_SCHEDULER=false
JWT_SECRET=${jwt_secret}
ADMIN_USERNAME=admin
ADMIN_PASSWORD=${admin_password}
ADMIN_WALLET=admin-root
EOF
}
install_python_deps() {
if [[ ! -x "${INSTALL_ROOT}/.venv/bin/python" ]]; then
python3 -m venv "${INSTALL_ROOT}/.venv"
fi
"${INSTALL_ROOT}/.venv/bin/pip" install --upgrade pip
"${INSTALL_ROOT}/.venv/bin/pip" install -r "${INSTALL_ROOT}/requirements.txt"
}
write_service() {
cat >"/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=HyperNode-AI control plane
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${INSTALL_ROOT}
EnvironmentFile=${ENV_FILE}
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart=${INSTALL_ROOT}/.venv/bin/uvicorn app.main:app --host ${BIND_HOST} --port ${BIND_PORT} --workers 1
Restart=always
RestartSec=3
TimeoutStopSec=20
KillSignal=SIGINT
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
}
fix_permissions() {
mkdir -p "${DATA_DIR}"
chown -R "${APP_USER}:${APP_USER}" "${INSTALL_ROOT}" "${DATA_DIR}"
chmod 750 "${INSTALL_ROOT}" "${DATA_DIR}"
if [[ -f "${ENV_FILE}" ]]; then
chmod 640 "${ENV_FILE}"
fi
}
wait_until_ready() {
local ready=0
local _i
echo "等待服务监听 ${BIND_PORT} ..."
for ((_i = 1; _i <= 40; _i++)); do
if curl -fsS "http://127.0.0.1:${BIND_PORT}/health" >/dev/null 2>&1; then
ready=1
break
fi
sleep 1
done
if [[ "${ready}" -ne 1 ]]; then
echo "服务未在 40 秒内就绪。状态和最近日志:" >&2
systemctl status "${SERVICE_NAME}.service" --no-pager >&2 || true
journalctl -u "${SERVICE_NAME}.service" -n 80 --no-pager >&2 || true
return 1
fi
echo "健康检查通过:http://127.0.0.1:${BIND_PORT}/health"
}
verify_app() {
log "检查程序能否启动"
if ! /usr/sbin/runuser -u "${APP_USER}" -- bash -c "cd '${INSTALL_ROOT}' && exec '${INSTALL_ROOT}/.venv/bin/python' -c 'import app.main'"; then
echo "程序导入失败,没有重启服务。请把上面的报错发出来。" >&2
return 1
fi
}
cmd_deploy() {
log "一键部署"
install_packages
ensure_user
ensure_install_tree
write_env
install_python_deps
fix_permissions
write_service
verify_app || return 1
systemctl enable "${SERVICE_NAME}.service"
systemctl restart "${SERVICE_NAME}.service"
wait_until_ready || return 1
log "部署完成"
echo "监听地址:${BIND_HOST}:${BIND_PORT}"
echo "健康检查:curl http://127.0.0.1:${BIND_PORT}/health"
echo "控制台:http://<服务器 IP>:${BIND_PORT}/"
echo "接口文档:http://<服务器 IP>:${BIND_PORT}/docs"
if [[ -n "${GENERATED_ADMIN_PASSWORD}" ]]; then
echo "管理员账号:admin"
echo "管理员密码:${GENERATED_ADMIN_PASSWORD}"
echo "密码写在 ${ENV_FILE},请立刻记下。"
fi
echo "程序目录:${INSTALL_ROOT}"
echo "数据库:${DATA_DIR}/hypernode.db"
echo "日志:journalctl -u ${SERVICE_NAME}.service -f"
echo "反代请自行配置,上游地址使用 127.0.0.1:${BIND_PORT}"
}
cmd_update() {
if [[ ! -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
echo "尚未部署。请先选择「一键部署」。" >&2
return 1
fi
log "更新程序"
install_packages
if [[ -d "${INSTALL_ROOT}/.git" ]]; then
if ! git_in_install diff --quiet -- web; then
log "控制台文件与仓库不一致,按仓库版本覆盖 web/"
git_in_install checkout -- web
fi
git_in_install pull --ff-only origin "${GIT_BRANCH}"
else
ensure_install_tree
fi
install_python_deps
fix_permissions
write_service
verify_app || return 1
systemctl restart "${SERVICE_NAME}.service"
wait_until_ready || return 1
log "更新完成,配置和数据库已保留"
}
cmd_uninstall() {
local answer
echo "即将停止服务并删除 ${INSTALL_ROOT} 与 systemd 单元。"
answer="$(ask "确认卸载请输入 yes:")"
if [[ "${answer}" != "yes" ]]; then
echo "已取消卸载。"
return 0
fi
log "卸载服务"
systemctl stop "${SERVICE_NAME}.service" >/dev/null 2>&1 || true
systemctl disable "${SERVICE_NAME}.service" >/dev/null 2>&1 || true
rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
systemctl daemon-reload
rm -rf "${INSTALL_ROOT}"
answer="$(ask "同时删除数据库 ${DATA_DIR} ?输入 yes 删除,其他键保留:")"
if [[ "${answer}" == "yes" ]]; then
rm -rf "${DATA_DIR}"
echo "数据库已删除。"
else
echo "数据库已保留在 ${DATA_DIR}"
fi
if id -u "${APP_USER}" >/dev/null 2>&1; then
userdel "${APP_USER}" || echo "系统用户 ${APP_USER} 未删除,可稍后执行 userdel ${APP_USER}"
fi
log "卸载完成"
}
detect_ip() {
hostname -I 2>/dev/null | awk '{print $1}'
}
show_banner() {
local ip path
ip="$(detect_ip)"
path="${INSTALL_ROOT}"
[[ -n "${ip}" ]] || ip="?"
echo ""
echo "========================================"
echo " HyperNode-AI 部署管理器"
echo " 仓库: git.bz121.com/dekun/HyperNode-AI"
echo " 路径: ${path}"
echo " 端口: ${BIND_PORT}"
echo " IP: ${ip}"
echo "========================================"
}
show_menu() {
echo " 1) 一键部署"
echo " 2) 一键卸载"
echo " 3) 更新"
echo " 0) 退出"
echo ""
}
main_menu() {
local choice
bootstrap_repo "$@"
require_root
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
source /etc/os-release
if [[ "${ID:-}" != "ubuntu" ]]; then
echo "当前系统是 ${ID:-unknown},这个脚本用于 Ubuntu 22.04。" >&2
exit 1
fi
if [[ "${VERSION_ID:-}" != "22.04" ]]; then
echo "警告:当前是 Ubuntu ${VERSION_ID:-unknown},脚本按 22.04 编写,将继续。" >&2
fi
fi
while true; do
show_banner
show_menu
choice="$(ask "请选择 [0-3]: ")"
case "${choice}" in
1) cmd_deploy || echo "部署失败。" ;;
2) cmd_uninstall || echo "卸载失败。" ;;
3) cmd_update || echo "更新失败。" ;;
0)
echo "已退出。"
exit 0
;;
*) echo "无效选项:${choice}" ;;
esac
ask "按 Enter 返回菜单..." >/dev/null
done
}
main_menu "$@"