From d92b1b9bda37e591492f6f43b2712f6c97f539a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 06:31:55 +0000 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B1=D0=BE=D1=80=D0=BA=D0=B0=20=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BE=D1=87=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20ISO-=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D0=B0=20Virtu?= =?UTF-8?q?ality=20=D0=B1=D1=8B=D0=BB=D0=B0=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image/build-iso.sh пересобирает официальный Ubuntu Server 24.04 live-server ISO (amd64/arm64) в установщик Virtuality: - /autoinstall.yaml: установщик спрашивает только сеть, диск и пользователя, остальное автоматически; есть полностью автоматический режим --unattended; - исходники Virtuality (git-репозиторий, чтобы работал центр обновлений) и Python-колёса для офлайн-установки панели; - virtuality-firstboot.service при первой загрузке ставит KVM, libvirt, Cockpit, web-панель, vhealth и консольный dashboard, повторяя попытку до успеха. Также: зависимости панели закреплены полностью, установщики работают неинтерактивно и учитывают VIRTUALITY_USER, bootstrap.sh стал обёрткой над install.sh. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg --- bootstrap.sh | 298 +---------------------- image/autoinstall.yaml | 31 +++ image/build-iso.sh | 244 +++++++++++++++++++ image/files/virtuality-firstboot.service | 19 ++ install.sh | 5 +- install_virtuality_node.sh | 5 +- scripts/install_console_dashboard.sh | 7 +- scripts/install_healthcheck_command.sh | 2 +- scripts/install_web_panel.sh | 1 + scripts/virtuality_firstboot.sh | 95 ++++++++ web/requirements.txt | 20 ++ 11 files changed, 431 insertions(+), 296 deletions(-) create mode 100644 image/autoinstall.yaml create mode 100755 image/build-iso.sh create mode 100644 image/files/virtuality-firstboot.service create mode 100755 scripts/virtuality_firstboot.sh diff --git a/bootstrap.sh b/bootstrap.sh index 15de57e..e162e4e 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -1,296 +1,16 @@ #!/usr/bin/env bash set -euo pipefail -# ========================================================== -# Virtuality One-command Bootstrap Installer -# Public install entrypoint for clean Ubuntu/Debian servers. -# ========================================================== +# Legacy entrypoint kept for old documentation links. +# All installation logic lives in install.sh. -REPO_URL="${VIRTUALITY_REPO_URL:-https://github.com/viktor138irk/virtuality.git}" -DEFAULT_INSTALL_USER="${SUDO_USER:-root}" -INSTALL_USER="${VIRTUALITY_USER:-$DEFAULT_INSTALL_USER}" -PROJECT_BASE_DIR="${VIRTUALITY_PROJECT_BASE_DIR:-/opt/virtuality}" -PROJECT_DIR="${VIRTUALITY_PROJECT_DIR:-${PROJECT_BASE_DIR}/source}" -RUN_BRIDGE="${VIRTUALITY_SETUP_BRIDGE:-0}" -BRIDGE_IFACE="${VIRTUALITY_BRIDGE_IFACE:-}" -RUN_TEST_VM="${VIRTUALITY_CREATE_TEST_VM:-0}" -WEB_PORT="${VIRTUALITY_WEB_PORT:-8088}" -LOG_DIR="/var/log/virtuality" -LOG_FILE="${LOG_DIR}/bootstrap_$(date +%Y%m%d_%H%M%S).log" -MIN_ROOT_FREE_MB="${VIRTUALITY_MIN_ROOT_FREE_MB:-8192}" -MIN_VAR_FREE_MB="${VIRTUALITY_MIN_VAR_FREE_MB:-20480}" -MIN_RAM_MB="${VIRTUALITY_MIN_RAM_MB:-4096}" -MIN_CPU_CORES="${VIRTUALITY_MIN_CPU_CORES:-2}" -SKIP_REQUIREMENTS="${VIRTUALITY_SKIP_REQUIREMENTS:-0}" +INSTALL_URL="${VIRTUALITY_INSTALL_URL:-https://raw.githubusercontent.com/viktor138irk/virtuality/main/install.sh}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || true)" -ESC="\033" -RESET="${ESC}[0m" -BOLD="${ESC}[1m" -GREEN="${ESC}[32m" -YELLOW="${ESC}[33m" -RED="${ESC}[31m" -CYAN="${ESC}[36m" -BLUE="${ESC}[34m" -GRAY="${ESC}[90m" - -mkdir -p "$LOG_DIR" - -timestamp() { date '+%Y-%m-%d %H:%M:%S'; } -log() { echo "[$(timestamp)] $*" >> "$LOG_FILE"; } -ok() { echo -e " ${GREEN}✓${RESET} $*"; log "OK: $*"; } -warn() { echo -e " ${YELLOW}!${RESET} $*"; log "WARN: $*"; } -fail() { echo -e " ${RED}✗${RESET} $*"; log "ERROR: $*"; echo; echo -e "${RED}${BOLD}Bootstrap остановлен.${RESET} Лог: ${LOG_FILE}"; exit 1; } -step() { echo; echo -e "${BLUE}${BOLD}▶${RESET} ${BOLD}$*${RESET}"; log "STEP: $*"; } - -run_logged() { - local desc="$1" - shift - local start_ts pid code spinner elapsed symbol i - log "RUN: $*" - start_ts="$(date +%s)" - "$@" >> "$LOG_FILE" 2>&1 & - pid=$! - spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") - i=0 - while kill -0 "$pid" 2>/dev/null; do - elapsed=$(( $(date +%s) - start_ts )) - symbol="${spinner[$((i % ${#spinner[@]}))]}" - printf "\r ${CYAN}%s${RESET} %s... ${GRAY}%ss${RESET} ${GRAY}(лог: %s)${RESET}" "$symbol" "$desc" "$elapsed" "$LOG_FILE" - sleep 1 - i=$((i + 1)) - done - wait "$pid" - code=$? - printf "\r%*s\r" 120 "" - if [[ "$code" -eq 0 ]]; then - ok "$desc" - else - fail "$desc" - fi -} - -header() { - clear 2>/dev/null || true - echo -e "${CYAN}${BOLD}╭────────────────────────────────────────────────────────────╮${RESET}" - echo -e "${CYAN}${BOLD}│${RESET} ${BOLD}Virtuality Bootstrap Installer${RESET} ${CYAN}${BOLD}│${RESET}" - echo -e "${CYAN}${BOLD}│${RESET} One-command KVM / libvirt / Cockpit / Web Panel setup ${CYAN}${BOLD}│${RESET}" - echo -e "${CYAN}${BOLD}╰────────────────────────────────────────────────────────────╯${RESET}" - echo - echo -e "${GRAY}Repository:${RESET} ${REPO_URL}" - echo -e "${GRAY}Auth/install user:${RESET} ${INSTALL_USER}" - echo -e "${GRAY}Project dir:${RESET} ${PROJECT_DIR}" - echo -e "${GRAY}Log:${RESET} ${LOG_FILE}" - echo -} - -require_root() { - [[ "$EUID" -eq 0 ]] || fail "Запусти от root: curl ... | sudo bash" -} - -free_mb_for_path() { - local path="$1" - mkdir -p "$path" 2>/dev/null || true - df -Pm "$path" | awk 'NR==2 {print $4}' -} - -check_password_state() { - local user="$1" - local hash - hash="$(getent shadow "$user" | cut -d: -f2 || true)" - if [[ -z "$hash" || "$hash" == "!" || "$hash" == "*" || "$hash" == "!!" ]]; then - warn "У пользователя ${user} пароль не задан или заблокирован. Для входа в панель выполни: passwd ${user}" - else - ok "У пользователя ${user} есть пароль для входа в панель" - fi -} - -check_requirements() { - step "Проверяем минимальные системные требования" - if [[ "$SKIP_REQUIREMENTS" == "1" ]]; then - warn "Проверка требований отключена через VIRTUALITY_SKIP_REQUIREMENTS=1" - return 0 - fi - - local root_free var_free ram_mb cpu_cores - root_free="$(free_mb_for_path /)" - var_free="$(free_mb_for_path /var/lib 2>/dev/null || free_mb_for_path /)" - ram_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)" - cpu_cores="$(nproc 2>/dev/null || echo 1)" - - if (( root_free >= MIN_ROOT_FREE_MB )); then - ok "Свободно на /: ${root_free} MB минимум ${MIN_ROOT_FREE_MB} MB" - else - fail "Недостаточно места на /: ${root_free} MB. Нужно минимум ${MIN_ROOT_FREE_MB} MB. Очисти диск: apt clean; journalctl --vacuum-time=3d" - fi - - if (( var_free >= MIN_VAR_FREE_MB )); then - ok "Свободно для /var/lib: ${var_free} MB минимум ${MIN_VAR_FREE_MB} MB" - else - fail "Недостаточно места для VM-хранилища /var/lib: ${var_free} MB. Нужно минимум ${MIN_VAR_FREE_MB} MB" - fi - - if (( ram_mb >= MIN_RAM_MB )); then - ok "RAM: ${ram_mb} MB минимум ${MIN_RAM_MB} MB" - else - warn "RAM: ${ram_mb} MB меньше рекомендуемых ${MIN_RAM_MB} MB. Установка возможна, но VM будут ограничены" - fi - - if (( cpu_cores >= MIN_CPU_CORES )); then - ok "CPU cores: ${cpu_cores} минимум ${MIN_CPU_CORES}" - else - warn "CPU cores: ${cpu_cores} меньше рекомендуемых ${MIN_CPU_CORES}" - fi - - if grep -E -q '(vmx|svm)' /proc/cpuinfo; then - ok "CPU virtualization vmx/svm найдена" - else - warn "CPU virtualization vmx/svm не найдена. Проверь BIOS/UEFI или настройки VPS" - fi -} - -ensure_user() { - if [[ "$INSTALL_USER" == "root" ]]; then - ok "Пользователь авторизации: root" - check_password_state root - return 0 - fi - - if id "$INSTALL_USER" >/dev/null 2>&1; then - ok "Пользователь уже существует: ${INSTALL_USER}" - else - run_logged "Создан пользователь ${INSTALL_USER}" adduser --disabled-password --gecos "" "$INSTALL_USER" - warn "Пароль для ${INSTALL_USER} не задан. После установки выполни: sudo passwd ${INSTALL_USER}" - fi - usermod -aG sudo "$INSTALL_USER" >> "$LOG_FILE" 2>&1 || true - ok "Пользователь ${INSTALL_USER} добавлен в sudo" - check_password_state "$INSTALL_USER" -} - -run_as_user() { - if [[ "$INSTALL_USER" == "root" ]]; then - bash -lc "$*" - else - sudo -H -u "$INSTALL_USER" bash -lc "$*" - fi -} - -prepare_project_dir() { - mkdir -p "$PROJECT_BASE_DIR" - if [[ "$INSTALL_USER" == "root" ]]; then - chown root:root "$PROJECT_BASE_DIR" - else - chown "$INSTALL_USER:$INSTALL_USER" "$PROJECT_BASE_DIR" - fi - chmod 755 "$PROJECT_BASE_DIR" - ok "Рабочая директория подготовлена: ${PROJECT_BASE_DIR}" -} - -header - -step "Проверяем root и apt" -require_root -command -v apt >/dev/null 2>&1 || fail "apt не найден. Нужен Debian/Ubuntu" -ok "root подтверждён" -ok "apt найден" - -check_requirements - -step "Устанавливаем базовые пакеты" -run_logged "apt update выполнен" apt update -run_logged "Установлены sudo/git/curl/ca-certificates" apt install -y sudo git curl ca-certificates openssh-client nano htop - -step "Создаём или проверяем пользователя" -ensure_user - -step "Готовим рабочую директорию в /opt" -prepare_project_dir - -step "Проверяем место после подготовки пользователя" -root_free_after="$(free_mb_for_path /)" -if (( root_free_after < MIN_ROOT_FREE_MB )); then - fail "После подготовки пользователя свободного места стало мало: ${root_free_after} MB на /. Очисти диск и повтори установку" -fi -ok "Место после подготовки пользователя: ${root_free_after} MB на /" - -step "Клонируем или обновляем репозиторий" -if [[ -d "$PROJECT_DIR/.git" ]]; then - run_logged "Репозиторий обновлён" run_as_user "cd '$PROJECT_DIR' && git reset --hard origin/main && git pull" -else - run_logged "Репозиторий склонирован" run_as_user "cd '$PROJECT_BASE_DIR' && git clone '$REPO_URL' source" +if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/install.sh" ]]; then + exec bash "${SCRIPT_DIR}/install.sh" "$@" fi -step "Запускаем основной установщик ноды" -run_logged "Virtuality Node установлен" bash "$PROJECT_DIR/install_virtuality_node.sh" - -step "Устанавливаем healthcheck-команду" -run_logged "Healthcheck установлен" bash "$PROJECT_DIR/scripts/install_healthcheck_command.sh" - -step "Устанавливаем консольный dashboard" -run_logged "Console dashboard установлен" bash "$PROJECT_DIR/scripts/install_console_dashboard.sh" - -step "Устанавливаем web-панель" -VIRTUALITY_AUTH_USER="$INSTALL_USER" VIRTUALITY_WEB_PORT="$WEB_PORT" bash "$PROJECT_DIR/scripts/install_web_panel.sh" >> "$LOG_FILE" 2>&1 & -web_pid=$! -web_start="$(date +%s)" -web_i=0 -web_spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") -while kill -0 "$web_pid" 2>/dev/null; do - web_elapsed=$(( $(date +%s) - web_start )) - printf "\r ${CYAN}%s${RESET} Web-панель устанавливается... ${GRAY}%ss${RESET} ${GRAY}(лог: %s)${RESET}" "${web_spinner[$((web_i % ${#web_spinner[@]}))]}" "$web_elapsed" "$LOG_FILE" - sleep 1 - web_i=$((web_i + 1)) -done -wait "$web_pid" -web_code=$? -printf "\r%*s\r" 120 "" -[[ "$web_code" -eq 0 ]] && ok "Web-панель установлена" || fail "Web-панель не установлена" - -if [[ "$RUN_BRIDGE" == "1" ]]; then - step "Настраиваем bridge br0" - if [[ -z "$BRIDGE_IFACE" ]]; then - BRIDGE_IFACE="$(ip route | awk '/default/ {print $5; exit}')" - fi - if [[ -z "$BRIDGE_IFACE" ]]; then - warn "Не удалось определить интерфейс для br0. Пропускаю bridge setup" - else - warn "Bridge меняет сеть. Если SSH зависнет, нужен физический доступ или консоль провайдера." - bash "$PROJECT_DIR/scripts/setup_bridge_br0.sh" "$BRIDGE_IFACE" static >> "$LOG_FILE" 2>&1 && ok "Bridge br0 настроен на ${BRIDGE_IFACE}" || warn "Bridge setup завершился с предупреждением. Проверь лог" - fi -else - warn "Bridge br0 не настраивался автоматически. Запуск вручную: sudo bash scripts/setup_bridge_br0.sh INTERFACE static" -fi - -if [[ "$RUN_TEST_VM" == "1" ]]; then - step "Создаём тестовую VM" - run_logged "Тестовая VM создана" bash "$PROJECT_DIR/scripts/create_test_vm.sh" || warn "Тестовая VM не создана. Проверь ISO/bridge/log" -fi - -step "Финальная диагностика" -if command -v vhealth >/dev/null 2>&1; then - vhealth >> "$LOG_FILE" 2>&1 && ok "vhealth: OK" || warn "vhealth вернул warnings/errors. Проверь: sudo vhealth" -else - warn "Команда vhealth не найдена" -fi - -SERVER_IP="$(hostname -I | awk '{print $1}')" - -echo -echo -e "${GREEN}${BOLD}╭────────────────────────────────────────────────────────────╮${RESET}" -echo -e "${GREEN}${BOLD}│${RESET} ${BOLD}Virtuality Bootstrap завершён${RESET} ${GREEN}${BOLD}│${RESET}" -echo -e "${GREEN}${BOLD}╰────────────────────────────────────────────────────────────╯${RESET}" -echo -echo -e "${BOLD}Cockpit:${RESET} https://${SERVER_IP}:9090" -echo -e "${BOLD}Web Panel:${RESET} http://${SERVER_IP}:${WEB_PORT}" -echo -e "${BOLD}Login:${RESET} ${INSTALL_USER} / пароль Linux-пользователя" -echo -e "${BOLD}Project:${RESET} ${PROJECT_DIR}" -echo -e "${BOLD}Log:${RESET} ${LOG_FILE}" -echo -echo -e "${BOLD}После установки:${RESET}" -echo " passwd ${INSTALL_USER} # если пароль ещё не задан или заблокирован" -echo " sudo vhealth" -echo " cd ${PROJECT_DIR}" -echo " ip -br a" -echo -echo -e "${YELLOW}Важно:${RESET} bridge br0 по умолчанию не включается в one-command режиме, чтобы не уронить SSH." -echo "Для bridge: cd ${PROJECT_DIR} && sudo bash scripts/setup_bridge_br0.sh INTERFACE static" -echo +tmp_installer="$(mktemp /tmp/virtuality-install.XXXXXX.sh)" +curl --connect-timeout 20 --max-time 180 -fsSL "$INSTALL_URL" -o "$tmp_installer" +exec bash "$tmp_installer" "$@" diff --git a/image/autoinstall.yaml b/image/autoinstall.yaml new file mode 100644 index 0000000..4026739 --- /dev/null +++ b/image/autoinstall.yaml @@ -0,0 +1,31 @@ +#cloud-config +# Virtuality installer configuration for Ubuntu Server 24.04 (subiquity autoinstall). +# build-iso.sh puts this file at /autoinstall.yaml on the ISO. +# @@VARIABLES@@ are substituted at build time. +autoinstall: + version: 1 + # The installer asks only for these screens; everything else is automated. + interactive-sections: +@@INTERACTIVE_SECTIONS@@ + locale: @@LOCALE@@ + keyboard: + layout: @@KEYBOARD@@ + timezone: @@TIMEZONE@@ + source: + id: ubuntu-server + ssh: + install-server: true + allow-pw: true +@@IDENTITY@@ + storage: + layout: + name: lvm + sizing-policy: all + late-commands: + - mkdir -p /target/opt/virtuality /target/etc/virtuality + - cp -a /cdrom/virtuality/source /target/opt/virtuality/source + - chown -R root:root /target/opt/virtuality + - chmod -R u+w /target/opt/virtuality/source + - cp /cdrom/virtuality/image.env /target/etc/virtuality/image.env + - cp /cdrom/virtuality/virtuality-firstboot.service /target/etc/systemd/system/virtuality-firstboot.service + - curtin in-target --target=/target -- systemctl enable virtuality-firstboot.service diff --git a/image/build-iso.sh b/image/build-iso.sh new file mode 100755 index 0000000..29e6cdc --- /dev/null +++ b/image/build-iso.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ========================================================== +# Virtuality ISO builder +# Remasters the official Ubuntu Server 24.04 live ISO into a +# Virtuality installer: Ubuntu autoinstall + bundled Virtuality +# source and Python wheels + first boot setup service. +# +# Usage: image/build-iso.sh [options] (see --help) +# Needs: xorriso, git, curl, python3 with pip, sha256sum. +# Does not need root. +# ========================================================== + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +IMAGE_DIR="${REPO_ROOT}/image" + +ARCH="amd64" +UBUNTU_SERIES="24.04" +UBUNTU_ISO="" +CACHE_DIR="${VIRTUALITY_ISO_CACHE:-${REPO_ROOT}/.cache/iso}" +OUTPUT_DIR="${REPO_ROOT}/dist" +GIT_REF="HEAD" +REPO_URL="https://github.com/viktor138irk/virtuality.git" +WEB_PORT="8088" +AUTO_UPDATE="1" +UNATTENDED="0" +USERNAME="" +PASSWORD_HASH="" +HOSTNAME_VALUE="virtuality" +LOCALE="en_US.UTF-8" +KEYBOARD="us" +TIMEZONE="Etc/UTC" +WITH_WHEELS="1" +TARGET_PYTHON="3.12" + +usage() { + cat <&2; exit 1; } +say() { echo "==> $*"; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --arch) ARCH="$2"; shift 2 ;; + --ubuntu-iso) UBUNTU_ISO="$2"; shift 2 ;; + --cache-dir) CACHE_DIR="$2"; shift 2 ;; + --output) OUTPUT_DIR="$2"; shift 2 ;; + --ref) GIT_REF="$2"; shift 2 ;; + --repo-url) REPO_URL="$2"; shift 2 ;; + --web-port) WEB_PORT="$2"; shift 2 ;; + --no-auto-update) AUTO_UPDATE="0"; shift ;; + --locale) LOCALE="$2"; shift 2 ;; + --keyboard) KEYBOARD="$2"; shift 2 ;; + --timezone) TIMEZONE="$2"; shift 2 ;; + --no-wheels) WITH_WHEELS="0"; shift ;; + --unattended) UNATTENDED="1"; shift ;; + --username) USERNAME="$2"; shift 2 ;; + --password-hash) PASSWORD_HASH="$2"; shift 2 ;; + --hostname) HOSTNAME_VALUE="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; die "unknown option: $1" ;; + esac +done + +case "$ARCH" in + amd64) PY_PLATFORM_ARCH="x86_64"; MIRROR="https://releases.ubuntu.com/${UBUNTU_SERIES}" ;; + arm64) PY_PLATFORM_ARCH="aarch64"; MIRROR="https://cdimage.ubuntu.com/releases/${UBUNTU_SERIES}/release" ;; + *) die "unsupported arch: $ARCH (amd64 or arm64)" ;; +esac +[[ "$WEB_PORT" =~ ^[0-9]+$ ]] && (( WEB_PORT >= 1 && WEB_PORT <= 65535 )) || die "invalid --web-port: $WEB_PORT" +if [[ "$UNATTENDED" == "1" ]]; then + [[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--unattended needs a valid --username" + [[ "$PASSWORD_HASH" == \$* ]] || die "--unattended needs --password-hash (generate: openssl passwd -6)" + [[ "$HOSTNAME_VALUE" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}$ ]] || die "invalid --hostname" +fi +for cmd in xorriso git curl python3 sha256sum md5sum; do + command -v "$cmd" >/dev/null 2>&1 || die "$cmd not found (Ubuntu: sudo apt install xorriso git curl python3-pip)" +done + +VERSION="$(git -C "$REPO_ROOT" show "${GIT_REF}:VERSION" | tr -d '[:space:]')" +[[ -n "$VERSION" ]] || die "cannot read VERSION at ${GIT_REF}" +SUFFIX="" +[[ "$UNATTENDED" == "1" ]] && SUFFIX="-unattended" +OUTPUT_ISO="${OUTPUT_DIR}/virtuality-${VERSION}-ubuntu-${UBUNTU_SERIES}-${ARCH}${SUFFIX}.iso" + +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/virtuality-iso.XXXXXX")" +trap 'rm -rf "$WORK_DIR"' EXIT +mkdir -p "$OUTPUT_DIR" "$CACHE_DIR" + +# ---------------------------------------------------------- Ubuntu ISO +if [[ -z "$UBUNTU_ISO" ]]; then + say "Resolving the latest Ubuntu ${UBUNTU_SERIES} live-server ISO for ${ARCH}" + curl -fsSL --retry 3 "${MIRROR}/SHA256SUMS" -o "${WORK_DIR}/SHA256SUMS" + iso_name="$(grep -oE "ubuntu-${UBUNTU_SERIES}(\.[0-9]+)*-live-server-${ARCH}\.iso" "${WORK_DIR}/SHA256SUMS" | sort -uV | tail -n1)" + [[ -n "$iso_name" ]] || die "live-server ISO for ${ARCH} not found in ${MIRROR}/SHA256SUMS" + expected_sha="$(awk -v n="*${iso_name}" '$2 == n || $2 == substr(n, 2) {print $1}' "${WORK_DIR}/SHA256SUMS")" + UBUNTU_ISO="${CACHE_DIR}/${iso_name}" + if [[ -f "$UBUNTU_ISO" ]] && echo "${expected_sha} ${UBUNTU_ISO}" | sha256sum -c --status; then + say "Using cached ${iso_name}" + else + say "Downloading ${iso_name}" + curl -fL --retry 5 -C - -o "${UBUNTU_ISO}.part" "${MIRROR}/${iso_name}" + mv "${UBUNTU_ISO}.part" "$UBUNTU_ISO" + echo "${expected_sha} ${UBUNTU_ISO}" | sha256sum -c --status || die "checksum mismatch for ${iso_name}" + fi + say "Checksum OK: ${expected_sha}" +fi +[[ -f "$UBUNTU_ISO" ]] || die "Ubuntu ISO not found: $UBUNTU_ISO" + +# ---------------------------------------------------------- payload +PAYLOAD="${WORK_DIR}/virtuality" +mkdir -p "$PAYLOAD" +say "Bundling Virtuality ${VERSION} (${GIT_REF})" +git clone --quiet --no-hardlinks "$REPO_ROOT" "${PAYLOAD}/source" +git -C "${PAYLOAD}/source" checkout --quiet --detach "$(git -C "$REPO_ROOT" rev-parse "${GIT_REF}^{commit}")" +git -C "${PAYLOAD}/source" remote set-url origin "$REPO_URL" + +if [[ "$WITH_WHEELS" == "1" ]]; then + say "Downloading Python ${TARGET_PYTHON} wheels for ${PY_PLATFORM_ARCH}" + platform_args=(--platform "manylinux2014_${PY_PLATFORM_ARCH}") + for glibc_minor in 17 28 31 34 35 39; do + platform_args+=(--platform "manylinux_2_${glibc_minor}_${PY_PLATFORM_ARCH}") + done + python3 -m pip download --quiet --disable-pip-version-check \ + -r "${PAYLOAD}/source/web/requirements.txt" -d "${PAYLOAD}/source/wheels" \ + --only-binary=:all: --implementation cp --python-version "$TARGET_PYTHON" \ + --abi "cp${TARGET_PYTHON/./}" --abi abi3 --abi none "${platform_args[@]}" +fi + +cat > "${PAYLOAD}/image.env" <> "${PAYLOAD}/image.env" +cp "${IMAGE_DIR}/files/virtuality-firstboot.service" "${PAYLOAD}/virtuality-firstboot.service" + +# ---------------------------------------------------------- autoinstall +if [[ "$UNATTENDED" == "1" ]]; then + interactive_sections=" []" + identity=" identity: + hostname: ${HOSTNAME_VALUE} + username: ${USERNAME} + realname: Virtuality Admin + password: '${PASSWORD_HASH}'" +else + interactive_sections=" - network + - storage + - identity" + identity="" +fi +python3 - "${IMAGE_DIR}/autoinstall.yaml" "${WORK_DIR}/autoinstall.yaml" </dev/null \ + || python3 -c 'import sys, json; print("PyYAML not available, skipping YAML validation")' + +# ---------------------------------------------------------- boot menu +xorriso -osirrox on -indev "$UBUNTU_ISO" \ + -extract /boot/grub/grub.cfg "${WORK_DIR}/grub.cfg" \ + -extract /md5sum.txt "${WORK_DIR}/md5sum.txt" >/dev/null 2>&1 || die "cannot read grub.cfg from $UBUNTU_ISO" +chmod u+w "${WORK_DIR}/grub.cfg" "${WORK_DIR}/md5sum.txt" +python3 - "${WORK_DIR}/grub.cfg" "$UNATTENDED" "$VERSION" <<'PYEOF' +import re +import sys + +path, unattended, version = sys.argv[1], sys.argv[2] == "1", sys.argv[3] +text = open(path).read() +match = re.search(r'menuentry "[^"]*" \{\n(.*?)\n\}', text, re.S) +if not match: + raise SystemExit("no menuentry found in grub.cfg") +body = match.group(1) +if "linux" not in body or "---" not in body: + raise SystemExit("unexpected grub.cfg layout: " + body) +kernel_args = "autoinstall ---" if unattended else "---" +title = "Install Virtuality %s%s" % (version, " (UNATTENDED: erases disk)" if unattended else "") +entry = 'menuentry "%s" {\n%s\n}\n' % (title, body.replace("---", kernel_args, 1)) +text = text[: match.start()] + entry + text[match.start():] +text = re.sub(r"^set timeout=\d+", "set default=0\nset timeout=10", text, count=1, flags=re.M) +text = text.replace('menuentry "Try or Install Ubuntu Server"', 'menuentry "Ubuntu Server (without Virtuality)"') +open(path, "w").write(text) +PYEOF +new_md5="$(md5sum "${WORK_DIR}/grub.cfg" | cut -d' ' -f1)" +sed -i "s|^[0-9a-f]\{32\} \./boot/grub/grub.cfg$|${new_md5} ./boot/grub/grub.cfg|" "${WORK_DIR}/md5sum.txt" + +# ---------------------------------------------------------- repack +say "Writing ${OUTPUT_ISO}" +rm -f "$OUTPUT_ISO" +chmod -R a+rX,go-w "$PAYLOAD" +if ! xorriso -indev "$UBUNTU_ISO" -outdev "$OUTPUT_ISO" \ + -uid 0 -gid 0 \ + -map "$PAYLOAD" /virtuality \ + -map "${WORK_DIR}/autoinstall.yaml" /autoinstall.yaml \ + -map "${WORK_DIR}/grub.cfg" /boot/grub/grub.cfg \ + -map "${WORK_DIR}/md5sum.txt" /md5sum.txt \ + -boot_image any replay > "${WORK_DIR}/xorriso.log" 2>&1; then + tail -n 30 "${WORK_DIR}/xorriso.log" >&2 + die "xorriso failed" +fi +[[ -s "$OUTPUT_ISO" ]] || die "xorriso did not produce $OUTPUT_ISO" + +(cd "$OUTPUT_DIR" && sha256sum "$(basename "$OUTPUT_ISO")" > "$(basename "$OUTPUT_ISO").sha256") +say "Done: ${OUTPUT_ISO} ($(du -h "$OUTPUT_ISO" | cut -f1))" +say "SHA256: $(cut -d' ' -f1 "${OUTPUT_ISO}.sha256")" diff --git a/image/files/virtuality-firstboot.service b/image/files/virtuality-firstboot.service new file mode 100644 index 0000000..9129c5c --- /dev/null +++ b/image/files/virtuality-firstboot.service @@ -0,0 +1,19 @@ +[Unit] +Description=Virtuality first boot setup +Documentation=https://github.com/viktor138irk/virtuality/blob/main/docs/IMAGE.md +Wants=network-online.target +After=network-online.target +ConditionPathExists=!/var/lib/virtuality/.firstboot-done +StartLimitIntervalSec=0 + +[Service] +# simple, not oneshot: boot must not wait for a multi-minute package install. +Type=simple +ExecStart=/bin/bash /opt/virtuality/source/scripts/virtuality_firstboot.sh +Restart=on-failure +RestartSec=60 +StandardOutput=journal+console +StandardError=journal+console + +[Install] +WantedBy=multi-user.target diff --git a/install.sh b/install.sh index ee392e2..4547bca 100644 --- a/install.sh +++ b/install.sh @@ -21,7 +21,7 @@ if [[ "${EUID}" -ne 0 ]]; then exit 1 fi chmod +x "$tmp_installer" - exec sudo --preserve-env=VIRTUALITY_INSTALL_URL,VIRTUALITY_USER,VIRTUALITY_WEB_PORT,VIRTUALITY_MIN_ROOT_FREE_MB,VIRTUALITY_MIN_VAR_FREE_MB,VIRTUALITY_MIN_RAM_MB,VIRTUALITY_MIN_CPU_CORES,VIRTUALITY_SKIP_REQUIREMENTS,VIRTUALITY_SETUP_BRIDGE,VIRTUALITY_BRIDGE_IFACE,VIRTUALITY_CREATE_TEST_VM,VIRTUALITY_REPO_URL,VIRTUALITY_PROJECT_BASE_DIR,VIRTUALITY_PROJECT_DIR,VIRTUALITY_INSTALL_VERBOSE,VIRTUALITY_CLEAN_BEFORE_INSTALL bash "$tmp_installer" "$@" + exec sudo --preserve-env=VIRTUALITY_INSTALL_URL,VIRTUALITY_USER,VIRTUALITY_WEB_PORT,VIRTUALITY_MIN_ROOT_FREE_MB,VIRTUALITY_MIN_VAR_FREE_MB,VIRTUALITY_MIN_RAM_MB,VIRTUALITY_MIN_CPU_CORES,VIRTUALITY_SKIP_REQUIREMENTS,VIRTUALITY_SETUP_BRIDGE,VIRTUALITY_BRIDGE_IFACE,VIRTUALITY_CREATE_TEST_VM,VIRTUALITY_REPO_URL,VIRTUALITY_PROJECT_BASE_DIR,VIRTUALITY_PROJECT_DIR,VIRTUALITY_INSTALL_VERBOSE,VIRTUALITY_CLEAN_BEFORE_INSTALL,VIRTUALITY_AUTO_UPDATE bash "$tmp_installer" "$@" fi echo "Root or sudo is required. Run: curl -fsSL $SELF_INSTALL_URL | sudo bash" >&2 exit 1 @@ -45,6 +45,7 @@ MIN_CPU_CORES="${VIRTUALITY_MIN_CPU_CORES:-2}" SKIP_REQUIREMENTS="${VIRTUALITY_SKIP_REQUIREMENTS:-0}" INSTALL_VERBOSE="${VIRTUALITY_INSTALL_VERBOSE:-0}" CLEAN_BEFORE_INSTALL="${VIRTUALITY_CLEAN_BEFORE_INSTALL:-0}" +export DEBIAN_FRONTEND=noninteractive ESC="\033" RESET="${ESC}[0m" @@ -277,7 +278,7 @@ if [[ "$CLEAN_BEFORE_INSTALL" == "1" && -x "$PROJECT_DIR/scripts/clean_install.s fi step "Запускаем основной установщик ноды" -run_logged "Virtuality Node установлен" bash "$PROJECT_DIR/install_virtuality_node.sh" +run_logged "Virtuality Node установлен" env VIRTUALITY_USER="$INSTALL_USER" bash "$PROJECT_DIR/install_virtuality_node.sh" step "Устанавливаем healthcheck-команду" run_logged "Healthcheck установлен" bash "$PROJECT_DIR/scripts/install_healthcheck_command.sh" diff --git a/install_virtuality_node.sh b/install_virtuality_node.sh index 2fb237d..2eb5c59 100644 --- a/install_virtuality_node.sh +++ b/install_virtuality_node.sh @@ -22,6 +22,7 @@ MIN_VAR_FREE_MB="${VIRTUALITY_MIN_VAR_FREE_MB:-20480}" MIN_RAM_MB="${VIRTUALITY_MIN_RAM_MB:-4096}" MIN_CPU_CORES="${VIRTUALITY_MIN_CPU_CORES:-2}" SKIP_REQUIREMENTS="${VIRTUALITY_SKIP_REQUIREMENTS:-0}" +export DEBIAN_FRONTEND=noninteractive ESC="\033" RESET="${ESC}[0m" @@ -174,14 +175,14 @@ ok "Images: ${IMAGES_DIR}" ok "Backups: ${BACKUP_DIR}" step "Добавляем пользователя в группы libvirt/kvm" -REAL_USER="${SUDO_USER:-root}" +REAL_USER="${VIRTUALITY_USER:-${SUDO_USER:-root}}" if [[ "$REAL_USER" != "root" ]]; then usermod -aG libvirt "$REAL_USER" >> "$LOG_FILE" 2>&1 || warn "Не удалось добавить ${REAL_USER} в libvirt" usermod -aG kvm "$REAL_USER" >> "$LOG_FILE" 2>&1 || warn "Не удалось добавить ${REAL_USER} в kvm" ok "Пользователь ${REAL_USER} добавлен в группы libvirt/kvm" warn "Чтобы группы применились, нужно выйти из SSH и зайти снова" else - warn "Запуск под root без SUDO_USER; пользователь не добавлен в группы" + warn "Запуск под root без SUDO_USER/VIRTUALITY_USER; пользователь не добавлен в группы" fi step "Включаем systemd-сервисы" diff --git a/scripts/install_console_dashboard.sh b/scripts/install_console_dashboard.sh index 53e99f4..8de090e 100644 --- a/scripts/install_console_dashboard.sh +++ b/scripts/install_console_dashboard.sh @@ -5,7 +5,8 @@ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DASH_SRC="${REPO_DIR}/scripts/virtuality_console_dashboard.sh" DASH_DST="/usr/local/bin/virtuality-console-dashboard" SERVICE_FILE="/etc/systemd/system/virtuality-console-dashboard.service" -TARGET_USER="${SUDO_USER:-${USER:-root}}" +export DEBIAN_FRONTEND=noninteractive +TARGET_USER="${VIRTUALITY_USER:-${SUDO_USER:-${USER:-root}}}" if [[ "$EUID" -ne 0 ]]; then echo "Ошибка: запусти через sudo: sudo bash scripts/install_console_dashboard.sh" @@ -49,7 +50,9 @@ EOF systemctl daemon-reload systemctl disable --now getty@tty1.service >/dev/null 2>&1 || true -systemctl enable --now virtuality-console-dashboard.service +systemctl enable virtuality-console-dashboard.service +# --no-block: the unit is ordered after multi-user.target, which may still be starting (first boot). +systemctl start --no-block virtuality-console-dashboard.service USER_HOME="$(getent passwd "$TARGET_USER" | cut -d: -f6 || true)" if [[ -n "$USER_HOME" && -d "$USER_HOME" ]]; then diff --git a/scripts/install_healthcheck_command.sh b/scripts/install_healthcheck_command.sh index f23bded..8aab78e 100644 --- a/scripts/install_healthcheck_command.sh +++ b/scripts/install_healthcheck_command.sh @@ -4,7 +4,7 @@ set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SRC="${REPO_DIR}/scripts/virtuality_healthcheck.sh" DST="/usr/local/bin/vhealth" -TARGET_USER="${SUDO_USER:-${USER:-root}}" +TARGET_USER="${VIRTUALITY_USER:-${SUDO_USER:-${USER:-root}}}" if [[ "$EUID" -ne 0 ]]; then echo "Ошибка: запусти через sudo: sudo bash scripts/install_healthcheck_command.sh" diff --git a/scripts/install_web_panel.sh b/scripts/install_web_panel.sh index 4d3dc20..d4ea679 100644 --- a/scripts/install_web_panel.sh +++ b/scripts/install_web_panel.sh @@ -21,6 +21,7 @@ NODE_CONFIG_FILE="${PROFILE_DIR}/web.env" UPLOAD_TMP_DIR="/var/lib/virtuality/tmp" WHEELS_DIR="${VIRTUALITY_WHEELS_DIR:-${REPO_DIR}/wheels}" TOTAL_STEPS=13 +export DEBIAN_FRONTEND=noninteractive # Settings survive updates: explicit env > saved node config > previous install > defaults. saved_setting() { diff --git a/scripts/virtuality_firstboot.sh b/scripts/virtuality_firstboot.sh new file mode 100755 index 0000000..2986219 --- /dev/null +++ b/scripts/virtuality_firstboot.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ========================================================== +# Virtuality first boot setup +# Runs once on a system installed from a Virtuality image: +# installs KVM/libvirt/Cockpit, the web panel and helpers +# from the bundled source in /opt/virtuality/source. +# ========================================================== + +SOURCE_DIR="${VIRTUALITY_SOURCE_DIR:-/opt/virtuality/source}" +IMAGE_ENV="/etc/virtuality/image.env" +STATE_DIR="/var/lib/virtuality" +DONE_FLAG="${STATE_DIR}/.firstboot-done" +LOG_DIR="/var/log/virtuality" +LOG_FILE="${LOG_DIR}/firstboot.log" +APT_LOCK_CONF="/etc/apt/apt.conf.d/90virtuality-lock-timeout" +ISSUE_FILE="/etc/issue.d/virtuality.issue" +NETWORK_WAIT_SECONDS="${VIRTUALITY_NETWORK_WAIT_SECONDS:-300}" + +mkdir -p "$LOG_DIR" "$STATE_DIR" "$(dirname "$ISSUE_FILE")" +exec > >(tee -a "$LOG_FILE") 2>&1 + +now() { date '+%Y-%m-%d %H:%M:%S'; } +say() { echo "[$(now)] [virtuality-firstboot] $*"; } + +if [[ -f "$DONE_FLAG" ]]; then + say "already completed, nothing to do" + exit 0 +fi + +if [[ -f "$IMAGE_ENV" ]]; then + set -a + # shellcheck disable=SC1090 + . "$IMAGE_ENV" + set +a +fi + +first_human_user() { + getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 && $7 !~ /(nologin|false)$/ {print $1; exit}' +} + +AUTH_USER="${VIRTUALITY_USER:-$(first_human_user)}" +AUTH_USER="${AUTH_USER:-root}" +WEB_PORT="${VIRTUALITY_WEB_PORT:-8088}" +export VIRTUALITY_USER="$AUTH_USER" +export VIRTUALITY_AUTH_USER="$AUTH_USER" +export VIRTUALITY_WEB_PORT="$WEB_PORT" +export VIRTUALITY_AUTO_UPDATE="${VIRTUALITY_AUTO_UPDATE:-1}" +# Disk/RAM limits are reported by vhealth; they must not block an image install. +export VIRTUALITY_SKIP_REQUIREMENTS="${VIRTUALITY_SKIP_REQUIREMENTS:-1}" +export DEBIAN_FRONTEND=noninteractive +export TERM="${TERM:-dumb}" + +cat > "$ISSUE_FILE" <<'EOF' +Virtuality: first boot setup is running. Progress: sudo journalctl -fu virtuality-firstboot + +EOF + +say "source: $SOURCE_DIR" +say "panel user: $AUTH_USER, port: $WEB_PORT" +[[ -f "$SOURCE_DIR/install_virtuality_node.sh" ]] || { say "ERROR: Virtuality source not found in $SOURCE_DIR"; exit 1; } + +# unattended-upgrades often holds the dpkg lock right after the first boot. +printf 'DPkg::Lock::Timeout "900";\n' > "$APT_LOCK_CONF" + +say "waiting for network (up to ${NETWORK_WAIT_SECONDS}s)" +deadline=$(( $(date +%s) + NETWORK_WAIT_SECONDS )) +until ip route show default 2>/dev/null | grep -q . && getent hosts archive.ubuntu.com >/dev/null 2>&1; do + if (( $(date +%s) >= deadline )); then + say "ERROR: no network with DNS. Configure networking; the setup will be retried automatically" + exit 1 + fi + sleep 5 +done +say "network is ready" + +cd "$SOURCE_DIR" +say "step 1/4: virtualization node (KVM, libvirt, Cockpit)" +bash ./install_virtuality_node.sh +say "step 2/4: healthcheck command" +bash ./scripts/install_healthcheck_command.sh +say "step 3/4: web panel" +bash ./scripts/install_web_panel.sh +say "step 4/4: console dashboard" +bash ./scripts/install_console_dashboard.sh + +touch "$DONE_FLAG" +cat > "$ISSUE_FILE" </dev/null 2>&1 || true +say "completed. Web panel: http://$(hostname -I | awk '{print $1}'):${WEB_PORT} login: ${AUTH_USER}" diff --git a/web/requirements.txt b/web/requirements.txt index 61e3fac..c95f958 100644 --- a/web/requirements.txt +++ b/web/requirements.txt @@ -1,5 +1,25 @@ +# Virtuality web panel dependencies. +# Direct dependencies: fastapi==0.115.6 uvicorn[standard]==0.34.0 jinja2==3.1.5 python-multipart==0.0.20 itsdangerous==2.2.0 +# Transitive dependencies, pinned for reproducible installs and offline image wheels: +annotated-types==0.8.0 +anyio==4.15.1 +click==8.5.0 +h11==0.16.0 +httptools==0.8.0 +idna==3.20 +markupsafe==3.0.3 +pydantic==2.13.5 +pydantic-core==2.46.5 +python-dotenv==1.2.3 +pyyaml==6.0.3 +starlette==0.41.3 +typing-extensions==4.16.0 +typing-inspection==0.4.4 +uvloop==0.22.1 +watchfiles==1.3.0 +websockets==17.1