1
0
forked from Eeveid/lightOps
Files
lightOps/scripts/install-server-release.sh

456 lines
12 KiB
Bash
Executable File
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
set -Eeuo pipefail
PACKAGE_URL=""
SHA256=""
INSTALL_DIR="/opt/lightops"
CONFIG_DIR="/etc/lightops"
BIND_ADDR=""
BIND_ADDR_SET="false"
BIND_ADDR_SOURCE="auto"
PUBLIC_URL=""
PUBLIC_URL_SET="false"
PUBLIC_URL_SOURCE="auto"
ADMIN_USER=""
ADMIN_PASS=""
usage() {
cat <<'EOF'
LightOps Server 发布包安装脚本
用法:
bash install-server-release.sh --url <发布包下载地址> [选项]
选项:
--url <url> lightops-*.tar.gz 下载地址,必填
--sha256 <hash> 发布包 SHA256可选
--install-dir <path> 安装目录,默认 /opt/lightops
--config-dir <path> 配置目录,默认 /etc/lightops
--bind <addr> 监听地址,默认随机选择一个空闲端口
--public-url <url> 面板访问地址,默认根据公网 IP 或内网 IP 输出
-h, --help 显示帮助
示例:
curl -fsSL https://gitea.kmux.cn/Eeveid/lightOps/raw/branch/main/scripts/install-server-release.sh | bash -s -- --url https://example.com/lightops.tar.gz
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--url)
PACKAGE_URL="${2:?缺少 --url 参数值}"
shift 2
;;
--sha256)
SHA256="${2:?缺少 --sha256 参数值}"
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
;;
-h|--help)
usage
exit 0
;;
*)
echo "未知参数:$1" >&2
usage >&2
exit 2
;;
esac
done
if [[ "$(id -u)" -ne 0 ]]; then
echo "请使用 root 用户运行" >&2
exit 1
fi
if [[ "$(uname -s)" != "Linux" ]]; then
echo "发布包安装当前只支持 Linux 服务器" >&2
exit 1
fi
if [[ -z "$PACKAGE_URL" ]]; then
usage >&2
exit 2
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' "$*"
}
fail() {
printf '\033[1;31m[LightOps]\033[0m %s\n' "$*" >&2
exit 1
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "缺少命令:$1"
}
install_runtime_deps() {
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y ca-certificates curl tar sqlite3
elif command -v dnf >/dev/null 2>&1; then
dnf install -y ca-certificates curl tar sqlite
elif command -v yum >/dev/null 2>&1; then
yum install -y ca-certificates curl tar sqlite
elif command -v pacman >/dev/null 2>&1; then
pacman -Sy --noconfirm ca-certificates curl tar sqlite
fi
need_cmd curl
need_cmd tar
need_cmd sqlite3
}
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
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))
}
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
}
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 手动指定"
}
download_package() {
local tmp_dir archive
tmp_dir="$(mktemp -d)"
archive="$tmp_dir/lightops.tar.gz"
log "下载发布包:$PACKAGE_URL" >&2
curl -fL --retry 3 --connect-timeout 20 -o "$archive" "$PACKAGE_URL"
if [[ -n "$SHA256" ]]; then
local actual
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$archive" | awk '{print $1}')"
else
actual="$(openssl dgst -sha256 "$archive" | awk '{print $2}')"
fi
[[ "$actual" == "$SHA256" ]] || fail "发布包 SHA256 校验失败"
fi
echo "$archive"
}
install_package() {
local archive tmp_dir src
archive="$1"
tmp_dir="$(mktemp -d)"
tar -xzf "$archive" -C "$tmp_dir"
src="$(find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
[[ -n "$src" ]] || fail "发布包结构无效"
[[ -x "$src/bin/lightops-server" ]] || fail "发布包缺少 bin/lightops-server"
[[ -x "$src/bin/lightops-agent" ]] || fail "发布包缺少 bin/lightops-agent"
[[ -d "$src/web/dist" ]] || fail "发布包缺少 web/dist"
log "安装发布包到:$INSTALL_DIR"
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR"
rm -rf "$INSTALL_DIR/web/dist"
mkdir -p "$INSTALL_DIR/bin" "$INSTALL_DIR/web"
cp -a "$src/bin/lightops-server" "$INSTALL_DIR/lightops-server"
cp -a "$src/bin/lightops-agent" "$INSTALL_DIR/lightops-agent"
cp -a "$src/bin" "$INSTALL_DIR/bin"
cp -a "$src/web/dist" "$INSTALL_DIR/web/dist"
[[ -d "$src/store" ]] && cp -a "$src/store" "$INSTALL_DIR/store"
[[ -d "$src/scripts" ]] && cp -a "$src/scripts" "$INSTALL_DIR/scripts"
chmod 0755 "$INSTALL_DIR/lightops-server" "$INSTALL_DIR/lightops-agent"
install -m 0755 "$INSTALL_DIR/lightops-server" /usr/local/bin/lightops-server
install -m 0755 "$INSTALL_DIR/lightops-agent" /usr/local/bin/lightops-agent
}
write_config() {
if [[ -z "$PUBLIC_URL" ]]; then
PUBLIC_URL="http://$(primary_lan_ip):${BIND_ADDR##*:}"
fi
if [[ -f "$CONFIG_DIR/server.toml" ]]; then
log "保留已有配置:$CONFIG_DIR/server.toml"
return
fi
local jwt_secret
jwt_secret="$(random_token 64)"
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() {
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
}
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 接口未就绪"
}
start_service() {
systemctl restart lightops-server
wait_service
}
database_file() {
echo "$INSTALL_DIR/lightops.db"
}
admin_exists() {
local db_file count
db_file="$(database_file)"
[[ -f "$db_file" ]] || return 1
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
ADMIN_USER=""
ADMIN_PASS=""
return
fi
ADMIN_USER="lightops_$(random_token 8)"
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"
response="$(curl -fsS --max-time 10 -H 'Content-Type: application/json' -d "$payload" "$init_url" 2>/dev/null || true)"
if [[ "$response" == *"管理员已初始化"* ]]; then
ADMIN_USER=""
ADMIN_PASS=""
return
fi
[[ "$response" == *'"success":true'* ]] || fail "管理员初始化失败"
cat >"$credential_file" <<EOF
LightOps 首次安装管理员凭据
用户名:$ADMIN_USER
密码:$ADMIN_PASS
请登录面板后立即修改密码。
EOF
chmod 0600 "$credential_file"
}
print_result() {
local port 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
}
install_runtime_deps
load_existing_config_values
choose_bind_addr
ARCHIVE="$(download_package)"
install_package "$ARCHIVE"
write_config
write_service
start_service
initialize_admin
print_result