netx/packaging/linux/install.sh
oliver 846aa3e680 Linux x86_64 offline release; configurable API and bundled PostgreSQL ports.
- packaging/linux: netx-<ver>-linux-x86_64.tar.gz with bundled Python 3.12,
  PostgreSQL 16 and web/dist (glibc >= 2.17). install.sh (install/update/
  reinstall), netxctl (start/stop/status/logs, set-port, set-bind,
  set-pg-port, reconfigure-db, check-update/update/auto-update, backup,
  uninstall), systemd units, nohup fallback without systemd. Privilege
  drop, detaching and tar go through the bundled Python, so bare images
  without su/runuser/setsid/tar work.
- Updates: same GitHub/Forgejo/manifest sources as Windows, sha256
  verified, pre-update pg_dump, automatic rollback when the new version
  does not come up.
- Non-loopback bind: generated bootstrap admin password and
  NETX_UME_VERIFY_TLS=true, as security_bootstrap requires.
- Build in manylinux2014 + check_bundle.sh (GLIBC <= 2.17, no missing
  libs); release-linux.yml builds on v* tags and smoke-tests offline on
  CentOS 7, Rocky 9, Ubuntu 18.04/24.04, Debian 12, openSUSE Leap 15 and
  openEuler 22.03 before attaching to the release.
- Windows: Setup DB page gets a bundled PG port field and
  /BundledPgPort=; set_pg_port.ps1 + tray "Built-in database port";
  reconfigure keeps the configured port; postgresql.conf is written
  without a BOM (PS 5.1 Set-Content -Encoding utf8 broke postgres).
- Forgejo/GitHub publish scripts include the Linux package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:09:07 +08:00

614 lines
26 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
# NetX Linux installer / updater. Ships at the root of netx-<ver>-linux-x86_64.tar.gz.
#
# sudo ./install.sh # interactive install (or update when already installed)
# sudo ./install.sh --yes --db bundled --port 8890 --pg-port 15432
# sudo ./install.sh --mode update --yes # what `netxctl update` runs
#
# Needs only bash, coreutils and useradd (tar to unpack it): Python, PostgreSQL
# and the web UI are inside the package.
set -euo pipefail
PKG="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
# shellcheck source=lib.sh
. "${PKG}/packaging/linux/lib.sh"
NETX_PY="${PKG}/python/bin/python3"
NETX_ADMIN_PY="${PKG}/packaging/linux/netx_admin.py"
PREFIX="/opt/netx"
DATA="/var/lib/netx"
MODE=""
DB_MODE=""
API_PORT=""
BIND=""
PG_PORT=""
PG_PASSWORD=""
DB_URL=""
DB_HOST="" DB_PORT_EXT="" DB_USER="" DB_PASS="" DB_NAME=""
CRED_KEY_FILE=""
ADMIN_PASSWORD=""
ADMIN_PW_GENERATED=0
NO_SERVICE=0
AUTO_UPDATE=0
OPEN_FIREWALL=0
NO_BACKUP=0
YES=0
usage() {
cat <<EOF
$(t "用法" "Usage"): sudo ./install.sh [options]
--mode install|update|reinstall $(t "已安装时默认 update(保留数据与配置)" "default: update when already installed (keeps data/config)")
--prefix DIR $(t "程序目录" "program dir") (/opt/netx)
--data DIR $(t "数据目录" "data dir") (/var/lib/netx)
--port N $(t "Web/API 端口" "Web/API port") (8890)
--bind HOST $(t "监听地址" "listen address") (0.0.0.0)
--db bundled|external
--pg-port N $(t "内置 PostgreSQL 端口" "bundled PostgreSQL port") (15432)
--pg-password PW $(t "内置库密码(默认自动生成)" "bundled DB password (default: generated)")
--db-url URL postgresql+psycopg://USER:PASS@HOST:5432/DB
--db-host/--db-port/--db-user/--db-password/--db-name $(t "外置库参数(替代 --db-url)" "external DB parts")
--admin-password PW $(t "初始管理员 admin 的密码(默认随机生成,安装结束时显示)" "initial password for admin (default: generated, shown at the end)")
--credential-key-file F $(t "沿用其他安装的 NE 凭据加密密钥" "reuse NETX_CREDENTIAL_SECRET_KEY from another install")
--no-service $(t "不注册 systemd 服务" "do not install the systemd service")
--auto-update $(t "开启每日自动更新" "enable daily automatic updates")
--open-firewall $(t "自动放行防火墙端口(firewalld/ufw)" "open the port in firewalld/ufw")
--no-backup $(t "更新前不备份数据库" "skip the pre-update database dump")
-y, --yes $(t "非交互,使用默认值" "non-interactive, accept defaults")
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="${2:-}"; shift 2 ;;
--prefix) PREFIX="${2:-}"; shift 2 ;;
--data) DATA="${2:-}"; shift 2 ;;
--port) API_PORT="${2:-}"; shift 2 ;;
--bind) BIND="${2:-}"; shift 2 ;;
--db) DB_MODE="${2:-}"; shift 2 ;;
--pg-port) PG_PORT="${2:-}"; shift 2 ;;
--pg-password) PG_PASSWORD="${2:-}"; shift 2 ;;
--db-url) DB_URL="${2:-}"; shift 2 ;;
--db-host) DB_HOST="${2:-}"; shift 2 ;;
--db-port) DB_PORT_EXT="${2:-}"; shift 2 ;;
--db-user) DB_USER="${2:-}"; shift 2 ;;
--db-password) DB_PASS="${2:-}"; shift 2 ;;
--db-name) DB_NAME="${2:-}"; shift 2 ;;
--credential-key-file) CRED_KEY_FILE="${2:-}"; shift 2 ;;
--admin-password) ADMIN_PASSWORD="${2:-}"; shift 2 ;;
--no-service) NO_SERVICE=1; shift ;;
--auto-update) AUTO_UPDATE=1; shift ;;
--open-firewall) OPEN_FIREWALL=1; shift ;;
--no-backup) NO_BACKUP=1; shift ;;
-y|--yes) YES=1; shift ;;
-h|--help) usage; exit 0 ;;
*) err "unknown option: $1"; usage; exit 2 ;;
esac
done
INTERACTIVE=1
if [[ "${YES}" == "1" || ! -t 0 ]]; then INTERACTIVE=0; fi
PREFIX="${PREFIX%/}"
DATA="${DATA%/}"
ENV_FILE="${DATA}/.env"
NETX_LOG_DIR="${DATA}/logs"
PGDATA_DIR="${DATA}/pgdata"
ask() { # ask "prompt" default -> echo answer
local a
read -r -p "$1 [$2]: " a || true
echo "${a:-$2}"
}
# ------------------------------------------------------------------ preflight
preflight() {
is_root || die "$(t "请用 root 运行:sudo ./install.sh" "run as root: sudo ./install.sh")"
local arch
arch="$(uname -m)"
[[ "${arch}" == "x86_64" || "${arch}" == "amd64" ]] || die "$(t "本安装包仅支持 x86_64,当前" "this package is x86_64 only; this machine is") ${arch}"
local glibc
glibc="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || true
[[ -n "${glibc}" ]] || die "$(t "未检测到 glibc(不支持 Alpine/musl)" "glibc not found (Alpine/musl is not supported)")"
if [[ "$(printf '%s\n2.17\n' "${glibc}" | sort -V | head -n1)" != "2.17" ]]; then
die "$(t "glibc 版本过低" "glibc too old"): ${glibc} (>= 2.17)"
fi
"${NETX_PY}" -c "import sys" 2>/dev/null || die "$(t "内置 Python 无法运行,安装包可能损坏" "bundled Python does not run; package may be corrupt")"
[[ -f "${PKG}/version.json" ]] || die "version.json missing in ${PKG}"
local parent avail
parent="$(dirname "${PREFIX}")"
mkdir -p "${parent}"
avail="$(df -Pk "${parent}" | awk 'NR==2 {print $4}')"
if [[ -n "${avail}" && "${avail}" -lt 1048576 ]]; then
die "$(t "磁盘空间不足(至少 1GB)" "not enough disk space (need 1GB)"): ${parent}"
fi
}
ensure_user() {
if id "${NETX_USER}" >/dev/null 2>&1; then return 0; fi
info "$(t "创建系统用户" "creating system user") ${NETX_USER}"
local nologin=/sbin/nologin
[[ -x "${nologin}" ]] || nologin=/usr/sbin/nologin
[[ -x "${nologin}" ]] || nologin=/bin/false
if command -v useradd >/dev/null 2>&1; then
useradd -r -U -d "${DATA}" -M -s "${nologin}" "${NETX_USER}" 2>/dev/null ||
useradd -r -d "${DATA}" -M -s "${nologin}" "${NETX_USER}"
elif command -v adduser >/dev/null 2>&1; then
adduser -S -D -H -h "${DATA}" -s "${nologin}" "${NETX_USER}" 2>/dev/null ||
adduser --system --group --no-create-home --home "${DATA}" "${NETX_USER}"
else
die "$(t "缺少 useradd/adduser,无法创建用户" "useradd/adduser not found")"
fi
# Some useradd builds do not create a same-named group with -r.
getent group "${NETX_USER}" >/dev/null 2>&1 || groupadd -r "${NETX_USER}" 2>/dev/null || true
}
# ------------------------------------------------------------------ prompts
pick_port() { # pick_port <label> <value> <default> <host> <allow_current>
local label="$1" val="$2" def="$3" host="$4" allow="$5" p
while :; do
if [[ -z "${val}" ]]; then
if [[ "${INTERACTIVE}" == "1" ]]; then val="$(ask "${label}" "${def}")"; else val="${def}"; fi
fi
if ! p="$(netx_admin valid-port "${val}" 2>&1)"; then
[[ "${INTERACTIVE}" == "1" ]] || die "${p}"
err "${p}"; val=""; continue
fi
if [[ "${p}" == "${allow}" ]] || netx_admin port-free "${p}" --host "${host}"; then
echo "${p}"; return 0
fi
local msg
msg="$(t "端口 ${p} 已被占用" "port ${p} is in use")"
[[ "${INTERACTIVE}" == "1" ]] || die "port_in_use: ${p} — ${msg}"
err "${msg}"
val=""
done
}
configure() {
local old_port old_bind old_mode old_pg
old_port="$(env_get "${ENV_FILE}" NETX_PORT)"
old_bind="$(env_get "${ENV_FILE}" NETX_HOST)"
old_mode="$(env_get "${ENV_FILE}" NETX_DB_MODE)"
old_pg="$(env_get "${ENV_FILE}" NETX_BUNDLED_PG_PORT)"
echo
info "$(t "配置 NetX" "Configure NetX")"
if [[ -z "${BIND}" ]]; then
if [[ "${INTERACTIVE}" == "1" ]]; then
BIND="$(ask "$(t "监听地址(0.0.0.0=所有网卡,127.0.0.1=仅本机)" "Listen address (0.0.0.0 = all, 127.0.0.1 = local only)")" "${old_bind:-0.0.0.0}")"
else
BIND="${old_bind:-0.0.0.0}"
fi
fi
API_PORT="$(pick_port "$(t "Web/API 端口" "Web/API port")" "${API_PORT}" "${old_port:-8890}" "${BIND}" "${old_port}")"
if [[ -z "${DB_MODE}" ]]; then
if [[ -n "${DB_URL}${DB_HOST}" ]]; then
DB_MODE="external"
elif [[ "${INTERACTIVE}" == "1" ]]; then
echo "$(t "数据库:" "Database:")"
echo " 1) $(t "内置 PostgreSQL(离线可用,推荐)" "bundled PostgreSQL (offline, recommended)")"
echo " 2) $(t "外置 PostgreSQL(已有的库)" "external PostgreSQL (existing server)")"
local def=1
[[ "${old_mode}" == "external" ]] && def=2
[[ "$(ask "$(t "请选择" "Choose")" "${def}")" == "2" ]] && DB_MODE="external" || DB_MODE="bundled"
else
DB_MODE="${old_mode:-bundled}"
fi
fi
case "${DB_MODE}" in bundled|external) ;; *) die "invalid --db: ${DB_MODE}" ;; esac
if [[ "${DB_MODE}" == "bundled" ]]; then
PG_PORT="$(pick_port "$(t "内置 PostgreSQL 端口(仅本机 127.0.0.1)" "Bundled PostgreSQL port (127.0.0.1 only)")" "${PG_PORT}" "${old_pg:-15432}" 127.0.0.1 "${old_pg}")"
else
configure_external
fi
CRED_KEY=""
if [[ -n "${CRED_KEY_FILE}" ]]; then
CRED_KEY="$(tr -d ' \r\n' <"${CRED_KEY_FILE}")"
elif [[ -z "$(env_get "${ENV_FILE}" NETX_CREDENTIAL_SECRET_KEY)" && "${INTERACTIVE}" == "1" ]]; then
echo "$(t "可选:粘贴其他 NetX 安装的 NETX_CREDENTIAL_SECRET_KEY,以沿用已加密的网元密码;回车跳过(自动生成)" \
"Optional: paste NETX_CREDENTIAL_SECRET_KEY from another install to reuse encrypted NE passwords; Enter = generate")"
read -r -p "NETX_CREDENTIAL_SECRET_KEY: " CRED_KEY || true
fi
}
configure_external() {
local existing
existing="$(env_get "${ENV_FILE}" NETX_DATABASE_URL)"
while :; do
if [[ -z "${DB_URL}" && -n "${DB_HOST}" ]]; then
DB_URL="$(netx_admin build-url -- "${DB_USER:-netx}" "${DB_PASS}" "${DB_HOST}" "${DB_PORT_EXT:-5432}" "${DB_NAME:-netx}")" || exit 1
fi
if [[ -z "${DB_URL}" ]]; then
if [[ "${INTERACTIVE}" != "1" ]]; then
[[ -n "${existing}" && "$(env_get "${ENV_FILE}" NETX_DB_MODE)" == "external" ]] && DB_URL="${existing}"
[[ -n "${DB_URL}" ]] || die "external_url_required: --db-url or --db-host/--db-user/--db-password/--db-name"
else
echo "$(t "外置 PostgreSQL(库和用户需事先建好)" "External PostgreSQL (database and role must exist)")"
local h po u pw n
h="$(ask "$(t "主机" "Host")" "127.0.0.1")"
po="$(ask "$(t "端口" "Port")" "5432")"
u="$(ask "$(t "用户" "User")" "netx")"
read -r -s -p "$(t "密码" "Password"): " pw || true
echo
n="$(ask "$(t "数据库" "Database")" "netx")"
DB_URL="$(netx_admin build-url -- "${u}" "${pw}" "${h}" "${po}" "${n}")" || { DB_URL=""; continue; }
fi
fi
info "$(t "测试外置数据库连接" "probing external PostgreSQL")"
if netx_admin probe-db "${DB_URL}"; then
ok "$(t "外置数据库连接成功" "external PostgreSQL OK")"
return 0
fi
[[ "${INTERACTIVE}" == "1" ]] || die "external_db_probe_failed"
DB_URL="" DB_HOST=""
done
}
# ------------------------------------------------------------------ program files
# Set by load_pkg_meta (after preflight proved the bundled Python runs).
VERSION=""
PKG_PG_MAJOR=""
REL=""
load_pkg_meta() {
VERSION="$(netx_admin json-get "${PKG}/version.json" version)" || die "bad version.json"
PKG_PG_MAJOR="$(netx_admin json-get "${PKG}/version.json" pg_major 2>/dev/null || echo 16)"
REL="${PREFIX}/releases/${VERSION}"
}
deploy_release() {
info "$(t "安装程序文件到" "installing program files to") ${REL}"
mkdir -p "${PREFIX}/releases"
if [[ "$(readlink -f "${PKG}")" != "$(readlink -f "${REL}" 2>/dev/null || echo "${REL}")" ]]; then
rm -rf "${REL}.new"
mkdir -p "${REL}.new"
cp -a "${PKG}/." "${REL}.new/"
rm -rf "${REL}"
mv "${REL}.new" "${REL}"
fi
chown -R root:root "${REL}"
chmod -R u+rwX,go+rX,go-w "${REL}"
info "$(t "预编译 Python 字节码" "precompiling Python bytecode")"
"${REL}/python/bin/python3" -m compileall -q -j 0 "${REL}/netx_api" "${REL}/alembic" \
"${REL}/python/lib/python3.12" >/dev/null 2>&1 || true
printf 'NETX_DATA=%s\n' "${DATA}" >"${PREFIX}/netx.conf"
}
switch_current() { # switch_current <version>
ln -sfn "releases/$1" "${PREFIX}/current.tmp"
mv -Tf "${PREFIX}/current.tmp" "${PREFIX}/current"
}
install_integration() {
# /usr/bin: sudo's secure_path on RHEL-family systems omits /usr/local/bin.
ln -sfn "${PREFIX}/current/packaging/linux/netxctl" /usr/bin/netxctl
if [[ -d /etc/logrotate.d ]]; then
sed -e "s#@DATA@#${DATA}#g" -e "s#@USER@#${NETX_USER}#g" \
"${REL}/packaging/linux/logrotate.netx" >/etc/logrotate.d/netx
chmod 0644 /etc/logrotate.d/netx
fi
if [[ "${NO_SERVICE}" != "1" ]] && have_systemd; then
sed -e "s#@PREFIX@#${PREFIX}#g" -e "s#@USER@#${NETX_USER}#g" \
"${REL}/packaging/linux/systemd/netx.service.in" >/etc/systemd/system/netx.service
if [[ -f /etc/systemd/system/netx-update.service ]]; then
sed -e "s#@PREFIX@#${PREFIX}#g" "${REL}/packaging/linux/systemd/netx-update.service.in" \
>/etc/systemd/system/netx-update.service
fi
systemctl daemon-reload
systemctl enable netx.service >/dev/null 2>&1
fi
if command -v restorecon >/dev/null 2>&1 && command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then
restorecon -R "${PREFIX}" "${DATA}" >/dev/null 2>&1 || true
fi
}
prune_releases() { # keep current + $1
local keep1 keep2 d
keep1="$(basename "$(readlink -f "${PREFIX}/current")")"
keep2="${1:-}"
for d in "${PREFIX}"/releases/*; do
[[ -d "${d}" ]] || continue
case "$(basename "${d}")" in
"${keep1}"|"${keep2}") ;;
*) rm -rf "${d}" ;;
esac
done
}
# ------------------------------------------------------------------ data / db
write_env() {
local pairs=()
pairs+=("NETX_DB_MODE=${DB_MODE}" "NETX_HOST=${BIND}" "NETX_PORT=${API_PORT}")
pairs+=("NETX_UI_DIST_DIR=${PREFIX}/current/web/dist" "NETX_RUN_INLINE_SCHEDULERS=false")
local kv
while IFS= read -r kv; do pairs+=("${kv}"); done < <(data_env_pairs "${DATA}")
if [[ "${DB_MODE}" == "bundled" ]]; then
local url
url="$(netx_admin build-url -- netx "${PG_PASSWORD}" 127.0.0.1 "${PG_PORT}" netx)" || die "build_db_url_failed"
pairs+=("NETX_BUNDLED_PG_PORT=${PG_PORT}" "NETX_DATABASE_URL=${url}")
else
pairs+=("NETX_DATABASE_URL=${DB_URL}")
fi
# NetX refuses a non-loopback bind while the bootstrap admin password is the lab
# default (admin123) or UME TLS verification is off (security_bootstrap.py).
if [[ -n "${ADMIN_PASSWORD}" ]]; then
pairs+=("NETX_BOOTSTRAP_ADMIN_PASSWORD=${ADMIN_PASSWORD}")
elif [[ -z "$(env_get "${ENV_FILE}" NETX_BOOTSTRAP_ADMIN_PASSWORD)" ]]; then
ADMIN_PASSWORD="$(netx_admin gen-secret password)"
ADMIN_PW_GENERATED=1
pairs+=("NETX_BOOTSTRAP_ADMIN_PASSWORD=${ADMIN_PASSWORD}")
fi
if ! is_loopback "${BIND}" && [[ -z "$(env_get "${ENV_FILE}" NETX_UME_VERIFY_TLS)" ]]; then
pairs+=("NETX_UME_VERIFY_TLS=true")
fi
if [[ -n "${CRED_KEY}" ]]; then
pairs+=("NETX_CREDENTIAL_SECRET_KEY=${CRED_KEY}")
elif [[ -z "$(env_get "${ENV_FILE}" NETX_CREDENTIAL_SECRET_KEY)" ]]; then
pairs+=("NETX_CREDENTIAL_SECRET_KEY=$(netx_admin gen-secret fernet)")
fi
touch "${ENV_FILE}"
chmod 0600 "${ENV_FILE}"
netx_admin env-set "${ENV_FILE}" "${pairs[@]}"
chown "${NETX_USER}:${NETX_USER}" "${ENV_FILE}"
chmod 0600 "${ENV_FILE}"
}
setup_bundled_pg() {
PGBIN="${REL}/pgsql/bin"
local major
major="$(pg_data_major || true)"
if [[ -n "${major}" && "${major}" != "${PKG_PG_MAJOR}" ]]; then
die "pg_major_mismatch: ${PGDATA_DIR} is PostgreSQL ${major}, package ships ${PKG_PG_MAJOR}"
fi
if [[ -z "${PG_PASSWORD}" ]]; then
local old_url
old_url="$(env_get "${ENV_FILE}" NETX_DATABASE_URL)"
if [[ -n "${major}" ]]; then
# Existing cluster: keep its password (it was set at first install).
[[ -n "${old_url}" ]] && PG_PASSWORD="$(netx_admin url-part "${old_url}" password 2>/dev/null || true)"
[[ -n "${PG_PASSWORD}" ]] || die "$(t "已有内置数据库但找不到原密码,请用 --pg-password 指定" "existing bundled cluster but its password is unknown; pass --pg-password")"
else
PG_PASSWORD="$(netx_admin gen-secret password)"
fi
fi
# Record the password before initdb so re-running a failed install can reuse the cluster.
local early_url
early_url="$(netx_admin build-url -- netx "${PG_PASSWORD}" 127.0.0.1 "${PG_PORT}" netx)" || die "build_db_url_failed"
touch "${ENV_FILE}"
netx_admin env-set "${ENV_FILE}" "NETX_DATABASE_URL=${early_url}"
chown "${NETX_USER}:${NETX_USER}" "${ENV_FILE}"
chmod 0600 "${ENV_FILE}"
if [[ -z "${major}" ]]; then
info "$(t "初始化内置 PostgreSQL" "initialising bundled PostgreSQL") (${PGDATA_DIR})"
rm -rf "${PGDATA_DIR:?}"/* "${PGDATA_DIR:?}"/.[!.]* 2>/dev/null || true
local pw="${DATA}/.pginit.pw"
(umask 077 && printf '%s' "${PG_PASSWORD}" >"${pw}")
chown "${NETX_USER}:${NETX_USER}" "${pw}"
as_netx env LC_ALL=C LANG=C "${PGBIN}/initdb" -D "${PGDATA_DIR}" -U postgres --auth=scram-sha-256 \
--pwfile="${pw}" -E UTF8 --locale=C >"${NETX_LOG_DIR}/initdb.log" 2>&1 ||
{ rm -f "${pw}"; tail -n 20 "${NETX_LOG_DIR}/initdb.log" >&2; die "initdb_failed"; }
rm -f "${pw}"
fi
netx_admin pg-conf "${PGDATA_DIR}/postgresql.conf" --set "port=${PG_PORT}" \
--set "listen_addresses='127.0.0.1'" --set "unix_socket_directories=''" \
--set "logging_collector=off"
chown "${NETX_USER}:${NETX_USER}" "${PGDATA_DIR}/postgresql.conf"
local started=0
if ! pg_is_running; then
pg_start || { tail -n 20 "${NETX_LOG_DIR}/pg.log" >&2; die "pg_start_failed"; }
started=1
fi
local sqlpw="${PG_PASSWORD//\'/\'\'}" out
psql_su() {
as_netx env PGPASSWORD="${PG_PASSWORD}" LC_ALL=C "${PGBIN}/psql" -h 127.0.0.1 -p "${PG_PORT}" -U postgres \
-d postgres -v ON_ERROR_STOP=1 -tAc "$1"
}
if ! out="$(psql_su "SELECT 1 FROM pg_roles WHERE rolname='netx'" 2>&1)"; then
[[ "${started}" == "1" ]] && pg_stop
die "bundled_pg_login_failed: ${out}"
fi
if [[ "${out}" == "1" ]]; then
psql_su "ALTER ROLE netx WITH LOGIN PASSWORD '${sqlpw}'" >/dev/null
else
psql_su "CREATE ROLE netx LOGIN PASSWORD '${sqlpw}'" >/dev/null
fi
if [[ "$(psql_su "SELECT 1 FROM pg_database WHERE datname='netx'")" != "1" ]]; then
psql_su "CREATE DATABASE netx OWNER netx" >/dev/null
fi
[[ "${started}" == "1" ]] && pg_stop
ok "$(t "内置 PostgreSQL 就绪" "bundled PostgreSQL ready"): 127.0.0.1:${PG_PORT}"
}
backup_before_update() { # $1 = new version
[[ "${NO_BACKUP}" == "1" ]] && return 0
[[ "$(env_get "${ENV_FILE}" NETX_DB_MODE)" == "bundled" ]] || return 0
[[ -n "$(pg_data_major || true)" ]] || return 0
# Installed (root-owned, world-readable) copy: the unpacked package may sit in a
# directory the netx user cannot enter (e.g. /root).
PGBIN="${REL}/pgsql/bin"
local url port pw out started=0
url="$(env_get "${ENV_FILE}" NETX_DATABASE_URL)"
port="$(netx_admin url-part "${url}" port)"
pw="$(netx_admin url-part "${url}" password)"
out="${DATA}/backups/pre-$1-$(date +%Y%m%d-%H%M%S).dump"
if ! pg_is_running; then
pg_start || die "pg_start_failed (backup)"
started=1
fi
info "$(t "更新前备份数据库" "backing up database before update") → ${out}"
if ! as_netx env PGPASSWORD="${pw}" LC_ALL=C "${PGBIN}/pg_dump" -Fc -h 127.0.0.1 -p "${port}" -U netx -d netx -f "${out}"; then
[[ "${started}" == "1" ]] && pg_stop
die "$(t "备份失败,已中止更新(可加 --no-backup 跳过)" "backup failed; update aborted (use --no-backup to skip)")"
fi
[[ "${started}" == "1" ]] && pg_stop
# Keep the 3 newest pre-update dumps.
local old
ls -1t "${DATA}"/backups/pre-*.dump 2>/dev/null | tail -n +4 | while IFS= read -r old; do rm -f "${old}"; done
}
health_url() {
echo "http://$(client_host "$(env_get "${ENV_FILE}" NETX_HOST)"):$(env_get "${ENV_FILE}" NETX_PORT)/health"
}
start_and_wait() {
svc_start "${PREFIX}" "${DATA}"
info "$(t "等待 NetX 就绪(首次启动会建表,可能需要 1-2 分钟)" "waiting for NetX (first start creates tables; may take 1-2 min)")"
netx_admin wait-health "$(health_url)" --timeout 180
}
open_firewall() {
local p="$1"
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
if [[ "${OPEN_FIREWALL}" == "1" ]]; then
firewall-cmd --permanent --add-port="${p}/tcp" >/dev/null && firewall-cmd --reload >/dev/null &&
ok "firewalld: ${p}/tcp $(t "已放行" "opened")"
else
echo " $(t "防火墙放行" "open firewall"): firewall-cmd --permanent --add-port=${p}/tcp && firewall-cmd --reload"
fi
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
if [[ "${OPEN_FIREWALL}" == "1" ]]; then
ufw allow "${p}/tcp" >/dev/null && ok "ufw: ${p}/tcp $(t "已放行" "opened")"
else
echo " $(t "防火墙放行" "open firewall"): ufw allow ${p}/tcp"
fi
fi
}
summary() {
local port bind host
port="$(env_get "${ENV_FILE}" NETX_PORT)"
bind="$(env_get "${ENV_FILE}" NETX_HOST)"
host="$(primary_ip)"
[[ "${bind}" == "127.0.0.1" ]] && host="127.0.0.1"
echo
ok "NetX ${VERSION} $(t "已就绪" "is up")"
echo " $(t "访问地址" "URL") : http://${host}:${port}/"
if [[ "${MODE}" == "install" && -n "${ADMIN_PASSWORD}" ]]; then
echo " $(t "初始账号" "first login"): admin / ${ADMIN_PASSWORD} ($(t "首次登录后必须修改密码" "you must change it at first login"))"
[[ "${ADMIN_PW_GENERATED}" == "1" ]] && echo " $(t "(随机生成,只显示这一次;也保存在" "(generated; shown once, also kept in") ${ENV_FILE} NETX_BOOTSTRAP_ADMIN_PASSWORD)"
fi
echo " $(t "程序目录" "program") : ${PREFIX}/current -> releases/${VERSION}"
echo " $(t "数据目录" "data") : ${DATA}"
echo " $(t "管理命令" "manage") : sudo netxctl status | stop | start | set-port | set-pg-port | update"
if svc_unit_installed; then
echo " systemd : netx.service ($(t "开机自启" "starts at boot"))"
else
echo " $(t "未注册 systemd 服务,重启后需执行 netxctl start" "no systemd service: run netxctl start after reboot")"
fi
[[ "${bind}" != "127.0.0.1" ]] && open_firewall "${port}"
return 0
}
# ------------------------------------------------------------------ flows
do_install() { # install or reinstall
ensure_user
mkdir -p "${DATA}"
ensure_data_dirs "${DATA}"
if [[ -x "${PREFIX}/current/packaging/linux/netxctl" ]]; then
svc_stop "${PREFIX}" "${DATA}"
fi
configure
deploy_release
if [[ "${DB_MODE}" == "bundled" ]]; then setup_bundled_pg; fi
write_env
ensure_data_dirs "${DATA}"
switch_current "${VERSION}"
install_integration
if [[ "${AUTO_UPDATE}" == "1" ]]; then "${PREFIX}/current/packaging/linux/netxctl" auto-update on || true; fi
if ! start_and_wait; then
err "$(t "NetX 启动失败,日志" "NetX failed to start; logs"): ${DATA}/logs/"
tail -n 30 "${DATA}/logs/api.log" >&2 2>/dev/null || true
exit 1
fi
prune_releases ""
summary
}
do_update() {
[[ -f "${ENV_FILE}" ]] || die "$(t "未找到已有安装(${ENV_FILE}),请用 --mode install" "no existing install (${ENV_FILE}); use --mode install")"
ensure_user
ensure_data_dirs "${DATA}"
local old_ver=""
if [[ -f "${PREFIX}/current/version.json" ]]; then
old_ver="$(netx_admin json-get "${PREFIX}/current/version.json" version || true)"
fi
if [[ "$(env_get "${ENV_FILE}" NETX_DB_MODE)" == "bundled" ]]; then
local major
major="$(pg_data_major || true)"
if [[ -n "${major}" && "${major}" != "${PKG_PG_MAJOR}" ]]; then
die "pg_major_mismatch: data=${major} package=${PKG_PG_MAJOR}"
fi
fi
info "$(t "更新" "Updating") NetX ${old_ver:-?} → ${VERSION}"
if [[ -n "${old_ver}" ]]; then svc_stop "${PREFIX}" "${DATA}"; fi
if [[ "${old_ver}" == "${VERSION}" ]]; then
# Reinstalling the same version: keep a copy so a failed start can roll back.
rm -rf "${PREFIX}/releases/${VERSION}.prev"
cp -a "${REL}" "${PREFIX}/releases/${VERSION}.prev" 2>/dev/null || true
fi
deploy_release
backup_before_update "${VERSION}"
# Re-assert keys newer releases rely on; never touches ports or DB settings.
local pairs=("NETX_UI_DIST_DIR=${PREFIX}/current/web/dist" "NETX_RUN_INLINE_SCHEDULERS=false") kv
while IFS= read -r kv; do pairs+=("${kv}"); done < <(data_env_pairs "${DATA}")
netx_admin env-set "${ENV_FILE}" "${pairs[@]}"
chown "${NETX_USER}:${NETX_USER}" "${ENV_FILE}"
switch_current "${VERSION}"
install_integration
if [[ "${AUTO_UPDATE}" == "1" ]]; then "${PREFIX}/current/packaging/linux/netxctl" auto-update on || true; fi
if start_and_wait; then
rm -rf "${PREFIX}/releases/${VERSION}.prev"
prune_releases "${old_ver}"
summary
return 0
fi
err "$(t "新版本启动失败" "new version failed to start")"
tail -n 30 "${DATA}/logs/api.log" >&2 2>/dev/null || true
svc_stop "${PREFIX}" "${DATA}"
local back=""
if [[ -n "${old_ver}" && "${old_ver}" != "${VERSION}" && -d "${PREFIX}/releases/${old_ver}" ]]; then
back="${old_ver}"
elif [[ -d "${PREFIX}/releases/${VERSION}.prev" ]]; then
rm -rf "${REL}"
mv "${PREFIX}/releases/${VERSION}.prev" "${REL}"
back="${VERSION}"
fi
if [[ -n "${back}" ]]; then
warn "$(t "回滚到" "rolling back to") ${back}"
switch_current "${back}"
REL="${PREFIX}/releases/${back}"
install_integration
svc_start "${PREFIX}" "${DATA}"
netx_admin wait-health "$(health_url)" --timeout 180 && ok "$(t "已回滚并恢复运行" "rolled back and running"): ${back}"
local dump
dump="$(ls -1t "${DATA}"/backups/pre-*.dump 2>/dev/null | head -n1 || true)"
[[ -n "${dump}" ]] && warn "$(t "如数据库已被新版本迁移,可用此备份恢复" "if the new version migrated the database, restore from"): ${dump}"
fi
exit 3
}
preflight
load_pkg_meta
if [[ -z "${MODE}" ]]; then
if [[ -n "$(env_get "${ENV_FILE}" NETX_DB_MODE)" ]]; then MODE="update"; else MODE="install"; fi
if [[ "${MODE}" == "update" && "${INTERACTIVE}" == "1" ]]; then
echo "$(t "检测到已有安装(${DATA}):" "Existing installation found (${DATA}):")"
echo " 1) $(t "更新(保留数据和配置,推荐)" "update (keep data and settings, recommended)")"
echo " 2) $(t "重新配置(重新选择端口/数据库,不删数据)" "reconfigure (choose ports/database again; data kept)")"
[[ "$(ask "$(t "请选择" "Choose")" 1)" == "2" ]] && MODE="reinstall"
fi
fi
case "${MODE}" in
install|reinstall) do_install ;;
update) do_update ;;
*) die "invalid --mode: ${MODE}" ;;
esac