- 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
706 строки
28 KiB
Bash
Исполняемый файл
706 строки
28 KiB
Bash
Исполняемый файл
#!/usr/bin/env bash
|
|
# virtuality-ctl — node management for Virtuality.
|
|
# Installed to /usr/local/bin by scripts/install_web_panel.sh. Run as root.
|
|
set -euo pipefail
|
|
|
|
CONFIG_DIR="/var/lib/virtuality/config"
|
|
NODE_ENV="${CONFIG_DIR}/web.env"
|
|
TLS_DIR="${CONFIG_DIR}/tls"
|
|
STATE_FILE="${CONFIG_DIR}/ports.state"
|
|
DATA_DIR="/var/lib/virtuality"
|
|
LOG_DIR="/var/log/virtuality"
|
|
APP_DIR="/opt/virtuality/web"
|
|
VENV_DIR="/opt/virtuality/venv"
|
|
UNIT_DIR="/etc/systemd/system"
|
|
NETPLAN_DIR="/etc/netplan"
|
|
NETPLAN_BACKUP="${CONFIG_DIR}/netplan-backup"
|
|
BRIDGE_FILE="${NETPLAN_DIR}/60-virtuality-br0.yaml"
|
|
REVERT_UNIT="virtuality-netplan-revert"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: virtuality-ctl <command> [options]
|
|
|
|
status Что установлено, порты, адреса панели
|
|
url Адрес панели управления
|
|
reconfigure Пересоздать systemd-юниты из ${NODE_ENV} и перезапустить панель
|
|
set KEY=VALUE ... Изменить настройку в web.env (например set VIRTUALITY_TLS=1) и применить
|
|
passwd [USER] Сменить пароль пользователя панели (пароль читается со стандартного ввода)
|
|
timezone <Area/City> Установить часовой пояс сервера
|
|
power reboot|poweroff Перезагрузить или выключить сервер (машины выключаются корректно)
|
|
backup-config [FILE] Сохранить настройки ноды и описания машин в архив
|
|
restore-config FILE Восстановить настройки ноды из архива
|
|
rollback Вернуть предыдущую версию панели после неудачного обновления
|
|
maintenance Ежедневное обслуживание: старые задачи, временные файлы, свободное место
|
|
bridge <iface> [dhcp|static] Подключить машины к локальной сети через мост br0 с автооткатом
|
|
bridge-confirm Подтвердить новую сеть (отменяет автооткат)
|
|
bridge-revert Вернуть прежнюю сеть
|
|
storage-use /dev/sdX Отдать пустой диск под хранилище машин (${DATA_DIR})
|
|
support-bundle [FILE] Собрать отчёт для поддержки
|
|
uninstall [--purge] [--yes] Удалить Virtuality (по умолчанию машины и образы остаются)
|
|
EOF
|
|
}
|
|
|
|
die() { echo "virtuality-ctl: $*" >&2; exit 1; }
|
|
need_root() { [[ "$EUID" -eq 0 ]] || die "нужны права root: sudo virtuality-ctl $*"; }
|
|
now() { date '+%Y-%m-%d %H:%M:%S'; }
|
|
log() { mkdir -p "$LOG_DIR"; echo "[$(now)] [virtuality-ctl] $*" >> "${LOG_DIR}/virtuality-ctl.log"; }
|
|
|
|
# ---------------------------------------------------------------- settings
|
|
env_get() {
|
|
local key="$1" default="${2:-}"
|
|
local value=""
|
|
if [[ -f "$NODE_ENV" ]]; then
|
|
value="$(sed -n "s/^${key}=//p" "$NODE_ENV" | tail -n1 | tr -d '"' | tr -d "'")"
|
|
fi
|
|
printf '%s' "${value:-$default}"
|
|
}
|
|
|
|
env_set() {
|
|
local key="$1" value="$2"
|
|
mkdir -p "$CONFIG_DIR"
|
|
touch "$NODE_ENV"
|
|
if grep -q "^${key}=" "$NODE_ENV"; then
|
|
sed -i "s|^${key}=.*|${key}=${value}|" "$NODE_ENV"
|
|
else
|
|
printf '%s=%s\n' "$key" "$value" >> "$NODE_ENV"
|
|
fi
|
|
chmod 644 "$NODE_ENV"
|
|
}
|
|
|
|
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
|
|
tls="$(env_get VIRTUALITY_TLS 0)"
|
|
if [[ "$tls" == "1" ]]; then
|
|
port="$(env_get VIRTUALITY_TLS_PORT 8443)"
|
|
printf 'https://%s:%s' "$(primary_ip)" "$port"
|
|
else
|
|
port="$(env_get VIRTUALITY_WEB_PORT 8088)"
|
|
printf 'http://%s:%s' "$(primary_ip)" "$port"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------- tls
|
|
ensure_cert() {
|
|
local ip host
|
|
mkdir -p "$TLS_DIR"
|
|
chmod 700 "$TLS_DIR"
|
|
if [[ -s "${TLS_DIR}/panel.key" && -s "${TLS_DIR}/panel.crt" ]]; then
|
|
return 0
|
|
fi
|
|
ip="$(primary_ip)"
|
|
host="$(hostname)"
|
|
local san="DNS:${host},DNS:localhost,IP:127.0.0.1"
|
|
[[ -n "$ip" ]] && san="${san},IP:${ip}"
|
|
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 3650 \
|
|
-subj "/CN=Virtuality on ${host}" -addext "subjectAltName=${san}" \
|
|
-keyout "${TLS_DIR}/panel.key" -out "${TLS_DIR}/panel.crt" >/dev/null 2>&1 \
|
|
|| die "не удалось создать TLS-сертификат (нужен openssl)"
|
|
chmod 600 "${TLS_DIR}/panel.key"
|
|
log "TLS certificate created for ${host} ${ip}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- units
|
|
write_units() {
|
|
local port host tls tls_port auth_user auto_update
|
|
port="$(env_get VIRTUALITY_WEB_PORT 8088)"
|
|
host="$(env_get VIRTUALITY_WEB_HOST 0.0.0.0)"
|
|
tls="$(env_get VIRTUALITY_TLS 0)"
|
|
tls_port="$(env_get VIRTUALITY_TLS_PORT 8443)"
|
|
auth_user="$(env_get VIRTUALITY_AUTH_USER root)"
|
|
auto_update="$(env_get VIRTUALITY_AUTO_UPDATE 1)"
|
|
local listen_port="$port" ssl_args=""
|
|
if [[ "$tls" == "1" ]]; then
|
|
ensure_cert
|
|
listen_port="$tls_port"
|
|
ssl_args="--ssl-keyfile ${TLS_DIR}/panel.key --ssl-certfile ${TLS_DIR}/panel.crt"
|
|
env_set VIRTUALITY_COOKIE_SECURE 1
|
|
else
|
|
env_set VIRTUALITY_COOKIE_SECURE 0
|
|
fi
|
|
|
|
cat > "${UNIT_DIR}/virtuality-web.service" <<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 ${host} --port ${listen_port} --workers 1 --no-server-header ${ssl_args}
|
|
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=${SOURCE_DIR}
|
|
Environment=VIRTUALITY_NODE_ENV=${NODE_ENV}
|
|
Environment=TMPDIR=${DATA_DIR}/tmp
|
|
Environment=TEMP=${DATA_DIR}/tmp
|
|
Environment=TMP=${DATA_DIR}/tmp
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
if [[ "$tls" == "1" ]]; then
|
|
cat > "${UNIT_DIR}/virtuality-redirect.service" <<EOF
|
|
[Unit]
|
|
Description=Virtuality HTTP to HTTPS redirect
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=${VENV_DIR}/bin/python ${APP_DIR}/redirect_http.py --port ${port} --target-port ${tls_port}
|
|
Restart=always
|
|
RestartSec=3
|
|
User=root
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
else
|
|
rm -f "${UNIT_DIR}/virtuality-redirect.service"
|
|
fi
|
|
|
|
cat > "${UNIT_DIR}/virtuality-network.service" <<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
|
|
|
|
cat > "${UNIT_DIR}/virtuality-auto-update.service" <<EOF
|
|
[Unit]
|
|
Description=Virtuality automatic update check
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
User=root
|
|
Environment=VIRTUALITY_SOURCE_DIR=${SOURCE_DIR}
|
|
Environment=TMPDIR=${DATA_DIR}/tmp
|
|
ExecStart=/bin/bash ${SOURCE_DIR}/scripts/auto_update_check.sh
|
|
EOF
|
|
cat > "${UNIT_DIR}/virtuality-auto-update.timer" <<EOF
|
|
[Unit]
|
|
Description=Virtuality nightly update check
|
|
|
|
[Timer]
|
|
OnCalendar=*-*-* 04:00:00
|
|
RandomizedDelaySec=30min
|
|
AccuracySec=1min
|
|
Persistent=true
|
|
Unit=virtuality-auto-update.service
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
|
|
cat > "${UNIT_DIR}/virtuality-maintenance.service" <<EOF
|
|
[Unit]
|
|
Description=Virtuality daily maintenance
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
User=root
|
|
ExecStart=/usr/local/bin/virtuality-ctl maintenance
|
|
EOF
|
|
cat > "${UNIT_DIR}/virtuality-maintenance.timer" <<EOF
|
|
[Unit]
|
|
Description=Virtuality daily maintenance
|
|
|
|
[Timer]
|
|
OnCalendar=daily
|
|
RandomizedDelaySec=1h
|
|
Persistent=true
|
|
Unit=virtuality-maintenance.service
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
|
|
mkdir -p /etc/logrotate.d
|
|
cat > /etc/logrotate.d/virtuality <<EOF
|
|
${LOG_DIR}/*.log {
|
|
weekly
|
|
rotate 4
|
|
compress
|
|
delaycompress
|
|
missingok
|
|
notifempty
|
|
copytruncate
|
|
}
|
|
EOF
|
|
|
|
# Guests shut down gracefully with the host instead of being killed (Proxmox behaviour).
|
|
cat > /etc/default/libvirt-guests <<EOF
|
|
ON_BOOT=ignore
|
|
ON_SHUTDOWN=shutdown
|
|
SHUTDOWN_TIMEOUT=180
|
|
PARALLEL_SHUTDOWN=5
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable virtuality-web.service virtuality-network.service virtuality-maintenance.timer >/dev/null 2>&1 || true
|
|
systemctl enable libvirt-guests.service >/dev/null 2>&1 || true
|
|
if [[ "$tls" == "1" ]]; then
|
|
systemctl enable virtuality-redirect.service >/dev/null 2>&1 || true
|
|
else
|
|
systemctl disable --now virtuality-redirect.service >/dev/null 2>&1 || true
|
|
fi
|
|
if [[ "$auto_update" == "1" ]]; then
|
|
systemctl enable virtuality-auto-update.timer >/dev/null 2>&1 || true
|
|
else
|
|
systemctl disable --now virtuality-auto-update.timer >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
apply_firewall() {
|
|
command -v ufw >/dev/null 2>&1 || return 0
|
|
local port tls tls_port old
|
|
port="$(env_get VIRTUALITY_WEB_PORT 8088)"
|
|
tls="$(env_get VIRTUALITY_TLS 0)"
|
|
tls_port="$(env_get VIRTUALITY_TLS_PORT 8443)"
|
|
if [[ -f "$STATE_FILE" ]]; then
|
|
while read -r old; do
|
|
[[ -n "$old" ]] && ufw delete allow "${old}/tcp" >/dev/null 2>&1 || true
|
|
done < "$STATE_FILE"
|
|
fi
|
|
# VM consoles are proxied by the panel over 127.0.0.1; VNC must never be reachable directly.
|
|
ufw delete allow 5900:5999/tcp >/dev/null 2>&1 || true
|
|
ufw allow "${port}/tcp" >/dev/null 2>&1 || true
|
|
printf '%s\n' "$port" > "$STATE_FILE"
|
|
if [[ "$tls" == "1" ]]; then
|
|
ufw allow "${tls_port}/tcp" >/dev/null 2>&1 || true
|
|
printf '%s\n' "$tls_port" >> "$STATE_FILE"
|
|
fi
|
|
}
|
|
|
|
restart_panel() {
|
|
systemctl restart virtuality-web.service
|
|
if [[ "$(env_get VIRTUALITY_TLS 0)" == "1" ]]; then
|
|
systemctl restart virtuality-redirect.service || true
|
|
fi
|
|
systemctl enable --now virtuality-maintenance.timer >/dev/null 2>&1 || true
|
|
if [[ "$(env_get VIRTUALITY_AUTO_UPDATE 1)" == "1" ]]; then
|
|
systemctl enable --now virtuality-auto-update.timer >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
cmd_reconfigure() {
|
|
need_root reconfigure
|
|
[[ -d "$APP_DIR" ]] || die "панель не установлена: ${APP_DIR} не найден"
|
|
write_units
|
|
apply_firewall
|
|
restart_panel
|
|
log "reconfigured: $(panel_url)"
|
|
echo "Панель перезапущена: $(panel_url)"
|
|
}
|
|
|
|
cmd_set() {
|
|
need_root set
|
|
[[ $# -gt 0 ]] || die "usage: virtuality-ctl set KEY=VALUE [KEY=VALUE ...]"
|
|
local pair key value
|
|
for pair in "$@"; do
|
|
key="${pair%%=*}"
|
|
value="${pair#*=}"
|
|
[[ "$key" =~ ^VIRTUALITY_[A-Z_]+$ ]] || die "недопустимый ключ: $key"
|
|
case "$key" in
|
|
VIRTUALITY_WEB_PORT|VIRTUALITY_TLS_PORT) [[ "$value" =~ ^[0-9]+$ ]] && (( value >= 1 && value <= 65535 )) || die "порт должен быть числом 1–65535" ;;
|
|
VIRTUALITY_TLS|VIRTUALITY_AUTO_UPDATE) [[ "$value" == "0" || "$value" == "1" ]] || die "$key принимает 0 или 1" ;;
|
|
VIRTUALITY_UPDATE_CHANNEL) [[ "$value" == "stable" || "$value" == "main" ]] || die "канал: stable или main" ;;
|
|
VIRTUALITY_WEB_HOST) [[ "$value" =~ ^[0-9a-fA-F.:]+$ ]] || die "адрес должен быть IP" ;;
|
|
esac
|
|
env_set "$key" "$value"
|
|
done
|
|
cmd_reconfigure
|
|
}
|
|
|
|
# ---------------------------------------------------------------- account
|
|
cmd_passwd() {
|
|
need_root passwd
|
|
local user="${1:-$(env_get VIRTUALITY_AUTH_USER root)}"
|
|
id "$user" >/dev/null 2>&1 || die "пользователь $user не найден"
|
|
local password
|
|
if [[ -t 0 ]]; then
|
|
read -r -s -p "Новый пароль для ${user}: " password; echo
|
|
else
|
|
IFS= read -r password
|
|
fi
|
|
[[ ${#password} -ge 6 ]] || die "пароль должен быть не короче 6 символов"
|
|
printf '%s:%s\n' "$user" "$password" | chpasswd || die "не удалось сменить пароль"
|
|
passwd -u "$user" >/dev/null 2>&1 || true
|
|
log "password changed for ${user}"
|
|
echo "Пароль пользователя ${user} обновлён"
|
|
}
|
|
|
|
cmd_timezone() {
|
|
need_root timezone
|
|
local tz="${1:-}"
|
|
[[ "$tz" =~ ^[A-Za-z0-9_+-]+(/[A-Za-z0-9_+-]+){0,2}$ ]] || die "usage: virtuality-ctl timezone Europe/Moscow"
|
|
timedatectl set-timezone "$tz" || die "часовой пояс $tz не найден"
|
|
timedatectl set-ntp true >/dev/null 2>&1 || true
|
|
log "timezone set to ${tz}"
|
|
echo "Часовой пояс: $(timedatectl show -p Timezone --value)"
|
|
}
|
|
|
|
cmd_power() {
|
|
need_root power
|
|
case "${1:-}" in
|
|
reboot) log "reboot requested"; systemctl reboot ;;
|
|
poweroff|shutdown) log "poweroff requested"; systemctl poweroff ;;
|
|
*) die "usage: virtuality-ctl power reboot|poweroff" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------- config backup
|
|
cmd_backup_config() {
|
|
need_root backup-config
|
|
local out="${1:-${DATA_DIR}/backups/config-$(date +%Y%m%d-%H%M).tar.gz}"
|
|
local work
|
|
work="$(mktemp -d)"
|
|
mkdir -p "$work/node" "$work/vms" "$work/networks" "$(dirname "$out")"
|
|
cp -a "$CONFIG_DIR" "$work/node/config" 2>/dev/null || true
|
|
cp -a "${DATA_DIR}/network" "$work/node/network" 2>/dev/null || true
|
|
cp -a /etc/virtuality "$work/node/etc-virtuality" 2>/dev/null || true
|
|
cp -a "$BRIDGE_FILE" "$work/node/" 2>/dev/null || true
|
|
cp -a /etc/default/libvirt-guests "$work/node/" 2>/dev/null || true
|
|
cat "${SOURCE_DIR}/VERSION" > "$work/VERSION" 2>/dev/null || true
|
|
if command -v virsh >/dev/null 2>&1; then
|
|
local name
|
|
while read -r name; do
|
|
[[ -n "$name" ]] && virsh dumpxml --migratable "$name" > "$work/vms/${name}.xml" 2>/dev/null || true
|
|
done < <(virsh list --all --name 2>/dev/null)
|
|
while read -r name; do
|
|
[[ -n "$name" ]] && virsh net-dumpxml "$name" > "$work/networks/${name}.xml" 2>/dev/null || true
|
|
done < <(virsh net-list --all --name 2>/dev/null)
|
|
fi
|
|
tar -czf "$out" -C "$work" .
|
|
rm -rf "$work"
|
|
chmod 600 "$out"
|
|
log "config backup written to ${out}"
|
|
echo "$out"
|
|
}
|
|
|
|
cmd_restore_config() {
|
|
need_root restore-config
|
|
local file="${1:-}"
|
|
[[ -f "$file" ]] || die "usage: virtuality-ctl restore-config FILE.tar.gz"
|
|
local work
|
|
work="$(mktemp -d)"
|
|
tar -xzf "$file" -C "$work" || die "архив повреждён"
|
|
[[ -d "$work/node/config" ]] || die "в архиве нет настроек Virtuality"
|
|
mkdir -p "$CONFIG_DIR" "${DATA_DIR}/network" /etc/virtuality
|
|
cp -a "$work/node/config/." "$CONFIG_DIR/"
|
|
[[ -d "$work/node/network" ]] && cp -a "$work/node/network/." "${DATA_DIR}/network/"
|
|
[[ -d "$work/node/etc-virtuality" ]] && cp -a "$work/node/etc-virtuality/." /etc/virtuality/
|
|
[[ -f "$work/node/libvirt-guests" ]] && cp -a "$work/node/libvirt-guests" /etc/default/libvirt-guests
|
|
if command -v virsh >/dev/null 2>&1; then
|
|
local xml
|
|
for xml in "$work"/networks/*.xml; do
|
|
[[ -f "$xml" ]] || continue
|
|
virsh net-define "$xml" >/dev/null 2>&1 || true
|
|
virsh net-autostart "$(basename "$xml" .xml)" >/dev/null 2>&1 || true
|
|
virsh net-start "$(basename "$xml" .xml)" >/dev/null 2>&1 || true
|
|
done
|
|
for xml in "$work"/vms/*.xml; do
|
|
[[ -f "$xml" ]] || continue
|
|
virsh define "$xml" >/dev/null 2>&1 || echo "Машина $(basename "$xml" .xml): описание восстановлено с ошибкой (диски отсутствуют?)"
|
|
done
|
|
fi
|
|
rm -rf "$work"
|
|
log "config restored from ${file}"
|
|
if [[ -d "$APP_DIR" ]]; then
|
|
cmd_reconfigure
|
|
fi
|
|
echo "Настройки восстановлены"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- updates
|
|
cmd_rollback() {
|
|
need_root rollback
|
|
[[ -d "${APP_DIR}.prev" ]] || die "предыдущей версии панели нет"
|
|
rm -rf "${APP_DIR}.broken"
|
|
mv "$APP_DIR" "${APP_DIR}.broken"
|
|
mv "${APP_DIR}.prev" "$APP_DIR"
|
|
systemctl restart virtuality-web.service
|
|
log "rolled back to previous panel version"
|
|
echo "Возвращена предыдущая версия панели. Сломанная сохранена в ${APP_DIR}.broken"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- maintenance
|
|
cmd_maintenance() {
|
|
need_root maintenance
|
|
local ops="${LOG_DIR}/operations"
|
|
if [[ -d "$ops" ]]; then
|
|
find "$ops" -type f -mtime +30 -delete 2>/dev/null || true
|
|
# keep at most the 300 newest operations
|
|
ls -t "$ops"/*.json 2>/dev/null | tail -n +301 | while read -r f; do rm -f "$f" "${f%.json}.log"; done
|
|
fi
|
|
find "${DATA_DIR}/tmp" -mindepth 1 -mtime +1 -delete 2>/dev/null || true
|
|
find "${DATA_DIR}/iso" "${DATA_DIR}/disk-images" -maxdepth 1 -name '.*.uploading' -mtime +1 -delete 2>/dev/null || true
|
|
find "${DATA_DIR}/iso" "${DATA_DIR}/disk-images" -maxdepth 1 -name '.*.part' -mtime +1 -delete 2>/dev/null || true
|
|
find "$LOG_DIR" -maxdepth 1 -name 'install_*.log' -mtime +30 -delete 2>/dev/null || true
|
|
journalctl --vacuum-size=300M >/dev/null 2>&1 || true
|
|
local used
|
|
used="$(df -P "$DATA_DIR" | awk 'NR==2 {gsub("%","",$5); print $5}')"
|
|
mkdir -p "$CONFIG_DIR"
|
|
printf '{"disk_used_pct": %s, "checked_at": "%s"}\n' "${used:-0}" "$(now)" > "${CONFIG_DIR}/health.json"
|
|
log "maintenance done, disk used ${used}%"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- network bridge
|
|
cmd_bridge() {
|
|
need_root bridge
|
|
local iface="${1:-}" mode="${2:-dhcp}"
|
|
[[ -n "$iface" ]] || die "usage: virtuality-ctl bridge <iface> [dhcp|static]"
|
|
ip link show "$iface" >/dev/null 2>&1 || die "интерфейс $iface не найден"
|
|
[[ -d "/sys/class/net/${iface}/wireless" ]] && die "Wi-Fi нельзя объединить в мост; используйте режим NAT"
|
|
command -v netplan >/dev/null 2>&1 || die "netplan не найден"
|
|
local mac ip gw
|
|
mac="$(cat "/sys/class/net/${iface}/address")"
|
|
ip="$(ip -4 addr show "$iface" | awk '/inet / {print $2; exit}')"
|
|
gw="$(ip route show default | awk '/default/ {print $3; exit}')"
|
|
|
|
rm -rf "$NETPLAN_BACKUP"
|
|
mkdir -p "$NETPLAN_BACKUP"
|
|
cp -a "$NETPLAN_DIR"/*.yaml "$NETPLAN_BACKUP"/ 2>/dev/null || true
|
|
# Neutralise the interface in existing files so it is not configured twice.
|
|
local file
|
|
for file in "$NETPLAN_DIR"/*.yaml; do
|
|
[[ -f "$file" && "$file" != "$BRIDGE_FILE" ]] || continue
|
|
if grep -q "^\s*${iface}:" "$file"; then
|
|
cat > "$file" <<EOF
|
|
network:
|
|
version: 2
|
|
ethernets:
|
|
${iface}:
|
|
dhcp4: false
|
|
dhcp6: false
|
|
EOF
|
|
fi
|
|
done
|
|
{
|
|
echo "network:"
|
|
echo " version: 2"
|
|
echo " ethernets:"
|
|
echo " ${iface}:"
|
|
echo " dhcp4: false"
|
|
echo " dhcp6: false"
|
|
echo " bridges:"
|
|
echo " br0:"
|
|
echo " interfaces: [${iface}]"
|
|
echo " macaddress: \"${mac}\""
|
|
echo " parameters:"
|
|
echo " stp: false"
|
|
echo " forward-delay: 0"
|
|
if [[ "$mode" == "static" ]]; then
|
|
[[ -n "$ip" && -n "$gw" ]] || die "не удалось определить текущий адрес и шлюз"
|
|
echo " addresses: [${ip}]"
|
|
echo " routes:"
|
|
echo " - to: default"
|
|
echo " via: ${gw}"
|
|
echo " nameservers:"
|
|
echo " addresses: [${gw}, 1.1.1.1]"
|
|
else
|
|
echo " dhcp4: true"
|
|
echo " dhcp-identifier: mac"
|
|
echo " dhcp6: false"
|
|
fi
|
|
} > "$BRIDGE_FILE"
|
|
chmod 600 "$NETPLAN_DIR"/*.yaml 2>/dev/null || true
|
|
netplan generate || { cmd_bridge_revert; die "конфигурация сети не прошла проверку, изменения отменены"; }
|
|
# Auto-revert unless confirmed from the panel within two minutes.
|
|
systemctl stop "${REVERT_UNIT}.timer" >/dev/null 2>&1 || true
|
|
systemd-run --unit="$REVERT_UNIT" --on-active=120 --quiet /usr/local/bin/virtuality-ctl bridge-revert
|
|
netplan apply
|
|
log "bridge br0 on ${iface} applied (${mode}); auto-revert armed"
|
|
echo "Мост br0 включён. Подтвердите в течение 2 минут: virtuality-ctl bridge-confirm"
|
|
}
|
|
|
|
cmd_bridge_confirm() {
|
|
need_root bridge-confirm
|
|
systemctl stop "${REVERT_UNIT}.timer" >/dev/null 2>&1 || true
|
|
systemctl stop "${REVERT_UNIT}.service" >/dev/null 2>&1 || true
|
|
rm -rf "$NETPLAN_BACKUP"
|
|
log "bridge confirmed"
|
|
echo "Сеть подтверждена"
|
|
}
|
|
|
|
cmd_bridge_revert() {
|
|
need_root bridge-revert
|
|
systemctl stop "${REVERT_UNIT}.timer" >/dev/null 2>&1 || true
|
|
[[ -d "$NETPLAN_BACKUP" ]] || die "нечего откатывать"
|
|
rm -f "$NETPLAN_DIR"/*.yaml
|
|
cp -a "$NETPLAN_BACKUP"/*.yaml "$NETPLAN_DIR"/ 2>/dev/null || true
|
|
chmod 600 "$NETPLAN_DIR"/*.yaml 2>/dev/null || true
|
|
netplan generate && netplan apply
|
|
ip link delete br0 >/dev/null 2>&1 || true
|
|
rm -rf "$NETPLAN_BACKUP"
|
|
log "bridge reverted"
|
|
echo "Прежняя сеть восстановлена"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- storage
|
|
cmd_storage_use() {
|
|
need_root storage-use
|
|
local dev="${1:-}"
|
|
[[ "$dev" =~ ^/dev/[a-zA-Z0-9]+$ ]] || die "usage: virtuality-ctl storage-use /dev/sdX"
|
|
[[ -b "$dev" ]] || die "$dev не является блочным устройством"
|
|
local children fstype mounted
|
|
children="$(lsblk -n -o NAME "$dev" | tail -n +2 | wc -l)"
|
|
fstype="$(lsblk -n -d -o FSTYPE "$dev")"
|
|
mounted="$(lsblk -n -o MOUNTPOINT "$dev" | tr -d ' \n')"
|
|
[[ "$children" -eq 0 && -z "$fstype" && -z "$mounted" ]] || die "на $dev уже есть разделы или данные — используйте только пустой диск"
|
|
findmnt -n --target "$DATA_DIR" -o SOURCE | grep -q "^${dev}" && die "$dev уже используется"
|
|
log "storage-use ${dev}: formatting"
|
|
wipefs -a "$dev" >/dev/null
|
|
mkfs.ext4 -F -q -L virtuality "$dev" >/dev/null || die "не удалось создать файловую систему"
|
|
local uuid tmp
|
|
uuid="$(blkid -s UUID -o value "$dev")"
|
|
[[ -n "$uuid" ]] || die "не удалось прочитать UUID"
|
|
tmp="$(mktemp -d)"
|
|
mount "$dev" "$tmp" || die "не удалось смонтировать диск"
|
|
mkdir -p "$DATA_DIR"
|
|
rsync -a "$DATA_DIR"/ "$tmp"/ || { umount "$tmp"; die "не удалось перенести данные"; }
|
|
umount "$tmp"; rmdir "$tmp"
|
|
sed -i "\#[[:space:]]${DATA_DIR}[[:space:]]#d" /etc/fstab
|
|
printf 'UUID=%s %s ext4 defaults,nofail 0 2\n' "$uuid" "$DATA_DIR" >> /etc/fstab
|
|
systemctl daemon-reload
|
|
mount "$DATA_DIR" || die "не удалось смонтировать ${DATA_DIR}"
|
|
chmod 755 "$DATA_DIR"
|
|
mkdir -p "$DATA_DIR"/{iso,images,disk-images,backups,tmp,network,config}
|
|
chmod 1777 "$DATA_DIR/tmp"
|
|
command -v virsh >/dev/null 2>&1 && { virsh pool-refresh virtuality-images >/dev/null 2>&1 || true; virsh pool-refresh virtuality-iso >/dev/null 2>&1 || true; }
|
|
log "storage-use ${dev}: mounted at ${DATA_DIR}"
|
|
echo "Диск ${dev} подключён как ${DATA_DIR}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- support
|
|
cmd_support_bundle() {
|
|
need_root support-bundle
|
|
local out="${1:-${DATA_DIR}/tmp/virtuality-support-$(date +%Y%m%d-%H%M).tar.gz}"
|
|
local work
|
|
work="$(mktemp -d)"
|
|
mkdir -p "$(dirname "$out")"
|
|
{
|
|
echo "Virtuality $(cat "${SOURCE_DIR}/VERSION" 2>/dev/null || echo unknown)"
|
|
echo "Host: $(hostname) $(uname -r)"; date
|
|
echo; echo "== web.env"; sed 's/SECRET=.*/SECRET=***/' "$NODE_ENV" 2>/dev/null
|
|
echo; echo "== services"; systemctl --no-pager --plain list-units 'virtuality-*' 'libvirtd*' 2>/dev/null
|
|
echo; echo "== virsh"; virsh list --all 2>&1; virsh net-list --all 2>&1; virsh pool-list --all 2>&1
|
|
echo; echo "== network"; ip -br a 2>&1; ip route 2>&1
|
|
echo; echo "== firewall"; ufw status 2>&1; nft list ruleset 2>&1 | head -200
|
|
echo; echo "== disk"; df -h / "$DATA_DIR" 2>&1
|
|
} > "$work/summary.txt"
|
|
journalctl -u virtuality-web -n 500 --no-pager > "$work/virtuality-web.log" 2>&1 || true
|
|
journalctl -u libvirtd -n 200 --no-pager > "$work/libvirtd.log" 2>&1 || true
|
|
cp "${LOG_DIR}"/update.log "${LOG_DIR}"/firstboot.log "${LOG_DIR}"/virtuality-ctl.log "$work"/ 2>/dev/null || true
|
|
mkdir -p "$work/operations"
|
|
ls -t "${LOG_DIR}/operations"/*.json 2>/dev/null | head -20 | while read -r f; do cp "$f" "${f%.json}.log" "$work/operations/" 2>/dev/null || true; done
|
|
tar -czf "$out" -C "$work" .
|
|
rm -rf "$work"
|
|
echo "$out"
|
|
}
|
|
|
|
cmd_status() {
|
|
echo "Virtuality $(cat "${SOURCE_DIR}/VERSION" 2>/dev/null || echo unknown)"
|
|
echo "Панель: $(panel_url) ($(systemctl is-active virtuality-web.service 2>/dev/null || echo unknown))"
|
|
echo "Пользователь: $(env_get VIRTUALITY_AUTH_USER root)"
|
|
echo "HTTPS: $([[ "$(env_get VIRTUALITY_TLS 0)" == "1" ]] && echo включён || echo выключен)"
|
|
echo "Автообновление: $([[ "$(env_get VIRTUALITY_AUTO_UPDATE 1)" == "1" ]] && echo "включено ($(env_get VIRTUALITY_UPDATE_CHANNEL stable))" || echo выключено)"
|
|
echo "Часовой пояс: $(timedatectl show -p Timezone --value 2>/dev/null || echo unknown)"
|
|
echo "Хранилище: $(df -h "$DATA_DIR" 2>/dev/null | awk 'NR==2 {print $4" свободно из "$2}')"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- uninstall
|
|
cmd_uninstall() {
|
|
need_root uninstall
|
|
local purge=0 yes=0 arg
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--purge) purge=1 ;;
|
|
--yes|-y) yes=1 ;;
|
|
*) die "unknown option: $arg" ;;
|
|
esac
|
|
done
|
|
if [[ "$yes" -ne 1 ]]; then
|
|
echo "Будут удалены панель, службы и настройки Virtuality."
|
|
[[ "$purge" -eq 1 ]] && echo "Режим --purge: также удаляются ВСЕ машины, диски, образы и резервные копии."
|
|
read -r -p "Продолжить? Введите YES: " answer
|
|
[[ "$answer" == "YES" ]] || { echo "Отменено."; exit 0; }
|
|
fi
|
|
local unit
|
|
for unit in virtuality-web.service virtuality-redirect.service virtuality-network.service virtuality-auto-update.timer virtuality-auto-update.service virtuality-maintenance.timer virtuality-maintenance.service virtuality-console-dashboard.service virtuality-firstboot.service; do
|
|
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
|
rm -f "${UNIT_DIR}/${unit}"
|
|
done
|
|
systemctl enable --now getty@tty1.service >/dev/null 2>&1 || true
|
|
systemctl daemon-reload
|
|
nft delete table ip virtuality >/dev/null 2>&1 || true
|
|
if command -v ufw >/dev/null 2>&1 && [[ -f "$STATE_FILE" ]]; then
|
|
while read -r old; do [[ -n "$old" ]] && ufw delete allow "${old}/tcp" >/dev/null 2>&1 || true; done < "$STATE_FILE"
|
|
fi
|
|
rm -rf /opt/virtuality /etc/virtuality /etc/logrotate.d/virtuality /etc/sysctl.d/98-virtuality-rpfilter.conf /etc/sysctl.d/99-virtuality-forward.conf
|
|
rm -f /usr/local/bin/vhealth /usr/local/bin/virtuality-console-dashboard /etc/issue.d/virtuality.issue
|
|
if [[ "$purge" -eq 1 ]]; then
|
|
if command -v virsh >/dev/null 2>&1; then
|
|
local name
|
|
while read -r name; do
|
|
[[ -n "$name" ]] || continue
|
|
virsh destroy "$name" >/dev/null 2>&1 || true
|
|
virsh undefine "$name" --remove-all-storage --nvram --managed-save --snapshots-metadata >/dev/null 2>&1 || true
|
|
done < <(virsh list --all --name 2>/dev/null)
|
|
virsh net-destroy virtuality-nat >/dev/null 2>&1 || true
|
|
virsh net-undefine virtuality-nat >/dev/null 2>&1 || true
|
|
fi
|
|
rm -rf "$DATA_DIR" "$LOG_DIR"
|
|
else
|
|
rm -rf "$CONFIG_DIR" "${DATA_DIR}/update" "${DATA_DIR}/tmp"
|
|
echo "Машины, образы и резервные копии сохранены в ${DATA_DIR}"
|
|
fi
|
|
rm -f /usr/local/bin/virtuality-ctl
|
|
echo "Virtuality удалена"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
status) cmd_status ;;
|
|
url) panel_url; echo ;;
|
|
reconfigure) cmd_reconfigure ;;
|
|
set) shift; cmd_set "$@" ;;
|
|
passwd) shift; cmd_passwd "$@" ;;
|
|
timezone) shift; cmd_timezone "$@" ;;
|
|
power) shift; cmd_power "$@" ;;
|
|
backup-config) shift; cmd_backup_config "$@" ;;
|
|
restore-config) shift; cmd_restore_config "$@" ;;
|
|
rollback) cmd_rollback ;;
|
|
maintenance) cmd_maintenance ;;
|
|
bridge) shift; cmd_bridge "$@" ;;
|
|
bridge-confirm) cmd_bridge_confirm ;;
|
|
bridge-revert) cmd_bridge_revert ;;
|
|
storage-use) shift; cmd_storage_use "$@" ;;
|
|
support-bundle) shift; cmd_support_bundle "$@" ;;
|
|
uninstall) shift; cmd_uninstall "$@" ;;
|
|
-h|--help|help|"") usage ;;
|
|
*) usage >&2; die "неизвестная команда: $1" ;;
|
|
esac
|