261 lines
9.7 KiB
Bash
261 lines
9.7 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# Общие функции установщика и служебных скриптов glchat.
|
||
|
|
# Подключается через `source`; сам по себе ничего не выполняет.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}"
|
||
|
|
GLCHAT_DATA_DIR="${GLCHAT_DATA_DIR:-${GLCHAT_ETC_DIR}/data}"
|
||
|
|
GLCHAT_INSTALL_LOG="${GLCHAT_INSTALL_LOG:-${GLCHAT_ETC_DIR}/install.log}"
|
||
|
|
GLCHAT_ENV_FILE="${GLCHAT_ENV_FILE:-${GLCHAT_ETC_DIR}/.env}"
|
||
|
|
|
||
|
|
GLCHAT_GREEN=$'\033[32m'
|
||
|
|
GLCHAT_YELLOW=$'\033[33m'
|
||
|
|
GLCHAT_RED=$'\033[31m'
|
||
|
|
GLCHAT_BOLD=$'\033[1m'
|
||
|
|
GLCHAT_RESET=$'\033[0m'
|
||
|
|
|
||
|
|
log() {
|
||
|
|
local line
|
||
|
|
line="[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"
|
||
|
|
printf '%s\n' "$line"
|
||
|
|
if [ -n "${GLCHAT_INSTALL_LOG:-}" ] && [ -w "$(dirname "$GLCHAT_INSTALL_LOG")" ] 2>/dev/null; then
|
||
|
|
printf '%s\n' "$line" >>"$GLCHAT_INSTALL_LOG"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
log_file_only() {
|
||
|
|
if [ -n "${GLCHAT_INSTALL_LOG:-}" ]; then
|
||
|
|
mkdir -p "$(dirname "$GLCHAT_INSTALL_LOG")" 2>/dev/null || true
|
||
|
|
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$GLCHAT_INSTALL_LOG" 2>/dev/null || true
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
info() { printf '%s==>%s %s\n' "$GLCHAT_BOLD" "$GLCHAT_RESET" "$*"; log "INFO $*"; }
|
||
|
|
ok() { printf '%s ok%s %s\n' "$GLCHAT_GREEN" "$GLCHAT_RESET" "$*"; log "OK $*"; }
|
||
|
|
warn() { printf '%swarn%s %s\n' "$GLCHAT_YELLOW" "$GLCHAT_RESET" "$*" >&2; log "WARN $*"; }
|
||
|
|
die() {
|
||
|
|
printf '%serr %s %s\n' "$GLCHAT_RED" "$GLCHAT_RESET" "$*" >&2
|
||
|
|
log "ERROR $*"
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
need_root() {
|
||
|
|
if [ "$(id -u)" -ne 0 ]; then
|
||
|
|
die "запустите установщик от root: sudo bash deploy/install.sh"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||
|
|
|
||
|
|
require_cmd() {
|
||
|
|
for cmd in "$@"; do
|
||
|
|
have "$cmd" || die "не найдена обязательная команда: $cmd"
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
confirm() {
|
||
|
|
local prompt="$1" default="${2:-no}" reply
|
||
|
|
if [ "${GLCHAT_ASSUME_YES:-0}" = "1" ]; then
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
if [ ! -t 0 ]; then
|
||
|
|
[ "$default" = "yes" ] && return 0 || return 1
|
||
|
|
fi
|
||
|
|
read -r -p "$prompt " reply || true
|
||
|
|
reply="${reply:-$default}"
|
||
|
|
case "$reply" in
|
||
|
|
y | Y | yes | YES | да | Да) return 0 ;;
|
||
|
|
*) return 1 ;;
|
||
|
|
esac
|
||
|
|
}
|
||
|
|
|
||
|
|
prompt_value() {
|
||
|
|
local prompt="$1" default="${2:-}" reply
|
||
|
|
if [ ! -t 0 ]; then
|
||
|
|
printf '%s' "$default"
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
if [ -n "$default" ]; then
|
||
|
|
read -r -p "$prompt [$default]: " reply || true
|
||
|
|
printf '%s' "${reply:-$default}"
|
||
|
|
else
|
||
|
|
read -r -p "$prompt: " reply || true
|
||
|
|
printf '%s' "$reply"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
prompt_secret() {
|
||
|
|
local prompt="$1" first second
|
||
|
|
if [ ! -t 0 ]; then
|
||
|
|
die "$prompt: требуется интерактивный ввод или флаг --password"
|
||
|
|
fi
|
||
|
|
while true; do
|
||
|
|
read -r -s -p "$prompt: " first || true
|
||
|
|
printf '\n' >&2
|
||
|
|
read -r -s -p "повторите пароль: " second || true
|
||
|
|
printf '\n' >&2
|
||
|
|
if [ "$first" != "$second" ]; then
|
||
|
|
warn "пароли не совпадают, попробуйте снова"
|
||
|
|
continue
|
||
|
|
fi
|
||
|
|
if [ "${#first}" -lt 8 ]; then
|
||
|
|
warn "пароль короче 8 символов"
|
||
|
|
continue
|
||
|
|
fi
|
||
|
|
printf '%s' "$first"
|
||
|
|
return 0
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
rand_hex() { openssl rand -hex "${1:-32}"; }
|
||
|
|
rand_base64() { openssl rand -base64 "${1:-32}" | tr -d '\n=' | tr '+/' '-_'; }
|
||
|
|
|
||
|
|
json_escape() {
|
||
|
|
local value="$1"
|
||
|
|
value="${value//\\/\\\\}"
|
||
|
|
value="${value//\"/\\\"}"
|
||
|
|
printf '%s' "$value"
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- профили производительности (AGENT.md 10.10) -------------------------
|
||
|
|
|
||
|
|
profile_values() {
|
||
|
|
local tier="$1"
|
||
|
|
case "$tier" in
|
||
|
|
lite)
|
||
|
|
PROFILE_SQLITE_CACHE_MB=8 PROFILE_SQLITE_MMAP_MB=128 PROFILE_SQLITE_READ_POOL=2
|
||
|
|
PROFILE_GOMAXPROCS=1 PROFILE_LRU_ENTRIES=2000
|
||
|
|
PROFILE_LIVEKIT_MAX_PARTICIPANTS=10 PROFILE_LIVEKIT_MAX_VIDEO=4 PROFILE_LIVEKIT_SIMULCAST=0
|
||
|
|
PROFILE_UPLOAD_MB=25 PROFILE_APP_LIMIT=384m PROFILE_LIVEKIT_LIMIT=640m
|
||
|
|
PROFILE_SWAP_GB=2 PROFILE_BACKUP_KEEP=7 PROFILE_SHARE_SOFT=1440p/30 PROFILE_SHARE_MAX=4K/60
|
||
|
|
;;
|
||
|
|
standard)
|
||
|
|
PROFILE_SQLITE_CACHE_MB=16 PROFILE_SQLITE_MMAP_MB=256 PROFILE_SQLITE_READ_POOL=4
|
||
|
|
PROFILE_GOMAXPROCS=2 PROFILE_LRU_ENTRIES=10000
|
||
|
|
PROFILE_LIVEKIT_MAX_PARTICIPANTS=25 PROFILE_LIVEKIT_MAX_VIDEO=8 PROFILE_LIVEKIT_SIMULCAST=2
|
||
|
|
PROFILE_UPLOAD_MB=50 PROFILE_APP_LIMIT=768m PROFILE_LIVEKIT_LIMIT=1536m
|
||
|
|
PROFILE_SWAP_GB=2 PROFILE_BACKUP_KEEP=14 PROFILE_SHARE_SOFT=1440p/30 PROFILE_SHARE_MAX=4K/60
|
||
|
|
;;
|
||
|
|
plus)
|
||
|
|
PROFILE_SQLITE_CACHE_MB=32 PROFILE_SQLITE_MMAP_MB=512 PROFILE_SQLITE_READ_POOL=8
|
||
|
|
PROFILE_GOMAXPROCS=4 PROFILE_LRU_ENTRIES=50000
|
||
|
|
PROFILE_LIVEKIT_MAX_PARTICIPANTS=50 PROFILE_LIVEKIT_MAX_VIDEO=16 PROFILE_LIVEKIT_SIMULCAST=3
|
||
|
|
PROFILE_UPLOAD_MB=100 PROFILE_APP_LIMIT=1536m PROFILE_LIVEKIT_LIMIT=3g
|
||
|
|
PROFILE_SWAP_GB=2 PROFILE_BACKUP_KEEP=30 PROFILE_SHARE_SOFT=4K/30 PROFILE_SHARE_MAX=4K/60
|
||
|
|
;;
|
||
|
|
pro)
|
||
|
|
PROFILE_SQLITE_CACHE_MB=64 PROFILE_SQLITE_MMAP_MB=1024 PROFILE_SQLITE_READ_POOL="$(nproc)"
|
||
|
|
PROFILE_GOMAXPROCS="$(nproc)" PROFILE_LRU_ENTRIES=200000
|
||
|
|
PROFILE_LIVEKIT_MAX_PARTICIPANTS=100 PROFILE_LIVEKIT_MAX_VIDEO=32 PROFILE_LIVEKIT_SIMULCAST=3
|
||
|
|
PROFILE_UPLOAD_MB=250 PROFILE_APP_LIMIT=4g PROFILE_LIVEKIT_LIMIT=6g
|
||
|
|
PROFILE_SWAP_GB=0 PROFILE_BACKUP_KEEP=60 PROFILE_SHARE_SOFT=4K/60 PROFILE_SHARE_MAX=4K/60
|
||
|
|
;;
|
||
|
|
none)
|
||
|
|
PROFILE_SQLITE_CACHE_MB=8 PROFILE_SQLITE_MMAP_MB=128 PROFILE_SQLITE_READ_POOL=2
|
||
|
|
PROFILE_GOMAXPROCS=0 PROFILE_LRU_ENTRIES=2000
|
||
|
|
PROFILE_LIVEKIT_MAX_PARTICIPANTS=10 PROFILE_LIVEKIT_MAX_VIDEO=4 PROFILE_LIVEKIT_SIMULCAST=0
|
||
|
|
PROFILE_UPLOAD_MB=25 PROFILE_APP_LIMIT=512m PROFILE_LIVEKIT_LIMIT=768m
|
||
|
|
PROFILE_SWAP_GB=0 PROFILE_BACKUP_KEEP=7 PROFILE_SHARE_SOFT=1080p/30 PROFILE_SHARE_MAX=4K/60
|
||
|
|
;;
|
||
|
|
*) die "неизвестный профиль: $tier" ;;
|
||
|
|
esac
|
||
|
|
}
|
||
|
|
|
||
|
|
detect_profile() {
|
||
|
|
local cpus ram_mb
|
||
|
|
cpus="$(nproc)"
|
||
|
|
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
|
||
|
|
if [ "$cpus" -le 1 ] && [ "$ram_mb" -lt 3072 ]; then
|
||
|
|
printf 'lite'
|
||
|
|
elif [ "$cpus" -le 2 ] || [ "$ram_mb" -lt 5120 ]; then
|
||
|
|
printf 'standard'
|
||
|
|
elif [ "$cpus" -le 4 ] || [ "$ram_mb" -lt 9216 ]; then
|
||
|
|
printf 'plus'
|
||
|
|
else
|
||
|
|
printf 'pro'
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- рендеринг шаблонов ---------------------------------------------------
|
||
|
|
|
||
|
|
render_template() {
|
||
|
|
local src="$1" dst="$2"
|
||
|
|
shift 2
|
||
|
|
local content
|
||
|
|
content="$(cat "$src")"
|
||
|
|
while [ "$#" -gt 0 ]; do
|
||
|
|
local key="${1%%=*}" value="${1#*=}"
|
||
|
|
content="${content//"{{${key}}}"/$value}"
|
||
|
|
done
|
||
|
|
if printf '%s' "$content" | grep -q '{{[A-Z_]\{2,\}}}'; then
|
||
|
|
local leftover
|
||
|
|
leftover="$(printf '%s' "$content" | grep -o '{{[A-Z_]\{2,\}}}' | sort -u | tr '\n' ' ')"
|
||
|
|
warn "в шаблоне $src остались незаполненные плейсхолдеры: $leftover"
|
||
|
|
fi
|
||
|
|
printf '%s\n' "$content" >"$dst"
|
||
|
|
}
|
||
|
|
|
||
|
|
env_get() {
|
||
|
|
local key="$1" file="${2:-$GLCHAT_ENV_FILE}"
|
||
|
|
[ -f "$file" ] || return 1
|
||
|
|
grep -E "^${key}=" "$file" | tail -1 | cut -d= -f2-
|
||
|
|
}
|
||
|
|
|
||
|
|
env_set() {
|
||
|
|
local key="$1" value="$2" file="${3:-$GLCHAT_ENV_FILE}"
|
||
|
|
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
||
|
|
local tmp
|
||
|
|
tmp="$(mktemp)"
|
||
|
|
awk -v k="$key" -v v="$value" 'BEGIN { FS = "=" } $1 == k { print k "=" v; next } { print }' "$file" >"$tmp"
|
||
|
|
cat "$tmp" >"$file"
|
||
|
|
rm -f "$tmp"
|
||
|
|
else
|
||
|
|
printf '%s=%s\n' "$key" "$value" >>"$file"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- общие проверки -------------------------------------------------------
|
||
|
|
|
||
|
|
check_os() {
|
||
|
|
[ -r /etc/os-release ] || die "не удалось прочитать /etc/os-release"
|
||
|
|
# shellcheck disable=SC1091
|
||
|
|
. /etc/os-release
|
||
|
|
case "${ID:-}" in
|
||
|
|
ubuntu)
|
||
|
|
case "${VERSION_ID:-}" in
|
||
|
|
24.04 | 26.04) ok "ОС: Ubuntu ${VERSION_ID} (${PRETTY_NAME})" ;;
|
||
|
|
*)
|
||
|
|
if [ "${GLCHAT_FORCE_OS:-0}" = "1" ]; then
|
||
|
|
warn "Ubuntu ${VERSION_ID:-?} не в списке проверенных; продолжаем из-за --force-os"
|
||
|
|
else
|
||
|
|
die "поддерживаются Ubuntu 24.04 LTS и 26.04 LTS, обнаружено: ${PRETTY_NAME:-unknown}. Используйте --force-os для продолжения"
|
||
|
|
fi
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
;;
|
||
|
|
debian)
|
||
|
|
warn "Debian ${VERSION_ID:-?}: установка не проверялась, продолжаем (пакеты Debian-совместимы)"
|
||
|
|
;;
|
||
|
|
*) die "поддерживается Ubuntu 24.04/26.04 LTS, обнаружено: ${PRETTY_NAME:-unknown}" ;;
|
||
|
|
esac
|
||
|
|
}
|
||
|
|
|
||
|
|
check_resources() {
|
||
|
|
local cpus ram_mb disk_mb
|
||
|
|
cpus="$(nproc)"
|
||
|
|
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
|
||
|
|
disk_mb="$(df -Pm / | awk 'NR==2 {print $4}')"
|
||
|
|
info "ресурсы: ${cpus} vCPU, ${ram_mb} MB RAM, ${disk_mb} MB свободно на /"
|
||
|
|
if [ "$ram_mb" -lt 1800 ]; then
|
||
|
|
warn "меньше 2 GB RAM: профиль lite может не уложиться в бюджет (AGENT.md 9.1)"
|
||
|
|
fi
|
||
|
|
if [ "$disk_mb" -lt 8000 ]; then
|
||
|
|
warn "меньше 8 GB свободного места: сборка образа и бэкапы могут не поместиться"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
check_command() {
|
||
|
|
have "$1" && ok "найдено: $1" || warn "отсутствует: $1 (будет установлено)"
|
||
|
|
}
|