Files

177 lines
8.3 KiB
Bash
Executable File
Raw Permalink 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
# 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
SRC=""
ACTION=""
INSTANCE=""
die() { printf 'Ошибка: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
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
Для каждого Гуся нужны его адрес, собственный токен бота и привязка владельца.
Текущее подключение v1 сохраняется под именем default. VPN-клиенты общие.
EOF
}
while (($#)); do
case "$1" in
--src) [[ $# -ge 2 ]] || die 'После --src нужен каталог'; SRC="$2"; shift 2 ;;
--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 && [[ -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)
[[ -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#*:}")
done
if ((${#missing[@]})); then
command -v apt-get >/dev/null || die "Установите вручную: ${missing[*]} и ca-certificates"
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${missing[@]}" ca-certificates
fi
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 не отвечает'
TMP=$(mktemp -d /tmp/awg-gus-install.XXXXXXXX)
old=""
activated=0
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=$? i
trap - EXIT
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"
else
rm -f "$BASE/current"
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
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 'Некорректная ветка/версия'
printf 'Скачиваю AWG Gus bot из %s (%s)…\n' "$REPO_URL" "$REF"
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"
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():
if not (root / m.name).resolve().is_relative_to(root) or not (m.isfile() or m.isdir()):
raise SystemExit('Небезопасный путь/тип в архиве')
archive.extractall(root)
PY
dirs=("$TMP/source"/*)
((${#dirs[@]} == 1)) && [[ -d "${dirs[0]}" ]] || die 'Неожиданная структура архива'
SRC="${dirs[0]}"
fi
SRC=$(cd "$SRC" && pwd -P)
[[ -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"
# 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"
for i in "${!artifacts[@]}"; do
[[ ! -f "${artifacts[$i]}" ]] || cp -p "${artifacts[$i]}" "$TMP/artifact.$i"
done
activated=1
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
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'
# 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