Files
lightOps/scripts/install-server.sh

549 lines
14 KiB
Bash
Executable File
Raw Permalink 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
set -Eeuo pipefail
REPO_URL="https://gitea.kmux.cn/Eeveid/lightOps.git"
BRANCH="main"
INSTALL_DIR="/opt/lightops"
CONFIG_DIR="/etc/lightops"
BIND_ADDR=""
BIND_ADDR_SET="false"
BIND_ADDR_SOURCE="auto"
ADMIN_USER=""
ADMIN_PASS=""
PUBLIC_URL=""
PUBLIC_URL_SET="false"
PUBLIC_URL_SOURCE="auto"
SKIP_DEPS="false"
FORCE="false"
usage() {
cat <<'EOF'
LightOps Server 一键安装脚本
用法:
bash install-server.sh [选项]
选项:
--repo <url> Git 仓库地址,默认 https://gitea.kmux.cn/Eeveid/lightOps.git
--branch <name> Git 分支,默认 main
--install-dir <path> 安装目录,默认 /opt/lightops
--config-dir <path> 配置目录,默认 /etc/lightops
--bind <addr> 监听地址,默认随机选择一个空闲端口
--public-url <url> 面板访问地址,默认根据本机内网 IP 和随机端口生成
--skip-deps 跳过系统依赖、Rust、Node.js 检查安装
--force 仓库存在未提交改动时仍继续,谨慎使用
-h, --help 显示帮助
示例:
curl -fsSL https://gitea.kmux.cn/Eeveid/lightOps/raw/branch/main/scripts/install-server.sh | bash
curl -fsSL https://gitea.kmux.cn/Eeveid/lightOps/raw/branch/main/scripts/install-server.sh | bash -s -- --public-url https://panel.example.com
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--repo)
REPO_URL="${2:?缺少 --repo 参数值}"
shift 2
;;
--branch)
BRANCH="${2:?缺少 --branch 参数值}"
shift 2
;;
--install-dir)
INSTALL_DIR="${2:?缺少 --install-dir 参数值}"
shift 2
;;
--config-dir)
CONFIG_DIR="${2:?缺少 --config-dir 参数值}"
shift 2
;;
--bind)
BIND_ADDR="${2:?缺少 --bind 参数值}"
BIND_ADDR_SET="true"
BIND_ADDR_SOURCE="user"
shift 2
;;
--public-url)
PUBLIC_URL="${2:?缺少 --public-url 参数值}"
PUBLIC_URL_SET="true"
PUBLIC_URL_SOURCE="user"
shift 2
;;
--skip-deps)
SKIP_DEPS="true"
shift
;;
--force)
FORCE="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "未知参数:$1" >&2
usage >&2
exit 2
;;
esac
done
if [[ "$(id -u)" -ne 0 ]]; then
echo "请使用 root 用户运行例如curl -fsSL ... | bash" >&2
exit 1
fi
if [[ "$(uname -s)" != "Linux" ]]; then
echo "LightOps Server 一键安装当前只支持 Linux 服务器" >&2
exit 1
fi
if ! command -v systemctl >/dev/null 2>&1; then
echo "未检测到 systemd当前一键安装脚本需要 systemd 管理服务" >&2
exit 1
fi
case "$INSTALL_DIR" in
""|"/"|"/opt"|"/etc"|"/usr"|"/usr/local")
echo "安装目录过于危险:$INSTALL_DIR" >&2
exit 1
;;
esac
if [[ "$INSTALL_DIR" == *" "* || "$CONFIG_DIR" == *" "* ]]; then
echo "安装目录和配置目录暂不支持空格" >&2
exit 1
fi
log() {
printf '\033[1;32m[LightOps]\033[0m %s\n' "$*"
}
warn() {
printf '\033[1;33m[LightOps]\033[0m %s\n' "$*" >&2
}
fail() {
printf '\033[1;31m[LightOps]\033[0m %s\n' "$*" >&2
exit 1
}
detect_public_url() {
local port
port="${BIND_ADDR##*:}"
local ip
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
if [[ -z "$ip" ]]; then
ip="127.0.0.1"
fi
echo "http://${ip}:${port}"
}
primary_lan_ip() {
local ip
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
if [[ -z "$ip" ]]; then
ip="127.0.0.1"
fi
echo "$ip"
}
external_ip() {
curl -fsS --max-time 3 https://api.ipify.org 2>/dev/null || true
}
port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$" && return 0
fi
if command -v netstat >/dev/null 2>&1; then
netstat -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$" && return 0
fi
if command -v lsof >/dev/null 2>&1; then
lsof -iTCP:"$port" -sTCP:LISTEN -n -P >/dev/null 2>&1 && return 0
fi
return 1
}
random_port() {
local value
if command -v shuf >/dev/null 2>&1; then
shuf -i 20000-49999 -n 1
return
fi
value="$(od -An -N2 -tu2 /dev/urandom 2>/dev/null | tr -d ' ')"
echo $((20000 + value % 30000))
}
choose_bind_addr() {
local host port candidate i
if [[ "$BIND_ADDR_SET" == "true" ]]; then
port="${BIND_ADDR##*:}"
if [[ ! "$port" =~ ^[0-9]+$ || "$port" -lt 1 || "$port" -gt 65535 ]]; then
fail "监听端口无效:$BIND_ADDR"
fi
if [[ "$BIND_ADDR_SOURCE" == "user" ]] && port_in_use "$port"; then
fail "端口 $port 已被占用,请使用 --bind 指定其他端口"
fi
return
fi
host="0.0.0.0"
for i in $(seq 1 80); do
candidate="$(random_port)"
if ! port_in_use "$candidate"; then
BIND_ADDR="${host}:${candidate}"
log "已选择空闲端口:$candidate"
return
fi
done
fail "未能找到可用端口,请使用 --bind 手动指定"
}
random_token() {
local length="${1:-20}"
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$((length / 2 + 4))" | cut -c1-"$length"
else
od -An -N"$((length / 2 + 4))" -tx1 /dev/urandom | tr -d ' \n' | cut -c1-"$length"
fi
}
json_escape() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//$'\n'/\\n}"
printf '%s' "$value"
}
toml_string_value() {
local key="$1"
local file="$2"
sed -nE "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*\"(.*)\"[[:space:]]*$/\\1/p" "$file" | tail -n 1
}
load_existing_config_values() {
local config_file existing_bind existing_public_url
config_file="$CONFIG_DIR/server.toml"
if [[ ! -f "$config_file" ]]; then
return
fi
existing_bind="$(toml_string_value "bind" "$config_file")"
existing_public_url="$(toml_string_value "public_url" "$config_file")"
if [[ "$BIND_ADDR_SOURCE" != "user" && -n "$existing_bind" ]]; then
BIND_ADDR="$existing_bind"
BIND_ADDR_SET="true"
BIND_ADDR_SOURCE="existing"
log "沿用已有监听地址:$BIND_ADDR"
fi
if [[ "$PUBLIC_URL_SET" != "true" && -n "$existing_public_url" ]]; then
PUBLIC_URL="$existing_public_url"
PUBLIC_URL_SOURCE="existing"
fi
}
install_packages_apt() {
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y ca-certificates curl git build-essential pkg-config sqlite3
}
install_packages_generic() {
if command -v apt-get >/dev/null 2>&1; then
install_packages_apt
elif command -v dnf >/dev/null 2>&1; then
dnf install -y ca-certificates curl git gcc gcc-c++ make pkgconf-pkg-config sqlite
elif command -v yum >/dev/null 2>&1; then
yum install -y ca-certificates curl git gcc gcc-c++ make pkgconfig sqlite
elif command -v pacman >/dev/null 2>&1; then
pacman -Sy --noconfirm ca-certificates curl git base-devel pkgconf sqlite
else
fail "未识别系统包管理器,请手动安装 curl、git、C/C++ 构建工具、pkg-config、sqlite3"
fi
}
node_major() {
node -v 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/' || true
}
ensure_node() {
local major
major="$(node_major)"
if [[ -n "$major" && "$major" -ge 18 ]] && command -v npm >/dev/null 2>&1; then
log "Node.js 已安装:$(node -v)"
return
fi
if command -v apt-get >/dev/null 2>&1; then
log "安装 Node.js 20"
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
elif command -v dnf >/dev/null 2>&1; then
dnf install -y nodejs npm
elif command -v yum >/dev/null 2>&1; then
yum install -y nodejs npm
elif command -v pacman >/dev/null 2>&1; then
pacman -Sy --noconfirm nodejs npm
else
fail "无法自动安装 Node.js请手动安装 Node.js 18 或更高版本"
fi
major="$(node_major)"
if [[ -z "$major" || "$major" -lt 18 ]]; then
fail "Node.js 版本过低,需要 18 或更高版本,当前:$(node -v 2>/dev/null || echo 未安装)"
fi
}
ensure_rust() {
if command -v cargo >/dev/null 2>&1; then
log "Rust 已安装:$(cargo --version)"
return
fi
log "安装 Rust 稳定版工具链"
curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal
# shellcheck disable=SC1091
source "$HOME/.cargo/env"
if ! command -v cargo >/dev/null 2>&1; then
fail "Rust 安装失败,请检查网络或手动安装 rustup"
fi
}
ensure_deps() {
if [[ "$SKIP_DEPS" == "true" ]]; then
warn "已跳过依赖安装检查"
return
fi
log "安装系统依赖"
install_packages_generic
ensure_node
ensure_rust
}
prepare_repo() {
log "准备代码仓库:$INSTALL_DIR"
mkdir -p "$(dirname "$INSTALL_DIR")"
if [[ -d "$INSTALL_DIR/.git" ]]; then
cd "$INSTALL_DIR"
if [[ "$FORCE" != "true" && -n "$(git status --porcelain)" ]]; then
fail "安装目录存在未提交改动:$INSTALL_DIR。请先处理改动,或使用 --force"
fi
git remote set-url origin "$REPO_URL" || true
git fetch origin "$BRANCH"
git checkout "$BRANCH"
git pull --ff-only origin "$BRANCH"
elif [[ -e "$INSTALL_DIR" && -n "$(find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then
fail "安装目录已存在且不是 Git 仓库:$INSTALL_DIR"
else
rm -rf "$INSTALL_DIR"
git clone --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
fi
}
build_project() {
log "构建前端"
cd "$INSTALL_DIR/web"
if [[ -f package-lock.json ]]; then
npm ci
else
npm install
fi
npm run build
log "构建 Server 和 Agent"
cd "$INSTALL_DIR"
cargo build --release -p lightops-server -p lightops-agent
install -m 0755 "$INSTALL_DIR/target/release/lightops-server" "$INSTALL_DIR/lightops-server"
install -m 0755 "$INSTALL_DIR/target/release/lightops-agent" "$INSTALL_DIR/lightops-agent"
install -m 0755 "$INSTALL_DIR/target/release/lightops-server" /usr/local/bin/lightops-server
install -m 0755 "$INSTALL_DIR/target/release/lightops-agent" /usr/local/bin/lightops-agent
}
write_config() {
mkdir -p "$CONFIG_DIR" "$INSTALL_DIR"
if [[ -z "$PUBLIC_URL" ]]; then
PUBLIC_URL="$(detect_public_url)"
fi
local jwt_secret
if command -v openssl >/dev/null 2>&1; then
jwt_secret="$(openssl rand -hex 32)"
else
jwt_secret="$(date +%s%N)-$(hostname)-$RANDOM-$RANDOM"
fi
if [[ -f "$CONFIG_DIR/server.toml" ]]; then
log "保留已有配置:$CONFIG_DIR/server.toml"
return
fi
log "生成 Server 配置:$CONFIG_DIR/server.toml"
cat >"$CONFIG_DIR/server.toml" <<EOF
bind = "$BIND_ADDR"
database_url = "sqlite://$INSTALL_DIR/lightops.db?mode=rwc"
jwt_secret = "$jwt_secret"
public_url = "$PUBLIC_URL"
static_dir = "$INSTALL_DIR/web/dist"
registration_token_ttl_minutes = 30
task_timeout_secs = 20
EOF
chmod 0600 "$CONFIG_DIR/server.toml"
}
write_service() {
log "写入 systemd 服务"
cat >/etc/systemd/system/lightops-server.service <<EOF
[Unit]
Description=LightOps Server
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=10
[Service]
Type=simple
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/lightops-server --config $CONFIG_DIR/server.toml
Restart=always
RestartSec=5
KillSignal=SIGINT
TimeoutStopSec=20
User=root
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable lightops-server
}
start_service() {
log "启动 LightOps Server"
systemctl restart lightops-server
wait_service
}
wait_service() {
local i
for i in $(seq 1 30); do
if systemctl is-active --quiet lightops-server; then
if curl -fsS --max-time 2 "http://127.0.0.1:${BIND_ADDR##*:}/" >/dev/null 2>&1; then
return
fi
fi
sleep 1
done
journalctl -u lightops-server -n 120 --no-pager >&2 || true
fail "LightOps Server 启动失败或 HTTP 接口未就绪"
}
database_file() {
echo "$INSTALL_DIR/lightops.db"
}
admin_exists() {
local db_file
db_file="$(database_file)"
if [[ ! -f "$db_file" ]]; then
return 1
fi
local count
count="$(sqlite3 "$db_file" "SELECT COUNT(*) FROM users;" 2>/dev/null || echo 0)"
[[ "${count:-0}" -gt 0 ]]
}
initialize_admin() {
local credential_file payload init_url response
credential_file="$CONFIG_DIR/initial-admin.txt"
if admin_exists; then
log "检测到已有管理员账号,跳过随机初始化"
ADMIN_USER=""
ADMIN_PASS=""
return
fi
ADMIN_USER="lightops_$(random_token 8 | tr 'A-Z' 'a-z')"
ADMIN_PASS="$(random_token 24)"
payload="{\"username\":\"$(json_escape "$ADMIN_USER")\",\"password\":\"$(json_escape "$ADMIN_PASS")\"}"
init_url="http://127.0.0.1:${BIND_ADDR##*:}/api/auth/init"
log "初始化随机管理员账号"
response="$(curl -fsS --max-time 10 -H 'Content-Type: application/json' -d "$payload" "$init_url" 2>/dev/null || true)"
if [[ "$response" == *"管理员已初始化"* ]]; then
log "检测到管理员已初始化,跳过随机初始化"
ADMIN_USER=""
ADMIN_PASS=""
return
fi
if [[ -z "$response" || "$response" != *'"success":true'* ]]; then
fail "管理员初始化失败请查看服务日志journalctl -u lightops-server -n 120 --no-pager"
fi
cat >"$credential_file" <<EOF
LightOps 首次安装管理员凭据
用户名:$ADMIN_USER
密码:$ADMIN_PASS
请登录面板后立即修改密码。
EOF
chmod 0600 "$credential_file"
}
print_result() {
local port
local lan_ip wan_ip local_url lan_url wan_url credential_file
port="${BIND_ADDR##*:}"
lan_ip="$(primary_lan_ip)"
wan_ip="$(external_ip)"
local_url="http://127.0.0.1:${port}"
lan_url="http://${lan_ip}:${port}"
credential_file="$CONFIG_DIR/initial-admin.txt"
if [[ "$PUBLIC_URL_SOURCE" != "auto" && -n "$PUBLIC_URL" ]]; then
wan_url="$PUBLIC_URL"
elif [[ -n "$wan_ip" ]]; then
wan_url="http://${wan_ip}:${port}"
else
wan_url="未检测到公网 IP可使用 --public-url 指定"
fi
cat <<EOF
LightOps Server 已安装完成
内网地址:$lan_url
本机地址:$local_url
公网地址:$wan_url
监听端口:$port
安装目录:$INSTALL_DIR
配置文件:$CONFIG_DIR/server.toml
服务名称lightops-server
查看日志journalctl -u lightops-server -f
首次凭据:$credential_file
EOF
if [[ -n "$ADMIN_USER" && -n "$ADMIN_PASS" ]]; then
cat <<EOF
管理员账号:$ADMIN_USER
管理员密码:$ADMIN_PASS
EOF
else
cat <<EOF
管理员账号:已存在,未重置
管理员密码:已存在,未重置
EOF
fi
cat <<EOF
请登录面板后修改管理员密码。
如果服务器启用了防火墙,请放行 TCP $port。
EOF
}
ensure_deps
load_existing_config_values
choose_bind_addr
prepare_repo
build_project
write_config
write_service
start_service
initialize_admin
print_result