#!/bin/bash # ============================================================================ # init_system.sh — первичная инициализация системы (BBox / OpenDoor base) # ---------------------------------------------------------------------------- # Выполняется ОДИН раз после развёртывания образа на новом устройстве: # * обновление пакетов # * установка базовых утилит (tmux, mc, git, net-tools, curl, wget, btop) # * установка Docker + Docker Compose # * настройка hostname и баннера BBox (bbox.wtf) # * сброс уникальных идентификаторов (machine-id, SSH host keys) # * установка вспомогательных скриптов и автозапуска # # Безопасно повторять: повторный запуск пропускает уже выполненные шаги. # ============================================================================ set -euo pipefail # Цвета RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' BBOX_VERSION="1.2" BBOX_URL="bbox.wtf" HOSTNAME_DEFAULT="bbox-node" DEFAULT_TIMEOUT="${BBOX_TIMEOUT:-5}" # секунд на подтверждение (переопределяется env BBOX_TIMEOUT) log() { echo -e "${GREEN}[init]${NC} $*"; } warn() { echo -e "${YELLOW}[init][warn]${NC} $*"; } fail() { echo -e "${RED}[init][error]${NC} $*"; exit 1; } # --------------------------------------------------------------------------- # Подтверждение с таймером: если пользователь не ответил за TIMEOUT секунд, # принимается DEFAULT (автовыбор). Возвращает 0 = yes, 1 = no. # usage: confirm_default "Вопрос" y|n [timeout] # --------------------------------------------------------------------------- confirm_default() { local msg="$1" default="$2" timeout="${3:-$DEFAULT_TIMEOUT}" local ans case "$default" in y|Y|yes) default="y"; prompt="[Y/n]";; *) default="n"; prompt="[y/N]";; esac printf " %s %s (авто: %s через %ss): " "$msg" "$prompt" "$default" "$timeout" if ! read -r -t "$timeout" ans 2>/dev/null; then echo "" echo " → таймаут, принято значение по умолчанию: $default" ans="$default" fi case "${ans,,}" in y|yes) return 0 ;; n|no) return 1 ;; *) [ "$default" = "y" ] ;; esac } # --------------------------------------------------------------------------- # 1. Определение дистрибутива и архитектуры # --------------------------------------------------------------------------- detect_distro() { if [ -f /etc/os-release ]; then . /etc/os-release DISTRO=$ID VERSION_CODENAME=$VERSION_CODENAME else fail "Не удалось определить дистрибутив." fi ARCH=$(uname -m) log "Дистрибутив: $DISTRO ($VERSION_CODENAME), архитектура: $ARCH" } check_install() { if command -v "$1" &> /dev/null; then log "$1 уже установлен." else log "Установка $1..." if ! apt-get install -y "$1" &> /dev/null; then fail "Ошибка при установке $1." fi log "$1 установлен." fi } # --------------------------------------------------------------------------- # 2. Обновление системы # --------------------------------------------------------------------------- update_system() { log "Обновление списков пакетов..." apt-get update || fail "Ошибка apt-get update" apt-get upgrade -y || warn "apt-get upgrade завершился с предупреждениями" } # --------------------------------------------------------------------------- # 3. Установка базовых утилит # --------------------------------------------------------------------------- install_base_utils() { log "Установка базовых утилит..." check_install tmux check_install mc check_install git check_install net-tools check_install curl check_install wget check_install build-essential check_install btop } # --------------------------------------------------------------------------- # 4. Установка Docker # --------------------------------------------------------------------------- install_docker() { if command -v docker &> /dev/null; then log "Docker уже установлен." return fi log "Установка Docker для $DISTRO..." apt-get install -y apt-transport-https ca-certificates curl gnupg mkdir -p /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/$DISTRO/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$DISTRO $VERSION_CODENAME stable" \ > /etc/apt/sources.list.d/docker.list apt-get update if ! apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then warn "Официальный репозиторий не подошёл, пробуем docker.io..." apt-get install -y docker.io docker-compose-plugin || fail "Не удалось установить Docker." fi systemctl enable --now docker log "Docker установлен: $(docker --version)" } # --------------------------------------------------------------------------- # 5. Настройка hostname # --------------------------------------------------------------------------- setup_hostname() { local NEW_HOSTNAME="${HOSTNAME:-$HOSTNAME_DEFAULT}" local CUR_HOSTNAME CUR_HOSTNAME=$(hostname) if [ "$CUR_HOSTNAME" != "$NEW_HOSTNAME" ]; then log "Смена hostname: $CUR_HOSTNAME -> $NEW_HOSTNAME" hostnamectl set-hostname "$NEW_HOSTNAME" || warn "Не удалось сменить hostname" echo "$NEW_HOSTNAME" > /etc/hostname else log "Hostname уже установлен: $CUR_HOSTNAME" fi } # --------------------------------------------------------------------------- # 6. Баннер BBox (MOTD) — шаблон из репозитория # --------------------------------------------------------------------------- # Шаблон: motd/10-armbian-header.tpl с плейсхолдерами __BBOX_VERSION__ / __BBOX_URL__. # При каждом запуске файл целиком перезаписывается из шаблона, поэтому смена # версии в репозитории автоматически обновляет баннер на устройстве. # --------------------------------------------------------------------------- MOTD_HEADER="/etc/update-motd.d/10-armbian-header" MOTD_TPL_REPO="motd/10-armbian-header.tpl" render_motd() { local tpl="$1" sed -e "s|__BBOX_VERSION__|${BBOX_VERSION}|g" \ -e "s|__BBOX_URL__|${BBOX_URL}|g" "$tpl" } install_motd_template() { local tpl_src="" local script_dir script_dir="$(dirname "$(readlink -f "$0")")" # Источники шаблона: # 1) рядом со скриптом (репозиторий / временная загрузка) # 2) в общем хранилище (после install_scripts.sh -> /usr/local/share/bbox/motd) for candidate in \ "$script_dir/$MOTD_TPL_REPO" \ "$script_dir/../$MOTD_TPL_REPO" \ "$script_dir/motd/10-armbian-header.tpl" \ "/usr/local/share/bbox/motd/$(basename "$MOTD_TPL_REPO")" do if [ -f "$candidate" ]; then tpl_src="$candidate" break fi done if [ -z "$tpl_src" ]; then warn "Шаблон баннера не найден ($MOTD_TPL_REPO), баннер не изменён." return 1 fi # Бэкап дефолтного баннера только один раз if [ ! -f "${MOTD_HEADER}.bak" ]; then cp -f "$MOTD_HEADER" "${MOTD_HEADER}.bak" 2>/dev/null || true fi # Запись шаблона с подстановкой версии/URL render_motd "$tpl_src" > "$MOTD_HEADER" chmod 755 "$MOTD_HEADER" log "Баннер BBox установлен из шаблона: v${BBOX_VERSION} (${BBOX_URL})" } setup_motd() { if [ -f "$MOTD_HEADER" ]; then install_motd_template else warn "Баннер входа не найден ($MOTD_HEADER), пропуск." fi } # --------------------------------------------------------------------------- # 7. Сброс уникальных идентификаторов (для клонированных образов) # --------------------------------------------------------------------------- reset_unique_ids() { log "Сброс machine-id..." rm -f /etc/machine-id /var/lib/dbus/machine-id systemd-machine-id-setup || warn "Не удалось пересоздать machine-id" log "Сброс SSH host keys (будут пересозданы при старте sshd)..." rm -f /etc/ssh/ssh_host_* systemctl restart ssh || systemctl restart sshd || warn "SSH будет перезапущен при перезагрузке" } # --------------------------------------------------------------------------- # 8. Установка вспомогательных скриптов и автозапуска # --------------------------------------------------------------------------- install_helper_scripts() { local SRC_DIR SRC_DIR=$(dirname "$(readlink -f "$0")") if [ -x "$SRC_DIR/install_scripts.sh" ]; then log "Установка вспомогательных скриптов..." "$SRC_DIR/install_scripts.sh" || warn "install_scripts.sh завершился с ошибкой" else warn "install_scripts.sh не найден рядом ($SRC_DIR), пропуск." fi } # --------------------------------------------------------------------------- # MAIN # --------------------------------------------------------------------------- FIRSTBOOT=0 FIRSTBOOT_MARKER="/var/lib/bbox-firstboot-done" main() { if [ "$(id -u)" -ne 0 ]; then fail "Запустите скрипт от root: sudo ./init_system.sh" fi # Парсинг аргументов for arg in "$@"; do case "$arg" in --firstboot) FIRSTBOOT=1 ;; --yes|-y) ALL_YES=1 ;; --timeout=*) DEFAULT_TIMEOUT="${arg#*=}" ;; esac done if [ $FIRSTBOOT -eq 1 ]; then log "Режим firstboot (автозапуск при первой загрузке)" if [ -f "$FIRSTBOOT_MARKER" ]; then log "Инициализация уже выполнялась, пропуск." exit 0 fi fi log "=== BBox (bbox.wtf) v${BBOX_VERSION} — первичная инициализация ===" echo echo " Будет выполнено:" echo " 1. Обновление системы" echo " 2. Установка базовых утилит (tmux, mc, git, net-tools, curl, wget, build-essential, btop)" echo " 3. Установка Docker + Docker Compose" echo " 4. Настройка hostname" echo " 5. Установка баннера BBox (bbox.wtf)" echo " 6. Установка вспомогательных скриптов и автозапуска" echo # Подтверждение запуска (автовыбор YES через 5 сек) if [ "${ALL_YES:-0}" -eq 1 ] || confirm_default "Начать инициализацию?" "y"; then log "Запуск инициализации..." else fail "Отменено пользователем." fi detect_distro update_system install_base_utils install_docker # Выбор hostname (автовыбор bbox-node через 5 сек) if [ "${ALL_YES:-0}" -eq 1 ] || confirm_default "Сменить hostname на '${HOSTNAME:-bbox-node}'?" "y"; then setup_hostname else log "Hostname оставлен прежним: $(hostname)" fi # Баннер BBox (автовыбор YES через 5 сек) if [ "${ALL_YES:-0}" -eq 1 ] || confirm_default "Установить баннер BBox (bbox.wtf)?" "y"; then setup_motd else warn "Баннер не установлен." fi if [ $FIRSTBOOT -eq 1 ]; then # При автозапуске не трогаем ssh/machine-id до перезагрузки warn "Режим firstboot: сброс идентификаторов выполняется при следующем запуске" touch "$FIRSTBOOT_MARKER" else # Сброс идентификаторов (автовыбор YES — иначе клоны будут одинаковыми) if [ "${ALL_YES:-0}" -eq 1 ] || confirm_default "Сбросить machine-id и SSH host keys (для клонирования)?" "y"; then reset_unique_ids else warn "Идентификаторы не сброшены." fi fi # Установка вспомогательных скриптов (автовыбор YES через 5 сек) if [ "${ALL_YES:-0}" -eq 1 ] || confirm_default "Установить вспомогательные скрипты и автозапуск?" "y"; then install_helper_scripts else warn "Скрипты не установлены." fi log "Инициализация завершена." if [ $FIRSTBOOT -eq 1 ]; then log "Сервис автозапуска завершён. Перезагрузка не требуется." else echo echo "Рекомендуется перезагрузить устройство: reboot" fi } main "$@"