diff --git a/bootstrap.sh b/bootstrap.sh deleted file mode 100644 index e162e4e..0000000 --- a/bootstrap.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Legacy entrypoint kept for old documentation links. -# All installation logic lives in install.sh. - -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)" - -if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/install.sh" ]]; then - exec bash "${SCRIPT_DIR}/install.sh" "$@" -fi - -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/build-iso.sh b/image/build-iso.sh index 880c1b4..22a396e 100755 --- a/image/build-iso.sh +++ b/image/build-iso.sh @@ -30,7 +30,7 @@ PASSWORD_HASH="" HOSTNAME_VALUE="virtuality" LOCALE="en_US.UTF-8" KEYBOARD="us" -TIMEZONE="Etc/UTC" +TIMEZONE="geoip" WITH_WHEELS="1" TARGET_PYTHON="" @@ -50,7 +50,7 @@ Options: --no-auto-update Disable the nightly GitHub auto-update on installed nodes --locale LOCALE Installer/system locale (default: ${LOCALE}) --keyboard LAYOUT Keyboard layout (default: ${KEYBOARD}) - --timezone TZ System timezone (default: ${TIMEZONE}) + --timezone TZ System timezone (default: ${TIMEZONE} — detected by the installer; the wizard can change it) --no-wheels Do not bundle Python wheels (first boot downloads them from PyPI) Unattended mode (no questions at all, ERASES THE LARGEST DISK): @@ -157,8 +157,10 @@ if [[ "$WITH_WHEELS" == "1" ]]; then for glibc_minor in 17 28 31 34 35 39; do platform_args+=(--platform "manylinux_2_${glibc_minor}_${PY_PLATFORM_ARCH}") done + # pip itself is bundled as well: Ubuntu Server has no python3-venv, the panel + # installer bootstraps pip from this wheel into a --without-pip virtualenv. python3 -m pip download --quiet --disable-pip-version-check \ - -r "${PAYLOAD}/source/web/requirements.txt" -d "${PAYLOAD}/source/wheels" \ + -r "${PAYLOAD}/source/web/requirements.txt" pip -d "${PAYLOAD}/source/wheels" \ --only-binary=:all: --implementation cp --python-version "$TARGET_PYTHON" \ --abi "cp${TARGET_PYTHON/./}" --abi abi3 --abi none "${platform_args[@]}" fi diff --git a/install.sh b/install.sh index 4547bca..9e0fd6a 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,VIRTUALITY_AUTO_UPDATE 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_REPO_URL,VIRTUALITY_PROJECT_BASE_DIR,VIRTUALITY_PROJECT_DIR,VIRTUALITY_INSTALL_VERBOSE,VIRTUALITY_AUTO_UPDATE,VIRTUALITY_BRANCH bash "$tmp_installer" "$@" fi echo "Root or sudo is required. Run: curl -fsSL $SELF_INSTALL_URL | sudo bash" >&2 exit 1 @@ -32,9 +32,7 @@ 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}" +BRANCH="${VIRTUALITY_BRANCH:-main}" WEB_PORT="${VIRTUALITY_WEB_PORT:-8088}" LOG_DIR="/var/log/virtuality" LOG_FILE="${LOG_DIR}/install_$(date +%Y%m%d_%H%M%S).log" @@ -44,7 +42,6 @@ MIN_RAM_MB="${VIRTUALITY_MIN_RAM_MB:-4096}" 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" @@ -118,7 +115,7 @@ header() { clear 2>/dev/null || true echo -e "${CYAN}${BOLD}╭────────────────────────────────────────────────────────────╮${RESET}" echo -e "${CYAN}${BOLD}│${RESET} ${BOLD}Virtuality 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} One-command KVM / libvirt / web panel setup ${CYAN}${BOLD}│${RESET}" echo -e "${CYAN}${BOLD}╰────────────────────────────────────────────────────────────╯${RESET}" echo echo -e "${GRAY}Repository:${RESET} ${REPO_URL}" @@ -251,13 +248,6 @@ ensure_user step "Готовим рабочую директорию в /opt" prepare_project_dir -if [[ "$CLEAN_BEFORE_INSTALL" == "1" && -x "$PROJECT_DIR/scripts/clean_install.sh" ]]; then - step "Очищаем старую кривую установку перед установкой" - run_logged "Старая установка очищена" bash "$PROJECT_DIR/scripts/clean_install.sh" --yes -elif [[ "$CLEAN_BEFORE_INSTALL" == "1" ]]; then - warn "Очистка запрошена, но clean_install.sh пока недоступен. Продолжаю установку." -fi - step "Проверяем место после подготовки пользователя" root_free_after="$(free_mb_for_path /)" if (( root_free_after < MIN_ROOT_FREE_MB )); then @@ -267,14 +257,9 @@ ok "Место после подготовки пользователя: ${root_ step "Клонируем или обновляем репозиторий" if [[ -d "$PROJECT_DIR/.git" ]]; then - run_logged "Репозиторий обновлён" run_as_user "cd '$PROJECT_DIR' && git fetch origin main && git reset --hard origin/main && git pull --ff-only" + run_logged "Репозиторий обновлён (${BRANCH})" run_as_user "cd '$PROJECT_DIR' && git fetch origin '$BRANCH' && git checkout -q -B '$BRANCH' 'origin/$BRANCH'" else - run_logged "Репозиторий склонирован" run_as_user "cd '$PROJECT_BASE_DIR' && git clone '$REPO_URL' source" -fi - -if [[ "$CLEAN_BEFORE_INSTALL" == "1" && -x "$PROJECT_DIR/scripts/clean_install.sh" ]]; then - step "Очищаем старую кривую установку после обновления исходников" - run_logged "Старая установка очищена" bash "$PROJECT_DIR/scripts/clean_install.sh" --yes + run_logged "Репозиторий склонирован (${BRANCH})" run_as_user "cd '$PROJECT_BASE_DIR' && git clone --branch '$BRANCH' '$REPO_URL' source" fi step "Запускаем основной установщик ноды" @@ -283,9 +268,6 @@ run_logged "Virtuality Node установлен" env VIRTUALITY_USER="$INSTALL_ 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-панель" if [[ "$INSTALL_VERBOSE" == "1" ]]; then VIRTUALITY_AUTH_USER="$INSTALL_USER" VIRTUALITY_WEB_PORT="$WEB_PORT" bash "$PROJECT_DIR/scripts/install_web_panel.sh" 2>&1 | tee -a "$LOG_FILE" @@ -313,25 +295,6 @@ else [[ "$web_code" -eq 0 ]] && ok "Web-панель установлена" || fail "Web-панель не установлена" fi -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 @@ -347,18 +310,12 @@ echo -e "${GREEN}${BOLD}╭───────────────── echo -e "${GREEN}${BOLD}│${RESET} ${BOLD}Virtuality установка завершена${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 -e "${BOLD}Панель:${RESET} http://${SERVER_IP}:${WEB_PORT}" +echo -e "${BOLD}Логин:${RESET} ${INSTALL_USER} / пароль пользователя Linux" +echo -e "${BOLD}Исходники:${RESET} ${PROJECT_DIR}" +echo -e "${BOLD}Лог:${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 -e "${BOLD}Дальше:${RESET} откройте панель в браузере — мастер настройки поможет включить HTTPS," +echo "выбрать сеть для машин (NAT или ваша локальная сеть) и хранилище." +echo "Если пароль ещё не задан: passwd ${INSTALL_USER}" echo diff --git a/install_virtuality_node.sh b/install_virtuality_node.sh index 0da8ff7..17f1db5 100644 --- a/install_virtuality_node.sh +++ b/install_virtuality_node.sh @@ -3,7 +3,7 @@ set -euo pipefail # ========================================================== # Virtuality Node Installer -# Clean step-by-step installer for KVM/QEMU/libvirt/Cockpit +# Clean step-by-step installer for KVM/QEMU/libvirt # ========================================================== PROJECT_NAME="Virtuality" @@ -13,9 +13,8 @@ ISO_DIR="${STORAGE_DIR}/iso" IMAGES_DIR="${STORAGE_DIR}/images" BACKUP_DIR="${STORAGE_DIR}/backups" LOG_DIR="/var/log/virtuality" -COCKPIT_PORT="9090" LOG_FILE="${LOG_DIR}/install_node_$(date +%Y%m%d_%H%M%S).log" -TOTAL_STEPS=13 +TOTAL_STEPS=12 CURRENT_STEP=0 MIN_ROOT_FREE_MB="${VIRTUALITY_MIN_ROOT_FREE_MB:-8192}" MIN_VAR_FREE_MB="${VIRTUALITY_MIN_VAR_FREE_MB:-20480}" @@ -45,12 +44,11 @@ print_header() { clear 2>/dev/null || true echo -e "${CYAN}${BOLD}╭────────────────────────────────────────────────────────────╮${RESET}" echo -e "${CYAN}${BOLD}│${RESET} ${BOLD}Virtuality Node Installer${RESET} ${CYAN}${BOLD}│${RESET}" - echo -e "${CYAN}${BOLD}│${RESET} KVM / QEMU / libvirt / Cockpit virtualization node ${CYAN}${BOLD}│${RESET}" + echo -e "${CYAN}${BOLD}│${RESET} KVM / QEMU / libvirt virtualization node ${CYAN}${BOLD}│${RESET}" echo -e "${CYAN}${BOLD}╰────────────────────────────────────────────────────────────╯${RESET}" echo echo -e "${GRAY}Лог установки:${RESET} ${LOG_FILE}" echo -e "${GRAY}Storage:${RESET} ${STORAGE_DIR}" - echo -e "${GRAY}Cockpit:${RESET} ${COCKPIT_PORT}/tcp" echo } @@ -182,9 +180,9 @@ install_available "Пакеты виртуализации установлен "${QEMU_SYSTEM_PACKAGES[@]}" qemu-utils libvirt-daemon-system libvirt-clients virtinst \ bridge-utils dnsmasq-base swtpm cloud-image-utils -step "Устанавливаем Cockpit и модули" -install_available "Cockpit установлен" \ - cockpit cockpit-machines cockpit-networkmanager cockpit-storaged cockpit-packagekit +step "Устанавливаем зависимости панели управления" +install_available "Пакеты панели установлены" \ + python3 python3-venv rsync openssl nftables novnc python3-websockify step "Создаём структуру Virtuality" run_logged "Директории Virtuality созданы" mkdir -p "$PROJECT_DIR" "$STORAGE_DIR" "$ISO_DIR" "$IMAGES_DIR" "$BACKUP_DIR" "$LOG_DIR" @@ -208,15 +206,13 @@ fi step "Включаем systemd-сервисы" run_logged "libvirtd включён и запущен" systemctl enable --now libvirtd run_logged "virtlogd включён и запущен" systemctl enable --now virtlogd -run_logged "cockpit.socket включён и запущен" systemctl enable --now cockpit.socket ok "libvirtd: $(service_state libvirtd) / $(service_enabled libvirtd)" ok "virtlogd: $(service_state virtlogd) / $(service_enabled virtlogd)" -ok "cockpit.socket: $(service_state cockpit.socket) / $(service_enabled cockpit.socket)" step "Настраиваем firewall" run_logged "Разрешён OpenSSH" ufw allow OpenSSH -run_logged "Разрешён Cockpit ${COCKPIT_PORT}/tcp" ufw allow "${COCKPIT_PORT}/tcp" -run_logged "Разрешены VNC-порты 5900:5999/tcp" ufw allow 5900:5999/tcp +# Consoles are proxied by the panel; VNC ports stay closed from outside. +ufw delete allow 5900:5999/tcp >> "$LOG_FILE" 2>&1 || true if ufw status | grep -qi inactive; then echo "y" | ufw enable >> "$LOG_FILE" 2>&1 && ok "UFW включён" || warn "Не удалось включить UFW" else @@ -243,7 +239,6 @@ ISO_DIR="${ISO_DIR}" IMAGES_DIR="${IMAGES_DIR}" BACKUP_DIR="${BACKUP_DIR}" LOG_DIR="${LOG_DIR}" -COCKPIT_PORT="${COCKPIT_PORT}" EOF ok "Создан ${PROJECT_DIR}/virtuality.env" @@ -254,11 +249,6 @@ if command -v virsh >/dev/null 2>&1; then else warn "virsh не найден после установки" fi -if ss -tulpn 2>/dev/null | grep -q ':9090'; then - ok "Cockpit слушает порт 9090" -else - warn "Порт 9090 не виден в ss; Cockpit может быть socket-activated" -fi if [[ -e /dev/kvm ]]; then ok "/dev/kvm доступен" else @@ -272,7 +262,6 @@ echo -e "${GREEN}${BOLD}╭───────────────── echo -e "${GREEN}${BOLD}│${RESET} ${BOLD}Установка Virtuality Node завершена${RESET} ${GREEN}${BOLD}│${RESET}" echo -e "${GREEN}${BOLD}╰────────────────────────────────────────────────────────────╯${RESET}" echo -echo -e "${BOLD}Cockpit:${RESET} https://${SERVER_IP}:${COCKPIT_PORT}" echo -e "${BOLD}Project:${RESET} ${PROJECT_DIR}" echo -e "${BOLD}ISO:${RESET} ${ISO_DIR}" echo -e "${BOLD}Images:${RESET} ${IMAGES_DIR}" @@ -283,9 +272,7 @@ echo -e "${BOLD}Проверка:${RESET}" echo " virsh list --all" echo " virsh pool-list --all" echo " systemctl status libvirtd --no-pager" -echo " systemctl status cockpit.socket --no-pager" echo line -echo -e "${DIM}Следующий шаг: sudo bash scripts/setup_bridge_br0.sh enp2s0 static${RESET}" -echo -e "${DIM}После bridge: sudo bash scripts/create_test_vm.sh${RESET}" +echo -e "${DIM}Дальше: sudo bash scripts/install_web_panel.sh — панель управления и мастер настройки${RESET}" line diff --git a/scripts/check_node.sh b/scripts/check_node.sh deleted file mode 100644 index aff3e3c..0000000 --- a/scripts/check_node.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -echo "==================================================" -echo " Virtuality Node Diagnostics" -echo "==================================================" - -echo - echo "[System]" -if [[ -f /etc/os-release ]]; then - . /etc/os-release - echo "OS: ${PRETTY_NAME}" -fi -uname -a - -echo - echo "[CPU virtualization]" -if grep -E -q '(vmx|svm)' /proc/cpuinfo; then - echo "OK: hardware virtualization flag found" -else - echo "WARNING: vmx/svm flag not found" -fi - -echo - echo "[Services]" -for service in libvirtd virtlogd cockpit.socket; do - if systemctl list-unit-files | grep -q "^${service}"; then - echo -n "${service}: " - systemctl is-active "$service" || true - else - echo "${service}: not installed" - fi -done - -echo - echo "[Packages]" -for bin in virsh qemu-system-x86_64 virt-install cockpit-bridge; do - if command -v "$bin" >/dev/null 2>&1; then - echo "OK: $bin -> $(command -v "$bin")" - else - echo "MISSING: $bin" - fi -done - -echo - echo "[libvirt VMs]" -if command -v virsh >/dev/null 2>&1; then - virsh list --all || true -else - echo "virsh not found" -fi - -echo - echo "[libvirt storage pools]" -if command -v virsh >/dev/null 2>&1; then - virsh pool-list --all || true -else - echo "virsh not found" -fi - -echo - echo "[Network interfaces]" -ip -br a || true - -echo - echo "[Routes]" -ip route || true - -echo - echo "[Virtuality paths]" -for dir in /opt/virtuality /var/lib/virtuality /var/lib/virtuality/iso /var/lib/virtuality/images /var/lib/virtuality/backups /var/log/virtuality; do - if [[ -d "$dir" ]]; then - echo "OK: $dir" - else - echo "MISSING: $dir" - fi -done - -echo - echo "[Firewall]" -if command -v ufw >/dev/null 2>&1; then - ufw status || true -else - echo "ufw not installed" -fi - -echo - echo "==================================================" -echo " Diagnostics completed" -echo "==================================================" diff --git a/scripts/clean_install.sh b/scripts/clean_install.sh deleted file mode 100644 index 8cadafe..0000000 --- a/scripts/clean_install.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Virtuality clean install helper. -# Default mode preserves user data: VM disks, ISO files, uploaded disk images and backups. -# Use --purge-data only when you intentionally want to remove Virtuality data directories too. - -PURGE_DATA=0 -YES=0 - -for arg in "$@"; do - case "$arg" in - --purge-data) PURGE_DATA=1 ;; - --yes|-y) YES=1 ;; - *) echo "Unknown argument: $arg" >&2; exit 2 ;; - esac -done - -if [[ "$EUID" -ne 0 ]]; then - exec sudo bash "$0" "$@" -fi - -RED="\033[31m" -GREEN="\033[32m" -YELLOW="\033[33m" -BOLD="\033[1m" -RESET="\033[0m" - -say() { echo -e "${GREEN}✓${RESET} $*"; } -warn() { echo -e "${YELLOW}!${RESET} $*"; } -remove_path() { - local path="$1" - if [[ -e "$path" || -L "$path" ]]; then - rm -rf "$path" - say "removed: $path" - else - warn "not found: $path" - fi -} - -cat </dev/null || true -systemctl disable virtuality-web.service 2>/dev/null || true - -remove_path /etc/systemd/system/virtuality-web.service -systemctl daemon-reload || true - -remove_path /opt/virtuality/web -remove_path /opt/virtuality/venv -remove_path /var/lib/virtuality/config -remove_path /var/lib/virtuality/update -remove_path /var/log/virtuality/update.log -find /var/log/virtuality -maxdepth 1 -type f -name 'install_web_panel_*.log' -delete 2>/dev/null || true -say "old installer logs removed" - -if [[ "$PURGE_DATA" -eq 1 ]]; then - remove_path /var/lib/virtuality/iso - remove_path /var/lib/virtuality/images - remove_path /var/lib/virtuality/disk-images - remove_path /var/lib/virtuality/backups - remove_path /var/lib/virtuality/network -fi - -mkdir -p /opt/virtuality /var/log/virtuality /var/lib/virtuality -say "base directories recreated" - -echo -say "Clean install cleanup finished. Run: sudo bash scripts/install_web_panel.sh" diff --git a/scripts/create_test_vm.sh b/scripts/create_test_vm.sh deleted file mode 100644 index 08c864b..0000000 --- a/scripts/create_test_vm.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -VM_NAME="${1:-test-alpine}" -ISO_URL="${VIRTUALITY_TEST_ISO_URL:-https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.3-x86_64.iso}" -ISO_PATH="/var/lib/virtuality/iso/alpine-standard.iso" -DISK_PATH="/var/lib/virtuality/images/${VM_NAME}.qcow2" -BRIDGE="${VIRTUALITY_BRIDGE:-br0}" -MEMORY="${VIRTUALITY_TEST_VM_MEMORY:-1024}" -VCPUS="${VIRTUALITY_TEST_VM_VCPUS:-1}" -DISK_SIZE="${VIRTUALITY_TEST_VM_DISK_SIZE:-8}" - -if [[ "$EUID" -ne 0 ]]; then - echo "Ошибка: запусти через sudo: sudo bash scripts/create_test_vm.sh" - exit 1 -fi - -if ! command -v virt-install >/dev/null 2>&1; then - echo "Ошибка: virt-install не найден. Запусти install_virtuality_node.sh" - exit 1 -fi - -if ! command -v virsh >/dev/null 2>&1; then - echo "Ошибка: virsh не найден. Запусти install_virtuality_node.sh" - exit 1 -fi - -if ! ip link show "$BRIDGE" >/dev/null 2>&1; then - echo "Ошибка: bridge $BRIDGE не найден. Сначала настрой br0." - ip -br a - exit 1 -fi - -mkdir -p /var/lib/virtuality/iso /var/lib/virtuality/images - -if virsh dominfo "$VM_NAME" >/dev/null 2>&1; then - echo "Виртуальная машина $VM_NAME уже существует." - virsh list --all - exit 0 -fi - -if [[ ! -f "$ISO_PATH" ]]; then - echo "Скачиваю тестовый ISO: $ISO_URL" - wget -O "$ISO_PATH" "$ISO_URL" -fi - -virsh pool-refresh virtuality-iso >/dev/null 2>&1 || true -virsh pool-refresh virtuality-images >/dev/null 2>&1 || true - -echo "Создаю тестовую VM: $VM_NAME" -virt-install \ - --name "$VM_NAME" \ - --memory "$MEMORY" \ - --vcpus "$VCPUS" \ - --disk "path=${DISK_PATH},size=${DISK_SIZE},format=qcow2,bus=virtio" \ - --cdrom "$ISO_PATH" \ - --os-variant generic \ - --network "bridge=${BRIDGE},model=virtio" \ - --graphics vnc,listen=0.0.0.0 \ - --noautoconsole - -echo - echo "VM создана." -virsh list --all - -echo - echo "VNC display:" -virsh vncdisplay "$VM_NAME" || true - -echo - echo "Открыть в Cockpit: https://SERVER_IP:9090 → Virtual machines → ${VM_NAME}" diff --git a/scripts/fix_cockpit_auth.sh b/scripts/fix_cockpit_auth.sh deleted file mode 100644 index 1a83378..0000000 --- a/scripts/fix_cockpit_auth.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Virtuality Cockpit auth helper -# Checks Linux user password state, Cockpit disallowed-users and required groups. - -USER_NAME="${1:-${VIRTUALITY_USER:-${SUDO_USER:-root}}}" -DISALLOWED_FILE="/etc/cockpit/disallowed-users" - -ESC="\033" -RESET="${ESC}[0m" -BOLD="${ESC}[1m" -GREEN="${ESC}[32m" -YELLOW="${ESC}[33m" -RED="${ESC}[31m" -CYAN="${ESC}[36m" -GRAY="${ESC}[90m" - -ok() { echo -e "${GREEN}✓${RESET} $*"; } -warn() { echo -e "${YELLOW}!${RESET} $*"; } -fail() { echo -e "${RED}✗${RESET} $*"; exit 1; } - -if [[ "$EUID" -ne 0 ]]; then - fail "Запусти от root: sudo bash scripts/fix_cockpit_auth.sh [user]" -fi - -clear 2>/dev/null || true -cat </dev/null 2>&1; then - fail "Пользователь ${USER_NAME} не найден" -fi -ok "Пользователь найден: ${USER_NAME}" - -HASH="$(getent shadow "$USER_NAME" | cut -d: -f2 || true)" -if [[ -z "$HASH" || "$HASH" == "!" || "$HASH" == "*" || "$HASH" == "!!" ]]; then - warn "У пользователя ${USER_NAME} пароль не задан или заблокирован" - echo - echo -e "${BOLD}Для входа в Cockpit задай пароль:${RESET}" - if [[ "$USER_NAME" == "root" ]]; then - echo " passwd root" - else - echo " sudo passwd ${USER_NAME}" - fi -else - ok "У пользователя ${USER_NAME} есть пароль" -fi - -if [[ -f "$DISALLOWED_FILE" ]]; then - if grep -qx "$USER_NAME" "$DISALLOWED_FILE"; then - cp "$DISALLOWED_FILE" "${DISALLOWED_FILE}.bak.$(date +%Y%m%d_%H%M%S)" - sed -i "/^${USER_NAME}$/d" "$DISALLOWED_FILE" - ok "Пользователь ${USER_NAME} удалён из ${DISALLOWED_FILE}" - else - ok "Пользователь ${USER_NAME} не запрещён в ${DISALLOWED_FILE}" - fi -else - warn "Файл ${DISALLOWED_FILE} не найден, пропускаю проверку запрета пользователей" -fi - -if [[ "$USER_NAME" != "root" ]]; then - usermod -aG sudo "$USER_NAME" || true - usermod -aG libvirt "$USER_NAME" || true - usermod -aG kvm "$USER_NAME" || true - ok "Пользователь ${USER_NAME} добавлен в группы sudo/libvirt/kvm" - warn "Для применения групп нужно выйти из SSH и войти снова" -else - ok "root не требует добавления в группы" -fi - -if command -v apt >/dev/null 2>&1; then - apt install -y cockpit cockpit-machines sscg >/dev/null 2>&1 || true - ok "Пакеты cockpit/cockpit-machines/sscg проверены" -fi - -systemctl enable --now cockpit.socket >/dev/null 2>&1 || fail "Не удалось запустить cockpit.socket" -ok "cockpit.socket включён и запущен" - -if command -v ufw >/dev/null 2>&1; then - ufw allow 9090/tcp >/dev/null 2>&1 || true - ok "UFW правило 9090/tcp добавлено/проверено" -fi - -systemctl restart cockpit.socket >/dev/null 2>&1 || true -systemctl restart cockpit.service >/dev/null 2>&1 || true -ok "Cockpit перезапущен" - -SERVER_IP="$(hostname -I | awk '{print $1}')" -PUBLIC_IP="$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") {print $(i+1); exit}}')" - -echo -echo -e "${GREEN}${BOLD}Готово.${RESET}" -echo -e "${BOLD}Cockpit local/private:${RESET} https://${SERVER_IP}:9090" -if [[ -n "$PUBLIC_IP" && "$PUBLIC_IP" != "$SERVER_IP" ]]; then - echo -e "${BOLD}Cockpit route IP:${RESET} https://${PUBLIC_IP}:9090" -fi -echo -e "${BOLD}Login:${RESET} ${USER_NAME}" -echo -e "${BOLD}Password:${RESET} пароль Linux-пользователя ${USER_NAME}" -echo diff --git a/scripts/install_console_dashboard.sh b/scripts/install_console_dashboard.sh deleted file mode 100644 index 8de090e..0000000 --- a/scripts/install_console_dashboard.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -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" -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" - exit 1 -fi - -if [[ ! -f "$DASH_SRC" ]]; then - echo "Ошибка: не найден файл $DASH_SRC" - exit 1 -fi - -apt update -apt install -y btop procps iproute2 util-linux coreutils - -install -m 0755 "$DASH_SRC" "$DASH_DST" - -cat > "$SERVICE_FILE" </dev/null 2>&1 || true -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 - BASHRC="$USER_HOME/.bashrc" - touch "$BASHRC" - grep -q "alias bt=" "$BASHRC" || cat >> "$BASHRC" <<'EOF' - -# Virtuality quick aliases -alias bt='btop' -alias dash='sudo systemctl restart virtuality-console-dashboard && sudo chvt 1' -alias dash-stop='sudo systemctl stop virtuality-console-dashboard' -alias dash-start='sudo systemctl start virtuality-console-dashboard' -alias dash-status='systemctl status virtuality-console-dashboard --no-pager' -alias vcheck='cd ~/virtuality && sudo bash scripts/check_node.sh' -EOF - chown "$TARGET_USER:$TARGET_USER" "$BASHRC" || true -fi - -echo "============================================================" -echo "Virtuality console dashboard installed" -echo "============================================================" -echo "Физический монитор: tty1" -echo "Перейти на дашборд: sudo chvt 1" -echo "Команда btop: bt" -echo "Перезапустить дашборд: dash" -echo "Остановить дашборд: dash-stop" -echo "Запустить дашборд: dash-start" -echo "Статус: dash-status" -echo "============================================================" diff --git a/scripts/install_web_panel.sh b/scripts/install_web_panel.sh index d4ea679..b9e1eb2 100644 --- a/scripts/install_web_panel.sh +++ b/scripts/install_web_panel.sh @@ -1,63 +1,65 @@ #!/usr/bin/env bash set -euo pipefail -# Virtuality Web Panel Installer -# Auto-detects x86_64 / Raspberry Pi ARM64 / Orange Pi 5 ARM64 / generic ARM64 profile. +# ========================================================== +# Virtuality web panel installer +# Copies web/ to /opt/virtuality/web (keeping the previous +# version for rollback), builds the virtualenv (offline from +# bundled wheels when available), installs virtuality-ctl and +# lets it create the systemd units, TLS and firewall rules. +# +# VIRTUALITY_PANEL_ONLY=1 skip apt entirely (first boot from the image, +# before the network is up) +# VIRTUALITY_AUTH_USER Linux user that logs in to the panel +# VIRTUALITY_WEB_PORT HTTP port (default 8088) +# ========================================================== REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WEB_DIR="${REPO_DIR}/web" APP_DIR="/opt/virtuality/web" VENV_DIR="/opt/virtuality/venv" -SERVICE_FILE="/etc/systemd/system/virtuality-web.service" -AUTO_UPDATE_SERVICE_FILE="/etc/systemd/system/virtuality-auto-update.service" -AUTO_UPDATE_TIMER_FILE="/etc/systemd/system/virtuality-auto-update.timer" -NETWORK_SERVICE_FILE="/etc/systemd/system/virtuality-network.service" +CTL_SRC="${REPO_DIR}/scripts/virtuality-ctl" +CTL_DST="/usr/local/bin/virtuality-ctl" LOG_DIR="/var/log/virtuality" LOG_FILE="${LOG_DIR}/install_web_panel_$(date +%Y%m%d_%H%M%S).log" -PROFILE_DIR="/var/lib/virtuality/config" -PROFILE_FILE="${PROFILE_DIR}/host_profile.json" -SESSION_SECRET_FILE="${PROFILE_DIR}/session_secret" -NODE_CONFIG_FILE="${PROFILE_DIR}/web.env" +CONFIG_DIR="/var/lib/virtuality/config" +PROFILE_FILE="${CONFIG_DIR}/host_profile.json" +SESSION_SECRET_FILE="${CONFIG_DIR}/session_secret" +NODE_CONFIG_FILE="${CONFIG_DIR}/web.env" +SETUP_STATE_FILE="${CONFIG_DIR}/setup.json" UPLOAD_TMP_DIR="/var/lib/virtuality/tmp" WHEELS_DIR="${VIRTUALITY_WHEELS_DIR:-${REPO_DIR}/wheels}" -TOTAL_STEPS=13 +PANEL_ONLY="${VIRTUALITY_PANEL_ONLY:-0}" export DEBIAN_FRONTEND=noninteractive -# Settings survive updates: explicit env > saved node config > previous install > defaults. +# Settings survive updates: explicit env > saved node config > defaults. saved_setting() { local key="$1" [[ -f "$NODE_CONFIG_FILE" ]] || return 0 sed -n "s/^${key}=//p" "$NODE_CONFIG_FILE" | tail -n1 | tr -d "\"'" } -legacy_auth_user() { - if [[ -f "${APP_DIR}/.env" ]]; then - sed -n 's/^VIRTUALITY_AUTH_USER=//p' "${APP_DIR}/.env" | tail -n1 - fi -} -legacy_port() { - if [[ -f "$SERVICE_FILE" ]]; then - sed -n 's/.*--port \([0-9][0-9]*\).*/\1/p' "$SERVICE_FILE" | tail -n1 - fi -} first_human_user() { getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 && $7 !~ /(nologin|false)$/ {print $1; exit}' } +env_set() { + local key="$1" value="$2" + if grep -q "^${key}=" "$NODE_CONFIG_FILE" 2>/dev/null; then + sed -i "s|^${key}=.*|${key}=${value}|" "$NODE_CONFIG_FILE" + else + printf '%s=%s\n' "$key" "$value" >> "$NODE_CONFIG_FILE" + fi +} PORT="${VIRTUALITY_WEB_PORT:-$(saved_setting VIRTUALITY_WEB_PORT)}" -PORT="${PORT:-$(legacy_port)}" PORT="${PORT:-8088}" AUTH_USER="${VIRTUALITY_AUTH_USER:-$(saved_setting VIRTUALITY_AUTH_USER)}" -AUTH_USER="${AUTH_USER:-$(legacy_auth_user)}" AUTH_USER="${AUTH_USER:-${SUDO_USER:-}}" AUTH_USER="${AUTH_USER:-$(first_human_user)}" AUTH_USER="${AUTH_USER:-root}" AUTO_UPDATE="${VIRTUALITY_AUTO_UPDATE:-$(saved_setting VIRTUALITY_AUTO_UPDATE)}" AUTO_UPDATE="${AUTO_UPDATE:-1}" -WEB_HOST="${VIRTUALITY_WEB_HOST:-$(saved_setting VIRTUALITY_WEB_HOST)}" -WEB_HOST="${WEB_HOST:-0.0.0.0}" -COOKIE_SECURE="${VIRTUALITY_COOKIE_SECURE:-$(saved_setting VIRTUALITY_COOKIE_SECURE)}" -COOKIE_SECURE="${COOKIE_SECURE:-0}" CURRENT_STEP=0 +TOTAL_STEPS=9 ESC="\033" RESET="${ESC}[0m" @@ -70,36 +72,22 @@ CYAN="${ESC}[36m" BLUE="${ESC}[34m" GRAY="${ESC}[90m" -mkdir -p "$LOG_DIR" "$PROFILE_DIR" "$UPLOAD_TMP_DIR" +mkdir -p "$LOG_DIR" "$CONFIG_DIR" "$UPLOAD_TMP_DIR" chmod 1777 "$UPLOAD_TMP_DIR" timestamp() { date '+%Y-%m-%d %H:%M:%S'; } line() { printf '%*s\n' 72 '' | tr ' ' '─'; } log() { echo "[$(timestamp)] $*" >> "$LOG_FILE"; } -print_header() { - clear 2>/dev/null || true - echo -e "${CYAN}${BOLD}╭────────────────────────────────────────────────────────────╮${RESET}" - echo -e "${CYAN}${BOLD}│${RESET} ${BOLD}Virtuality Web Panel Installer${RESET} ${CYAN}${BOLD}│${RESET}" - echo -e "${CYAN}${BOLD}│${RESET} Auto profile: x86 / VPS / Raspberry / Orange Pi 5 ${CYAN}${BOLD}│${RESET}" - echo -e "${CYAN}${BOLD}╰────────────────────────────────────────────────────────────╯${RESET}" - echo - echo -e "${GRAY}Лог установки:${RESET} ${LOG_FILE}" - echo -e "${GRAY}Порт панели:${RESET} ${PORT}" - echo -e "${GRAY}Linux auth user:${RESET} ${AUTH_USER}" - echo -} - step() { CURRENT_STEP=$((CURRENT_STEP + 1)) echo echo -e "${BLUE}${BOLD}[${CURRENT_STEP}/${TOTAL_STEPS}]${RESET} ${BOLD}$*${RESET}" log "STEP ${CURRENT_STEP}/${TOTAL_STEPS}: $*" } - 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}Установка остановлена.${RESET} Подробности в логе: ${LOG_FILE}"; exit 1; } +fail() { echo -e " ${RED}✗${RESET} $*"; log "ERROR: $*"; echo; echo -e "${RED}${BOLD}Установка остановлена.${RESET} Подробности в логе: ${LOG_FILE}"; tail -n 20 "$LOG_FILE" 2>/dev/null || true; exit 1; } run_logged() { local description="$1" @@ -112,282 +100,145 @@ run_logged() { fi } -service_state() { systemctl is-active "$1" 2>/dev/null || echo "inactive"; } -require_root() { [[ "$EUID" -eq 0 ]] || fail "Запусти через sudo: sudo bash scripts/install_web_panel.sh"; } json_value() { python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2], ""))' "$PROFILE_FILE" "$1" 2>/dev/null || true; } -print_header +echo -e "${CYAN}${BOLD}Virtuality — установка панели управления${RESET}" +echo -e "${GRAY}Лог: ${LOG_FILE}${RESET}" -step "Проверяем права и структуру проекта" -require_root -ok "Права root подтверждены" -[[ -d "$WEB_DIR" ]] || fail "Не найдена директория web: $WEB_DIR" -ok "Исходники web найдены: $WEB_DIR" -[[ -f "$WEB_DIR/app.py" ]] || fail "Не найден web/app.py" -ok "FastAPI entrypoint найден: web/app.py" -[[ -f "$WEB_DIR/requirements.txt" ]] || fail "Не найден web/requirements.txt" -ok "requirements.txt найден" +step "Проверяем права и исходники" +[[ "$EUID" -eq 0 ]] || fail "Запустите через sudo: sudo bash scripts/install_web_panel.sh" +[[ -f "$WEB_DIR/app.py" && -f "$WEB_DIR/requirements.txt" ]] || fail "Не найдены исходники панели в ${WEB_DIR}" +[[ -f "$CTL_SRC" ]] || fail "Не найден ${CTL_SRC}" +for cmd in systemctl rsync python3 hostname openssl; do + command -v "$cmd" >/dev/null 2>&1 || fail "Не найдена команда ${cmd}" +done +ok "Исходники: ${WEB_DIR}" +if id "$AUTH_USER" >/dev/null 2>&1; then + ok "Пользователь для входа: ${AUTH_USER}" +else + fail "Пользователь ${AUTH_USER} не найден. Создайте его или передайте VIRTUALITY_AUTH_USER=имя" +fi +if getent shadow "$AUTH_USER" | cut -d: -f2 | grep -Eq '^(!|\*|!!)?$'; then + warn "У пользователя ${AUTH_USER} нет пароля — задайте его: sudo passwd ${AUTH_USER}" +fi -step "Автоматически определяем профиль сборки" +step "Определяем профиль сервера" if [[ -x "${REPO_DIR}/scripts/detect_host_profile.sh" ]]; then - PROFILE="$(${REPO_DIR}/scripts/detect_host_profile.sh 2>>"$LOG_FILE" | tail -n1)" + PROFILE="$("${REPO_DIR}/scripts/detect_host_profile.sh" 2>>"$LOG_FILE" | tail -n1 || true)" else bash "${REPO_DIR}/scripts/detect_host_profile.sh" >>"$LOG_FILE" 2>&1 || true PROFILE="$(json_value profile)" fi -[[ -n "$PROFILE" ]] || PROFILE="x86_64" -ARCH="$(json_value arch)" -MODEL="$(json_value model)" -LABEL="$(json_value label)" -PACKAGE_PROFILE="$(json_value package_profile)" -ok "Профиль: ${LABEL:-$PROFILE}" -ok "Архитектура: ${ARCH:-unknown}" -ok "Модель: ${MODEL:-unknown}" -ok "Конфиг профиля: $PROFILE_FILE" +ok "Профиль: $(json_value label)${PROFILE:+ (${PROFILE})}" -step "Проверяем Linux-пользователя для входа" -if id "$AUTH_USER" >/dev/null 2>&1; then - ok "Пользователь найден: $AUTH_USER" +step "Системные пакеты" +if [[ "$PANEL_ONLY" == "1" ]]; then + ok "Пропущено (VIRTUALITY_PANEL_ONLY=1): пакеты поставит установщик ноды" +elif command -v apt-get >/dev/null 2>&1; then + run_logged "apt update выполнен" apt-get update + run_logged "Пакеты панели установлены" apt-get install -y python3 python3-venv rsync openssl curl nftables novnc python3-websockify qemu-utils virtinst libvirt-clients cloud-image-utils else - fail "Пользователь $AUTH_USER не найден. Создай его или передай VIRTUALITY_AUTH_USER=username" -fi -if getent shadow "$AUTH_USER" | cut -d: -f2 | grep -Eq '^(!|\*|!!)?$'; then - warn "У пользователя $AUTH_USER может быть заблокирован пароль. Установи пароль: sudo passwd $AUTH_USER" -else - ok "У пользователя $AUTH_USER есть пароль для Linux-auth" + warn "apt не найден — пакеты не устанавливались" fi -step "Проверяем базовые команды" -for cmd in apt systemctl rsync python3 hostname openssl; do - if command -v "$cmd" >/dev/null 2>&1; then - ok "$cmd найден: $(command -v "$cmd")" - else - fail "$cmd не найден" - fi -done +step "Копируем панель в ${APP_DIR}" +run_logged "Новая версия скопирована" rsync -a --delete --exclude='__pycache__/' --exclude='.env' "$WEB_DIR/" "${APP_DIR}.new/" +install -m 0644 "${REPO_DIR}/VERSION" "${APP_DIR}.new/VERSION" +if [[ -d "$APP_DIR" ]]; then + rm -rf "${APP_DIR}.prev" + mv "$APP_DIR" "${APP_DIR}.prev" + ok "Предыдущая версия сохранена в ${APP_DIR}.prev (virtuality-ctl rollback)" +fi +mv "${APP_DIR}.new" "$APP_DIR" +ok "Версия панели: $(cat "${APP_DIR}/VERSION")" -step "Обновляем apt cache" -run_logged "apt update выполнен" apt update - -step "Устанавливаем системные зависимости под выбранную сборку" -COMMON_PACKAGES=(python3 python3-venv python3-pip rsync openssl curl wget unzip nftables novnc python3-websockify) -X86_PACKAGES=(qemu-system-x86 qemu-system-arm qemu-efi-aarch64 virtinst libvirt-daemon-system libvirt-clients bridge-utils cloud-image-utils) -ARM_PACKAGES=(qemu-system-arm qemu-efi-aarch64 virtinst libvirt-daemon-system libvirt-clients bridge-utils cloud-image-utils) -PACKAGES=("${COMMON_PACKAGES[@]}") -case "$PACKAGE_PROFILE" in - raspberry|orangepi5|arm64) - PACKAGES+=("${ARM_PACKAGES[@]}") - ;; - *) - PACKAGES+=("${X86_PACKAGES[@]}") - ;; -esac -run_logged "Системные пакеты установлены для профиля ${PACKAGE_PROFILE:-x86}" apt install -y "${PACKAGES[@]}" -if [[ "${PACKAGE_PROFILE:-x86}" != "raspberry" && "${PACKAGE_PROFILE:-x86}" != "orangepi5" && "${PACKAGE_PROFILE:-x86}" != "arm64" ]]; then - if command -v qemu-system-aarch64 >/dev/null 2>&1; then - ok "ARM64 QEMU эмулятор найден: $(command -v qemu-system-aarch64)" +step "Python-окружение" +if [[ -x "$VENV_DIR/bin/python" ]] && "$VENV_DIR/bin/python" -c 'import sys' >/dev/null 2>&1; then + ok "Virtualenv уже есть: ${VENV_DIR}" +else + [[ -d "$VENV_DIR" ]] && { warn "Virtualenv повреждён (например, после обновления Python) — пересоздаём"; rm -rf -- "${VENV_DIR:?}"; } + if python3 -c 'import ensurepip' >/dev/null 2>&1; then + run_logged "Virtualenv создан" python3 -m venv "$VENV_DIR" else - fail "Не найден qemu-system-aarch64. Установи пакет: sudo apt install -y qemu-system-arm qemu-efi-aarch64" + # Ubuntu Server without python3-venv: pip is bootstrapped from the bundled wheel. + run_logged "Virtualenv создан (без pip)" python3 -m venv --without-pip "$VENV_DIR" fi fi - -step "Копируем web-панель в /opt/virtuality" -run_logged "Создана директория /opt/virtuality" mkdir -p /opt/virtuality -run_logged "Файлы панели синхронизированы в $APP_DIR" rsync -a --delete --exclude='__pycache__/' --exclude='.env' "$WEB_DIR/" "$APP_DIR/" -run_logged "Версия панели записана" install -m 0644 "${REPO_DIR}/VERSION" "${APP_DIR}/VERSION" -run_logged "Конфиг профиля доступен web-панели" mkdir -p "$PROFILE_DIR" -if [[ -f "$PROFILE_FILE" ]]; then - ok "Профиль уже сохранён: $PROFILE_FILE" -else - warn "Профиль не найден после детекта, будет создан web-панелью при старте" +if ! "$VENV_DIR/bin/python" -m pip --version >/dev/null 2>&1; then + PIP_WHEEL="$(ls "${WHEELS_DIR}"/pip-*.whl 2>/dev/null | head -n1 || true)" + [[ -n "$PIP_WHEEL" ]] || fail "В virtualenv нет pip и нет wheel pip в ${WHEELS_DIR}. Установите python3-venv: sudo apt install python3-venv" + run_logged "pip установлен из ${PIP_WHEEL##*/}" "$VENV_DIR/bin/python" "${PIP_WHEEL}/pip" install --no-index --find-links "$WHEELS_DIR" pip fi +if compgen -G "${WHEELS_DIR}/*.whl" >/dev/null; then + run_logged "Зависимости установлены офлайн из ${WHEELS_DIR}" "$VENV_DIR/bin/python" -m pip install --no-index --find-links "$WHEELS_DIR" -r "$APP_DIR/requirements.txt" +else + run_logged "Зависимости установлены из PyPI" "$VENV_DIR/bin/python" -m pip install --disable-pip-version-check -r "$APP_DIR/requirements.txt" +fi +run_logged "Панель импортируется без ошибок" bash -c "cd '$APP_DIR' && '$VENV_DIR/bin/python' -c 'import app'" -step "Создаём конфиг авторизации" +step "Настройки ноды" +touch "$NODE_CONFIG_FILE" +grep -q '^#' "$NODE_CONFIG_FILE" || sed -i '1i # Настройки ноды Virtuality. Менять: sudo virtuality-ctl set KEY=VALUE' "$NODE_CONFIG_FILE" +env_set VIRTUALITY_WEB_PORT "$PORT" +env_set VIRTUALITY_AUTH_USER "$AUTH_USER" +env_set VIRTUALITY_AUTO_UPDATE "$AUTO_UPDATE" +env_set VIRTUALITY_SOURCE_DIR "$REPO_DIR" +[[ -n "$(saved_setting VIRTUALITY_WEB_HOST)" ]] || env_set VIRTUALITY_WEB_HOST 0.0.0.0 +[[ -n "$(saved_setting VIRTUALITY_TLS)" ]] || env_set VIRTUALITY_TLS 0 +[[ -n "$(saved_setting VIRTUALITY_UPDATE_CHANNEL)" ]] || env_set VIRTUALITY_UPDATE_CHANNEL stable +chmod 644 "$NODE_CONFIG_FILE" +ok "Сохранено в ${NODE_CONFIG_FILE}" if [[ -s "$SESSION_SECRET_FILE" ]]; then SESSION_SECRET="$(cat "$SESSION_SECRET_FILE")" - ok "Используем существующий session secret: $SESSION_SECRET_FILE" else SESSION_SECRET="$(openssl rand -hex 32)" printf '%s\n' "$SESSION_SECRET" > "$SESSION_SECRET_FILE" chmod 600 "$SESSION_SECRET_FILE" - ok "Создан постоянный session secret: $SESSION_SECRET_FILE" + ok "Создан ключ сессий: ${SESSION_SECRET_FILE}" fi -cat > "$NODE_CONFIG_FILE" < "${APP_DIR}/.env" </dev/null 2>&1; then - warn "Virtualenv уже существует, будет переиспользован: $VENV_DIR" -else - if [[ -d "$VENV_DIR" ]]; then - warn "Virtualenv повреждён (например, после обновления Python), пересоздаём" - rm -rf -- "${VENV_DIR:?}" - fi - run_logged "Virtualenv создан: $VENV_DIR" python3 -m venv "$VENV_DIR" -fi +step "Устанавливаем virtuality-ctl" +install -m 0755 "$CTL_SRC" "$CTL_DST" +ok "${CTL_DST}" -step "Устанавливаем Python-зависимости" -if compgen -G "${WHEELS_DIR}/*.whl" >/dev/null; then - run_logged "Python-зависимости установлены офлайн из ${WHEELS_DIR}" "$VENV_DIR/bin/pip" install --no-index --find-links "$WHEELS_DIR" -r "$APP_DIR/requirements.txt" -else - run_logged "pip обновлён" "$VENV_DIR/bin/pip" install --upgrade pip - run_logged "Python-зависимости установлены" "$VENV_DIR/bin/pip" install -r "$APP_DIR/requirements.txt" -fi -run_logged "Web-панель импортируется без ошибок" bash -c "cd '$APP_DIR' && '$VENV_DIR/bin/python' -c 'import app'" - -step "Создаём systemd service" -cat > "$SERVICE_FILE" < "$NETWORK_SERVICE_FILE" < "$AUTO_UPDATE_SERVICE_FILE" < "$AUTO_UPDATE_TIMER_FILE" <> "$LOG_FILE" 2>&1 || true - ok "Автообновление отключено (VIRTUALITY_AUTO_UPDATE=0). Обновления — вручную через /update" - fi -else - warn "auto_update_check.sh не найден, автообновление пропущено" -fi - -step "Запускаем Virtuality Web Panel" -run_logged "virtuality-web.service включён и запущен" systemctl enable --now virtuality-web.service +step "Службы, firewall и запуск" +run_logged "Юниты созданы, панель запущена" env VIRTUALITY_SOURCE_DIR="$REPO_DIR" "$CTL_DST" reconfigure sleep 2 -STATE="$(service_state virtuality-web.service)" -if [[ "$STATE" == "active" ]]; then - ok "virtuality-web.service active" +if systemctl is-active --quiet virtuality-web.service; then + ok "virtuality-web.service работает" else - warn "virtuality-web.service состояние: $STATE" journalctl -u virtuality-web.service -n 40 --no-pager >> "$LOG_FILE" 2>&1 || true + fail "virtuality-web.service не запустился" fi -step "Настраиваем firewall и проверяем порт" -if command -v ufw >/dev/null 2>&1; then - ufw allow "${PORT}/tcp" >> "$LOG_FILE" 2>&1 || warn "Не удалось добавить UFW правило для ${PORT}/tcp" - ok "UFW правило добавлено/проверено: ${PORT}/tcp" +step "Мастер настройки" +if [[ -f "$SETUP_STATE_FILE" ]]; then + ok "Состояние установки уже ведётся" +elif [[ "${VIRTUALITY_FIRSTBOOT:-0}" == "1" ]]; then + ok "Прогресс пишет первая загрузка" else - warn "ufw не установлен, firewall-правило пропущено" -fi -if command -v ss >/dev/null 2>&1 && ss -tulpn | grep -q ":${PORT}"; then - ok "Порт ${PORT} слушается" -else - warn "Порт ${PORT} пока не найден в ss; проверь service logs" + # A node installed by hand gets the same first-run wizard as one installed from the image. + printf '{"stage": "done", "message": "", "steps": []}\n' > "$SETUP_STATE_FILE" + ok "Мастер настройки откроется при первом входе в панель" fi -SERVER_IP="$(hostname -I | awk '{print $1}')" - -echo -echo -e "${GREEN}${BOLD}╭────────────────────────────────────────────────────────────╮${RESET}" -echo -e "${GREEN}${BOLD}│${RESET} ${BOLD}Установка Virtuality Web Panel завершена${RESET} ${GREEN}${BOLD}│${RESET}" -echo -e "${GREEN}${BOLD}╰────────────────────────────────────────────────────────────╯${RESET}" -echo -echo -e "${BOLD}URL:${RESET} http://${SERVER_IP}:${PORT}" -echo -e "${BOLD}Login:${RESET} ${AUTH_USER} / пароль Linux-пользователя" -echo -e "${BOLD}Profile:${RESET} ${LABEL:-$PROFILE}" -echo -e "${BOLD}Arch:${RESET} ${ARCH:-unknown}" -echo -e "${BOLD}Service:${RESET} virtuality-web.service" -echo -e "${BOLD}Auto update:${RESET} $([[ "$AUTO_UPDATE" == "1" ]] && echo "ежедневно в 00:00 по Москве" || echo "отключено")" -echo -e "${BOLD}Settings:${RESET} ${NODE_CONFIG_FILE}" -echo -e "${BOLD}Upload tmp:${RESET} ${UPLOAD_TMP_DIR}" -echo -e "${BOLD}Session key:${RESET} ${SESSION_SECRET_FILE}" -echo -e "${BOLD}Status:${RESET} systemctl status virtuality-web --no-pager" -echo -e "${BOLD}Logs:${RESET} journalctl -u virtuality-web -f" -echo -e "${BOLD}Install log:${RESET} ${LOG_FILE}" +PANEL_URL="$("$CTL_DST" url 2>/dev/null || echo "http://$(hostname -I | awk '{print $1}'):${PORT}")" echo +echo -e "${GREEN}${BOLD}Панель управления установлена${RESET}" line -echo -e "${DIM}Следующий шаг: /host, /network, /logs, /update, /vm/create, /vm/NAME/console.${RESET}" +echo -e "${BOLD}Адрес:${RESET} ${PANEL_URL}" +echo -e "${BOLD}Логин:${RESET} ${AUTH_USER} (пароль пользователя Linux)" +echo -e "${BOLD}Служба:${RESET} systemctl status virtuality-web --no-pager" +echo -e "${BOLD}Журнал:${RESET} journalctl -u virtuality-web -f" +echo -e "${BOLD}Настройки:${RESET} sudo virtuality-ctl status" line +echo -e "${DIM}Автообновление: $([[ "$AUTO_UPDATE" == "1" ]] && echo "включено (ночью)" || echo "отключено")${RESET}" diff --git a/scripts/setup_bridge_br0.sh b/scripts/setup_bridge_br0.sh deleted file mode 100644 index df4fa05..0000000 --- a/scripts/setup_bridge_br0.sh +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -IFACE="${1:-enp2s0}" -BRIDGE="br0" -NETPLAN_DIR="/etc/netplan" -BACKUP_DIR="/root/virtuality-netplan-backups/$(date +%Y%m%d_%H%M%S)" -BRIDGE_FILE="${NETPLAN_DIR}/60-virtuality-br0.yaml" -ROLLBACK_SCRIPT="/root/virtuality-rollback-netplan.sh" -MODE="${2:-dhcp}" - -if [[ "$EUID" -ne 0 ]]; then - echo "Ошибка: запусти через sudo: sudo bash scripts/setup_bridge_br0.sh enp2s0" - exit 1 -fi - -if ! command -v netplan >/dev/null 2>&1; then - echo "Ошибка: netplan не найден." - exit 1 -fi - -if ! ip link show "$IFACE" >/dev/null 2>&1; then - echo "Ошибка: интерфейс $IFACE не найден." - ip -br a - exit 1 -fi - -CURRENT_IP="$(ip -4 addr show "$IFACE" | awk '/inet / {print $2; exit}')" -CURRENT_GW="$(ip route | awk '/default/ {print $3; exit}')" - -# Use stable IPv4 DNS by default. Link-local IPv6 DNS can break YAML/routes without scope id. -DNS_1="${VIRTUALITY_DNS_1:-10.0.0.1}" -DNS_2="${VIRTUALITY_DNS_2:-1.1.1.1}" - -echo "============================================================" -echo "Virtuality safe bridge setup" -echo "============================================================" -echo "Interface: $IFACE" -echo "Bridge: $BRIDGE" -echo "Mode: $MODE" -echo "Current IP: ${CURRENT_IP:-unknown}" -echo "Current gateway: ${CURRENT_GW:-unknown}" -echo "DNS: ${DNS_1}, ${DNS_2}" -echo "Backup: $BACKUP_DIR" -echo - -echo "ВАЖНО: скрипт использует netplan try." -echo "Если сеть пропадёт, netplan должен автоматически откатить изменения." -echo "Если видишь подтверждение — нажми Enter, только если SSH/сеть не отвалилась." -echo - -mkdir -p "$BACKUP_DIR" -cp -a "$NETPLAN_DIR"/*.yaml "$BACKUP_DIR"/ 2>/dev/null || true - -cat > "$ROLLBACK_SCRIPT" </dev/null || true -netplan generate -netplan apply -echo "Netplan rollback applied from $BACKUP_DIR" -EOF -chmod +x "$ROLLBACK_SCRIPT" - -# Keep existing netplan files neutral for this interface to avoid duplicate DHCP/IP. -for file in "$NETPLAN_DIR"/*.yaml; do - [[ "$file" == "$BRIDGE_FILE" ]] && continue - if grep -q "${IFACE}:" "$file"; then - cp -a "$file" "${file}.virtuality-before-br0" - cat > "$file" < "$BRIDGE_FILE" < "$BRIDGE_FILE" </dev/null || true - -echo "[1/5] Generated config:" -cat "$BRIDGE_FILE" - -echo - echo "[2/5] Validating netplan..." -netplan generate - -echo - echo "[3/5] Testing config with netplan try..." -echo "Если после применения сеть работает — подтверди netplan try клавишей Enter." -echo "Если SSH зависнет — жди автооткат или используй физический монитор: sudo $ROLLBACK_SCRIPT" -netplan try --timeout 30 - -echo - echo "[4/5] Applying final netplan..." -netplan apply -sleep 3 - -echo - echo "[5/5] Result:" -ip -br a -ip route - -echo - echo "============================================================" -echo "Bridge setup completed" -echo "============================================================" -echo "Rollback command: sudo $ROLLBACK_SCRIPT" -echo "If DHCP mode failed, try static mode:" -echo "sudo bash scripts/setup_bridge_br0.sh ${IFACE} static" -echo "============================================================" diff --git a/scripts/virtuality-ctl b/scripts/virtuality-ctl index cfeac79..1992f1d 100755 --- a/scripts/virtuality-ctl +++ b/scripts/virtuality-ctl @@ -11,7 +11,6 @@ DATA_DIR="/var/lib/virtuality" LOG_DIR="/var/log/virtuality" APP_DIR="/opt/virtuality/web" VENV_DIR="/opt/virtuality/venv" -SOURCE_DIR="${VIRTUALITY_SOURCE_DIR:-/opt/virtuality/source}" UNIT_DIR="/etc/systemd/system" NETPLAN_DIR="/etc/netplan" NETPLAN_BACKUP="${CONFIG_DIR}/netplan-backup" @@ -23,6 +22,7 @@ usage() { Usage: virtuality-ctl [options] status Что установлено, порты, адреса панели + url Адрес панели управления reconfigure Пересоздать systemd-юниты из ${NODE_ENV} и перезапустить панель set KEY=VALUE ... Изменить настройку в web.env (например set VIRTUALITY_TLS=1) и применить passwd [USER] Сменить пароль пользователя панели (пароль читается со стандартного ввода) @@ -69,6 +69,8 @@ env_set() { } primary_ip() { hostname -I 2>/dev/null | awk '{print $1}'; } +# Source checkout used by updates; the installer records it in web.env. +SOURCE_DIR="${VIRTUALITY_SOURCE_DIR:-$(env_get VIRTUALITY_SOURCE_DIR /opt/virtuality/source)}" panel_url() { local tls port @@ -682,6 +684,7 @@ cmd_uninstall() { case "${1:-}" in status) cmd_status ;; + url) panel_url; echo ;; reconfigure) cmd_reconfigure ;; set) shift; cmd_set "$@" ;; passwd) shift; cmd_passwd "$@" ;; diff --git a/scripts/virtuality_console_dashboard.sh b/scripts/virtuality_console_dashboard.sh deleted file mode 100644 index 65cc6df..0000000 --- a/scripts/virtuality_console_dashboard.sh +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Virtuality Console Dashboard -# btop-like compact TUI for physical server console tty1 - -refresh_interval="${VIRTUALITY_DASHBOARD_INTERVAL:-2}" - -has_cmd() { command -v "$1" >/dev/null 2>&1; } - -ESC="\033" -RESET="${ESC}[0m" -BOLD="${ESC}[1m" -DIM="${ESC}[2m" -FG_CYAN="${ESC}[38;5;51m" -FG_BLUE="${ESC}[38;5;39m" -FG_GREEN="${ESC}[38;5;46m" -FG_YELLOW="${ESC}[38;5;226m" -FG_RED="${ESC}[38;5;196m" -FG_MAGENTA="${ESC}[38;5;201m" -FG_GRAY="${ESC}[38;5;245m" -FG_WHITE="${ESC}[38;5;255m" -BG_PANEL="${ESC}[48;5;235m" -BG_HEADER="${ESC}[48;5;24m" - -move() { printf "${ESC}[%s;%sH" "$1" "$2"; } -clear_screen() { printf "${ESC}[2J${ESC}[H"; } -hide_cursor() { printf "${ESC}[?25l"; } -show_cursor() { printf "${ESC}[?25h${RESET}"; } -trap show_cursor EXIT - -term_cols() { tput cols 2>/dev/null || echo 120; } -term_rows() { tput lines 2>/dev/null || echo 40; } - -repeat_char() { - local char="$1" count="$2" - printf '%*s' "$count" '' | tr ' ' "$char" -} - -strip_ansi() { - sed -E 's/\x1B\[[0-9;]*[A-Za-z]//g' -} - -print_fit() { - local text="$1" width="$2" - printf "%-${width}.${width}s" "$text" -} - -box() { - local row="$1" col="$2" height="$3" width="$4" title="$5" - local inner=$((width - 2)) - move "$row" "$col"; printf "${FG_BLUE}╭%s╮${RESET}" "$(repeat_char '─' "$inner")" - move "$row" $((col + 2)); printf "${BG_PANEL}${FG_CYAN}${BOLD} %s ${RESET}" "$title" - for ((i=1; i= 85 )); then color="$FG_RED"; elif (( percent >= 65 )); then color="$FG_YELLOW"; fi - printf "%b%s%b%s" "$color" "$(repeat_char '█' "$filled")" "$DIM" "$(repeat_char '░' "$empty")" - printf "%b" "$RESET" -} - -service_state() { - local unit="$1" - if systemctl list-unit-files "$unit" >/dev/null 2>&1; then - systemctl is-active "$unit" 2>/dev/null || echo "inactive" - else - echo "missing" - fi -} - -state_badge() { - local state="$1" - case "$state" in - active) printf "${FG_GREEN}● active${RESET}" ;; - inactive) printf "${FG_YELLOW}● inactive${RESET}" ;; - failed) printf "${FG_RED}● failed${RESET}" ;; - missing) printf "${FG_GRAY}● missing${RESET}" ;; - *) printf "${FG_YELLOW}● %s${RESET}" "$state" ;; - esac -} - -get_cpu_usage() { - local a b idle_a total_a idle_b total_b diff_idle diff_total usage - read -r _ a b c idle_a rest < /proc/stat - total_a=$((a+b+c+idle_a)) - sleep 0.15 - read -r _ a b c idle_b rest < /proc/stat - total_b=$((a+b+c+idle_b)) - diff_idle=$((idle_b-idle_a)) - diff_total=$((total_b-total_a)) - if (( diff_total <= 0 )); then echo 0; else echo $(( (100 * (diff_total - diff_idle)) / diff_total )); fi -} - -get_mem_percent() { - free | awk '/Mem:/ {printf "%d", ($3/$2)*100}' -} - -get_disk_percent() { - df / | awk 'NR==2 {gsub("%", "", $5); print $5}' -} - -while true; do - cols="$(term_cols)" - rows="$(term_rows)" - [[ "$cols" -lt 100 ]] && cols=100 - [[ "$rows" -lt 32 ]] && rows=32 - - cpu="$(get_cpu_usage)" - mem="$(get_mem_percent)" - disk="$(get_disk_percent)" - load="$(awk '{print $1" "$2" "$3}' /proc/loadavg)" - uptime_text="$(uptime -p 2>/dev/null | sed 's/up //' || true)" - host="$(hostname)" - now="$(date '+%Y-%m-%d %H:%M:%S')" - ip_main="$(hostname -I 2>/dev/null | awk '{print $1}')" - gateway="$(ip route | awk '/default/ {print $3" via "$5; exit}')" - - clear_screen - hide_cursor - - move 1 1 - printf "${BG_HEADER}${FG_WHITE}${BOLD} %-20s ${FG_CYAN}%-30s ${FG_GRAY}%s ${RESET}" "VIRTUALITY" "$host" "$now" - printf "%s" "$(repeat_char ' ' $((cols-70 > 0 ? cols-70 : 1)))" - - # Layout - left_w=48 - right_w=$((cols - left_w - 5)) - [[ "$right_w" -lt 48 ]] && right_w=48 - - box 3 2 10 "$left_w" "SYSTEM" - box 3 $((left_w + 4)) 10 "$right_w" "SERVICES" - box 14 2 8 "$left_w" "NETWORK" - box 14 $((left_w + 4)) 8 "$right_w" "VIRTUAL MACHINES" - box 23 2 8 "$left_w" "STORAGE" - box 23 $((left_w + 4)) 8 "$right_w" "LIBVIRT POOLS" - - # System panel - put 5 4 $((left_w-4)) "${FG_GRAY}CPU ${FG_WHITE}${cpu}% $(bar "$cpu" 26)" - put 6 4 $((left_w-4)) "${FG_GRAY}RAM ${FG_WHITE}${mem}% $(bar "$mem" 26)" - put 7 4 $((left_w-4)) "${FG_GRAY}DSK ${FG_WHITE}${disk}% $(bar "$disk" 26)" - put 8 4 $((left_w-4)) "${FG_GRAY}Load:${RESET} ${FG_WHITE}${load}${RESET}" - put 9 4 $((left_w-4)) "${FG_GRAY}Uptime:${RESET} ${FG_WHITE}${uptime_text}${RESET}" - put 10 4 $((left_w-4)) "${FG_GRAY}Kernel:${RESET} ${FG_WHITE}$(uname -r)${RESET}" - - # Services panel - svc_col=$((left_w + 6)) - svc_w=$((right_w - 4)) - put 5 "$svc_col" "$svc_w" "libvirtd.service $(state_badge "$(service_state libvirtd.service)")" - put 6 "$svc_col" "$svc_w" "virtlogd.service $(state_badge "$(service_state virtlogd.service)")" - put 7 "$svc_col" "$svc_w" "cockpit.socket $(state_badge "$(service_state cockpit.socket)")" - put 8 "$svc_col" "$svc_w" "libvirt-guests.service $(state_badge "$(service_state libvirt-guests.service)")" - put 10 "$svc_col" "$svc_w" "${FG_GRAY}Cockpit:${RESET} ${FG_WHITE}https://${ip_main}:9090${RESET}" - - # Network panel - put 16 4 $((left_w-4)) "${FG_GRAY}Main IP:${RESET} ${FG_GREEN}${ip_main}${RESET}" - put 17 4 $((left_w-4)) "${FG_GRAY}Gateway:${RESET} ${FG_WHITE}${gateway}${RESET}" - net_lines="$(ip -br a 2>/dev/null | head -n 3)" - n=0 - while IFS= read -r line; do - put $((18+n)) 4 $((left_w-4)) "${FG_WHITE}${line}${RESET}" - n=$((n+1)) - done <<< "$net_lines" - - # VM panel - vm_col=$((left_w + 6)) - vm_lines="$(virsh list --all 2>/dev/null | tail -n +3 | sed '/^$/d' | head -n 4 || true)" - if [[ -z "$vm_lines" ]]; then - put 16 "$vm_col" "$svc_w" "${FG_GRAY}VM пока нет. Следующий этап — создать тестовую VM.${RESET}" - else - n=0 - while IFS= read -r line; do - put $((16+n)) "$vm_col" "$svc_w" "${FG_WHITE}${line}${RESET}" - n=$((n+1)) - done <<< "$vm_lines" - fi - - # Storage panel - srow=25 - for dir in /var/lib/virtuality/iso /var/lib/virtuality/images /var/lib/virtuality/backups /var/log/virtuality; do - if [[ -d "$dir" ]]; then - used="$(du -sh "$dir" 2>/dev/null | awk '{print $1}')" - put "$srow" 4 $((left_w-4)) "${FG_GRAY}${dir}:${RESET} ${FG_WHITE}${used}${RESET}" - else - put "$srow" 4 $((left_w-4)) "${FG_GRAY}${dir}:${RESET} ${FG_RED}missing${RESET}" - fi - srow=$((srow+1)) - done - - # Pools panel - pool_lines="$(virsh pool-list --all 2>/dev/null | tail -n +3 | sed '/^$/d' | head -n 4 || true)" - if [[ -z "$pool_lines" ]]; then - put 25 "$vm_col" "$svc_w" "${FG_GRAY}Storage pools unavailable${RESET}" - else - n=0 - while IFS= read -r line; do - put $((25+n)) "$vm_col" "$svc_w" "${FG_WHITE}${line}${RESET}" - n=$((n+1)) - done <<< "$pool_lines" - fi - - move $((rows-2)) 2 - printf "${FG_GRAY}Refresh:${RESET} ${FG_WHITE}${refresh_interval}s${RESET} ${FG_GRAY}|${RESET} ${FG_CYAN}Ctrl+Alt+F2${RESET} login ${FG_GRAY}|${RESET} ${FG_GREEN}bt${RESET}=btop ${FG_GRAY}|${RESET} stop: ${FG_YELLOW}sudo systemctl stop virtuality-console-dashboard${RESET}" - move $((rows-1)) 1 - printf "${RESET}" - sleep "$refresh_interval" -done diff --git a/scripts/virtuality_firstboot.sh b/scripts/virtuality_firstboot.sh index 2986219..47988ae 100755 --- a/scripts/virtuality_firstboot.sh +++ b/scripts/virtuality_firstboot.sh @@ -2,28 +2,123 @@ 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. +# Virtuality first boot +# Runs once on a system installed from the Virtuality image. +# The web panel is installed first (offline, from bundled +# wheels) so the setup wizard in the browser can show the +# progress of everything that follows: network, KVM/libvirt, +# tools. Progress is written to config/setup.json, which the +# wizard polls. Every step is idempotent: on failure systemd +# restarts the script after a minute and it continues from +# the first unfinished step. # ========================================================== SOURCE_DIR="${VIRTUALITY_SOURCE_DIR:-/opt/virtuality/source}" -IMAGE_ENV="/etc/virtuality/image.env" -STATE_DIR="/var/lib/virtuality" +IMAGE_ENV="${VIRTUALITY_IMAGE_ENV:-/etc/virtuality/image.env}" +STATE_DIR="${VIRTUALITY_STATE_DIR:-/var/lib/virtuality}" +CONFIG_DIR="${STATE_DIR}/config" +STATE_FILE="${CONFIG_DIR}/setup.json" DONE_FLAG="${STATE_DIR}/.firstboot-done" -LOG_DIR="/var/log/virtuality" +STEP_FLAG_DIR="${STATE_DIR}/.firstboot-steps" +LOG_DIR="${VIRTUALITY_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}" +APT_LOCK_CONF="${VIRTUALITY_APT_LOCK_CONF:-/etc/apt/apt.conf.d/90virtuality-lock-timeout}" +ISSUE_FILE="${VIRTUALITY_ISSUE_FILE:-/etc/issue.d/virtuality.issue}" +CTL="${VIRTUALITY_CTL:-/usr/local/bin/virtuality-ctl}" +NETWORK_WAIT_SECONDS="${VIRTUALITY_NETWORK_WAIT_SECONDS:-0}" # 0 = wait forever -mkdir -p "$LOG_DIR" "$STATE_DIR" "$(dirname "$ISSUE_FILE")" +STEP_IDS=(panel network virtualization tools finish) +declare -A STEP_TITLE=( + [panel]="Панель управления" + [network]="Подключение к интернету" + [virtualization]="KVM, QEMU и libvirt" + [tools]="Инструменты диагностики" + [finish]="Завершение" +) +declare -A STEP_STATUS=() +CURRENT_STEP="" +WATCHER_PID="" + +mkdir -p "$LOG_DIR" "$STATE_DIR" "$CONFIG_DIR" "$STEP_FLAG_DIR" "$(dirname "$ISSUE_FILE")" exec > >(tee -a "$LOG_FILE") 2>&1 now() { date '+%Y-%m-%d %H:%M:%S'; } say() { echo "[$(now)] [virtuality-firstboot] $*"; } +# ---------------------------------------------------------------- progress for the wizard +json_escape() { + local text="$1" + text="${text//\\/\\\\}" + text="${text//\"/\\\"}" + text="${text//$'\n'/ }" + printf '%s' "$text" +} + +write_state() { + local stage="$1" message="$2" id first=1 + { + printf '{"stage": "%s", "message": "%s", "updated_at": "%s", "steps": [' "$stage" "$(json_escape "$message")" "$(now)" + for id in "${STEP_IDS[@]}"; do + (( first )) || printf ', ' + first=0 + printf '{"id": "%s", "title": "%s", "status": "%s"}' "$id" "${STEP_TITLE[$id]}" "${STEP_STATUS[$id]:-pending}" + done + printf ']}\n' + } > "${STATE_FILE}.tmp" + mv -f "${STATE_FILE}.tmp" "$STATE_FILE" + chmod 644 "$STATE_FILE" +} + +step_done_before() { [[ -f "${STEP_FLAG_DIR}/$1" ]]; } + +step_start() { + CURRENT_STEP="$1" + STEP_STATUS[$1]="running" + say "step ${1}: ${STEP_TITLE[$1]}" + write_state installing "$2" +} + +step_finish() { + STEP_STATUS[$1]="done" + touch "${STEP_FLAG_DIR}/$1" + CURRENT_STEP="" + write_state installing "${2:-}" +} + +# apt writes its progress to the installer logs; turn the last line into a friendly message. +watch_progress() { + local latest line pkg + while true; do + latest="$(ls -t "${LOG_DIR}"/install_*.log 2>/dev/null | head -n1 || true)" + if [[ -n "$latest" ]]; then + line="$(grep -aE '^(Get:[0-9]+ |Unpacking |Setting up |Preparing to unpack )' "$latest" 2>/dev/null | tail -n1 || true)" + pkg="" + case "$line" in + Get:*) pkg="$(printf '%s' "$line" | awk '{print $5}')" ; [[ -n "$pkg" ]] && write_state installing "Скачиваем ${pkg}…" ;; + "Unpacking "*|"Preparing to unpack "*) pkg="$(printf '%s' "$line" | sed -E 's/^(Preparing to unpack \.\.\.\/|Preparing to unpack |Unpacking )//; s/[ :].*//')"; [[ -n "$pkg" ]] && write_state installing "Распаковываем ${pkg}…" ;; + "Setting up "*) pkg="$(printf '%s' "$line" | sed -E 's/^Setting up //; s/[ :].*//')"; [[ -n "$pkg" ]] && write_state installing "Настраиваем ${pkg}…" ;; + esac + fi + sleep 3 + done +} + +watch_start() { watch_progress & WATCHER_PID=$!; } +watch_stop() { [[ -n "$WATCHER_PID" ]] && { kill "$WATCHER_PID" 2>/dev/null || true; wait "$WATCHER_PID" 2>/dev/null || true; }; WATCHER_PID=""; } + +on_error() { + local code=$? + watch_stop + if [[ -n "$CURRENT_STEP" ]]; then + STEP_STATUS[$CURRENT_STEP]="error" + fi + write_state error "Шаг «${STEP_TITLE[${CURRENT_STEP:-finish}]}» завершился с ошибкой (код ${code}). Повторим автоматически через минуту; подробности ниже в журнале." + say "ERROR: step ${CURRENT_STEP:-?} failed with code ${code}" + exit "$code" +} +trap on_error ERR + +# ---------------------------------------------------------------- environment if [[ -f "$DONE_FLAG" ]]; then say "already completed, nothing to do" exit 0 @@ -42,54 +137,103 @@ first_human_user() { 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_WEB_PORT="${VIRTUALITY_WEB_PORT:-8088}" export VIRTUALITY_AUTO_UPDATE="${VIRTUALITY_AUTO_UPDATE:-1}" -# Disk/RAM limits are reported by vhealth; they must not block an image install. +# Disk/RAM limits are reported in the panel; they must not block an image install. export VIRTUALITY_SKIP_REQUIREMENTS="${VIRTUALITY_SKIP_REQUIREMENTS:-1}" +export VIRTUALITY_FIRSTBOOT=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 +for id in "${STEP_IDS[@]}"; do + step_done_before "$id" && STEP_STATUS[$id]="done" +done -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; } +say "source: ${SOURCE_DIR}, panel user: ${AUTH_USER}, port: ${VIRTUALITY_WEB_PORT}" +[[ -f "${SOURCE_DIR}/install_virtuality_node.sh" ]] || { write_state error "Исходники Virtuality не найдены в ${SOURCE_DIR}"; say "ERROR: source not found"; exit 1; } +cd "$SOURCE_DIR" # 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" < "$ISSUE_FILE" <<'EOF' +Virtuality настраивается. Ход установки: sudo journalctl -fu virtuality-firstboot EOF + +network_ready() { + [[ -n "${VIRTUALITY_NETWORK_CHECK:-}" ]] && { "$VIRTUALITY_NETWORK_CHECK"; return; } + ip route show default 2>/dev/null | grep -q . && getent hosts archive.ubuntu.com >/dev/null 2>&1 +} + +wait_for_network() { + local started elapsed + started="$(date +%s)" + until network_ready; do + elapsed=$(( $(date +%s) - started )) + if (( NETWORK_WAIT_SECONDS > 0 && elapsed >= NETWORK_WAIT_SECONDS )); then + say "ERROR: no network with DNS after ${elapsed}s" + return 1 + fi + if (( elapsed >= 20 )); then + write_state waiting-network "Нет доступа в интернет. Подключите кабель или настройте сеть — установка продолжится сама." + fi + sleep 5 + done + write_state installing "Интернет доступен" +} + +# ---------------------------------------------------------------- 1. panel (offline) +if ! step_done_before panel; then + step_start panel "Устанавливаем панель управления…" + if ! VIRTUALITY_PANEL_ONLY=1 bash ./scripts/install_web_panel.sh; then + # No bundled wheels (image built with --no-wheels): install online instead. + say "offline panel install failed, retrying with network" + wait_for_network + bash ./scripts/install_web_panel.sh + fi + step_finish panel "Панель управления запущена" +fi + +# ---------------------------------------------------------------- 2. network +if ! step_done_before network; then + step_start network "Проверяем подключение к интернету…" + wait_for_network + step_finish network "Интернет доступен" +fi + +# ---------------------------------------------------------------- 3. virtualization +if ! step_done_before virtualization; then + step_start virtualization "Устанавливаем пакеты виртуализации (это самый долгий шаг)…" + watch_start + bash ./install_virtuality_node.sh + watch_stop + step_finish virtualization "Виртуализация установлена" +fi + +# ---------------------------------------------------------------- 4. tools +if ! step_done_before tools; then + step_start tools "Устанавливаем инструменты диагностики…" + bash ./scripts/install_healthcheck_command.sh + step_finish tools "Инструменты установлены" +fi + +# ---------------------------------------------------------------- 5. finish +step_start finish "Завершаем настройку…" +# The panel was installed before libvirt: refresh units/firewall and restart it with everything in place. +"$CTL" reconfigure +PANEL_URL="$("$CTL" url 2>/dev/null || echo "http://$(hostname -I | awk '{print $1}'):${VIRTUALITY_WEB_PORT}")" +cat > "$ISSUE_FILE" </dev/null 2>&1 || true -say "completed. Web panel: http://$(hostname -I | awk '{print $1}'):${WEB_PORT} login: ${AUTH_USER}" +rm -f "$APT_LOCK_CONF" +say "completed. Panel: ${PANEL_URL} login: ${AUTH_USER}" diff --git a/setup_github_sync.sh b/setup_github_sync.sh deleted file mode 100644 index 83cd391..0000000 --- a/setup_github_sync.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PROJECT_DIR="/opt/virtuality" -DEFAULT_BRANCH="main" - -if [[ "$EUID" -ne 0 ]]; then - echo "Ошибка: запусти от root: sudo bash setup_github_sync.sh" - exit 1 -fi - -apt update -apt install -y git openssh-client - -if [[ ! -d "$PROJECT_DIR" ]]; then - echo "Ошибка: папка проекта не найдена: $PROJECT_DIR" - echo "Сначала запусти install_virtuality_node.sh" - exit 1 -fi - -cd "$PROJECT_DIR" - -read -rp "GitHub repo URL [git@github.com:viktor138irk/virtuality.git]: " GITHUB_REPO -GITHUB_REPO="${GITHUB_REPO:-git@github.com:viktor138irk/virtuality.git}" - -read -rp "Git author name [Virtuality Node]: " GIT_NAME -GIT_NAME="${GIT_NAME:-Virtuality Node}" - -read -rp "Git author email [virtuality@local]: " GIT_EMAIL -GIT_EMAIL="${GIT_EMAIL:-virtuality@local}" - -git config --global user.name "$GIT_NAME" -git config --global user.email "$GIT_EMAIL" - -cat > "$PROJECT_DIR/git_sync.sh" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -PROJECT_DIR="/opt/virtuality" -cd "$PROJECT_DIR" - -MESSAGE="${1:-Auto sync $(date '+%Y-%m-%d %H:%M:%S')}" - -git add . -git commit -m "$MESSAGE" || { - echo "Нет изменений для commit." - exit 0 -} - -git push origin main -EOF - -chmod +x "$PROJECT_DIR/git_sync.sh" - -if [[ ! -d "$PROJECT_DIR/.git" ]]; then - git init -fi - -git branch -M "$DEFAULT_BRANCH" - -if git remote get-url origin >/dev/null 2>&1; then - git remote set-url origin "$GITHUB_REPO" -else - git remote add origin "$GITHUB_REPO" -fi - -git add . -git commit -m "Initial Virtuality node base" || true -git push -u origin "$DEFAULT_BRANCH" - -echo "GitHub sync готов." -echo "Ручная синхронизация: sudo /opt/virtuality/git_sync.sh \"Описание изменений\"" diff --git a/web/app.py b/web/app.py index 28977a8..5ad52da 100644 --- a/web/app.py +++ b/web/app.py @@ -1344,7 +1344,7 @@ def dashboard(request: Request): return auth_redirect if setup.wizard_pending(): return RedirectResponse(url="/setup", status_code=303) - services = {"libvirtd": service_state("libvirtd.service"), "virtlogd": service_state("virtlogd.service"), "cockpit": service_state("cockpit.socket"), "dashboard": service_state("virtuality-console-dashboard.service"), "web": service_state("virtuality-web.service")} + services = {"libvirtd": service_state("libvirtd.service"), "virtlogd": service_state("virtlogd.service"), "web": service_state("virtuality-web.service")} service_rows = [{"key": key, "name": presenters.SERVICE_LABELS.get(key, key), "state": state, **presenters.service_state(state)} for key, state in services.items()] return templates.TemplateResponse("dashboard.html", {"request": request, "app_name": APP_NAME, "system": system_summary(), "services": services, "service_rows": service_rows, "services_ok": all(row["tone"] == "success" for row in service_rows if row["key"] in ("libvirtd", "web")), "vms": parse_virsh_list(), "pools": parse_pool_list(), "network": network_summary(), "user": AUTH_USER, "profile": host_profile.load_host_profile(), "operations": list_operations(5), "operation_css": operation_css, "host": presenters.host_stats(STORAGE_DIR), "greeting": presenters.greeting()}) @@ -1999,7 +1999,7 @@ def live_status(request: Request): ip = "—" css = "ok" if "running" in state else "err" if "shut" in state else "warn" vms.append({"id": vm.get("id", "-"), "name": name, "state": state, "state_css": css, "ip": ip if ip and ip != "not available" else "—", "autostart_enabled": vm.get("autostart_enabled", False), "autostart_label": vm.get("autostart_label", "unknown"), "autostart_css": vm.get("autostart_css", "warn")}) - return JSONResponse({"ok": True, "generated_at": utc_now(), "vms": vms, "services": {"libvirtd": service_state("libvirtd.service"), "virtlogd": service_state("virtlogd.service"), "cockpit": service_state("cockpit.socket"), "web": service_state("virtuality-web.service")}, "operations": list_operations(5)}) + return JSONResponse({"ok": True, "generated_at": utc_now(), "vms": vms, "services": {"libvirtd": service_state("libvirtd.service"), "virtlogd": service_state("virtlogd.service"), "web": service_state("virtuality-web.service")}, "operations": list_operations(5)}) @app.get("/live/operations") @@ -2018,7 +2018,7 @@ def healthz(): def api_health(request: Request): if not get_current_user(request): return JSONResponse({"ok": False, "error": "Unauthorized"}, status_code=401) - return {"system": system_summary(), "host_profile": host_profile.load_host_profile(), "services": {"libvirtd": service_state("libvirtd.service"), "virtlogd": service_state("virtlogd.service"), "cockpit": service_state("cockpit.socket"), "dashboard": service_state("virtuality-console-dashboard.service"), "web": service_state("virtuality-web.service")}, "vms": parse_virsh_list(), "pools": parse_pool_list(), "network": network_summary(), "virtuality_nat": network_core.network_context()} + return {"system": system_summary(), "host_profile": host_profile.load_host_profile(), "services": {"libvirtd": service_state("libvirtd.service"), "virtlogd": service_state("virtlogd.service"), "web": service_state("virtuality-web.service")}, "vms": parse_virsh_list(), "pools": parse_pool_list(), "network": network_summary(), "virtuality_nat": network_core.network_context()} app.include_router(setup.router) diff --git a/web/presenters.py b/web/presenters.py index a8ec65e..f8810eb 100644 --- a/web/presenters.py +++ b/web/presenters.py @@ -29,8 +29,6 @@ OPERATION_STATES = { SERVICE_LABELS = { "libvirtd": "Виртуализация", "virtlogd": "Журналы машин", - "cockpit": "Cockpit", - "dashboard": "Экран сервера", "web": "Панель управления", } diff --git a/web/templates/help.html b/web/templates/help.html index ac012a7..7eb113b 100644 --- a/web/templates/help.html +++ b/web/templates/help.html @@ -67,7 +67,6 @@
  • Проверьте раздел Сервер — там видно, чего не хватает.
  • Посмотрите Задачи — у каждой есть подробный журнал.
  • Полная диагностика в терминале сервера: sudo vhealth.
  • -
  • Расширенное управление доступно в Cockpit: https://{{ host_ip }}:9090.