#!/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