Files
glchat/deploy/lib/common.sh
T
grendervill 8329507d8d fix(deploy): порядок вычисления FILES_DOMAIN и чистота рендера шаблонов
- resolve_derived_values(): производные значения (files.<domain>) считаются
  до записи .env; раньше set -u ронял установщик на write_env_file
- убраны local-объявления, маскировавшие turn_external_tls/use_external_ip/
  disable_simulcast/compose_template внутри render_all_templates
- из рендера compose убрана неиспользуемая пара IMAGE_TAG
- guard чётности аргументов рендера пишет в журнал, а не в консоль:
  проверка результата — отсутствие незаполненных плейсхолдеров
2026-09-19 19:29:18 +03:00

318 lines
12 KiB
Bash
Raw 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
# Общие функции установщика и служебных скриптов 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() {
# В режиме --dry-run изменения не применяются, поэтому root не требуется:
# это позволяет проверять установщик на машине разработчика.
if [ "${DRY_RUN:-0}" = "1" ]; then
return 0
fi
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="$(detect_cpus)"
PROFILE_GOMAXPROCS="$(detect_cpus)" 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="$(detect_cpus)"
if [ -r /proc/meminfo ]; then
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
else
ram_mb=4096
fi
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 src dst KEY=value...
#
# Значения передаются через окружение, а не подставляются в аргументы: это
# исключает разбор значений с пробелами и переносами строк и делает рендер
# предсказуемым (bash ${var//pat/repl} на многострочных значениях и variadic
# аргументах ведёт себя неочевидно). Если gettext-base (envsubst) недоступен,
# используется эквивалентная замена средствами bash.
render_template() {
local src="$1" dst="$2"
shift 2
# Нечётное число аргументов означает неполный вызов: пишем в журнал
# установки, но продолжаем — реальная проверка ниже (незаполненные
# плейсхолдеры в результате рендера).
if [ $(( $# % 2 )) -ne 0 ]; then
log_file_only "render_template($(basename "$src")): нечётное число аргументов: $#"
fi
# export "$pair" разбирает KEY=value сам; export "${pair}" трактует всю
# пару как имя переменной — из-за этого значения не попадали в окружение.
local pair
for pair in "$@"; do
export "$pair"
done
local content
if command -v envsubst >/dev/null 2>&1; then
content="$(envsubst <"$src")"
else
content="$(cat "$src")"
for pair in "$@"; do
local key="${pair%%=*}" value="${pair#*=}"
content="${content//"{{${key}}}"/$value}"
done
fi
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 "в шаблоне $(basename "$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() {
if [ ! -r /etc/os-release ]; then
# macOS и другие не-Linux системы: допустимо только для --dry-run.
if [ "${DRY_RUN:-0}" = "1" ]; then
warn "не Linux (нет /etc/os-release): проверка ОС пропущена в режиме --dry-run"
return 0
fi
die "не удалось прочитать /etc/os-release"
fi
# 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="$(detect_cpus)"
if [ -r /proc/meminfo ]; then
ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
else
ram_mb=0
fi
disk_mb="$(df -Pm / | awk 'NR==2 {print $4}')"
info "ресурсы: ${cpus} vCPU, ${ram_mb} MB RAM, ${disk_mb} MB свободно на /"
if [ "$ram_mb" -gt 0 ] && [ "$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
}
detect_cpus() {
if command -v nproc >/dev/null 2>&1; then
nproc
return
fi
if command -v sysctl >/dev/null 2>&1; then
sysctl -n hw.ncpu 2>/dev/null && return
fi
printf '2'
}
check_command() {
have "$1" && ok "найдено: $1" || warn "отсутствует: $1 (будет установлено)"
}