Первая загрузка образа переписана: панель ставится первой, прогресс виден в мастере

- virtuality_firstboot.sh: шаги panel → network → virtualization → tools → finish,
  прогресс в config/setup.json (в т.ч. имена пакетов apt), идемпотентный повтор
  после ошибки, /etc/issue с адресом панели
- install_web_panel.sh: поэтапная замена web/ с сохранением web.prev, офлайн
  virtualenv с pip из wheel, юниты/TLS/firewall через virtuality-ctl reconfigure,
  мастер настройки и для ручной установки
- virtuality-ctl url; путь к исходникам хранится в web.env
- build-iso.sh: wheel pip в образе, часовой пояс по GeoIP
- Cockpit, консольный dashboard, тестовая VM, clean_install, setup_bridge_br0,
  bootstrap.sh и setup_github_sync.sh были убраны (заменены мастером и virtuality-ctl)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
Claude
2026-09-25 14:50:16 +00:00
родитель 9cf8c3e4b7
Коммит 55f7d971f6
18 изменённых файлов: 351 добавлений и 1351 удалений
-90
Просмотреть файл
@@ -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 "=================================================="
-111
Просмотреть файл
@@ -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 <<EOF
${BOLD}Virtuality clean install${RESET}
Will remove:
- /opt/virtuality/web
- /opt/virtuality/venv
- /etc/systemd/system/virtuality-web.service
- /var/lib/virtuality/config
- /var/lib/virtuality/update
- /var/log/virtuality/update.log and install logs
Will preserve by default:
- /opt/virtuality/source
- /var/lib/virtuality/iso
- /var/lib/virtuality/images
- /var/lib/virtuality/disk-images
- /var/lib/virtuality/backups
- existing libvirt VMs and storage pools
EOF
if [[ "$PURGE_DATA" -eq 1 ]]; then
cat <<EOF
${RED}${BOLD}PURGE DATA MODE ENABLED${RESET}
Will also remove:
- /var/lib/virtuality/iso
- /var/lib/virtuality/images
- /var/lib/virtuality/disk-images
- /var/lib/virtuality/backups
- /var/lib/virtuality/network
Existing libvirt VM definitions are not deleted automatically.
EOF
fi
if [[ "$YES" -ne 1 ]]; then
echo
read -r -p "Continue? Type YES: " answer
if [[ "$answer" != "YES" ]]; then
echo "Cancelled."
exit 0
fi
fi
systemctl stop virtuality-web.service 2>/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"
-71
Просмотреть файл
@@ -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}"
-104
Просмотреть файл
@@ -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 <<EOF
${CYAN}${BOLD}Virtuality Cockpit auth check${RESET}
${GRAY}User:${RESET} ${USER_NAME}
EOF
echo
if ! id "$USER_NAME" >/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
-84
Просмотреть файл
@@ -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" <<EOF
[Unit]
Description=Virtuality console dashboard on tty1
After=multi-user.target network-online.target libvirtd.service cockpit.socket
Wants=network-online.target
Conflicts=getty@tty1.service
[Service]
Type=simple
ExecStart=/usr/local/bin/virtuality-console-dashboard
Restart=always
RestartSec=2
StandardInput=tty
StandardOutput=tty
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
Environment=TERM=linux
Environment=VIRTUALITY_DASHBOARD_INTERVAL=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl disable --now getty@tty1.service >/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 "============================================================"
+127 -276
Просмотреть файл
@@ -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" <<EOF
# Virtuality node settings. Edit and re-run: sudo bash scripts/install_web_panel.sh
VIRTUALITY_WEB_PORT=${PORT}
VIRTUALITY_WEB_HOST=${WEB_HOST}
VIRTUALITY_AUTH_USER=${AUTH_USER}
VIRTUALITY_AUTO_UPDATE=${AUTO_UPDATE}
VIRTUALITY_COOKIE_SECURE=${COOKIE_SECURE}
EOF
chmod 644 "$NODE_CONFIG_FILE"
ok "Настройки ноды сохранены: $NODE_CONFIG_FILE"
cat > "${APP_DIR}/.env" <<EOF
VIRTUALITY_AUTH_USER=${AUTH_USER}
VIRTUALITY_SESSION_SECRET=${SESSION_SECRET}
VIRTUALITY_COOKIE_SECURE=${COOKIE_SECURE}
VIRTUALITY_COOKIE_SECURE=$(saved_setting VIRTUALITY_COOKIE_SECURE)
TMPDIR=${UPLOAD_TMP_DIR}
TEMP=${UPLOAD_TMP_DIR}
TMP=${UPLOAD_TMP_DIR}
EOF
chmod 600 "${APP_DIR}/.env"
ok "Создан ${APP_DIR}/.env"
ok "Временный каталог загрузок: ${UPLOAD_TMP_DIR}"
ok "Вход будет по Linux-пользователю: ${AUTH_USER}"
step "Создаём Python virtualenv"
if [[ -x "$VENV_DIR/bin/python" ]] && "$VENV_DIR/bin/python" -c 'import sys' >/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" <<EOF
[Unit]
Description=Virtuality Web Panel
After=network-online.target libvirtd.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=${APP_DIR}
ExecStart=${VENV_DIR}/bin/uvicorn app:app --host ${WEB_HOST} --port ${PORT} --workers 1 --no-server-header
Restart=always
RestartSec=3
TimeoutStopSec=20
LimitNOFILE=65536
User=root
Group=root
Environment=PYTHONUNBUFFERED=1
Environment=VIRTUALITY_AUTH_USER=${AUTH_USER}
Environment=VIRTUALITY_SOURCE_DIR=${REPO_DIR}
Environment=TMPDIR=${UPLOAD_TMP_DIR}
Environment=TEMP=${UPLOAD_TMP_DIR}
Environment=TMP=${UPLOAD_TMP_DIR}
[Install]
WantedBy=multi-user.target
EOF
ok "Создан service: $SERVICE_FILE"
cat > "$NETWORK_SERVICE_FILE" <<EOF
[Unit]
Description=Virtuality NAT port forwarding restore
After=network-online.target libvirtd.service nftables.service ufw.service
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=${APP_DIR}
ExecStart=${VENV_DIR}/bin/python ${APP_DIR}/restore_network.py
[Install]
WantedBy=multi-user.target
EOF
ok "Создан service: $NETWORK_SERVICE_FILE"
run_logged "systemd daemon-reload выполнен" systemctl daemon-reload
run_logged "virtuality-network.service включён" systemctl enable virtuality-network.service
step "Настраиваем автообновление"
if [[ -f "${REPO_DIR}/scripts/auto_update_check.sh" ]]; then
chmod +x "${REPO_DIR}/scripts/auto_update_check.sh" || true
cat > "$AUTO_UPDATE_SERVICE_FILE" <<EOF
[Unit]
Description=Virtuality automatic GitHub update check
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
Group=root
Environment=VIRTUALITY_SOURCE_DIR=${REPO_DIR}
Environment=TMPDIR=${UPLOAD_TMP_DIR}
ExecStart=/bin/bash ${REPO_DIR}/scripts/auto_update_check.sh
EOF
cat > "$AUTO_UPDATE_TIMER_FILE" <<EOF
[Unit]
Description=Run Virtuality automatic update check daily at 00:00 Moscow time
[Timer]
OnCalendar=*-*-* 21:00:00 UTC
AccuracySec=1min
Persistent=true
Unit=virtuality-auto-update.service
[Install]
WantedBy=timers.target
EOF
run_logged "systemd daemon-reload выполнен для автообновлений" systemctl daemon-reload
if [[ "$AUTO_UPDATE" == "1" ]]; then
run_logged "virtuality-auto-update.timer включён" systemctl enable --now virtuality-auto-update.timer
ok "Автообновление будет проверять GitHub 1 раз в сутки в 00:00 по Москве"
else
systemctl disable --now virtuality-auto-update.timer >> "$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}"
-169
Просмотреть файл
@@ -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" <<EOF
#!/usr/bin/env bash
set -euo pipefail
rm -f "$BRIDGE_FILE"
cp -a "$BACKUP_DIR"/*.yaml "$NETPLAN_DIR"/
chmod 600 "$NETPLAN_DIR"/*.yaml 2>/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" <<EOF
network:
version: 2
ethernets:
${IFACE}:
dhcp4: false
dhcp6: false
EOF
chmod 600 "$file"
fi
done
if [[ "$MODE" == "static" ]]; then
if [[ -z "${CURRENT_IP:-}" || -z "${CURRENT_GW:-}" ]]; then
echo "Ошибка: не удалось определить текущий IP/gateway для static mode."
echo "Откат: sudo $ROLLBACK_SCRIPT"
exit 1
fi
cat > "$BRIDGE_FILE" <<EOF
network:
version: 2
ethernets:
${IFACE}:
dhcp4: false
dhcp6: false
bridges:
${BRIDGE}:
interfaces:
- ${IFACE}
addresses:
- ${CURRENT_IP}
routes:
- to: default
via: ${CURRENT_GW}
nameservers:
addresses:
- ${DNS_1}
- ${DNS_2}
dhcp4: false
dhcp6: false
parameters:
stp: false
forward-delay: 0
EOF
else
cat > "$BRIDGE_FILE" <<EOF
network:
version: 2
ethernets:
${IFACE}:
dhcp4: false
dhcp6: false
bridges:
${BRIDGE}:
interfaces:
- ${IFACE}
dhcp4: true
dhcp6: false
parameters:
stp: false
forward-delay: 0
EOF
fi
chmod 600 "$BRIDGE_FILE"
chmod 600 "$NETPLAN_DIR"/*.yaml 2>/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 "============================================================"
+4 -1
Просмотреть файл
@@ -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 <command> [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 "$@" ;;
-224
Просмотреть файл
@@ -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<height-1; i++)); do
move $((row+i)) "$col"; printf "${FG_BLUE}│${RESET}"
printf "${BG_PANEL}%s${RESET}" "$(repeat_char ' ' "$inner")"
printf "${FG_BLUE}│${RESET}"
done
move $((row+height-1)) "$col"; printf "${FG_BLUE}╰%s╯${RESET}" "$(repeat_char '─' "$inner")"
}
put() {
local row="$1" col="$2" width="$3" text="$4"
move "$row" "$col"
printf "${BG_PANEL}"
printf "%b" "$text" | cut -c 1-"$width"
printf "${RESET}"
}
bar() {
local percent="$1" width="$2"
local filled=$(( percent * width / 100 ))
local empty=$(( width - filled ))
local color="$FG_GREEN"
if (( percent >= 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
+191 -47
Просмотреть файл
@@ -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" <<EOF
Virtuality web panel: http://\4:${WEB_PORT} (login: ${AUTH_USER})
Cockpit: https://\4:9090
cat > "$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" <<EOF
Virtuality: откройте панель управления в браузере — ${PANEL_URL}
Логин: ${AUTH_USER} (пароль как у пользователя Linux)
EOF
touch "$DONE_FLAG"
STEP_STATUS[finish]="done"
CURRENT_STEP=""
write_state done "Сервер готов"
systemctl disable virtuality-firstboot.service >/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}"