forked from Eeveid/lightOps
549 lines
14 KiB
Bash
Executable File
549 lines
14 KiB
Bash
Executable File
#!/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
|