#!/usr/bin/env bash # glchat — служебные команды инстанса (AGENT.md 10.7). # Ставится установщиком в /usr/local/bin/glchat. set -euo pipefail GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}" COMPOSE_FILE="${GLCHAT_ETC_DIR}/compose.yaml" ENV_FILE="${GLCHAT_ETC_DIR}/.env" COMPOSE="docker compose -f ${COMPOSE_FILE}" # app_check_url печатает адрес /readyz с учётом привязки приложения: во внешнем # режиме оно слушает WireGuard-адрес, и проверка localhost всегда падала бы. app_check_url() { local port="$1" host="${2:-}" case "$host" in 0.0.0.0 | "" | "::") host="127.0.0.1" ;; esac printf 'http://%s:%s/readyz' "$host" "$port" } # env_get читает значение из .env инстанса (пусто, если ключа нет). env_get() { [ -f "$ENV_FILE" ] || return 0 sed -n "s/^$1=//p" "$ENV_FILE" | tail -1 } usage() { cat <<'EOF' glchat — управление инстансом glchat status состояние контейнеров, версия, здоровье, ресурсы glchat logs [service] логи (follow) glchat restart [service] перезапуск стека или сервиса glchat backup бэкап БД и файлов (age-шифрование) glchat restore <файл> восстановление из бэкапа glchat verify-backup <файл> проверить бэкап, не трогая данные glchat update [--channel X] обновление (бэкап → pull → up → smoke) glchat reset-password сброс пароля пользователя; --password <пароль> glchat make-admin выдать права инстанс-админа glchat remove-admin снять права инстанс-админа glchat totp-setup включить 2FA и напечатать секрет с резервными кодами glchat totp-reset удалить секрет 2FA (потеря устройства и кодов) glchat delete-user мягко удалить аккаунт (нужен --yes) glchat unban снять бан fail2ban (jail'ы этого хоста; на внешнем прокси — fail2ban-client там) glchat fail2ban-status статус jails и забаненных IP glchat profile [tier] показать/применить профиль производительности glchat security-check аудит: SSH, fail2ban, порты, лимиты glchat reindex пересобрать индекс поиска (FTS5) glchat vapid-keys сгенерировать пару VAPID-ключей для Web Push glchat cleanup чистка: сессии, аудит, файлы без ссылок glchat create-user создать аккаунты (--count/--prefix/--sessions) glchat change-domain сменить домен glchat uninstall [--keep-data] удалить стек glchat version версия CLI и приложения EOF } require_stack() { [ -f "$COMPOSE_FILE" ] || { echo "glchat: не найден ${COMPOSE_FILE}; сначала запустите install.sh" >&2 exit 1 } } cmd_status() { require_stack local meta version health echo "== compose ==" $COMPOSE ps echo local check_url check_url="$(app_check_url "${APP_PORT:-8080}" "$(env_get GLCHAT_APP_BIND)")" meta="$(curl -fsS "${check_url%/readyz}/api/v1/meta" 2>/dev/null || true)" version="$(printf '%s' "$meta" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')" health="$(curl -fsS "${check_url}" 2>/dev/null || echo '{"status":"unreachable"}')" echo "== app ==" echo "version: ${version:-unknown}" echo "readyz: ${health}" echo echo "== ресурсы ==" docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' 2>/dev/null || true echo df -h "${GLCHAT_ETC_DIR}/data" 2>/dev/null | tail -1 || true } cmd_logs() { require_stack $COMPOSE logs -f --tail=200 "$@" } cmd_restart() { require_stack if [ "$#" -gt 0 ]; then $COMPOSE restart "$@" else $COMPOSE restart fi } cmd_backup() { require_stack bash "${GLCHAT_ETC_DIR}/backup.sh" "$@" } cmd_restore() { require_stack bash "${GLCHAT_ETC_DIR}/restore.sh" "$@" } cmd_update() { require_stack bash "${GLCHAT_ETC_DIR}/update.sh" "$@" } cmd_unban() { [ "$#" -ge 1 ] || { echo "использование: glchat unban " >&2 exit 1 } fail2ban-client unban "$1" } cmd_fail2ban_status() { fail2ban-client status for jail in $(fail2ban-client status | sed -n 's/.*Jail list:\s*//p' | tr ',' ' '); do echo fail2ban-client status "${jail// /}" done } # cmd_profile показывает текущий профиль или переключает его. Переключение — # это повторная установка, поэтому форма развёртывания (внешний прокси, адреса # привязки, внешний TURN) восстанавливается из .env: иначе `glchat profile lite` # переписал бы compose на встроенный прокси и сломал инстанс за чужим прокси. cmd_profile() { if [ "$#" -eq 0 ]; then echo "профиль: $(sed -n 's/^PROFILE=//p' "$ENV_FILE" 2>/dev/null || echo unknown)" grep -E '^(SQLITE_|GOMAXPROCS|MAX_UPLOAD_SIZE|APP_CPUS|APP_MEMORY_LIMIT)' "$ENV_FILE" 2>/dev/null || true return fi require_stack local args=() local domain app_bind livekit_bind proxy_mode relay_host domain="$(env_get DOMAIN)" [ -n "$domain" ] || { echo "glchat: в ${ENV_FILE} нет DOMAIN" >&2 exit 1 } args=(--profile "$1" --reconfigure --yes --domain "$domain") proxy_mode="$(env_get PROXY_MODE)" if [ "$proxy_mode" = "external" ]; then args+=(--external-proxy) [ "$(env_get TLS_BY_PROXY)" = "true" ] && args+=(--tls-terminated-by-proxy) fi app_bind="$(env_get GLCHAT_APP_BIND)" [ -n "$app_bind" ] && args+=(--app-bind-addr "$app_bind") livekit_bind="$(env_get GLCHAT_LIVEKIT_BIND)" [ -n "$livekit_bind" ] && args+=(--livekit-bind-addr "$livekit_bind") if [ "$(env_get TURN_ENABLED)" = "false" ]; then args+=(--no-turn) fi relay_host="$(env_get TURN_RELAY_HOST)" if [ -n "$relay_host" ]; then args+=(--turn-relay-host "$relay_host") [ -n "$(env_get TURN_RELAY_PORT)" ] && args+=(--turn-relay-port "$(env_get TURN_RELAY_PORT)") [ -n "$(env_get TURN_RELAY_TLS_PORT)" ] && args+=(--turn-relay-tls-port "$(env_get TURN_RELAY_TLS_PORT)") [ -n "$(env_get TURN_RELAY_TLS_HOST)" ] && args+=(--turn-relay-tls-host "$(env_get TURN_RELAY_TLS_HOST)") fi local ip for ip in $(env_get FAIL2BAN_IGNOREIP); do args+=(--ignore-ip "$ip") done [ -n "$(env_get ADMIN_EMAIL)" ] && args+=(--admin "$(env_get ADMIN_EMAIL)") [ -n "$(env_get ADMIN_PASSWORD)" ] && args+=(--password "$(env_get ADMIN_PASSWORD)") echo "==> переключение профиля на $1 (форма развёртывания берётся из .env)" GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/install.sh" "${args[@]}" } cmd_security_check() { bash "${GLCHAT_ETC_DIR}/security-check.sh" } cmd_change_domain() { [ "$#" -ge 1 ] || { echo "использование: glchat change-domain " >&2 exit 1 } GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/install.sh" --domain "$1" --reconfigure --yes } cmd_uninstall() { require_stack local keep_data=0 [ "${1:-}" = "--keep-data" ] && keep_data=1 echo "Контейнеры glchat будут удалены. Данные: $([ "$keep_data" = 1 ] && echo 'сохраняются' || echo 'УДАЛЯЮТСЯ')" read -r -p "Продолжить? (yes/no): " reply [ "$reply" = "yes" ] || { echo "отменено" exit 0 } systemctl disable --now glchat.service 2>/dev/null || true $COMPOSE --profile embedded-proxy down --remove-orphans || true if [ "$keep_data" = 0 ]; then rm -rf "${GLCHAT_ETC_DIR}/data" fi rm -f /etc/systemd/system/glchat.service systemctl daemon-reload echo "Стек удалён. Каталог ${GLCHAT_ETC_DIR} оставлен для ручной проверки." } cmd_version() { echo "glchat CLI 0.1.0 (phase 0)" [ -f "$ENV_FILE" ] && grep -E '^(APP_VERSION|IMAGE_TAG|DOMAIN|PROFILE)=' "$ENV_FILE" || true } # cmd_verify_backup проверяет архив бэкапа: расшифровка, целостность базы и # состав данных (AGENT.md 10.8). cmd_verify_backup() { require_stack [ -n "${1:-}" ] || { echo "использование: glchat verify-backup <файл.tar.gz[.age]>" >&2 exit 2 } GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR}" bash "${GLCHAT_ETC_DIR}/verify-backup.sh" "$@" } # app_cli выполняет подкоманду обслуживания внутри контейнера приложения # (AGENT.md 10.7): команды, которым нужна БД, живут в Go-бинарнике, а shell-обёртка # доставляет их с хоста. `&2 usage >&2 exit 1 ;; esac } main "$@"