From 89b464d73e016f3e44d883c8bc36d53f9eed5c7e Mon Sep 17 00:00:00 2001 From: Eeveid <448859157@qq.com> Date: Mon, 25 May 2026 01:45:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E4=B8=80=E9=94=AE=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E8=BE=93=E5=87=BA=E5=92=8C=E9=9A=8F=E6=9C=BA=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- scripts/install-server.sh | 228 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 221 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9f20107..b9d0165 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ curl -fsSL https://gitea.kmux.cn/Eeveid/lightOps/raw/branch/main/scripts/install curl -fsSL https://gitea.kmux.cn/Eeveid/lightOps/raw/branch/main/scripts/install-server.sh | bash -s -- --public-url https://panel.example.com ``` -安装脚本会自动安装基础依赖、Rust、Node.js,拉取远程仓库,构建前端和 Rust 二进制,写入 `/etc/lightops/server.toml`,注册并启动 `lightops-server` systemd 服务。安装完成后访问 `/init` 初始化管理员。 +安装脚本会自动安装基础依赖、Rust、Node.js,随机选择未占用端口,拉取远程仓库,构建前端和 Rust 二进制,写入 `/etc/lightops/server.toml`,注册并启动 `lightops-server` systemd 服务。首次安装会自动生成随机管理员账号和密码,并在安装完成后输出内网地址、本机地址、公网地址、端口、用户名和密码。首次凭据会保存到 `/etc/lightops/initial-admin.txt`,请登录面板后立即修改密码。 常用参数: @@ -91,7 +91,7 @@ curl -fsSL https://gitea.kmux.cn/Eeveid/lightOps/raw/branch/main/scripts/install --repo Git 仓库地址 --branch Git 分支,默认 main --install-dir 安装目录,默认 /opt/lightops ---bind 监听地址,默认 0.0.0.0:8080 +--bind 监听地址,默认随机选择未占用端口 --public-url 面板外部访问地址 --skip-deps 跳过依赖安装检查 ``` diff --git a/scripts/install-server.sh b/scripts/install-server.sh index 4f12348..92a051c 100755 --- a/scripts/install-server.sh +++ b/scripts/install-server.sh @@ -5,8 +5,14 @@ REPO_URL="https://gitea.kmux.cn/Eeveid/lightOps.git" BRANCH="main" INSTALL_DIR="/opt/lightops" CONFIG_DIR="/etc/lightops" -BIND_ADDR="0.0.0.0:8080" +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" @@ -22,8 +28,8 @@ LightOps Server 一键安装脚本 --branch Git 分支,默认 main --install-dir 安装目录,默认 /opt/lightops --config-dir 配置目录,默认 /etc/lightops - --bind 监听地址,默认 0.0.0.0:8080 - --public-url 面板访问地址,默认根据本机 IP 生成 http://:8080 + --bind 监听地址,默认随机选择一个空闲端口 + --public-url 面板访问地址,默认根据本机内网 IP 和随机端口生成 --skip-deps 跳过系统依赖、Rust、Node.js 检查安装 --force 仓库存在未提交改动时仍继续,谨慎使用 -h, --help 显示帮助 @@ -54,10 +60,14 @@ while [[ $# -gt 0 ]]; do ;; --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) @@ -131,6 +141,110 @@ detect_public_url() { 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 @@ -304,35 +418,131 @@ EOF start_service() { log "启动 LightOps Server" systemctl restart lightops-server - sleep 2 - if ! systemctl is-active --quiet lightops-server; then - journalctl -u lightops-server -n 120 --no-pager >&2 || true - fail "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" <