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