Add v2 support for independent GusVoice connections to one AWG server
This commit is contained in:
1 parent
38c6a1a682
commit
0e62fe20ed
9 files changed
+759
-148
No files matched your search
+91
-106
@@ -1,51 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# AWG Gus bot: install/update only this service; existing awg2 and Telegram bot stay in place.
|
||||
# Install shared code, preserving the independent configuration/state of every Gus.
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
|
||||
REPO_URL="${AWG_GUS_REPO_URL:-https://gitea.bvrdo.online/wenil/awg-gus-bot}"
|
||||
REF="${AWG_GUS_REF:-main}"
|
||||
BASE=/opt/awg-gus-bot
|
||||
CONF=/etc/awg-gus-bot.json
|
||||
STATE=/var/lib/awg-gus-bot
|
||||
SERVICE=awg-gus-bot.service
|
||||
SRC=""
|
||||
RECONFIGURE=0
|
||||
|
||||
ACTION=""
|
||||
INSTANCE=""
|
||||
die() { printf 'Ошибка: %s\n' "$*" >&2; exit 1; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Установка/обновление отдельного AWG-бота для GusVoice:
|
||||
sudo bash install.sh
|
||||
sudo bash install.sh --src /path/to/awg-gus-bot
|
||||
sudo bash install.sh --configure
|
||||
AWG для Гуся v2: несколько серверов Гуся → один AWG.
|
||||
sudo bash install.sh # установка / меню обновления и добавления
|
||||
sudo bash install.sh --update # обновить код всех подключений
|
||||
sudo bash install.sh --add [--instance gus2]
|
||||
sudo bash install.sh --configure [--instance default]
|
||||
sudo bash install.sh --src /path/to/source --update
|
||||
|
||||
Нужны: Linux с systemd, Python 3.10+, установленный awg2 с JSON API.
|
||||
При первой установке запросит HTTPS-адрес Гуся и токен, затем покажет код привязки.
|
||||
Существующая служба awg-bot не меняется. VPN-клиенты у обоих ботов общие.
|
||||
Для каждого Гуся нужны его адрес, собственный токен бота и привязка владельца.
|
||||
Текущее подключение v1 сохраняется под именем default. VPN-клиенты общие.
|
||||
EOF
|
||||
}
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--src) [[ $# -ge 2 ]] || die 'После --src нужен каталог'; SRC="$2"; shift 2 ;;
|
||||
--configure) RECONFIGURE=1; shift ;;
|
||||
--instance) [[ $# -ge 2 ]] || die 'После --instance нужно имя'; INSTANCE="$2"; shift 2 ;;
|
||||
--add|--configure|--update)
|
||||
[[ -z "$ACTION" ]] || die 'Выберите одно действие'; ACTION="${1#--}"; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "Неизвестный параметр: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$INSTANCE" || "$INSTANCE" =~ ^[a-z][a-z0-9_-]{0,31}$ ]] || die 'Некорректное имя подключения'
|
||||
[[ $EUID -eq 0 ]] || die 'Запустите через sudo bash install.sh'
|
||||
[[ $(uname -s) == Linux ]] || die 'Устанавливается на Linux-сервер с awg2'
|
||||
command -v systemctl >/dev/null || die 'Нужен systemd'
|
||||
[[ -d /run/systemd/system ]] || die 'systemd не запущен'
|
||||
command -v flock >/dev/null || die 'Нужен flock (пакет util-linux)'
|
||||
command -v systemctl >/dev/null && [[ -d /run/systemd/system ]] || die 'Нужен запущенный systemd'
|
||||
command -v flock >/dev/null || die 'Нужен flock (util-linux)'
|
||||
shopt -s nullglob
|
||||
configs=(/etc/awg-gus-bot/instances/*.json)
|
||||
[[ ! -f /etc/awg-gus-bot.json ]] || configs+=(/etc/awg-gus-bot.json)
|
||||
if [[ -z "$ACTION" ]]; then
|
||||
if ((${#configs[@]} == 0)); then
|
||||
ACTION=add
|
||||
INSTANCE="${INSTANCE:-default}"
|
||||
elif [[ -t 0 ]]; then
|
||||
printf 'AWG для Гуся v2\n1. Обновить все подключения\n2. Добавить ещё одного Гуся\n0. Выход\n'
|
||||
read -r -p 'Выберите действие: ' choice
|
||||
case "$choice" in 1) ACTION=update ;; 2) ACTION=add ;; 0) exit 0 ;; *) die 'Неизвестный пункт' ;; esac
|
||||
else
|
||||
ACTION=update
|
||||
fi
|
||||
fi
|
||||
if [[ "$ACTION" != update ]]; then [[ -t 0 ]] || die 'Для привязки нужен терминал'; fi
|
||||
mkdir -p /run/lock
|
||||
exec 9>/run/lock/awg-gus-install.lock
|
||||
flock -n 9 || die 'Другой установщик уже работает'
|
||||
AWG2="$(command -v awg2 || true)"
|
||||
flock -n 9 || die 'Другая установка или настройка уже выполняется'
|
||||
AWG2=$(command -v awg2 || true)
|
||||
[[ -n "$AWG2" ]] || die 'Сначала установите AWG Toolza: команда awg2 не найдена'
|
||||
|
||||
missing=()
|
||||
for pair in 'python3:python3' 'curl:curl' 'tar:tar' 'qrencode:qrencode'; do
|
||||
command -v "${pair%%:*}" >/dev/null || missing+=("${pair#*:}")
|
||||
@@ -55,52 +67,44 @@ if ((${#missing[@]})); then
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${missing[@]}" ca-certificates
|
||||
fi
|
||||
PYTHON=$(command -v python3)
|
||||
python3 -c 'import sys; assert sys.version_info >= (3,10), "Нужен Python 3.10+"'
|
||||
"$AWG2" api version | python3 -c 'import sys,json; d=json.loads(sys.stdin.read().strip().splitlines()[-1]); sys.exit(0 if d.get("ok") is True else 1)' \
|
||||
|| die 'awg2 api version не отвечает. Обновите AWG Toolza штатным способом'
|
||||
|| die 'awg2 api version не отвечает'
|
||||
|
||||
TMP=$(mktemp -d /tmp/awg-gus-install.XXXXXXXX)
|
||||
old=""
|
||||
activated=0
|
||||
service_stopped=0
|
||||
configuring=0
|
||||
was_active=0
|
||||
systemctl is-active --quiet "$SERVICE" && was_active=1
|
||||
active=()
|
||||
artifacts=(/etc/systemd/system/awg-gus-bot.service /etc/systemd/system/awg-gus-bot@.service /usr/local/bin/awg-gus-bot)
|
||||
cleanup() {
|
||||
local rc=$?
|
||||
local rc=$? i
|
||||
trap - EXIT
|
||||
if ((rc != 0 && (activated || service_stopped || configuring))); then
|
||||
printf 'Запуск новой версии не удался. Откатываю только службу Гуся.\n' >&2
|
||||
systemctl stop "$SERVICE" || true
|
||||
if ((activated)) && [[ -n "$old" ]]; then
|
||||
if ((rc != 0 && activated)); then
|
||||
printf 'Обновление не удалось. Возвращаю прежний код и службы Гуся.\n' >&2
|
||||
for unit in "${active[@]}"; do systemctl stop "$unit" || true; done
|
||||
if [[ -n "$old" ]]; then
|
||||
ln -sfn "$old" "$BASE/.current-rollback"
|
||||
mv -Tf "$BASE/.current-rollback" "$BASE/current"
|
||||
elif ((activated)); then
|
||||
else
|
||||
rm -f "$BASE/current"
|
||||
fi
|
||||
if [[ -f "$TMP/config.before" ]]; then
|
||||
install -m 600 "$TMP/config.before" "$CONF"
|
||||
fi
|
||||
if [[ -f "$TMP/unit.before" ]]; then
|
||||
install -m 644 "$TMP/unit.before" "/etc/systemd/system/$SERVICE"
|
||||
elif ((activated)) && [[ -z "$old" ]]; then
|
||||
systemctl disable "$SERVICE" >/dev/null 2>&1 || true
|
||||
rm -f "/etc/systemd/system/$SERVICE"
|
||||
fi
|
||||
if [[ -f "$TMP/command.before" ]]; then
|
||||
install -m 755 "$TMP/command.before" /usr/local/bin/awg-gus-bot
|
||||
elif ((activated)) && [[ -z "$old" ]]; then
|
||||
rm -f /usr/local/bin/awg-gus-bot
|
||||
fi
|
||||
for i in "${!artifacts[@]}"; do
|
||||
if [[ -f "$TMP/artifact.$i" ]]; then
|
||||
cp -p "$TMP/artifact.$i" "${artifacts[$i]}"
|
||||
else
|
||||
rm -f "${artifacts[$i]}"
|
||||
fi
|
||||
done
|
||||
systemctl daemon-reload
|
||||
if ((was_active)) && [[ -n "$old" ]]; then systemctl start "$SERVICE" || true; fi
|
||||
for unit in "${active[@]}"; do
|
||||
systemctl reset-failed "$unit" || true
|
||||
systemctl start "$unit" || true
|
||||
done
|
||||
fi
|
||||
[[ "$TMP" == /tmp/awg-gus-install.* && -d "$TMP" ]] && rm -rf -- "$TMP"
|
||||
exit "$rc"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -z "$SRC" ]]; then
|
||||
[[ "$REPO_URL" == https://* && "$REPO_URL" != *'@'* ]] || die 'Источник должен быть HTTPS без учётных данных в URL'
|
||||
[[ "$REF" =~ ^[A-Za-z0-9._-]+$ ]] || die 'Некорректная ветка/версия'
|
||||
@@ -108,14 +112,12 @@ if [[ -z "$SRC" ]]; then
|
||||
curl --proto '=https' --proto-redir '=https' --tlsv1.2 -fSL --retry 3 --connect-timeout 15 --max-time 180 \
|
||||
"${REPO_URL%.git}/archive/$REF.tar.gz" -o "$TMP/source.tar.gz"
|
||||
mkdir "$TMP/source"
|
||||
# Reject paths escaping extraction root and any links/devices in the source archive.
|
||||
python3 - "$TMP/source.tar.gz" "$TMP/source" <<'PY'
|
||||
import pathlib, sys, tarfile
|
||||
root = pathlib.Path(sys.argv[2]).resolve()
|
||||
with tarfile.open(sys.argv[1]) as archive:
|
||||
for m in archive.getmembers():
|
||||
p = (root / m.name).resolve()
|
||||
if not p.is_relative_to(root) or not (m.isfile() or m.isdir()):
|
||||
if not (root / m.name).resolve().is_relative_to(root) or not (m.isfile() or m.isdir()):
|
||||
raise SystemExit('Небезопасный путь/тип в архиве')
|
||||
archive.extractall(root)
|
||||
PY
|
||||
@@ -124,68 +126,51 @@ PY
|
||||
SRC="${dirs[0]}"
|
||||
fi
|
||||
SRC=$(cd "$SRC" && pwd -P)
|
||||
[[ -f "$SRC/run.py" && -f "$SRC/awg_gus/bot.py" && -f "$SRC/vendor/gusvoice_bot/client.py" && -f "$SRC/install.sh" ]] \
|
||||
|| die 'В источнике нет файлов AWG Gus bot'
|
||||
[[ -f "$SRC/run.py" && -f "$SRC/awg_gus/manage.py" && -f "$SRC/vendor/gusvoice_bot/client.py" ]] || die 'Неполный исходный код v2'
|
||||
python3 -m compileall -q "$SRC/awg_gus" "$SRC/vendor/gusvoice_bot" "$SRC/run.py"
|
||||
mkdir -p "$BASE/releases" "$STATE"
|
||||
chmod 700 "$BASE" "$BASE/releases" "$STATE"
|
||||
# Validate locally: an unavailable Gus must not prevent updating the other connections.
|
||||
python3 "$SRC/run.py" manage validate
|
||||
python3 "$SRC/run.py" manage services > "$TMP/services"
|
||||
while IFS= read -r unit; do
|
||||
if systemctl is-active --quiet "$unit"; then active+=("$unit"); fi
|
||||
done < "$TMP/services"
|
||||
mkdir -p "$BASE/releases"
|
||||
chmod 700 "$BASE" "$BASE/releases"
|
||||
old=$(readlink "$BASE/current" || true)
|
||||
if [[ -n "$old" && "$old" != "$BASE/releases/"* ]]; then die 'current указывает вне каталога версий'; fi
|
||||
release="$BASE/releases/$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||||
mkdir "$release"
|
||||
cp -a "$SRC/awg_gus" "$SRC/vendor" "$SRC/run.py" "$SRC/install.sh" "$SRC/awg-gus-bot" "$SRC/README.md" "$SRC/LICENSE" "$SRC/licenses" "$release/"
|
||||
chmod -R go-rwx "$release"
|
||||
printf '%s\n' "$REPO_URL" > "$release/source-url"
|
||||
printf '%s\n' "$REF" > "$release/source-ref"
|
||||
[[ ! -f "$CONF" ]] || cp -p "$CONF" "$TMP/config.before"
|
||||
[[ ! -f "/etc/systemd/system/$SERVICE" ]] || cp -p "/etc/systemd/system/$SERVICE" "$TMP/unit.before"
|
||||
[[ ! -f /usr/local/bin/awg-gus-bot ]] || cp -p /usr/local/bin/awg-gus-bot "$TMP/command.before"
|
||||
old=$(readlink "$BASE/current" || true)
|
||||
if [[ -n "$old" && "$old" != "$BASE/releases/"* ]]; then die 'current указывает вне каталога версий'; fi
|
||||
|
||||
if [[ ! -s "$CONF" ]] || ((RECONFIGURE)); then
|
||||
[[ -t 0 ]] || die 'Для первого запуска нужен терминал: скачайте скрипт в файл, затем sudo bash install.sh'
|
||||
# Stop only our bot to release its polling connection while pairing.
|
||||
if ((was_active)); then systemctl stop "$SERVICE"; service_stopped=1; fi
|
||||
configuring=1
|
||||
if ! python3 "$release/run.py" setup --config "$CONF" --awg2 "$AWG2" --state-dir "$STATE"; then
|
||||
die 'Настройка не завершена'
|
||||
fi
|
||||
fi
|
||||
chmod 600 "$CONF"
|
||||
python3 "$release/run.py" check --config "$CONF"
|
||||
|
||||
for i in "${!artifacts[@]}"; do
|
||||
[[ ! -f "${artifacts[$i]}" ]] || cp -p "${artifacts[$i]}" "$TMP/artifact.$i"
|
||||
done
|
||||
activated=1
|
||||
cat > "/etc/systemd/system/$SERVICE" <<EOF
|
||||
[Unit]
|
||||
Description=AWG Toolza bot for GusVoice
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$BASE/current
|
||||
ExecStart=$PYTHON $BASE/current/run.py run --config $CONF
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
UMask=0077
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStopSec=20
|
||||
KillMode=control-group
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
for unit in "${active[@]}"; do systemctl stop "$unit"; done
|
||||
python3 "$release/run.py" manage install-units
|
||||
ln -sfn "$release" "$BASE/.current-next"
|
||||
mv -Tf "$BASE/.current-next" "$BASE/current"
|
||||
install -m 755 "$release/awg-gus-bot" /usr/local/bin/awg-gus-bot
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE" >/dev/null
|
||||
systemctl restart "$SERVICE"
|
||||
sleep 3
|
||||
systemctl is-active --quiet "$SERVICE" || die 'Служба не запустилась. journalctl -u awg-gus-bot -n 30'
|
||||
for unit in "${active[@]}"; do
|
||||
systemctl reset-failed "$unit" || true
|
||||
systemctl start "$unit"
|
||||
done
|
||||
if ((${#active[@]})); then
|
||||
sleep 3
|
||||
for unit in "${active[@]}"; do
|
||||
systemctl is-active --quiet "$unit" || die "Не запустилась $unit; откатываю обновление"
|
||||
done
|
||||
fi
|
||||
activated=0
|
||||
printf '\nГотово. В личной беседе с ботом Гуся отправьте /menu.\n'
|
||||
printf 'Служба: awg-gus-bot. Обновление: sudo awg-gus-bot update\n'
|
||||
printf 'Telegram-бот и awg2 не заменялись. Оба бота управляют общими VPN-клиентами.\n'
|
||||
printf 'Код обновлён. Существующие настройки и состояния сохранены.\n'
|
||||
# Pairing has its own lock and rollback, limited to the chosen connection.
|
||||
flock -u 9
|
||||
exec 9>&-
|
||||
case "$ACTION" in
|
||||
add) args=(manage add); [[ -z "$INSTANCE" ]] || args+=("$INSTANCE"); python3 "$BASE/current/run.py" "${args[@]}" ;;
|
||||
configure) python3 "$BASE/current/run.py" manage setup "${INSTANCE:-default}" ;;
|
||||
update) printf 'Добавить ещё одного Гуся: sudo awg-gus-bot add\n' ;;
|
||||
esac
|
||||
Reference in new issue
Block a user