Первая загрузка образа переписана: панель ставится первой, прогресс виден в мастере
- 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
Этот коммит содержится в:
@@ -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" "$@"
|
||||
@@ -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
|
||||
|
||||
+12
-55
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "=================================================="
|
||||
@@ -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"
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
|
||||
@@ -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 "============================================================"
|
||||
@@ -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 "$@" ;;
|
||||
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
|
||||
@@ -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 \"Описание изменений\""
|
||||
+3
-3
@@ -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)
|
||||
|
||||
@@ -29,8 +29,6 @@ OPERATION_STATES = {
|
||||
SERVICE_LABELS = {
|
||||
"libvirtd": "Виртуализация",
|
||||
"virtlogd": "Журналы машин",
|
||||
"cockpit": "Cockpit",
|
||||
"dashboard": "Экран сервера",
|
||||
"web": "Панель управления",
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
<li>Проверьте раздел <a href="/host">Сервер</a> — там видно, чего не хватает.</li>
|
||||
<li>Посмотрите <a href="/operations">Задачи</a> — у каждой есть подробный журнал.</li>
|
||||
<li>Полная диагностика в терминале сервера: <code>sudo vhealth</code>.</li>
|
||||
<li>Расширенное управление доступно в Cockpit: <a href="https://{{ host_ip }}:9090" target="_blank" rel="noopener">https://{{ host_ip }}:9090</a>.</li>
|
||||
</ul>
|
||||
</div></article>
|
||||
</section>
|
||||
|
||||
Ссылка в новой задаче
Block a user