新增发布包打包和安装流程

This commit is contained in:
2026-05-25 10:35:54 +08:00
parent 89b464d73e
commit 8ea2fd028e
3 changed files with 673 additions and 0 deletions

View File

@@ -71,6 +71,30 @@ lightops/
## 一条命令安装 Server
推荐生产环境使用“发布包安装”。目标服务器只需要下载发布包、解压并注册 systemd 服务,不需要安装 Rust、Node.js也不需要现场编译。
先在构建机或 CI 上生成发布包:
```bash
bash scripts/build-release.sh --target x86_64-unknown-linux-gnu
```
生成的文件在 `target/releases/`,把 `lightops-*.tar.gz` 上传到 Gitea Release、对象存储或任意可访问的下载地址。然后在目标 Linux 服务器上执行:
```bash
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
```
如果同时提供 SHA256
```bash
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 --sha256 <sha256>
```
发布包安装脚本会随机选择未占用端口,生成随机管理员账号和密码,并输出内网地址、本机地址、公网地址、端口、用户名和密码。首次凭据会保存到 `/etc/lightops/initial-admin.txt`
源码安装适合开发环境或没有发布包时使用,会在目标服务器现场安装 Rust、Node.js 并编译。
在目标 Linux 服务器上使用 root 执行:
```bash

194
scripts/build-release.sh Executable file
View File

@@ -0,0 +1,194 @@
#!/usr/bin/env bash
set -Eeuo pipefail
VERSION=""
TARGET=""
OUTPUT_DIR="target/releases"
SKIP_BUILD="false"
usage() {
cat <<'EOF'
LightOps 发布包打包脚本
用法:
bash scripts/build-release.sh [选项]
选项:
--version <version> 发布版本,默认读取 Cargo.toml workspace 版本并追加 git 短提交
--target <triple> Rust target triple例如 x86_64-unknown-linux-gnu
--output-dir <path> 输出目录,默认 target/releases
--skip-build 跳过构建,只打包现有产物
-h, --help 显示帮助
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
VERSION="${2:?缺少 --version 参数值}"
shift 2
;;
--target)
TARGET="${2:?缺少 --target 参数值}"
shift 2
;;
--output-dir)
OUTPUT_DIR="${2:?缺少 --output-dir 参数值}"
shift 2
;;
--skip-build)
SKIP_BUILD="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "未知参数:$1" >&2
usage >&2
exit 2
;;
esac
done
log() {
printf '\033[1;32m[LightOps]\033[0m %s\n' "$*"
}
fail() {
printf '\033[1;31m[LightOps]\033[0m %s\n' "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "缺少命令:$1"
}
repo_root() {
git rev-parse --show-toplevel 2>/dev/null || pwd
}
detect_version() {
local base sha
base="$(sed -nE '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p}' Cargo.toml | head -n 1)"
if [[ -z "$base" ]]; then
base="0.1.0"
fi
sha="$(git rev-parse --short HEAD 2>/dev/null || true)"
if [[ -n "$sha" ]]; then
echo "${base}-${sha}"
else
echo "$base"
fi
}
detect_platform() {
local os arch
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch="$(uname -m)"
case "$arch" in
x86_64|amd64) arch="x86_64" ;;
aarch64|arm64) arch="aarch64" ;;
esac
echo "${arch}-${os}"
}
ROOT="$(repo_root)"
cd "$ROOT"
require_cmd tar
if [[ "$SKIP_BUILD" != "true" ]]; then
require_cmd npm
require_cmd cargo
fi
if [[ -z "$VERSION" ]]; then
VERSION="$(detect_version)"
fi
PLATFORM="${TARGET:-$(detect_platform)}"
PACKAGE_NAME="lightops-${VERSION}-${PLATFORM}"
STAGE_DIR="target/release-package/${PACKAGE_NAME}"
ARCHIVE_PATH="${OUTPUT_DIR}/${PACKAGE_NAME}.tar.gz"
if [[ "$SKIP_BUILD" != "true" ]]; then
log "构建前端"
cd "$ROOT/web"
if [[ -f package-lock.json ]]; then
npm ci
else
npm install
fi
npm run build
log "构建 Rust 二进制"
cd "$ROOT"
if [[ -n "$TARGET" ]]; then
cargo build --release --target "$TARGET" -p lightops-server -p lightops-agent
BIN_DIR="target/${TARGET}/release"
else
cargo build --release -p lightops-server -p lightops-agent
BIN_DIR="target/release"
fi
else
cd "$ROOT"
if [[ -n "$TARGET" ]]; then
BIN_DIR="target/${TARGET}/release"
else
BIN_DIR="target/release"
fi
fi
SERVER_BIN="${BIN_DIR}/lightops-server"
AGENT_BIN="${BIN_DIR}/lightops-agent"
if [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* || "$(uname -s)" == CYGWIN* ]]; then
[[ -f "${SERVER_BIN}.exe" ]] && SERVER_BIN="${SERVER_BIN}.exe"
[[ -f "${AGENT_BIN}.exe" ]] && AGENT_BIN="${AGENT_BIN}.exe"
fi
[[ -f "$SERVER_BIN" ]] || fail "缺少 lightops-server 构建产物:$SERVER_BIN"
[[ -f "$AGENT_BIN" ]] || fail "缺少 lightops-agent 构建产物:$AGENT_BIN"
[[ -d "$ROOT/web/dist" ]] || fail "缺少前端构建产物web/dist"
log "整理发布目录:$STAGE_DIR"
rm -rf "$STAGE_DIR"
mkdir -p "$STAGE_DIR/bin" "$STAGE_DIR/web" "$STAGE_DIR/scripts" "$STAGE_DIR/config" "$OUTPUT_DIR"
install -m 0755 "$SERVER_BIN" "$STAGE_DIR/bin/lightops-server"
install -m 0755 "$AGENT_BIN" "$STAGE_DIR/bin/lightops-agent"
cp -a "$ROOT/web/dist" "$STAGE_DIR/web/dist"
cp -a "$ROOT/store" "$STAGE_DIR/store"
install -m 0755 "$ROOT/scripts/install-server-release.sh" "$STAGE_DIR/scripts/install-server-release.sh"
install -m 0755 "$ROOT/scripts/install-agent.sh" "$STAGE_DIR/scripts/install-agent.sh"
install -m 0755 "$ROOT/scripts/upgrade-agent.sh" "$STAGE_DIR/scripts/upgrade-agent.sh"
install -m 0755 "$ROOT/scripts/uninstall-agent.sh" "$STAGE_DIR/scripts/uninstall-agent.sh"
install -m 0755 "$ROOT/scripts/update-from-git.sh" "$STAGE_DIR/scripts/update-from-git.sh"
cp "$ROOT/config/server.toml.example" "$STAGE_DIR/config/server.toml.example"
cat >"$STAGE_DIR/RELEASE.txt" <<EOF
LightOps 发布包
版本:$VERSION
平台:$PLATFORM
构建时间:$(date -Is)
提交:$(git rev-parse HEAD 2>/dev/null || echo unknown)
EOF
log "生成压缩包:$ARCHIVE_PATH"
tar -C "$(dirname "$STAGE_DIR")" -czf "$ARCHIVE_PATH" "$(basename "$STAGE_DIR")"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$ARCHIVE_PATH" >"${ARCHIVE_PATH}.sha256"
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$ARCHIVE_PATH" >"${ARCHIVE_PATH}.sha256"
fi
cat <<EOF
发布包已生成:
$ARCHIVE_PATH
安装命令示例:
curl -fsSL https://gitea.kmux.cn/Eeveid/lightOps/raw/branch/main/scripts/install-server-release.sh | bash -s -- --url <发布包下载地址>
EOF

455
scripts/install-server-release.sh Executable file
View File

@@ -0,0 +1,455 @@
#!/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