mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 04:53:20 +08:00
- Deployments are on internal networks with self-signed UME certificates: installers write NETX_UME_VERIFY_TLS=false and NETX_ALLOW_INSECURE_DEFAULTS=1 when absent (Linux also on update), so binding 0.0.0.0 no longer stops NetX at the startup security check. Admin password and JWT secret stay unique per install. - Linux: after install, check app_user to report whether the generated initial password applies or the admin already exists (reused external DB); new 'netxctl admin-password'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
285 lines
11 KiB
Bash
285 lines
11 KiB
Bash
# Shared helpers for install.sh and netxctl. Sourced, never executed.
|
||
# Portable bash 4.2+ (CentOS 7): no `wait -n`, no `${var@Q}`, no mapfile -d.
|
||
|
||
NETX_ZH=0
|
||
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in zh*|ZH*) NETX_ZH=1 ;; esac
|
||
|
||
# t "中文" "English"
|
||
t() { if [[ "${NETX_ZH}" == "1" ]]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
||
info() { printf '==> %s\n' "$*"; }
|
||
ok() { printf '\033[32m==> %s\033[0m\n' "$*"; }
|
||
warn() { printf '\033[33m[WARN] %s\033[0m\n' "$*" >&2; }
|
||
err() { printf '\033[31m[ERR] %s\033[0m\n' "$*" >&2; }
|
||
die() { err "$*"; exit 1; }
|
||
|
||
NETX_USER="${NETX_USER:-netx}"
|
||
|
||
have_systemd() { [[ -d /run/systemd/system ]] && command -v systemctl >/dev/null 2>&1; }
|
||
is_root() { [[ "$(id -u)" -eq 0 ]]; }
|
||
|
||
# Run a command as the netx service user (no-op when already that user).
|
||
as_netx() {
|
||
if [[ "$(id -un)" == "${NETX_USER}" ]]; then
|
||
"$@"
|
||
else
|
||
# Bundled Python drops privileges itself: minimal images may lack su/runuser.
|
||
# It also cd's to / (the service user usually cannot read root's cwd).
|
||
netx_admin run-as "${NETX_USER}" "$@"
|
||
fi
|
||
}
|
||
|
||
# netx_admin <subcommand> ... — stdlib helper on the bundled Python.
|
||
# Needs NETX_PY and NETX_ADMIN_PY set by the caller.
|
||
netx_admin() { "${NETX_PY}" -I "${NETX_ADMIN_PY}" "$@"; }
|
||
|
||
env_get() { netx_admin env-get "$1" "$2" 2>/dev/null || true; }
|
||
|
||
# Export every KEY=VALUE from a .env file without eval (values may hold any char).
|
||
load_env_file() {
|
||
local f="$1" kv
|
||
[[ -f "${f}" ]] || return 0
|
||
while IFS= read -r -d '' kv; do
|
||
export "${kv%%=*}=${kv#*=}"
|
||
done < <(netx_admin env-export "${f}")
|
||
}
|
||
|
||
# Runtime paths always live under the data root (same keys as Windows Get-NetxDataEnvMap).
|
||
data_env_pairs() {
|
||
local d="$1"
|
||
printf '%s\n' \
|
||
"NETX_AUTH_MCP_TOKEN_FILE=${d}/data/auth/mcp_token" \
|
||
"NETX_AUTH_SECRET_FILE=${d}/data/auth/jwt_secret" \
|
||
"NETX_CREDENTIAL_SECRET_FILE=${d}/data/auth/credential_secret" \
|
||
"NETX_SCHEDULER_HEARTBEAT_PATH=${d}/data/runtime/scheduler_heartbeat.json" \
|
||
"NETX_RUN_DIR=${d}/data/runtime" \
|
||
"NETX_BIZ_STATE_SPOOL_DIR=${d}/data/biz_state_spool" \
|
||
"NETX_NE_COLLECTION_DATA_DIR=${d}/data/ne_collections" \
|
||
"NETX_NE_EXEC_JOB_DIR=${d}/data/ne_exec_jobs" \
|
||
"NETX_WEBCRT_DATA_DIR=${d}/data/webcrt"
|
||
}
|
||
|
||
ensure_data_dirs() {
|
||
local d="$1" sub
|
||
for sub in data data/auth data/runtime data/biz_state_spool data/ne_collections \
|
||
data/ne_exec_jobs data/webcrt backups logs updates pgdata; do
|
||
mkdir -p "${d}/${sub}"
|
||
done
|
||
if is_root; then
|
||
chown -R "${NETX_USER}:${NETX_USER}" "${d}"
|
||
chmod 0750 "${d}"
|
||
chmod 0700 "${d}/pgdata"
|
||
[[ -f "${d}/.env" ]] && chmod 0600 "${d}/.env"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Client-side host for health checks when bound to a wildcard.
|
||
client_host() {
|
||
case "$1" in
|
||
""|0.0.0.0|"::"|"[::]") echo "127.0.0.1" ;;
|
||
*) echo "$1" ;;
|
||
esac
|
||
}
|
||
|
||
# The service runs as ${NETX_USER}: it must be able to enter the program and data
|
||
# dirs. Typical failure: paths under /root (mode 550/700) -> systemd 203/EXEC and
|
||
# empty logs, because netxctl never runs.
|
||
netx_can_enter() { as_netx /bin/sh -c 'cd "$1"' sh "$1" 2>/dev/null; }
|
||
|
||
# Let NETX_USER traverse one directory: ACL for that user only, else o+x.
|
||
grant_traverse() {
|
||
local p="$1"
|
||
if command -v setfacl >/dev/null 2>&1 && setfacl -m "u:${NETX_USER}:x" "${p}" 2>/dev/null; then
|
||
info "setfacl -m u:${NETX_USER}:x ${p}"
|
||
else
|
||
chmod o+x "${p}" && info "chmod o+x ${p}"
|
||
fi
|
||
}
|
||
|
||
# Can NETX_USER pass through this dir judging by mode bits alone? (Used only to
|
||
# name the blocking dirs below an already blocked one, which cannot be probed.)
|
||
others_can_traverse() {
|
||
local m
|
||
m="$(stat -c %A "$1" 2>/dev/null)" || return 1
|
||
[[ "${m:9:1}" == "x" || "${m:9:1}" == "t" ]]
|
||
}
|
||
|
||
# check_netx_access DIR... — every dir must be enterable by NETX_USER.
|
||
# Blocked parents (typically /root) are granted traverse-only access when
|
||
# GRANT_ACCESS=1 or the interactive user agrees; otherwise abort with the commands.
|
||
# (Plain strings, not arrays: empty arrays trip `set -u` on bash 4.2 / CentOS 7.)
|
||
check_netx_access() {
|
||
local d p rest comp ans blocked="" seen_block
|
||
for d in "$@"; do
|
||
mkdir -p "${d}"
|
||
netx_can_enter "${d}" && continue
|
||
p=""
|
||
seen_block=0
|
||
rest="${d#/}"
|
||
while [[ -n "${rest}" ]]; do
|
||
comp="${rest%%/*}"
|
||
if [[ "${rest}" == */* ]]; then rest="${rest#*/}"; else rest=""; fi
|
||
[[ -n "${comp}" ]] || continue
|
||
p="${p}/${comp}"
|
||
if [[ "${GRANT_ACCESS:-0}" == "1" ]]; then
|
||
if ! netx_can_enter "${p}"; then
|
||
grant_traverse "${p}"
|
||
blocked="${blocked} ${p}"
|
||
fi
|
||
elif [[ "${seen_block}" == "0" ]]; then
|
||
if ! netx_can_enter "${p}"; then
|
||
seen_block=1
|
||
[[ " ${blocked} " == *" ${p} "* ]] || blocked="${blocked} ${p}"
|
||
fi
|
||
elif ! others_can_traverse "${p}" && [[ " ${blocked} " != *" ${p} "* ]]; then
|
||
blocked="${blocked} ${p}"
|
||
fi
|
||
done
|
||
done
|
||
blocked="${blocked# }"
|
||
[[ -n "${blocked}" ]] || return 0
|
||
if [[ "${GRANT_ACCESS:-0}" != "1" ]]; then
|
||
warn "$(t "服务用户 ${NETX_USER} 无法进入以下目录(常见于 /root 下面)" "service user ${NETX_USER} cannot enter these directories (typically under /root)"): ${blocked}"
|
||
if [[ "${INTERACTIVE:-0}" == "1" ]]; then
|
||
read -r -p "$(t "只给 ${NETX_USER} 开放这些目录的“进入”权限(不能查看内容)?[Y/n] " "Grant ${NETX_USER} traverse-only access to them? [Y/n] ")" ans || true
|
||
if [[ ! "${ans}" =~ ^[Nn] ]]; then
|
||
GRANT_ACCESS=1
|
||
check_netx_access "$@"
|
||
return
|
||
fi
|
||
fi
|
||
die "$(t "请加 --grant-access 自动授权,或手动执行:setfacl -m u:${NETX_USER}:x ${blocked}(没有 setfacl 时用 chmod o+x),或换到 /root 以外的目录" "re-run with --grant-access, or run: setfacl -m u:${NETX_USER}:x ${blocked} (chmod o+x without setfacl), or use a path outside /root")"
|
||
fi
|
||
for d in "$@"; do
|
||
netx_can_enter "${d}" || die "$(t "授权后 ${NETX_USER} 仍无法进入" "still not enterable by ${NETX_USER} after granting"): ${d}"
|
||
done
|
||
ok "$(t "已授权 ${NETX_USER} 进入程序/数据目录" "granted ${NETX_USER} access to the program/data directories")"
|
||
}
|
||
|
||
# Show why NetX did not come up: app log first, else supervisor log, else systemd.
|
||
show_start_failure() {
|
||
local data="$1" f
|
||
for f in "${data}/logs/api.log" "${data}/logs/netxctl.log"; do
|
||
if [[ -s "${f}" ]]; then
|
||
echo "--- ${f}" >&2
|
||
tail -n 30 "${f}" >&2
|
||
return 0
|
||
fi
|
||
done
|
||
if svc_unit_installed; then
|
||
echo "--- journalctl -u netx" >&2
|
||
journalctl -u netx.service -n 30 --no-pager >&2 2>/dev/null || systemctl status netx.service --no-pager >&2 || true
|
||
fi
|
||
}
|
||
|
||
# print_admin_login ENV_FILE — tell the truth about the admin login: the generated
|
||
# initial password only applies when NetX created the admin itself.
|
||
print_admin_login() {
|
||
local out state user pw
|
||
out="$(netx_admin admin-status --env "$1" 2>/dev/null)" || return 0
|
||
state="$(printf '%s
|
||
' "${out}" | sed -n 's/^state=//p')"
|
||
user="$(printf '%s
|
||
' "${out}" | sed -n 's/^username=//p')"
|
||
pw="$(printf '%s
|
||
' "${out}" | sed -n 's/^password=//p')"
|
||
case "${state}" in
|
||
initial)
|
||
echo " $(t "登录账号" "login") : ${user} / ${pw} ($(t "初始密码,首次登录后必须修改" "initial password; change it at first login"))" ;;
|
||
changed)
|
||
echo " $(t "登录账号" "login") : ${user} $(t "已有自己的密码(改过密码,或沿用了旧数据库里的账号),请用原来的密码登录" "already has its own password (changed, or reused from an existing database): use that one")" ;;
|
||
none)
|
||
echo " $(t "登录账号" "login") : ${user} / ${pw} ($(t "首次启动建表后生效,首次登录后必须修改" "takes effect at first start; change it at first login"))" ;;
|
||
*)
|
||
echo " $(t "登录账号" "login") : $(t "暂时无法读取数据库,稍后执行 sudo netxctl admin-password 查看" "database not readable right now; run sudo netxctl admin-password later")" ;;
|
||
esac
|
||
}
|
||
|
||
is_loopback() { case "$1" in 127.0.0.1|localhost|::1) return 0 ;; *) return 1 ;; esac; }
|
||
|
||
primary_ip() {
|
||
local ip=""
|
||
if command -v hostname >/dev/null 2>&1; then
|
||
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||
fi
|
||
[[ -n "${ip}" ]] || ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')"
|
||
echo "${ip:-<server-ip>}"
|
||
}
|
||
|
||
# ---------------------------------------------------------------- PostgreSQL
|
||
# Callers set: PGBIN (…/pgsql/bin), PGDATA_DIR, NETX_LOG_DIR.
|
||
|
||
pg_ctl_netx() { as_netx env LC_ALL=C LANG=C "${PGBIN}/pg_ctl" "$@"; }
|
||
pg_is_running() { pg_ctl_netx -D "${PGDATA_DIR}" status >/dev/null 2>&1; }
|
||
pg_start() {
|
||
pg_ctl_netx -D "${PGDATA_DIR}" -l "${NETX_LOG_DIR}/pg.log" -w -t 90 start >/dev/null
|
||
}
|
||
pg_stop() { pg_ctl_netx -D "${PGDATA_DIR}" -m fast -w -t 60 stop >/dev/null 2>&1 || true; }
|
||
|
||
# Major version of an initialised cluster (empty when none).
|
||
pg_data_major() { [[ -f "${PGDATA_DIR}/PG_VERSION" ]] && tr -d ' \n' <"${PGDATA_DIR}/PG_VERSION"; }
|
||
|
||
# ---------------------------------------------------------------- service control
|
||
# svc_* PREFIX DATA
|
||
|
||
svc_unit_installed() { have_systemd && [[ -f /etc/systemd/system/netx.service ]]; }
|
||
svc_pidfile() { echo "$1/data/runtime/netx.pid"; }
|
||
|
||
svc_pid_alive() {
|
||
local pf pid
|
||
pf="$(svc_pidfile "$1")"
|
||
[[ -f "${pf}" ]] || return 1
|
||
pid="$(cat "${pf}" 2>/dev/null)"
|
||
[[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null
|
||
}
|
||
|
||
svc_running() {
|
||
local data="$2"
|
||
if svc_unit_installed; then
|
||
systemctl is-active --quiet netx.service
|
||
else
|
||
svc_pid_alive "${data}"
|
||
fi
|
||
}
|
||
|
||
svc_start() {
|
||
local prefix="$1" data="$2"
|
||
if svc_unit_installed; then
|
||
systemctl start netx.service
|
||
return
|
||
fi
|
||
if svc_pid_alive "${data}"; then
|
||
return 0
|
||
fi
|
||
local ctl="${prefix}/current/packaging/linux/netxctl" pf
|
||
pf="$(svc_pidfile "${data}")"
|
||
# Own session => the pid in the file also names the process group to stop.
|
||
# (Bundled Python instead of setsid/nohup: minimal images lack util-linux.)
|
||
# Installed copy: install.sh's own NETX_PY may live where netx cannot enter (/root).
|
||
as_netx "${prefix}/current/python/bin/python3" -I "${prefix}/current/packaging/linux/netx_admin.py" spawn "${data}/logs/netxctl.log" "${pf}" "${ctl}" run
|
||
}
|
||
|
||
svc_stop() {
|
||
local prefix="$1" data="$2"
|
||
if svc_unit_installed; then
|
||
systemctl stop netx.service || true
|
||
elif svc_pid_alive "${data}"; then
|
||
local pid i
|
||
pid="$(cat "$(svc_pidfile "${data}")")"
|
||
kill -TERM "${pid}" 2>/dev/null || true
|
||
for ((i = 0; i < 90; i++)); do
|
||
kill -0 "${pid}" 2>/dev/null || break
|
||
sleep 1
|
||
done
|
||
if kill -0 "${pid}" 2>/dev/null; then
|
||
warn "$(t "进程未在 90 秒内退出,强制结束" "netx did not exit in 90s; killing")"
|
||
kill -KILL -- "-${pid}" 2>/dev/null || kill -KILL "${pid}" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
rm -f "$(svc_pidfile "${data}")"
|
||
# A crashed supervisor can leave the bundled cluster running.
|
||
if [[ -x "${prefix}/current/pgsql/bin/pg_ctl" && -f "${data}/pgdata/PG_VERSION" ]]; then
|
||
PGBIN="${prefix}/current/pgsql/bin" PGDATA_DIR="${data}/pgdata" pg_stop
|
||
fi
|
||
return 0
|
||
}
|