mirror of
https://github.com/p1neappleXpress/OpenFlux.git
synced 2026-10-21 06:20:22 +08:00
The exit-node build of this tree pinned by SHA-256 for linux amd64, arm64 and arm (reproducible, Go 1.26.4). provision.PinnedSHA256 follows the script; PinnedCommit is pointed at this commit next.
910 lines
38 KiB
Bash
910 lines
38 KiB
Bash
#!/bin/sh
|
|
# Managed by OpenFlux node-install.sh
|
|
#
|
|
# Installs one OpenFlux exit channel on a Linux VDS. The "Своя нода" wizard
|
|
# of the OpenFlux apps downloads this file by a pinned commit, checks
|
|
# its SHA-256 and runs it over SSH. Every channel is independent: its own
|
|
# transports (a Yandex document, a Mail.ru document, cups.online rooms, and
|
|
# always direct as the backup), key, port and systemd instance
|
|
# (openflux-node@<channel>). Nothing outside the paths below is touched, so
|
|
# existing services (other OpenFlux installs, Docker, VPNs) keep running.
|
|
#
|
|
# /opt/openflux-node/bin/ core binaries (from GitHub Releases)
|
|
# /opt/openflux-node/node-install.sh this script, for the updater
|
|
# /etc/openflux-node/<channel>/ node.conf, encryption-key (0640), port,
|
|
# firewall; the directory is 0751 so a
|
|
# plain user's plan sees the channel and
|
|
# its port but not its secrets
|
|
# /var/lib/openflux-node/<channel>/ cookie store (systemd StateDirectory)
|
|
# /etc/systemd/system/openflux-node@.service
|
|
# /etc/systemd/system/openflux-node-update.{service,timer}
|
|
# the core updater, when enabled
|
|
#
|
|
# Usage: node-install.sh probe
|
|
# node-install.sh plan|status (config on stdin)
|
|
# node-install.sh apply|remove CONFIG_FILE (run as root)
|
|
# node-install.sh remove CHANNEL (run as root: remove one
|
|
# channel by name)
|
|
# node-install.sh list (the channels and their state)
|
|
# node-install.sh uninstall (run as root: remove every
|
|
# channel and everything this
|
|
# script installed)
|
|
# node-install.sh upgrade (run as root: move every
|
|
# channel to this core)
|
|
# node-install.sh update (run as root, by the timer:
|
|
# move every channel to the
|
|
# newest node-v* release)
|
|
# node-install.sh autoupdate on|off (run as root: the updater)
|
|
# The config is "key=value" lines: channel, key, port, the transports
|
|
# (vyandex= or its old name url=: a Yandex document; mailru=: a Mail.ru
|
|
# public document; cupsonline=: the packed room list) and autoupdate=yes|no.
|
|
# apply and remove take it from a 0600 temp file, which they delete after reading, so that
|
|
# stdin stays free for `sudo -S` (a wrong sudo password would otherwise make
|
|
# sudo read the config as further password attempts). Secrets never appear
|
|
# in arguments, so they stay out of ps and shell history.
|
|
# Output is one JSON object on stdout.
|
|
#
|
|
# Works with any systemd from 219 (CentOS 7) on: no "systemctl --now" or
|
|
# "show --value", and the unit runs without StateDirectory= too.
|
|
|
|
set -u
|
|
umask 077
|
|
|
|
CORE_VERSION="node-v1.2.0"
|
|
SHA_amd64="25c4ca5aa9fd00aa4d8a82c4e218778c2bd4465e1dd81f51f5c1cc366c278653"
|
|
SHA_arm64="1761e52c24824634d8e7e993c8f04b7800fc267e9fdcc8d421e3ff0c7c01c94c"
|
|
SHA_arm="e04a888f1103b424c7db16d83ab715c140913fa91fadd2ccddbf8a1fe39890f5"
|
|
# The repository this script and its core come from: the core is one of its
|
|
# node-v* releases, and the updater follows them (UPDATE_CONF may override
|
|
# that with a "repo=owner/name" line).
|
|
RELEASE_REPO="p1neappleXpress/OpenFlux"
|
|
GITHUB_API="https://api.github.com"
|
|
GITHUB_RAW="https://raw.githubusercontent.com"
|
|
GITHUB_WEB="https://github.com"
|
|
CORE_BASE="$GITHUB_WEB/$RELEASE_REPO/releases/download/$CORE_VERSION"
|
|
|
|
BIN_DIR="/opt/openflux-node/bin"
|
|
CONF_ROOT="/etc/openflux-node"
|
|
STATE_ROOT="/var/lib/openflux-node"
|
|
UNIT_FILE="/etc/systemd/system/openflux-node@.service"
|
|
UPDATE_SERVICE="/etc/systemd/system/openflux-node-update.service"
|
|
UPDATE_TIMER="/etc/systemd/system/openflux-node-update.timer"
|
|
UPDATE_CONF="$CONF_ROOT/update.conf"
|
|
SELF_COPY="/opt/openflux-node/node-install.sh"
|
|
NODE_USER="openflux-node"
|
|
MARKER="# Managed by OpenFlux node-install.sh"
|
|
|
|
# ---- output -----------------------------------------------------------------
|
|
|
|
# fail STEP MESSAGE: prints the error object and exits. Messages are fixed
|
|
# strings or validated values, never secrets.
|
|
fail() {
|
|
printf '{"ok":false,"step":"%s","error":"%s"}\n' "$1" "$(json_escape "$2")"
|
|
exit 1
|
|
}
|
|
|
|
json_escape() {
|
|
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | tr '\n\t\r' ' '
|
|
}
|
|
|
|
json_list() {
|
|
first=1
|
|
printf '['
|
|
for item in "$@"; do
|
|
[ "$first" = 1 ] || printf ','
|
|
printf '"%s"' "$(json_escape "$item")"
|
|
first=0
|
|
done
|
|
printf ']'
|
|
}
|
|
|
|
# ---- environment ------------------------------------------------------------
|
|
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
# unit_start / unit_stop UNIT: enable and start, stop and disable, in two
|
|
# calls each: "systemctl --now" needs systemd 220.
|
|
unit_start() { systemctl enable "$1" >/dev/null 2>&1 && systemctl start "$1" >/dev/null 2>&1; }
|
|
unit_stop() {
|
|
systemctl stop "$1" >/dev/null 2>&1
|
|
systemctl disable "$1" >/dev/null 2>&1
|
|
return 0
|
|
}
|
|
|
|
# main_pid UNIT: its main process id, 0 when none ("show --value" needs 230).
|
|
main_pid() { systemctl show -p MainPID "$1" 2>/dev/null | sed -n 's/^MainPID=//p'; }
|
|
|
|
detect_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) echo amd64 ;;
|
|
aarch64|arm64) echo arm64 ;;
|
|
armv7l|armv6l|armhf) echo arm ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
core_sha() {
|
|
case "$1" in
|
|
amd64) echo "$SHA_amd64" ;;
|
|
arm64) echo "$SHA_arm64" ;;
|
|
arm) echo "$SHA_arm" ;;
|
|
esac
|
|
}
|
|
|
|
downloader() {
|
|
if have curl; then echo curl; elif have wget; then echo wget; else echo ""; fi
|
|
}
|
|
|
|
fetch() { # URL DEST
|
|
case "$(downloader)" in
|
|
curl) curl -fsSL --retry 3 --connect-timeout 20 -o "$2" "$1" ;;
|
|
wget) wget -q -T 20 -t 3 -O "$2" "$1" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
sha256_of() {
|
|
if have sha256sum; then sha256sum "$1" | cut -d' ' -f1
|
|
else openssl dgst -sha256 "$1" | sed 's/.*= //'
|
|
fi
|
|
}
|
|
|
|
firewall_kind() {
|
|
if have ufw && ufw status 2>/dev/null | grep -q '^Status: active'; then echo ufw
|
|
elif have firewall-cmd && firewall-cmd --state >/dev/null 2>&1; then echo firewalld
|
|
else echo none
|
|
fi
|
|
}
|
|
|
|
sudo_mode() {
|
|
if [ "$(id -u)" = 0 ]; then echo root
|
|
elif ! have sudo; then echo none
|
|
elif sudo -n true 2>/dev/null; then echo nopasswd
|
|
else echo password
|
|
fi
|
|
}
|
|
|
|
list_channels() {
|
|
[ -d "$CONF_ROOT" ] || return 0
|
|
for d in "$CONF_ROOT"/*/; do
|
|
[ -d "$d" ] && basename "$d"
|
|
done
|
|
}
|
|
|
|
# version_gt A B: whether release tag A (node-vX.Y.Z) is newer than B. An
|
|
# empty or malformed B counts as older than anything.
|
|
version_gt() {
|
|
printf '%s\n%s\n' "${1#node-v}" "${2#node-v}" | awk -F. '
|
|
NR == 1 { for (i = 1; i <= 3; i++) a[i] = $i + 0; ok = ($0 ~ /^[0-9]+\.[0-9]+\.[0-9]+$/) }
|
|
NR == 2 { for (i = 1; i <= 3; i++) b[i] = $i + 0 }
|
|
END {
|
|
if (!ok) exit 1
|
|
for (i = 1; i <= 3; i++) { if (a[i] > b[i]) exit 0; if (a[i] < b[i]) exit 1 }
|
|
exit 1
|
|
}'
|
|
}
|
|
|
|
# installed_core: the release BIN_DIR/openflux points at, "" if none.
|
|
installed_core() {
|
|
target=$(readlink "$BIN_DIR/openflux" 2>/dev/null) || return 0
|
|
case "$target" in
|
|
openflux-node-v*) [ -x "$BIN_DIR/$target" ] && printf '%s' "${target#openflux-}" ;;
|
|
esac
|
|
}
|
|
|
|
# managed_core REPO: installed_core, if this script or the updater put it
|
|
# there from REPO's releases (BIN_DIR/.managed lists "repo tag" lines).
|
|
# A core from anywhere else counts as none: another repository's release
|
|
# numbers, like an old fork's node-v1.4.0, say nothing about REPO's.
|
|
managed_core() {
|
|
cur=$(installed_core)
|
|
[ -n "$cur" ] && grep -qsxF "$1 $cur" "$BIN_DIR/.managed" && printf '%s' "$cur"
|
|
return 0
|
|
}
|
|
|
|
# mark_managed REPO TAG: BIN_DIR/openflux-TAG is REPO's release now.
|
|
mark_managed() {
|
|
{ grep -sv " $2\$" "$BIN_DIR/.managed"; printf '%s %s\n' "$1" "$2"; } > "$BIN_DIR/.managed.new" \
|
|
&& mv -f "$BIN_DIR/.managed.new" "$BIN_DIR/.managed"
|
|
# A plain user's plan reads it too.
|
|
chmod 0644 "$BIN_DIR/.managed"
|
|
}
|
|
|
|
# core_to_use: this script's core, or the installed one when the updater has
|
|
# already moved the server to a newer release of the same repository (an
|
|
# older app must not downgrade every channel).
|
|
core_to_use() {
|
|
cur=$(managed_core "$RELEASE_REPO")
|
|
if [ -n "$cur" ] && version_gt "$cur" "$CORE_VERSION"; then printf '%s' "$cur"; else printf '%s' "$CORE_VERSION"; fi
|
|
}
|
|
|
|
autoupdate_on() {
|
|
[ -f "$UPDATE_TIMER" ] && systemctl is-enabled --quiet openflux-node-update.timer 2>/dev/null
|
|
}
|
|
|
|
release_repo() {
|
|
repo=$(sed -n 's/^repo=\([A-Za-z0-9_.-]*\/[A-Za-z0-9_.-]*\)$/\1/p' "$UPDATE_CONF" 2>/dev/null | tail -n 1)
|
|
printf '%s' "${repo:-$RELEASE_REPO}"
|
|
}
|
|
|
|
port_busy() { # PORT
|
|
if have ss; then
|
|
# No -H: older iproute2 lacks it; the first line is the header.
|
|
[ -n "$(ss -ltn "sport = :$1" 2>/dev/null | tail -n +2)" ]
|
|
elif have netstat; then
|
|
netstat -ltn 2>/dev/null | awk '{print $4}' | grep -q "[:.]$1\$"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
port_claimed() { # PORT: another of our channels is configured for it
|
|
[ -d "$CONF_ROOT" ] || return 1
|
|
grep -qsx "$1" "$CONF_ROOT"/*/port
|
|
}
|
|
|
|
pick_port() {
|
|
seed=$(od -An -N2 -tu2 /dev/urandom | tr -d ' ')
|
|
i=0
|
|
while [ $i -lt 200 ]; do
|
|
p=$(( 20000 + (seed + i * 7919) % 40000 ))
|
|
if ! port_busy "$p" && ! port_claimed "$p"; then echo "$p"; return 0; fi
|
|
i=$((i + 1))
|
|
done
|
|
echo ""
|
|
}
|
|
|
|
# ---- input ------------------------------------------------------------------
|
|
|
|
CHANNEL=""; URL=""; MAILRU=""; CUPS=""; KEY=""; PORT=""; AUTOUPDATE=""
|
|
|
|
# read_config [FILE]: reads stdin, or FILE and then deletes it.
|
|
read_config() {
|
|
if [ $# -gt 0 ]; then
|
|
[ -f "$1" ] || fail input "нет файла конфигурации"
|
|
read_config < "$1"
|
|
rm -f "$1"
|
|
return
|
|
fi
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
case "$line" in
|
|
channel=*) CHANNEL=${line#channel=} ;;
|
|
url=*) URL=${line#url=} ;;
|
|
vyandex=*) URL=${line#vyandex=} ;;
|
|
mailru=*) MAILRU=${line#mailru=} ;;
|
|
cupsonline=*) CUPS=${line#cupsonline=} ;;
|
|
key=*) KEY=${line#key=} ;;
|
|
port=*) PORT=${line#port=} ;;
|
|
autoupdate=*) AUTOUPDATE=${line#autoupdate=} ;;
|
|
"") ;;
|
|
*) fail input "неизвестная строка конфигурации" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
valid_channel() { printf '%s' "$1" | grep -Eq '^[a-z0-9][a-z0-9-]{0,30}$'; }
|
|
valid_key() { printf '%s' "$1" | grep -Eq '^[0-9a-f]{64}$'; }
|
|
valid_url() {
|
|
printf '%s' "$1" | grep -Eq '^https://(docs|disk)\.yandex\.(ru|com|by|kz|uz)/edit/d/[A-Za-z0-9_-]{16,200}$'
|
|
}
|
|
valid_mailru() {
|
|
printf '%s' "$1" | grep -Eq '^https://cloud\.mail\.ru/public/[A-Za-z0-9_-]{2,64}/[A-Za-z0-9_-]{2,128}$'
|
|
}
|
|
# The cups.online room list: base64url of a JSON list of room ids.
|
|
valid_rooms() { [ ${#1} -ge 8 ] && [ ${#1} -le 4096 ] && printf '%s' "$1" | grep -Eq '^[A-Za-z0-9_-]+$'; }
|
|
valid_port() {
|
|
printf '%s' "$1" | grep -Eq '^[0-9]{4,5}$' && [ "$1" -ge 1024 ] && [ "$1" -le 65535 ]
|
|
}
|
|
|
|
# check_transports: validates the chosen transports (plan and apply).
|
|
check_transports() {
|
|
[ -z "$URL" ] || valid_url "$URL" || fail input "адрес документа должен быть вида https://docs.yandex.ru/edit/d/..."
|
|
[ -z "$MAILRU" ] || valid_mailru "$MAILRU" || fail input "ссылка Mail.ru должна быть вида https://cloud.mail.ru/public/..."
|
|
[ -z "$CUPS" ] || valid_rooms "$CUPS" || fail input "неверный список комнат cups.online"
|
|
case "$AUTOUPDATE" in ""|yes|no) ;; *) fail input "autoupdate: yes или no" ;; esac
|
|
}
|
|
|
|
# transport_names: the channel's transports for people, primary first.
|
|
transport_names() {
|
|
names=""
|
|
[ -n "$URL" ] && names="Яндекс Документ"
|
|
[ -n "$MAILRU" ] && names="${names:+$names, }Mail.ru Документ"
|
|
[ -n "$CUPS" ] && names="${names:+$names, }cups.online"
|
|
printf '%s' "$names"
|
|
}
|
|
|
|
# session_context: the encryption context both sides derive, as the core's
|
|
# pickSessionContext does: the highest-priority transport's URL, cups.online
|
|
# aside. "" leaves node.conf without a URL line (the core's "http://#").
|
|
session_context() {
|
|
if [ -n "$URL" ]; then printf '%s' "$URL"; elif [ -n "$MAILRU" ]; then printf '%s' "$MAILRU"; fi
|
|
}
|
|
|
|
check_channel() {
|
|
[ -n "$CHANNEL" ] || fail input "не указан канал"
|
|
valid_channel "$CHANNEL" || fail input "имя канала: только a-z, 0-9 и дефис, до 31 символа"
|
|
}
|
|
|
|
# ---- commands ---------------------------------------------------------------
|
|
|
|
cmd_probe() {
|
|
arch=$(detect_arch)
|
|
systemd=false
|
|
[ -d /run/systemd/system ] && have systemctl && systemd=true
|
|
os=""
|
|
[ -r /etc/os-release ] && os=$(. /etc/os-release && printf '%s %s' "${ID:-linux}" "${VERSION_ID:-}")
|
|
autoupdate=false
|
|
autoupdate_on && autoupdate=true
|
|
# shellcheck disable=SC2046
|
|
printf '{"ok":true,"arch":"%s","os":"%s","systemd":%s,"sudo":"%s","downloader":"%s","firewall":"%s","core":"%s","autoupdate":%s,"channels":%s}\n' \
|
|
"$arch" "$(json_escape "$os")" "$systemd" "$(sudo_mode)" "$(downloader)" "$(firewall_kind)" \
|
|
"$(core_to_use)" "$autoupdate" "$(json_list $(list_channels))"
|
|
}
|
|
|
|
# plan: read-only. Tells the app exactly what apply will change.
|
|
cmd_plan() {
|
|
read_config
|
|
check_channel
|
|
check_transports
|
|
arch=$(detect_arch)
|
|
[ -n "$arch" ] || fail plan "архитектура $(uname -m) не поддерживается"
|
|
[ -d /run/systemd/system ] && have systemctl || fail plan "на сервере нет systemd"
|
|
[ -n "$(downloader)" ] || fail plan "на сервере нет curl или wget"
|
|
[ -d "$CONF_ROOT/$CHANNEL" ] && fail plan "канал $CHANNEL уже существует на сервере"
|
|
if [ -n "$PORT" ]; then
|
|
valid_port "$PORT" || fail plan "порт должен быть в диапазоне 1024-65535"
|
|
if port_busy "$PORT" || port_claimed "$PORT"; then fail plan "порт $PORT занят"; fi
|
|
else
|
|
PORT=$(pick_port)
|
|
[ -n "$PORT" ] || fail plan "не удалось найти свободный порт"
|
|
fi
|
|
if [ -f "$UNIT_FILE" ] && ! grep -qF "$MARKER" "$UNIT_FILE"; then
|
|
fail plan "$UNIT_FILE создан не мастером OpenFlux, не трогаю его"
|
|
fi
|
|
|
|
core=$(core_to_use)
|
|
set --
|
|
id "$NODE_USER" >/dev/null 2>&1 || set -- "$@" "Создать системного пользователя $NODE_USER (без входа и домашней папки)"
|
|
# The installed file counts only if it is this release of RELEASE_REPO
|
|
# (another repository may have a release with the same number).
|
|
if [ "$core" != "$CORE_VERSION" ] || { [ -x "$BIN_DIR/openflux-$core" ] && [ "$(sha256_of "$BIN_DIR/openflux-$core")" = "$(core_sha "$arch")" ]; }; then
|
|
set -- "$@" "Использовать уже установленное ядро OpenFlux $core ($RELEASE_REPO)"
|
|
else
|
|
set -- "$@" "Скачать ядро OpenFlux $core (linux-$arch) из релизов $RELEASE_REPO на GitHub и сверить SHA-256 в $BIN_DIR"
|
|
fi
|
|
set -- "$@" "Создать $CONF_ROOT/$CHANNEL: node.conf и ключ шифрования канала (права 0640)"
|
|
[ -f "$UNIT_FILE" ] || set -- "$@" "Установить шаблон systemd $UNIT_FILE"
|
|
names=$(transport_names)
|
|
if [ -n "$names" ]; then
|
|
set -- "$@" "Запустить openflux-node@$CHANNEL: $names и direct на порту $PORT/tcp (резерв)"
|
|
else
|
|
set -- "$@" "Запустить openflux-node@$CHANNEL: только direct на порту $PORT/tcp"
|
|
fi
|
|
case "$(firewall_kind)" in
|
|
ufw) set -- "$@" "Разрешить входящий $PORT/tcp в ufw" ;;
|
|
firewalld) set -- "$@" "Разрешить входящий $PORT/tcp в firewalld" ;;
|
|
esac
|
|
case "$AUTOUPDATE" in
|
|
yes)
|
|
if autoupdate_on; then
|
|
set -- "$@" "Автообновление ядра уже включено на сервере (openflux-node-update.timer)"
|
|
else
|
|
set -- "$@" "Включить автообновление ядра (openflux-node-update.timer): раз в 6 часов проверять релизы node-v* в $(release_repo) на GitHub, сверять SHA-256, перезапускать каналы и откатываться, если нода не поднялась"
|
|
fi ;;
|
|
no)
|
|
autoupdate_on && set -- "$@" "Выключить автообновление ядра на сервере (для всех каналов)" ;;
|
|
esac
|
|
|
|
# shellcheck disable=SC2046
|
|
printf '{"ok":true,"channel":"%s","port":%s,"arch":"%s","core":"%s","actions":%s,"untouched":%s}\n' \
|
|
"$CHANNEL" "$PORT" "$arch" "$core" "$(json_list "$@")" "$(json_list $(list_channels))"
|
|
}
|
|
|
|
# Rollback state for apply: what this run created.
|
|
CREATED_USER=0; CREATED_UNIT=0; CREATED_BIN=0; CREATED_CONF=0; CREATED_FW=""; STARTED=0
|
|
|
|
rollback() {
|
|
[ "$STARTED" = 1 ] && unit_stop "openflux-node@$CHANNEL"
|
|
case "$CREATED_FW" in
|
|
ufw) ufw delete allow "$PORT/tcp" >/dev/null 2>&1 ;;
|
|
firewalld) firewall-cmd --permanent --remove-port="$PORT/tcp" >/dev/null 2>&1 && firewall-cmd --reload >/dev/null 2>&1 ;;
|
|
esac
|
|
[ "$CREATED_CONF" = 1 ] && rm -rf "${CONF_ROOT:?}/$CHANNEL" "${STATE_ROOT:?}/$CHANNEL"
|
|
[ "$CREATED_UNIT" = 1 ] && rm -f "$UNIT_FILE" && systemctl daemon-reload >/dev/null 2>&1
|
|
[ "$CREATED_BIN" = 1 ] && rm -f "$BIN_DIR/openflux-$CORE_VERSION"
|
|
[ "$CREATED_USER" = 1 ] && userdel "$NODE_USER" >/dev/null 2>&1
|
|
}
|
|
|
|
apply_fail() {
|
|
rollback
|
|
fail "$1" "$2"
|
|
}
|
|
|
|
# install_core ARCH: puts this script's core version into BIN_DIR, checked
|
|
# against its pinned SHA-256, and points BIN_DIR/openflux at it. Sets
|
|
# CREATED_BIN when it downloaded, CORE_ERROR on failure.
|
|
CORE_ERROR=""
|
|
install_core() {
|
|
mkdir -p "$BIN_DIR" && chmod 0755 /opt/openflux-node "$BIN_DIR"
|
|
# The updater already runs a newer release: keep it.
|
|
[ "$(core_to_use)" = "$CORE_VERSION" ] || return 0
|
|
core="$BIN_DIR/openflux-$CORE_VERSION"
|
|
want=$(core_sha "$1")
|
|
if [ ! -x "$core" ] || [ "$(sha256_of "$core")" != "$want" ]; then
|
|
tmp=$(mktemp "$BIN_DIR/.download.XXXXXX") || { CORE_ERROR="не удалось создать временный файл"; return 1; }
|
|
if ! fetch "$CORE_BASE/openflux-linux-$1" "$tmp"; then
|
|
rm -f "$tmp"
|
|
CORE_ERROR="не удалось скачать ядро с GitHub"
|
|
return 1
|
|
fi
|
|
if [ "$(sha256_of "$tmp")" != "$want" ]; then
|
|
rm -f "$tmp"
|
|
CORE_ERROR="SHA-256 скачанного ядра не совпал, установка остановлена"
|
|
return 1
|
|
fi
|
|
chmod 0755 "$tmp" && mv -f "$tmp" "$core"
|
|
CREATED_BIN=1
|
|
fi
|
|
ln -sfn "openflux-$CORE_VERSION" "$BIN_DIR/openflux"
|
|
mark_managed "$RELEASE_REPO" "$CORE_VERSION"
|
|
}
|
|
|
|
write_unit() {
|
|
cat > "$UNIT_FILE" <<EOF
|
|
$MARKER
|
|
[Unit]
|
|
Description=OpenFlux node channel %i
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$NODE_USER
|
|
Group=$NODE_USER
|
|
StateDirectory=openflux-node/%i
|
|
# apply creates the directory too: systemd before 235 ignores StateDirectory=.
|
|
ReadWritePaths=-$STATE_ROOT/%i
|
|
WorkingDirectory=$STATE_ROOT/%i
|
|
ExecStart=$BIN_DIR/openflux --config $CONF_ROOT/%i/node.conf
|
|
Restart=always
|
|
RestartSec=5
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectControlGroups=true
|
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
chmod 0644 "$UNIT_FILE"
|
|
}
|
|
|
|
# script_core FILE: the CORE_VERSION a copy of this script pins.
|
|
script_core() { sed -n 's/^CORE_VERSION="\(node-v[0-9.]*\)"$/\1/p' "$1" 2>/dev/null | head -n 1; }
|
|
|
|
# script_repo FILE: the RELEASE_REPO a copy of this script follows.
|
|
script_repo() { sed -n 's/^RELEASE_REPO="\([^"]*\)"$/\1/p' "$1" 2>/dev/null | head -n 1; }
|
|
|
|
# install_self FILE: makes FILE the script the updater runs, unless the copy
|
|
# there follows the same repository and already pins a newer core (the
|
|
# updater put a newer release's script). A script from another repository
|
|
# always replaces it: the server now runs that repository's core.
|
|
install_self() {
|
|
if [ -f "$SELF_COPY" ] && [ "$(script_repo "$SELF_COPY")" = "$(script_repo "$1")" ] &&
|
|
version_gt "$(script_core "$SELF_COPY")" "$(script_core "$1")"; then
|
|
return 0
|
|
fi
|
|
mkdir -p /opt/openflux-node && chmod 0755 /opt/openflux-node || return 1
|
|
tmp=$(mktemp /opt/openflux-node/.node-install.XXXXXX) || return 1
|
|
if cat "$1" > "$tmp" && chmod 0755 "$tmp" && mv -f "$tmp" "$SELF_COPY"; then return 0; fi
|
|
rm -f "$tmp"
|
|
return 1
|
|
}
|
|
|
|
# enable_updater: openflux-node-update.timer runs "update" from the copy of
|
|
# this script: shortly after boot or install, then every 6 hours.
|
|
enable_updater() {
|
|
install_self "$0" || return 1
|
|
cat > "$UPDATE_SERVICE" <<EOF
|
|
$MARKER
|
|
[Unit]
|
|
Description=Update the OpenFlux node core to the newest node-v* release
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/bin/sh $SELF_COPY update
|
|
EOF
|
|
cat > "$UPDATE_TIMER" <<EOF
|
|
$MARKER
|
|
[Unit]
|
|
Description=Check for a newer OpenFlux node core
|
|
|
|
[Timer]
|
|
OnBootSec=15min
|
|
OnUnitActiveSec=6h
|
|
RandomizedDelaySec=30min
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
chmod 0644 "$UPDATE_SERVICE" "$UPDATE_TIMER"
|
|
systemctl daemon-reload >/dev/null 2>&1 && unit_start openflux-node-update.timer
|
|
}
|
|
|
|
disable_updater() {
|
|
unit_stop openflux-node-update.timer
|
|
for f in "$UPDATE_TIMER" "$UPDATE_SERVICE"; do
|
|
[ -f "$f" ] && grep -qF "$MARKER" "$f" && rm -f "$f"
|
|
done
|
|
rm -f "$SELF_COPY"
|
|
systemctl daemon-reload >/dev/null 2>&1
|
|
}
|
|
|
|
# apply_autoupdate: AUTOUPDATE yes turns the updater on (and refreshes the
|
|
# script it runs), no turns it off; empty leaves it as it is.
|
|
apply_autoupdate() {
|
|
case "$AUTOUPDATE" in
|
|
yes) enable_updater ;;
|
|
no) autoupdate_on && disable_updater; return 0 ;;
|
|
*) autoupdate_on && install_self "$0"; return 0 ;;
|
|
esac
|
|
}
|
|
|
|
# write_node_conf: the channel's node.conf on stdout. Priorities and the
|
|
# URL line (the encryption context) match what the app puts into the
|
|
# channel's link (provision.ShareLink).
|
|
write_node_conf() {
|
|
cat <<EOF
|
|
# OpenFlux node channel $CHANNEL, written by node-install.sh
|
|
Role = exit
|
|
Mode = l4
|
|
EncryptionKeyFile = $CONF_ROOT/$CHANNEL/encryption-key
|
|
CookieStore = $STATE_ROOT/$CHANNEL/cookies.json
|
|
EOF
|
|
context=$(session_context)
|
|
[ -n "$context" ] && printf 'URL = %s\n' "$context"
|
|
[ -n "$URL" ] && printf '\n[Transport vyandex]\nType = vyandex\nPriority = 100\nURL = %s\n' "$URL"
|
|
[ -n "$MAILRU" ] && printf '\n[Transport mailru]\nType = mailru\nPriority = 90\nURL = %s\n' "$MAILRU"
|
|
[ -n "$CUPS" ] && printf '\n[Transport cupsonline]\nType = cupsonline\nPriority = 70\nURL = %s\n' "$CUPS"
|
|
printf '\n[Transport direct]\nType = direct\nPriority = 50\nListen = 0.0.0.0:%s\n' "$PORT"
|
|
}
|
|
|
|
cmd_apply() {
|
|
[ "$(id -u)" = 0 ] || fail apply "нужны права root (sudo)"
|
|
read_config "$@"
|
|
check_channel
|
|
valid_key "$KEY" || fail input "ключ канала должен быть 64 hex-символа"
|
|
check_transports
|
|
valid_port "$PORT" || fail input "не указан порт из плана"
|
|
arch=$(detect_arch)
|
|
[ -n "$arch" ] || fail apply "архитектура $(uname -m) не поддерживается"
|
|
[ -d "$CONF_ROOT/$CHANNEL" ] && fail apply "канал $CHANNEL уже существует на сервере"
|
|
if port_busy "$PORT" || port_claimed "$PORT"; then fail apply "порт $PORT занят"; fi
|
|
if [ -f "$UNIT_FILE" ] && ! grep -qF "$MARKER" "$UNIT_FILE"; then
|
|
fail apply "$UNIT_FILE создан не мастером OpenFlux"
|
|
fi
|
|
|
|
if ! id "$NODE_USER" >/dev/null 2>&1; then
|
|
nologin=/usr/sbin/nologin
|
|
[ -x "$nologin" ] || nologin=/sbin/nologin
|
|
[ -x "$nologin" ] || nologin=/bin/false
|
|
if have useradd; then
|
|
useradd --system --no-create-home --home-dir /nonexistent --shell "$nologin" "$NODE_USER" \
|
|
|| apply_fail user "не удалось создать пользователя $NODE_USER"
|
|
else
|
|
adduser -S -H -D -s "$nologin" "$NODE_USER" 2>/dev/null \
|
|
|| apply_fail user "не удалось создать пользователя $NODE_USER"
|
|
fi
|
|
CREATED_USER=1
|
|
fi
|
|
|
|
install_core "$arch" || apply_fail download "$CORE_ERROR"
|
|
|
|
mkdir -p "$CONF_ROOT" && chmod 0755 "$CONF_ROOT"
|
|
dir="$CONF_ROOT/$CHANNEL"
|
|
mkdir "$dir" || apply_fail config "не удалось создать $dir"
|
|
CREATED_CONF=1
|
|
printf '%s\n' "$KEY" > "$dir/encryption-key"
|
|
write_node_conf > "$dir/node.conf"
|
|
printf '%s\n' "$PORT" > "$dir/port"
|
|
chown -R "root:$NODE_USER" "$dir"
|
|
chmod 0751 "$dir"
|
|
chmod 0640 "$dir/encryption-key" "$dir/node.conf"
|
|
chmod 0644 "$dir/port"
|
|
# The node's state directory (systemd before 235 does not make it).
|
|
mkdir -p "$STATE_ROOT/$CHANNEL" && chmod 0755 "$STATE_ROOT" && chown "$NODE_USER:$NODE_USER" "$STATE_ROOT/$CHANNEL" \
|
|
&& chmod 0750 "$STATE_ROOT/$CHANNEL" || apply_fail config "не удалось создать $STATE_ROOT/$CHANNEL"
|
|
|
|
if [ ! -f "$UNIT_FILE" ]; then
|
|
write_unit
|
|
CREATED_UNIT=1
|
|
fi
|
|
systemctl daemon-reload || apply_fail systemd "systemctl daemon-reload не удался"
|
|
|
|
case "$(firewall_kind)" in
|
|
ufw)
|
|
ufw allow "$PORT/tcp" comment "openflux-node $CHANNEL" >/dev/null 2>&1 \
|
|
|| apply_fail firewall "не удалось открыть порт в ufw"
|
|
CREATED_FW=ufw ;;
|
|
firewalld)
|
|
{ firewall-cmd --permanent --add-port="$PORT/tcp" && firewall-cmd --reload; } >/dev/null 2>&1 \
|
|
|| apply_fail firewall "не удалось открыть порт в firewalld"
|
|
CREATED_FW=firewalld ;;
|
|
esac
|
|
[ -n "$CREATED_FW" ] && printf '%s %s\n' "$CREATED_FW" "$PORT" > "$dir/firewall"
|
|
|
|
unit_start "openflux-node@$CHANNEL" \
|
|
|| apply_fail start "не удалось запустить openflux-node@$CHANNEL"
|
|
STARTED=1
|
|
sleep 4
|
|
if ! systemctl is-active --quiet "openflux-node@$CHANNEL"; then
|
|
logs=$(journalctl -u "openflux-node@$CHANNEL" -n 8 -o cat --no-pager 2>/dev/null | tail -n 8)
|
|
apply_fail start "нода не запустилась: $logs"
|
|
fi
|
|
# The channel runs; the updater is extra, and its failure only shows.
|
|
autoupdate=false
|
|
apply_autoupdate
|
|
autoupdate_on && autoupdate=true
|
|
printf '{"ok":true,"channel":"%s","port":%s,"core":"%s","autoupdate":%s}\n' "$CHANNEL" "$PORT" "$(core_to_use)" "$autoupdate"
|
|
}
|
|
|
|
# remove_channel CHANNEL: stops the channel, closes its firewall port and
|
|
# deletes its config and state.
|
|
remove_channel() {
|
|
unit_stop "openflux-node@$1"
|
|
if [ -f "$CONF_ROOT/$1/firewall" ]; then
|
|
read -r kind port < "$CONF_ROOT/$1/firewall"
|
|
case "$kind" in
|
|
ufw) ufw delete allow "$port/tcp" >/dev/null 2>&1 ;;
|
|
firewalld) firewall-cmd --permanent --remove-port="$port/tcp" >/dev/null 2>&1 && firewall-cmd --reload >/dev/null 2>&1 ;;
|
|
esac
|
|
fi
|
|
rm -rf "${CONF_ROOT:?}/$1" "${STATE_ROOT:?}/$1"
|
|
}
|
|
|
|
# remove_everything: what this script installed besides the channels: the
|
|
# updater, the unit template, the cores, the folders and the node user.
|
|
# Files that do not carry this script's marker are left alone.
|
|
remove_everything() {
|
|
disable_updater
|
|
[ -f "$UNIT_FILE" ] && grep -qF "$MARKER" "$UNIT_FILE" && rm -f "$UNIT_FILE"
|
|
systemctl daemon-reload >/dev/null 2>&1
|
|
systemctl reset-failed 'openflux-node@*' >/dev/null 2>&1
|
|
rm -rf /opt/openflux-node "$CONF_ROOT" "$STATE_ROOT"
|
|
id "$NODE_USER" >/dev/null 2>&1 && userdel "$NODE_USER" >/dev/null 2>&1
|
|
return 0
|
|
}
|
|
|
|
# remove CONFIG_FILE (the apps: channel= in a temp file) or remove CHANNEL
|
|
# (by hand on the server).
|
|
cmd_remove() {
|
|
[ "$(id -u)" = 0 ] || fail remove "нужны права root (sudo)"
|
|
if [ $# -gt 0 ] && [ ! -f "$1" ] && valid_channel "$1"; then
|
|
CHANNEL=$1
|
|
else
|
|
read_config "$@"
|
|
fi
|
|
check_channel
|
|
[ -d "$CONF_ROOT/$CHANNEL" ] || fail remove "канала $CHANNEL нет на сервере"
|
|
remove_channel "$CHANNEL"
|
|
# The last channel is gone: remove everything this script installed.
|
|
[ -z "$(list_channels)" ] && remove_everything
|
|
printf '{"ok":true,"channel":"%s"}\n' "$CHANNEL"
|
|
}
|
|
|
|
# uninstall: every channel, then everything this script installed. Also
|
|
# stops channel instances whose config is already gone.
|
|
cmd_uninstall() {
|
|
[ "$(id -u)" = 0 ] || fail uninstall "нужны права root (sudo)"
|
|
set --
|
|
for ch in $(list_channels); do
|
|
remove_channel "$ch"
|
|
set -- "$@" "$ch"
|
|
done
|
|
for unit in $(systemctl list-units --all --plain --no-legend 'openflux-node@*' 2>/dev/null | awk '{print $1}'); do
|
|
unit_stop "$unit"
|
|
done
|
|
remove_everything
|
|
printf '{"ok":true,"removed":%s}\n' "$(json_list "$@")"
|
|
}
|
|
|
|
# list: the channels, their state and port; readable without root.
|
|
cmd_list() {
|
|
printf '{"ok":true,"core":"%s","autoupdate":%s,"channels":[' "$(installed_core)" "$(autoupdate_on && echo true || echo false)"
|
|
first=1
|
|
for ch in $(list_channels); do
|
|
[ "$first" = 1 ] || printf ','
|
|
first=0
|
|
port=$(cat "$CONF_ROOT/$ch/port" 2>/dev/null)
|
|
printf '{"channel":"%s","state":"%s","port":%s}' "$ch" "$(json_escape "$(systemctl is-active "openflux-node@$ch" 2>/dev/null)")" "${port:-0}"
|
|
done
|
|
printf ']}\n'
|
|
}
|
|
|
|
# upgrade: switches every channel to this script's core and restarts the
|
|
# running ones. Configs, keys and ports stay as they are.
|
|
cmd_upgrade() {
|
|
[ "$(id -u)" = 0 ] || fail upgrade "нужны права root (sudo)"
|
|
[ -n "$(list_channels)" ] || fail upgrade "на сервере нет каналов OpenFlux"
|
|
arch=$(detect_arch)
|
|
[ -n "$arch" ] || fail upgrade "архитектура $(uname -m) не поддерживается"
|
|
install_core "$arch" || fail upgrade "$CORE_ERROR"
|
|
set --
|
|
for ch in $(list_channels); do
|
|
if systemctl is-active --quiet "openflux-node@$ch"; then
|
|
systemctl restart "openflux-node@$ch" || fail upgrade "не удалось перезапустить openflux-node@$ch"
|
|
set -- "$@" "$ch"
|
|
fi
|
|
done
|
|
# Older cores nothing points at any more.
|
|
core=$(core_to_use)
|
|
for old in "$BIN_DIR"/openflux-node-v*; do
|
|
[ "$old" = "$BIN_DIR/openflux-$core" ] || rm -f "$old"
|
|
done
|
|
apply_autoupdate
|
|
printf '{"ok":true,"core":"%s","restarted":%s}\n' "$core" "$(json_list "$@")"
|
|
}
|
|
|
|
# latest_release FILE: the newest node-vX.Y.Z tag in a GitHub releases
|
|
# listing (JSON in FILE), prereleases left out.
|
|
latest_release() {
|
|
best=""
|
|
# Every release has one tag_name and one prerelease, in either order;
|
|
# the objects nested in it (author, assets) have neither.
|
|
for tag in $(tr ',{}' '\n\n\n' < "$1" | awk '
|
|
function pair() {
|
|
if (tag != "" && pre != "") {
|
|
if (pre == "false" && tag ~ /^node-v[0-9]+\.[0-9]+\.[0-9]+$/) print tag
|
|
tag = ""; pre = ""
|
|
}
|
|
}
|
|
/"tag_name"[ \t]*:/ { tag = $0; sub(/.*"tag_name"[ \t]*:[ \t]*"/, "", tag); sub(/".*/, "", tag); pair() }
|
|
/"prerelease"[ \t]*:[ \t]*false/ { pre = "false"; pair() }
|
|
/"prerelease"[ \t]*:[ \t]*true/ { pre = "true"; pair() }'); do
|
|
version_gt "$tag" "$best" && best=$tag
|
|
done
|
|
printf '%s' "$best"
|
|
}
|
|
|
|
# restart_channels: restarts every enabled or running channel and prints
|
|
# their names.
|
|
restart_channels() {
|
|
for ch in $(list_channels); do
|
|
if systemctl is-active --quiet "openflux-node@$ch" || systemctl is-enabled --quiet "openflux-node@$ch"; then
|
|
systemctl restart "openflux-node@$ch" >/dev/null 2>&1
|
|
printf '%s\n' "$ch"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# channels_stay_up CHANNEL...: whether each channel is running and did not
|
|
# restart during 20 seconds (a crashing core restarts every 5).
|
|
channels_stay_up() {
|
|
sleep 3
|
|
pids=""
|
|
for ch in "$@"; do pids="$pids $ch=$(main_pid "openflux-node@$ch")"; done
|
|
sleep 20
|
|
for pair in $pids; do
|
|
ch=${pair%%=*}; pid=${pair#*=}
|
|
systemctl is-active --quiet "openflux-node@$ch" || return 1
|
|
[ -n "$pid" ] && [ "$pid" != 0 ] && [ "$pid" = "$(main_pid "openflux-node@$ch")" ] || return 1
|
|
done
|
|
}
|
|
|
|
# update: what openflux-node-update.timer runs. When RELEASE_REPO has a
|
|
# node-v* release newer than the installed core, moves every channel to it:
|
|
# the release's own node-install.sh (at its tag) pins the core's SHA-256,
|
|
# and the release's SHA256SUMS must say the same. If a channel does not stay
|
|
# up on the new core, the previous one comes back and that release is
|
|
# skipped from then on.
|
|
cmd_update() {
|
|
[ "$(id -u)" = 0 ] || fail update "нужны права root"
|
|
if [ -z "$(list_channels)" ]; then
|
|
printf '{"ok":true,"updated":false,"reason":"на сервере нет каналов"}\n'
|
|
return 0
|
|
fi
|
|
arch=$(detect_arch)
|
|
[ -n "$arch" ] || fail update "архитектура $(uname -m) не поддерживается"
|
|
repo=$(release_repo)
|
|
current=$(managed_core "$repo")
|
|
work=$(mktemp -d /tmp/openflux-node-update.XXXXXX) || fail update "не удалось создать временную папку"
|
|
trap 'rm -rf "$work"' EXIT
|
|
fetch "$GITHUB_API/repos/$repo/releases?per_page=30" "$work/releases.json" \
|
|
|| fail update "GitHub не ответил на список релизов $repo"
|
|
latest=$(latest_release "$work/releases.json")
|
|
[ -n "$latest" ] || fail update "в $repo нет релизов node-v*"
|
|
skip="$STATE_ROOT/update-skip"
|
|
if [ -n "$current" ] && ! version_gt "$latest" "$current"; then
|
|
printf '{"ok":true,"updated":false,"core":"%s"}\n' "$current"
|
|
return 0
|
|
fi
|
|
if grep -qsx "$latest" "$skip"; then
|
|
printf '{"ok":true,"updated":false,"core":"%s","skipped":"%s"}\n' "$current" "$latest"
|
|
return 0
|
|
fi
|
|
|
|
fetch "$GITHUB_RAW/$repo/$latest/deploy/node-install.sh" "$work/node-install.sh" \
|
|
|| fail update "не удалось скачать node-install.sh релиза $latest"
|
|
grep -qxF "$MARKER" "$work/node-install.sh" && [ "$(script_core "$work/node-install.sh")" = "$latest" ] \
|
|
|| fail update "node-install.sh в $latest не относится к этому релизу"
|
|
want=$(sed -n "s/^SHA_$arch=\"\([0-9a-f]\{64\}\)\"\$/\1/p" "$work/node-install.sh")
|
|
[ -n "$want" ] || fail update "node-install.sh релиза $latest не знает хеш ядра для $arch"
|
|
base="$GITHUB_WEB/$repo/releases/download/$latest"
|
|
fetch "$base/SHA256SUMS" "$work/SHA256SUMS" || fail update "не удалось скачать SHA256SUMS релиза $latest"
|
|
grep -qx "$want openflux-linux-$arch" "$work/SHA256SUMS" \
|
|
|| fail update "SHA256SUMS релиза $latest расходится с его node-install.sh"
|
|
fetch "$base/openflux-linux-$arch" "$work/core" || fail update "не удалось скачать ядро $latest"
|
|
[ "$(sha256_of "$work/core")" = "$want" ] || fail update "SHA-256 скачанного ядра $latest не совпал"
|
|
|
|
new="openflux-$latest"
|
|
mkdir -p "$BIN_DIR" && chmod 0755 /opt/openflux-node "$BIN_DIR"
|
|
cp "$work/core" "$BIN_DIR/.$new.new" && chmod 0755 "$BIN_DIR/.$new.new" && mv -f "$BIN_DIR/.$new.new" "$BIN_DIR/$new" \
|
|
|| fail update "не удалось записать ядро в $BIN_DIR"
|
|
prev=$(readlink "$BIN_DIR/openflux" 2>/dev/null)
|
|
ln -sfn "$new" "$BIN_DIR/openflux"
|
|
# shellcheck disable=SC2046
|
|
set -- $(restart_channels)
|
|
if [ $# -gt 0 ] && ! channels_stay_up "$@"; then
|
|
if [ -n "$prev" ] && [ -x "$BIN_DIR/$prev" ]; then
|
|
ln -sfn "$prev" "$BIN_DIR/openflux"
|
|
restart_channels >/dev/null
|
|
fi
|
|
rm -f "$BIN_DIR/$new"
|
|
printf '%s\n' "$latest" >> "$skip"
|
|
fail update "на ядре $latest каналы не поднялись, вернул ${prev#openflux-}; этот релиз больше не ставлю"
|
|
fi
|
|
mark_managed "$repo" "$latest"
|
|
install_self "$work/node-install.sh" || true
|
|
# Keep the previous core for a manual rollback, drop the older ones.
|
|
for old in "$BIN_DIR"/openflux-node-v*; do
|
|
case "${old##*/}" in "$new"|"$prev") ;; *) rm -f "$old" ;; esac
|
|
done
|
|
printf '{"ok":true,"updated":true,"core":"%s","previous":"%s","restarted":%s}\n' \
|
|
"$latest" "$(json_escape "${prev#openflux-}")" "$(json_list "$@")"
|
|
}
|
|
|
|
# autoupdate on|off: turns the core updater on or off for the whole server,
|
|
# e.g. on channels installed before the wizard offered it.
|
|
cmd_autoupdate() {
|
|
[ "$(id -u)" = 0 ] || fail autoupdate "нужны права root (sudo)"
|
|
case "${1:-}" in
|
|
on)
|
|
[ -n "$(list_channels)" ] || fail autoupdate "на сервере нет каналов OpenFlux"
|
|
AUTOUPDATE=yes ;;
|
|
off) AUTOUPDATE=no ;;
|
|
*) fail usage "usage: node-install.sh autoupdate on|off" ;;
|
|
esac
|
|
apply_autoupdate || fail autoupdate "не удалось включить openflux-node-update.timer"
|
|
state=false
|
|
autoupdate_on && state=true
|
|
printf '{"ok":true,"autoupdate":%s}\n' "$state"
|
|
}
|
|
|
|
cmd_status() {
|
|
read_config
|
|
check_channel
|
|
[ -d "$CONF_ROOT/$CHANNEL" ] || fail status "канала $CHANNEL нет на сервере"
|
|
state=$(systemctl is-active "openflux-node@$CHANNEL" 2>/dev/null)
|
|
printf '{"ok":true,"channel":"%s","state":"%s"}\n' "$CHANNEL" "$(json_escape "$state")"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
probe) cmd_probe ;;
|
|
plan) cmd_plan ;;
|
|
apply) shift; cmd_apply "$@" ;;
|
|
remove) shift; cmd_remove "$@" ;;
|
|
uninstall) cmd_uninstall ;;
|
|
list) cmd_list ;;
|
|
status) cmd_status ;;
|
|
upgrade) cmd_upgrade ;;
|
|
update) cmd_update ;;
|
|
autoupdate) shift; cmd_autoupdate "$@" ;;
|
|
*) fail usage "usage: node-install.sh probe|plan|apply|remove|list|uninstall|status|upgrade|update|autoupdate" ;;
|
|
esac
|