diff --git a/.github/workflows/bash-syntax.yml b/.github/workflows/bash-syntax.yml index 70064e2..222bc1b 100644 --- a/.github/workflows/bash-syntax.yml +++ b/.github/workflows/bash-syntax.yml @@ -18,7 +18,10 @@ jobs: - name: Check every installer run: | set -e - for script in ./*.sh; do + while IFS= read -r -d '' script; do echo "Checking ${script}" bash -n "${script}" - done + done < <(find . -type f -name '*.sh' -print0) + + - name: Test step 1 selections + run: bash tests/test-step1-selection.sh diff --git a/README.md b/README.md index fc68712..aaade69 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ | Файл | Назначение | Можно запускать отдельно | | --- | --- | --- | | `ochenstarik-server-install.sh` | Единый пошаговый мастер: установить, пропустить или завершить на каждом этапе | Да; рекомендуемый способ полной настройки | -| `ochenstarik-server-1.sh` | Предлагает выбрать часовой пояс и русификацию терминала, создаёт swap-файл 2 ГБ и задаёт `vm.swappiness=20` | Да | -| `ochenstarik-server-2.sh` | Предлагает выбрать будущий SSH-порт и режим IPv4/IPv6, обновляет систему, ставит пакеты и настраивает UFW | Да | +| `ochenstarik-server-1.sh` | Предлагает выбрать часовой пояс, язык терминала и наборы программ, создаёт swap-файл 2 ГБ | Да | +| `ochenstarik-server-2.sh` | Предлагает будущий SSH-порт и IPv4/IPv6, обновляет систему, ставит обязательные пакеты и настраивает UFW | Да | | `ochenstarik-server-user-3.sh` | Создаёт администратора, переносит SSH на порт из этапа 2, проверяет права и включает fail2ban | После установки необходимых пакетов; проще всего сначала запустить скрипт 2 | | `ochenstarik-server-tg-4.sh` | Отправляет уведомления в Telegram при успешном входе по SSH | После установки OpenSSH и `curl`; обычно после скрипта 2 или 3 | | `ochenstarik-server-vpn-5.sh` | Устанавливает Xray и направляет системный трафик через VLESS + REALITY | Да; зависимости устанавливаются автоматически | @@ -67,13 +67,27 @@ chmod 700 ./*.sh bash -n ./ИМЯ-СКРИПТА.sh ``` -## 1. Часовой пояс и настройка swap +## 1. Часовой пояс, язык, программы и swap Файл: `ochenstarik-server-1.sh`. Сначала показывает нумерованное меню популярных часовых поясов, включая `Asia/Novosibirsk`, `Europe/Moscow` и `UTC`. Нужную зону достаточно выбрать по номеру. Для остальных зон предусмотрен полный список с последовательным выбором региона и часового пояса — вводить имя вручную не требуется. -После выбора времени скрипт спрашивает, нужна ли русификация терминала. При согласии он устанавливает доступные пакеты русской локали, создаёт `ru_RU.UTF-8` и задаёт русский язык для новых терминальных сессий. Прежний `/etc/default/locale` и список только что установленных языковых пакетов сохраняются в `/etc/ochenstarik-server`, поэтому отдельный скрипт очистки может вернуть исходную локаль. Без явного согласия языковые настройки не меняются. +После выбора времени скрипт показывает меню языков терминала: оставить текущий, русский, английский, немецкий, французский, испанский, итальянский, португальский, польский, украинский, турецкий, китайский, японский, корейский либо выбрать другую локаль из полного системного списка. Выбранный язык применяется к новым терминальным сессиям. Исходный `/etc/default/locale` сохраняется для последующего восстановления. + +Затем можно отметить несколько наборов программ номерами через пробел, например `1 2 4 9`. Пустой ввод устанавливает все наборы, а `0` пропускает дополнительные программы: + +1. Терминал и повседневная работа — Git, wget/curl, jq/yq, ripgrep, fd, fzf, lnav, редакторы, MC, tmux/screen, htop/btop, ncdu, rsync и strace. +2. Диагностика сети — DNS, ping, MTR, traceroute, tcpdump, nmap, netcat, HTTPie, whois, ethtool и socat. +3. Мониторинг системы и дисков — iotop, sysstat, SMART, lm-sensors, atop, nethogs, vnStat и psmisc. +4. Безопасность и обслуживание — auditd, AppArmor tools, Lynis, needrestart, unattended-upgrades, debsums, ACL и extended attributes. +5. Разработка и автоматизация — компилятор, ShellCheck, Python pip/venv и pipx. +6. Архивы и данные — 7-Zip, ZIP/RAR, SQLite, csvkit, клиенты PostgreSQL и Redis. +7. Документы, PDF и OCR — Poppler, qpdf, Ghostscript, OCRmyPDF/Tesseract, LibreOffice, Pandoc, antiword и catdoc. +8. Изображения, аудио и видео — ImageMagick, ExifTool, WebP, FFmpeg и MediaInfo. +9. Резервное копирование — restic, BorgBackup и rclone. + +Скрипт проверяет доступность каждого пакета в подключённых репозиториях. Недоступные для конкретной версии или набора репозиториев пакеты пропускаются с предупреждением. Список программ, которых не было до запуска, сохраняется в `/etc/ochenstarik-server/step1-installed-packages.list` для безопасной очистки. Затем скрипт создаёт `/swapfile` размером 2 ГБ, добавляет его в `/etc/fstab` и устанавливает `vm.swappiness=20`. Повторный запуск предусмотрен. @@ -95,7 +109,7 @@ sysctl vm.swappiness Файл: `ochenstarik-server-2.sh`. -При каждом запуске скрипт предлагает три действия: полную установку или обновление, изменение режима IP без переустановки пакетов либо добавление открытых портов. При полной установке он запрашивает SSH-порт для этапа 3, обновляет систему и устанавливает OpenSSH, UFW, fail2ban, инструменты обработки документов, изображений, аудио и видео. +При каждом запуске скрипт предлагает три действия: полную установку или обновление, изменение режима IP без переустановки пакетов либо добавление открытых портов. При полной установке он запрашивает SSH-порт для этапа 3, обновляет систему и устанавливает только обязательные компоненты: OpenSSH, UFW, fail2ban, `sudo`, TLS-сертификаты и сетевые инструменты. Дополнительные программы выбираются на этапе 1. Для входящих подключений можно выбрать: @@ -344,7 +358,7 @@ VPN, 3x-ui, WARP, Telegram-уведомления и автоматически Скрипт удаляет управляемые правила UFW, настройки IPv4/IPv6, SSH drop-in, fail2ban-конфигурацию, Telegram-hook, Xray, 3x-ui, WARP, расписания резервного копирования, swap и служебные файлы проекта. Перед изменением SSH открывается порт `22/tcp`. Правило порта, через который работает текущая SSH-сессия, сохраняется, чтобы не потерять доступ. -Удаление администратора и самих архивов резервных копий требует отдельных подтверждений. Общие системные пакеты, обновления Ubuntu и программы вроде OpenSSH, UFW и `curl` не удаляются: они могли существовать до запуска проекта и необходимы серверу. Пакеты русификации удаляются только если первый скрипт зафиксировал, что до русификации их не было. +Удаление администратора и самих архивов резервных копий требует отдельных подтверждений. Обязательные системные пакеты второго этапа и обновления Ubuntu сохраняются, чтобы не потерять доступ к серверу. Языковые пакеты и дополнительные программы удаляются только если первый скрипт зафиксировал, что до его запуска их не было. ```bash curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-uninstall.sh diff --git a/ochenstarik-server-1.sh b/ochenstarik-server-1.sh index b7db1b0..51fd1ab 100644 --- a/ochenstarik-server-1.sh +++ b/ochenstarik-server-1.sh @@ -6,53 +6,116 @@ readonly SWAPSIZE="2G" readonly SWAPPINESS="20" readonly SYSCTL_FILE="/etc/sysctl.d/60-hermes-swap.conf" readonly CONFIG_DIR="/etc/ochenstarik-server" -readonly LOCALE_BACKUP="${CONFIG_DIR}/locale-before-russian.conf" -readonly LOCALE_ABSENT_MARKER="${CONFIG_DIR}/locale-before-russian.absent" -readonly LOCALE_PACKAGES="${CONFIG_DIR}/russian-locale-installed-packages.list" +readonly LOCALE_BACKUP="${CONFIG_DIR}/locale-before-selection.conf" +readonly LOCALE_ABSENT_MARKER="${CONFIG_DIR}/locale-before-selection.absent" +readonly LOCALE_PACKAGES="${CONFIG_DIR}/locale-installed-packages.list" +readonly LEGACY_LOCALE_BACKUP="${CONFIG_DIR}/locale-before-russian.conf" +readonly LEGACY_LOCALE_ABSENT_MARKER="${CONFIG_DIR}/locale-before-russian.absent" +readonly LEGACY_LOCALE_PACKAGES="${CONFIG_DIR}/russian-locale-installed-packages.list" +readonly PROGRAM_PACKAGES="${CONFIG_DIR}/step1-installed-packages.list" + +readonly -a PROGRAM_GROUP_TITLES=( + "Терминал и повседневная работа" + "Диагностика сети" + "Мониторинг системы и дисков" + "Безопасность и обслуживание Ubuntu" + "Разработка и автоматизация" + "Архивы, таблицы и локальные данные" + "Документы, PDF и OCR" + "Изображения, аудио и видео" + "Резервное копирование и синхронизация" +) + +readonly -a PROGRAM_GROUP_DESCRIPTIONS=( + "git, wget, curl, jq/yq, ripgrep/fd/fzf, lnav, редакторы, mc, tmux, htop/btop, ncdu, rsync" + "DNS/ping/MTR/traceroute, tcpdump, nmap, netcat, HTTPie, whois, ethtool и socat" + "iotop, sysstat, SMART, датчики, atop, nethogs, vnStat и инструменты процессов" + "auditd, AppArmor tools, Lynis, needrestart, автоматические обновления, debsums, ACL/xattr" + "компилятор, ShellCheck, Python pip/venv и pipx" + "7-Zip/ZIP/RAR, SQLite, csvkit, клиенты PostgreSQL и Redis" + "Poppler, qpdf, Ghostscript, OCRmyPDF/Tesseract, LibreOffice, Pandoc, antiword/catdoc" + "ImageMagick, ExifTool, WebP, FFmpeg и MediaInfo" + "restic, BorgBackup и rclone" +) + +readonly -a PROGRAM_GROUP_PACKAGES=( + "git wget curl ca-certificates jq yq ripgrep fd-find fzf lnav tree less nano vim mc tmux screen htop btop ncdu lsof strace rsync" + "dnsutils iputils-ping mtr-tiny traceroute tcpdump nmap netcat-openbsd httpie whois ethtool socat" + "iotop sysstat smartmontools lm-sensors atop nethogs vnstat psmisc" + "auditd apparmor-utils lynis needrestart unattended-upgrades debsums acl attr" + "build-essential shellcheck python3-pip python3-venv pipx" + "p7zip-full unzip zip unrar sqlite3 csvkit postgresql-client redis-tools" + "poppler-utils qpdf ghostscript ocrmypdf tesseract-ocr tesseract-ocr-eng tesseract-ocr-rus libreoffice pandoc antiword catdoc" + "imagemagick libimage-exiftool-perl webp ffmpeg mediainfo" + "restic borgbackup rclone" +) + +APT_UPDATED=no log() { printf '[+] %s\n' "$*"; } warn() { printf '[!] %s\n' "$*" >&2; } die() { printf '[x] %s\n' "$*" >&2; exit 1; } -ask_yes_no() { - local prompt="$1" default_answer="${2:-no}" answer suffix - [[ "$default_answer" == yes || "$default_answer" == no ]] \ - || die "Invalid default answer: $default_answer" - [[ "$default_answer" == yes ]] && suffix='[Y/n]' || suffix='[y/N]' - - while :; do - read -rp "$prompt $suffix " answer || die "Input was interrupted" - answer="${answer:-$default_answer}" - case "${answer,,}" in - y|yes|д|да) return 0 ;; - n|no|н|нет) return 1 ;; - *) warn "Ответьте yes/no или да/нет" ;; - esac - done -} - package_is_installed() { dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -Fqx 'install ok installed' } -install_russian_terminal_locale() { - local package_name - local -a packages=(locales) newly_installed=() - - ask_yes_no "Установить русскую локаль терминала ru_RU.UTF-8?" no || { - log "Русификация терминала пропущена" - return 0 - } - - for package_name in apt-get apt-cache dpkg-query; do - command -v "$package_name" >/dev/null 2>&1 \ - || die "Required command not found: $package_name" +ensure_package_tools() { + local command_name + for command_name in apt-get apt-cache dpkg-query; do + command -v "$command_name" >/dev/null 2>&1 \ + || die "Required command not found: $command_name" done +} - log "Обновление списка пакетов для русификации" +ensure_apt_updated() { + [[ "$APT_UPDATED" == no ]] || return 0 + ensure_package_tools + log "Обновление списка пакетов" apt-get update + APT_UPDATED=yes +} +record_new_packages() { + local destination="$1" package_name + shift install -d -m 700 -o root -g root "$CONFIG_DIR" + [[ ! -L "$destination" ]] || die "Отказ от записи через ссылку: $destination" + touch "$destination" + for package_name in "$@"; do + grep -Fqx -- "$package_name" "$destination" \ + || printf '%s\n' "$package_name" >> "$destination" + done + chown root:root "$destination" + chmod 600 "$destination" +} + +migrate_legacy_locale_state() { + local package_name + install -d -m 700 -o root -g root "$CONFIG_DIR" + [[ ! -L "$LOCALE_BACKUP" && ! -L "$LOCALE_ABSENT_MARKER" \ + && ! -L "$LOCALE_PACKAGES" ]] || die "Файл состояния локали не должен быть ссылкой" + if [[ -f "$LEGACY_LOCALE_BACKUP" && ! -L "$LEGACY_LOCALE_BACKUP" \ + && ! -e "$LOCALE_BACKUP" ]]; then + mv -- "$LEGACY_LOCALE_BACKUP" "$LOCALE_BACKUP" + fi + if [[ -f "$LEGACY_LOCALE_ABSENT_MARKER" \ + && ! -L "$LEGACY_LOCALE_ABSENT_MARKER" && ! -e "$LOCALE_ABSENT_MARKER" ]]; then + mv -- "$LEGACY_LOCALE_ABSENT_MARKER" "$LOCALE_ABSENT_MARKER" + fi + if [[ -f "$LEGACY_LOCALE_PACKAGES" && ! -L "$LEGACY_LOCALE_PACKAGES" ]]; then + touch "$LOCALE_PACKAGES" + while IFS= read -r package_name || [[ -n "$package_name" ]]; do + [[ -z "$package_name" ]] || grep -Fqx -- "$package_name" "$LOCALE_PACKAGES" \ + || printf '%s\n' "$package_name" >> "$LOCALE_PACKAGES" + done < "$LEGACY_LOCALE_PACKAGES" + rm -f -- "$LEGACY_LOCALE_PACKAGES" + fi +} + +save_original_locale() { + migrate_legacy_locale_state + if [[ ! -e "$LOCALE_BACKUP" && ! -e "$LOCALE_ABSENT_MARKER" ]]; then if [[ -f /etc/default/locale && ! -L /etc/default/locale ]]; then cp -a -- /etc/default/locale "$LOCALE_BACKUP" @@ -62,31 +125,156 @@ install_russian_terminal_locale() { chmod 600 "$LOCALE_ABSENT_MARKER" fi fi +} - for package_name in language-pack-ru manpages-ru; do +choose_terminal_language() { + local answer selected_locale selected_language primary_language package_name index + local current_locale="${LANG:-не задана}" + local -a titles=( + "Не менять язык терминала" + "Русский — ru_RU.UTF-8" + "English (US) — en_US.UTF-8" + "Deutsch — de_DE.UTF-8" + "Français — fr_FR.UTF-8" + "Español — es_ES.UTF-8" + "Italiano — it_IT.UTF-8" + "Português (Brasil) — pt_BR.UTF-8" + "Polski — pl_PL.UTF-8" + "Українська — uk_UA.UTF-8" + "Türkçe — tr_TR.UTF-8" + "简体中文 — zh_CN.UTF-8" + "日本語 — ja_JP.UTF-8" + "한국어 — ko_KR.UTF-8" + "Другая локаль из полного списка" + ) + local -a locales=( + "" ru_RU.UTF-8 en_US.UTF-8 de_DE.UTF-8 fr_FR.UTF-8 es_ES.UTF-8 + it_IT.UTF-8 pt_BR.UTF-8 pl_PL.UTF-8 uk_UA.UTF-8 tr_TR.UTF-8 + zh_CN.UTF-8 ja_JP.UTF-8 ko_KR.UTF-8 "" + ) + local -a language_values=( + "" ru_RU:ru en_US:en de_DE:de fr_FR:fr es_ES:es it_IT:it + pt_BR:pt pl_PL:pl uk_UA:uk tr_TR:tr zh_CN:zh ja_JP:ja ko_KR:ko "" + ) + local -a language_packages=( + "" "language-pack-ru manpages-ru" language-pack-en language-pack-de + language-pack-fr language-pack-es language-pack-it language-pack-pt + language-pack-pl language-pack-uk language-pack-tr language-pack-zh-hans + language-pack-ja language-pack-ko "" + ) + local -a supported_locales packages=(locales) available_packages=() \ + newly_installed=() extra_packages=() + + while :; do + printf '\nВыберите язык новых терминальных сессий (сейчас: %s):\n' "$current_locale" + for index in "${!titles[@]}"; do + printf ' %d) %s\n' "$((index + 1))" "${titles[index]}" + done + read -rp 'Номер языка [1 — не менять]: ' answer || die "Ввод прерван" + answer="${answer:-1}" + [[ "$answer" =~ ^[0-9]+$ ]] && ((answer >= 1 && answer <= ${#titles[@]})) && break + warn "Введите номер от 1 до ${#titles[@]}" + done + + ((answer == 1)) && { log "Язык терминала не изменён"; return 0; } + ensure_apt_updated + save_original_locale + + if ((answer == ${#titles[@]})); then + package_is_installed locales || newly_installed+=(locales) + apt-get install -y locales + mapfile -t supported_locales < <(awk '{print $1}' /usr/share/i18n/SUPPORTED | sort -u) + choose_numbered_option "Выберите локаль из полного списка:" selected_locale \ + "${supported_locales[@]}" + primary_language="${selected_locale%%_*}" + selected_language="${selected_locale%%.*}:${primary_language}" + packages+=("language-pack-${primary_language}") + else + selected_locale="${locales[answer - 1]}" + selected_language="${language_values[answer - 1]}" + read -r -a extra_packages <<< "${language_packages[answer - 1]}" + packages+=("${extra_packages[@]}") + fi + + for package_name in "${packages[@]}"; do + [[ -n "$package_name" ]] || continue if apt-cache show "$package_name" >/dev/null 2>&1; then - packages+=("$package_name") + available_packages+=("$package_name") + package_is_installed "$package_name" || newly_installed+=("$package_name") + else + warn "Языковой пакет недоступен и будет пропущен: $package_name" fi done - for package_name in "${packages[@]}"; do - package_is_installed "$package_name" || newly_installed+=("$package_name") - done - log "Установка пакетов русской локали" export DEBIAN_FRONTEND=noninteractive - apt-get install -y "${packages[@]}" - locale-gen ru_RU.UTF-8 - update-locale LANG=ru_RU.UTF-8 LANGUAGE=ru_RU:ru LC_MESSAGES=ru_RU.UTF-8 + ((${#available_packages[@]} == 0)) || apt-get install -y "${available_packages[@]}" + locale-gen "$selected_locale" + update-locale LANG="$selected_locale" LANGUAGE="$selected_language" \ + LC_MESSAGES="$selected_locale" + record_new_packages "$LOCALE_PACKAGES" "${newly_installed[@]}" - touch "$LOCALE_PACKAGES" - for package_name in "${newly_installed[@]}"; do - grep -Fqx -- "$package_name" "$LOCALE_PACKAGES" \ - || printf '%s\n' "$package_name" >> "$LOCALE_PACKAGES" + log "Выбрана локаль ${selected_locale}. Она применится при следующем входе" +} + +choose_and_install_programs() { + local answer token group_index package_name + local -a selected_groups=() group_packages=() requested=() available=() newly_installed=() + local -A selected=() seen=() + + printf '\nВыберите наборы программ для установки:\n' + for group_index in "${!PROGRAM_GROUP_TITLES[@]}"; do + printf ' %d) %s\n %s\n' "$((group_index + 1))" \ + "${PROGRAM_GROUP_TITLES[group_index]}" "${PROGRAM_GROUP_DESCRIPTIONS[group_index]}" done - chown root:root "$LOCALE_PACKAGES" - chmod 600 "$LOCALE_PACKAGES" + printf ' 0) Не устанавливать дополнительные программы\n' + printf '\nВведите номера через пробел, например: 1 2 4 9.\n' + read -rp 'Выбор [Enter — установить все наборы]: ' answer || die "Ввод прерван" + answer="${answer//,/ }" - log "Русская локаль включена. Она применится при следующем входе в терминал" + if [[ -z "${answer//[[:space:]]/}" ]]; then + for group_index in "${!PROGRAM_GROUP_TITLES[@]}"; do + selected_groups+=("$group_index") + done + elif [[ "$answer" =~ ^[[:space:]]*0[[:space:]]*$ ]]; then + log "Установка дополнительных программ пропущена" + return 0 + else + for token in $answer; do + [[ "$token" =~ ^[0-9]+$ ]] \ + || die "Некорректный номер набора: $token" + ((token >= 1 && token <= ${#PROGRAM_GROUP_TITLES[@]})) \ + || die "Номер набора вне диапазона: $token" + group_index="$((token - 1))" + [[ -n "${selected[$group_index]:-}" ]] || selected_groups+=("$group_index") + selected[$group_index]=yes + done + fi + + ensure_apt_updated + for group_index in "${selected_groups[@]}"; do + log "Выбран набор: ${PROGRAM_GROUP_TITLES[group_index]}" + read -r -a group_packages <<< "${PROGRAM_GROUP_PACKAGES[group_index]}" + for package_name in "${group_packages[@]}"; do + [[ -n "${seen[$package_name]:-}" ]] && continue + seen[$package_name]=yes + requested+=("$package_name") + done + done + + for package_name in "${requested[@]}"; do + if apt-cache show "$package_name" >/dev/null 2>&1; then + available+=("$package_name") + package_is_installed "$package_name" || newly_installed+=("$package_name") + else + warn "Пакет отсутствует в подключённых репозиториях и пропущен: $package_name" + fi + done + + ((${#available[@]} > 0)) || die "Ни один выбранный пакет не найден" + export DEBIAN_FRONTEND=noninteractive + apt-get install -y "${available[@]}" + record_new_packages "$PROGRAM_PACKAGES" "${newly_installed[@]}" + log "Выбранные программы установлены" } choose_numbered_option() { @@ -165,7 +353,8 @@ for command_name in awk blkid fallocate grep mkswap swapon sysctl timedatectl; d done choose_timezone -install_russian_terminal_locale +choose_terminal_language +choose_and_install_programs log "Configuring ${SWAPSIZE} swap file" if [[ ! -e "$SWAPFILE" ]]; then diff --git a/ochenstarik-server-2.sh b/ochenstarik-server-2.sh index 65c138f..2969f69 100644 --- a/ochenstarik-server-2.sh +++ b/ochenstarik-server-2.sh @@ -326,14 +326,9 @@ if [[ "$ACTION" == install ]]; then apt-get update apt-get upgrade -y - log "Устанавливаю серверные пакеты и средства обработки документов" + log "Устанавливаю обязательные пакеты SSH, брандмауэра и защиты" apt-get install -y \ - sudo ufw fail2ban curl ca-certificates openssl openssh-server logrotate iproute2 \ - poppler-utils qpdf ghostscript ocrmypdf \ - tesseract-ocr tesseract-ocr-eng tesseract-ocr-rus \ - libreoffice pandoc antiword catdoc imagemagick libimage-exiftool-perl webp \ - ffmpeg mediainfo p7zip-full unzip zip unrar jq yq csvkit sqlite3 \ - python3-pip python3-venv mc + sudo ufw fail2ban curl ca-certificates openssl openssh-server logrotate iproute2 for command_name in ip sed sysctl ufw; do require_command "$command_name" diff --git a/ochenstarik-server-install.sh b/ochenstarik-server-install.sh index 0c38652..9c9240b 100644 --- a/ochenstarik-server-install.sh +++ b/ochenstarik-server-install.sh @@ -15,7 +15,7 @@ declare -a STEP_FILES=( ) declare -a STEP_TITLES=( - "Часовой пояс, русификация терминала и swap" + "Часовой пояс, язык терминала, набор программ и swap" "Базовые пакеты, будущий SSH-порт, IPv4/IPv6 и UFW" "Администратор, перенос SSH и fail2ban" "Telegram-уведомления о входах по SSH" diff --git a/ochenstarik-server-uninstall.sh b/ochenstarik-server-uninstall.sh index 5057d6c..07e4b72 100644 --- a/ochenstarik-server-uninstall.sh +++ b/ochenstarik-server-uninstall.sh @@ -26,9 +26,13 @@ readonly XRAY_HELPER="/usr/local/sbin/ochenstarik-xray-routing" readonly XRAY_SERVICE="/etc/systemd/system/ochenstarik-xray-routing.service" readonly BACKUP_CONFIG="${CONFIG_DIR}/backup.conf" readonly BACKUP_RUNNER="/usr/local/sbin/ochenstarik-server-backup" -readonly LOCALE_BACKUP="${CONFIG_DIR}/locale-before-russian.conf" -readonly LOCALE_ABSENT_MARKER="${CONFIG_DIR}/locale-before-russian.absent" -readonly LOCALE_PACKAGES="${CONFIG_DIR}/russian-locale-installed-packages.list" +readonly LOCALE_BACKUP="${CONFIG_DIR}/locale-before-selection.conf" +readonly LOCALE_ABSENT_MARKER="${CONFIG_DIR}/locale-before-selection.absent" +readonly LOCALE_PACKAGES="${CONFIG_DIR}/locale-installed-packages.list" +readonly LEGACY_LOCALE_BACKUP="${CONFIG_DIR}/locale-before-russian.conf" +readonly LEGACY_LOCALE_ABSENT_MARKER="${CONFIG_DIR}/locale-before-russian.absent" +readonly LEGACY_LOCALE_PACKAGES="${CONFIG_DIR}/russian-locale-installed-packages.list" +readonly PROGRAM_PACKAGES="${CONFIG_DIR}/step1-installed-packages.list" log() { printf '[+] %s\n' "$*"; } warn() { printf '[!] %s\n' "$*" >&2; } @@ -263,26 +267,52 @@ remove_backup_automation() { } restore_locale() { - local package_name + local package_name package_file local -a packages=() if [[ -f "$LOCALE_BACKUP" && ! -L "$LOCALE_BACKUP" ]]; then cp -a -- "$LOCALE_BACKUP" /etc/default/locale log "Предыдущая системная локаль восстановлена" + elif [[ -f "$LEGACY_LOCALE_BACKUP" && ! -L "$LEGACY_LOCALE_BACKUP" ]]; then + cp -a -- "$LEGACY_LOCALE_BACKUP" /etc/default/locale + log "Предыдущая системная локаль восстановлена" elif [[ -f "$LOCALE_ABSENT_MARKER" && ! -L "$LOCALE_ABSENT_MARKER" ]]; then rm -f -- /etc/default/locale log "Созданный скриптом файл локали удалён" + elif [[ -f "$LEGACY_LOCALE_ABSENT_MARKER" \ + && ! -L "$LEGACY_LOCALE_ABSENT_MARKER" ]]; then + rm -f -- /etc/default/locale + log "Созданный скриптом файл локали удалён" fi - if [[ -f "$LOCALE_PACKAGES" && ! -L "$LOCALE_PACKAGES" ]]; then + for package_file in "$LOCALE_PACKAGES" "$LEGACY_LOCALE_PACKAGES"; do + [[ -f "$package_file" && ! -L "$package_file" ]] || continue while IFS= read -r package_name || [[ -n "$package_name" ]]; do - [[ "$package_name" =~ ^[a-z0-9][a-z0-9+.-]*$ ]] && packages+=("$package_name") - done < "$LOCALE_PACKAGES" - if ((${#packages[@]} > 0)) && ask_yes_no \ - "Удалить пакеты русификации, которые ранее отсутствовали?" yes; then - DEBIAN_FRONTEND=noninteractive apt-get purge -y "${packages[@]}" - fi + [[ "$package_name" =~ ^[a-z0-9][a-z0-9+.-]*$ ]] \ + && [[ ! " ${packages[*]} " =~ " ${package_name} " ]] \ + && packages+=("$package_name") + done < "$package_file" + done + if ((${#packages[@]} > 0)) && ask_yes_no \ + "Удалить языковые пакеты, которые ранее отсутствовали?" yes; then + DEBIAN_FRONTEND=noninteractive apt-get purge -y "${packages[@]}" fi - rm -f -- "$LOCALE_BACKUP" "$LOCALE_ABSENT_MARKER" "$LOCALE_PACKAGES" + rm -f -- "$LOCALE_BACKUP" "$LOCALE_ABSENT_MARKER" "$LOCALE_PACKAGES" \ + "$LEGACY_LOCALE_BACKUP" "$LEGACY_LOCALE_ABSENT_MARKER" \ + "$LEGACY_LOCALE_PACKAGES" +} + +remove_step1_programs() { + local package_name + local -a packages=() + [[ -f "$PROGRAM_PACKAGES" && ! -L "$PROGRAM_PACKAGES" ]] || return 0 + while IFS= read -r package_name || [[ -n "$package_name" ]]; do + [[ "$package_name" =~ ^[a-z0-9][a-z0-9+.-]*$ ]] && packages+=("$package_name") + done < "$PROGRAM_PACKAGES" + if ((${#packages[@]} > 0)) && ask_yes_no \ + "Удалить дополнительные программы, установленные первым этапом?" yes; then + DEBIAN_FRONTEND=noninteractive apt-get purge -y "${packages[@]}" + fi + rm -f -- "$PROGRAM_PACKAGES" } remove_swap() { @@ -307,9 +337,9 @@ remove_swap() { cat <<'WARNING' Этот скрипт удалит настройки, службы и данные, созданные проектом lightweight-server. -Он не откатывает обновления Ubuntu и не удаляет общие системные пакеты (OpenSSH, -UFW, curl, мультимедийные библиотеки и другие), потому что они могли существовать -до установки и нужны для доступа к серверу. Их наличие не мешает установке заново. +Он не откатывает обновления Ubuntu и не удаляет обязательные общие пакеты второго +этапа (OpenSSH, UFW и другие), потому что они нужны для доступа к серверу. Пакеты, +которые первый этап зафиксировал как новые, можно удалить отдельным подтверждением. Сначала будет открыт SSH-порт 22. Не закрывайте текущую сессию, пока не проверите новый вход после очистки. @@ -326,6 +356,7 @@ remove_xray remove_panel_and_warp remove_backup_automation restore_locale +remove_step1_programs remove_swap rm -f -- "$SSH_PORT_CONFIG" "$IP_FAMILY_CONFIG" "$MANAGED_PORTS_CONFIG" diff --git a/tests/test-step1-selection.sh b/tests/test-step1-selection.sh new file mode 100644 index 0000000..59bae7c --- /dev/null +++ b/tests/test-step1-selection.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." +source <(awk '/^\[\[ "\$EUID"/{exit} {print}' ochenstarik-server-1.sh) + +declare -a MOCK_APT_PACKAGES=() +MOCK_LOCALE="" +MOCK_UPDATE_LOCALE="" + +ensure_apt_updated() { :; } +apt-cache() { return 0; } +package_is_installed() { return 1; } +record_new_packages() { :; } +save_original_locale() { :; } + +apt-get() { + [[ "${1:-}" == install ]] || return 0 + shift + [[ "${1:-}" != -y ]] || shift + MOCK_APT_PACKAGES=("$@") +} + +locale-gen() { MOCK_LOCALE="$1"; } +update-locale() { MOCK_UPDATE_LOCALE="$*"; } + +contains_package() { + local expected="$1" package_name + for package_name in "${MOCK_APT_PACKAGES[@]}"; do + [[ "$package_name" != "$expected" ]] || return 0 + done + return 1 +} + +choose_and_install_programs <<< '1 3' >/dev/null +contains_package git +contains_package iotop +! contains_package nmap +! contains_package ffmpeg + +MOCK_APT_PACKAGES=() +choose_and_install_programs <<< '' >/dev/null +contains_package nmap +contains_package ffmpeg +contains_package restic +contains_package postgresql-client + +MOCK_APT_PACKAGES=() +choose_and_install_programs <<< '0' >/dev/null +((${#MOCK_APT_PACKAGES[@]} == 0)) + +choose_terminal_language <<< '2' >/dev/null +[[ "$MOCK_LOCALE" == ru_RU.UTF-8 ]] +[[ "$MOCK_UPDATE_LOCALE" == *'LANG=ru_RU.UTF-8'* ]] +[[ "$MOCK_UPDATE_LOCALE" == *'LANGUAGE=ru_RU:ru'* ]] + +printf 'Step 1 selection tests passed.\n'