#!/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 < [options] status Что установлено, порты, адреса панели url Адрес панели управления reconfigure Пересоздать systemd-юниты из ${NODE_ENV} и перезапустить панель set KEY=VALUE ... Изменить настройку в web.env (например set VIRTUALITY_TLS=1) и применить passwd [USER] Сменить пароль пользователя панели (пароль читается со стандартного ввода) timezone Установить часовой пояс сервера power reboot|poweroff Перезагрузить или выключить сервер (машины выключаются корректно) backup-config [FILE] Сохранить настройки ноды и описания машин в архив restore-config FILE Восстановить настройки ноды из архива rollback Вернуть предыдущую версию панели после неудачного обновления maintenance Ежедневное обслуживание: старые задачи, временные файлы, свободное место bridge [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" < "${UNIT_DIR}/virtuality-redirect.service" < "${UNIT_DIR}/virtuality-network.service" < "${UNIT_DIR}/virtuality-auto-update.service" < "${UNIT_DIR}/virtuality-auto-update.timer" < "${UNIT_DIR}/virtuality-maintenance.service" < "${UNIT_DIR}/virtuality-maintenance.timer" < /etc/logrotate.d/virtuality < /etc/default/libvirt-guests </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 restart_needed=0 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" [[ "$key" == VIRTUALITY_AUTO_UPDATE || "$key" == VIRTUALITY_UPDATE_CHANNEL ]] || restart_needed=1 done if [[ "$restart_needed" == "1" ]]; then cmd_reconfigure else # Update settings only touch the timer: no need to restart the panel. need_root set write_units log "settings updated without restart: $*" echo "Настройки сохранены" fi } # ---------------------------------------------------------------- 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 [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" < "$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