2 Коммитов
Автор SHA1 Сообщение Дата
viktor c04e5285e7 Версия была обновлена до 0.12.7
CI / Tests (Python 3.11) (push) Canceled after 0s
CI / Tests (Python 3.12) (push) Canceled after 0s
CI / Tests (Python 3.13) (push) Canceled after 0s
CI / Tests (Python 3.14) (push) Canceled after 0s
CI / Shell scripts (push) Canceled after 0s
Build ISO image / ISO ${{ fromJSON(github.event_name == 'workflow_dispatch' && (inputs.arch == 'both' && '["amd64","arm64"]' || format('["{0}"]', inputs.arch)) || '["amd64","arm64"]') }} (push) Canceled after 0s
2026-09-27 21:24:19 +09:00
viktor 5d02cc81ff Установщик Windows узнаётся по метке ISO и имени файла: форма создания машины сама выбирает тип Windows, иначе диск был virtio и установщик его не видел 2026-09-27 21:24:19 +09:00
10 изменённых файлов: 114 добавлений и 16 удалений
+3 -3
Просмотреть файл
@@ -2,7 +2,7 @@
**Virtuality** — простая замена Proxmox для дома и малого офиса. Один сервер, виртуальные машины и понятная панель в браузере на русском языке. Внутри — проверенные KVM, QEMU и libvirt, снаружи — интерфейс, в котором не нужна командная строка. **Virtuality** — простая замена Proxmox для дома и малого офиса. Один сервер, виртуальные машины и понятная панель в браузере на русском языке. Внутри — проверенные KVM, QEMU и libvirt, снаружи — интерфейс, в котором не нужна командная строка.
Текущая версия: **0.12.6** (файл `VERSION`, журнал изменений — `updates/versions.json`). Текущая версия: **0.12.7** (файл `VERSION`, журнал изменений — `updates/versions.json`).
Что умеет: Что умеет:
@@ -76,7 +76,7 @@
sudo apt install -y xorriso git curl python3-pip sudo apt install -y xorriso git curl python3-pip
git clone https://git.stackworks.ru/viktor/virtuality.git git clone https://git.stackworks.ru/viktor/virtuality.git
cd virtuality cd virtuality
make iso # dist/virtuality-0.12.6-ubuntu-26.04-amd64.iso make iso # dist/virtuality-0.12.7-ubuntu-26.04-amd64.iso
``` ```
Дальше: записать образ на флешку (Rufus, balenaEtcher или `dd`), загрузиться с неё, выбрать **Установить Virtuality**, ответить на три экрана установщика и дождаться перезагрузки. Через минуту после неё панель уже открывается в браузере — адрес показан на экране сервера. Все подробности, параметры сборки и полностью автоматическая установка — в [docs/IMAGE.md](docs/IMAGE.md). Дальше: записать образ на флешку (Rufus, balenaEtcher или `dd`), загрузиться с неё, выбрать **Установить Virtuality**, ответить на три экрана установщика и дождаться перезагрузки. Через минуту после неё панель уже открывается в браузере — адрес показан на экране сервера. Все подробности, параметры сборки и полностью автоматическая установка — в [docs/IMAGE.md](docs/IMAGE.md).
@@ -166,7 +166,7 @@ sudo VIRTUALITY_AUTH_USER=admin bash scripts/install_web_panel.sh # панел
**Машины** **Машины**
- Создание из установочного ISO: три шага — название и система, мощность (готовые профили «Мини», «Стандарт», «Мощная» или свои значения), сеть. - Создание из установочного ISO: три шага — название и система, мощность (готовые профили «Мини», «Стандарт», «Мощная» или свои значения), сеть.
- Тип системы для ISO: Linux, **Windows 11 / Server 2025** (UEFI и TPM 2.0 включаются сами), Windows 10 / Server 2019–2022, другая. Для Windows подбираются диск SATA и сетевая карта Intel — драйверы не нужны. - Тип системы для ISO (установщик Windows узнаётся по образу и выбирается сам): Linux, **Windows 11 / Server 2025** (UEFI и TPM 2.0 включаются сами), Windows 10 / Server 2019–2022, другая. Для Windows подбираются диск SATA и сетевая карта Intel — драйверы не нужны.
- Создание из готового образа за минуту: образ конвертируется в qcow2, диск увеличивается до нужного размера, а cloud-init настраивает первый вход — пользователя, пароль и/или SSH-ключ. Пароль хранится только как хеш. При первой загрузке в машину ставится гостевой агент, поэтому панель видит её адрес в любой сети и выключает её корректно. - Создание из готового образа за минуту: образ конвертируется в qcow2, диск увеличивается до нужного размера, а cloud-init настраивает первый вход — пользователя, пароль и/или SSH-ключ. Пароль хранится только как хеш. При первой загрузке в машину ставится гостевой агент, поэтому панель видит её адрес в любой сети и выключает её корректно.
- Запуск, выключение, перезагрузка, принудительное выключение, автозапуск, удаление вместе с дисками и пробросами портов (работающая машина сначала выключается, установочные ISO остаются). - Запуск, выключение, перезагрузка, принудительное выключение, автозапуск, удаление вместе с дисками и пробросами портов (работающая машина сначала выключается, установочные ISO остаются).
- Пауза и **спящий режим** (память сохраняется на диск сервера, после запуска машина продолжает с того же места). - Пауза и **спящий режим** (память сохраняется на диск сервера, после запуска машина продолжает с того же места).
+1 -1
Просмотреть файл
@@ -1 +1 @@
0.12.6 0.12.7
+7 -7
Просмотреть файл
@@ -44,8 +44,8 @@ make iso ARCH=arm64 # ARM64-серверы с UEFI
`image/build-iso.sh` сам находит последний Ubuntu 26.04.x live-server ISO, скачивает его (около 3 ГБ), проверяет SHA256 и кладёт результат в `dist/`: `image/build-iso.sh` сам находит последний Ubuntu 26.04.x live-server ISO, скачивает его (около 3 ГБ), проверяет SHA256 и кладёт результат в `dist/`:
```text ```text
dist/virtuality-0.12.6-ubuntu-26.04-amd64.iso dist/virtuality-0.12.7-ubuntu-26.04-amd64.iso
dist/virtuality-0.12.6-ubuntu-26.04-amd64.iso.sha256 dist/virtuality-0.12.7-ubuntu-26.04-amd64.iso.sha256
``` ```
Скачанный ISO Ubuntu кешируется в `.cache/iso` (или в `VIRTUALITY_ISO_CACHE`). Уже скачанный образ можно передать явно: Скачанный ISO Ubuntu кешируется в `.cache/iso` (или в `VIRTUALITY_ISO_CACHE`). Уже скачанный образ можно передать явно:
@@ -109,7 +109,7 @@ make iso ISO_ARGS="--unattended --username sysadmin --password-hash '$(openssl p
## Что спрашивает установщик ## Что спрашивает установщик
Меню загрузки: **Установить Virtuality 0.12.6** (выбирается сам через 10 секунд), тот же пункт с новым ядром HWE, загрузка с другого диска, настройки UEFI, проверка памяти. Меню загрузки: **Установить Virtuality 0.12.7** (выбирается сам через 10 секунд), тот же пункт с новым ядром HWE, загрузка с другого диска, настройки UEFI, проверка памяти.
Сценарий `image/autoinstall.yaml` оставляет интерактивными только три экрана установщика Ubuntu: Сценарий `image/autoinstall.yaml` оставляет интерактивными только три экрана установщика Ubuntu:
@@ -161,14 +161,14 @@ cat /var/log/virtuality/firstboot.log
Сверьте контрольную сумму скачанного или собранного образа: Сверьте контрольную сумму скачанного или собранного образа:
```bash ```bash
sha256sum -c virtuality-0.12.6-ubuntu-26.04-amd64.iso.sha256 sha256sum -c virtuality-0.12.7-ubuntu-26.04-amd64.iso.sha256
``` ```
Убедиться, что образ загрузочный и содержит нужные файлы (то же делает CI): Убедиться, что образ загрузочный и содержит нужные файлы (то же делает CI):
```bash ```bash
xorriso -indev virtuality-0.12.6-ubuntu-26.04-amd64.iso -report_el_torito plain 2>/dev/null | grep UEFI xorriso -indev virtuality-0.12.7-ubuntu-26.04-amd64.iso -report_el_torito plain 2>/dev/null | grep UEFI
xorriso -osirrox on -indev virtuality-0.12.6-ubuntu-26.04-amd64.iso -extract /autoinstall.yaml autoinstall.yaml xorriso -osirrox on -indev virtuality-0.12.7-ubuntu-26.04-amd64.iso -extract /autoinstall.yaml autoinstall.yaml
``` ```
--- ---
@@ -182,7 +182,7 @@ xorriso -osirrox on -indev virtuality-0.12.6-ubuntu-26.04-amd64.iso -extract /au
- **Linux**: balenaEtcher или `dd`, где `/dev/sdX` — устройство флешки (проверьте через `lsblk`): - **Linux**: balenaEtcher или `dd`, где `/dev/sdX` — устройство флешки (проверьте через `lsblk`):
```bash ```bash
sudo dd if=dist/virtuality-0.12.6-ubuntu-26.04-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync sudo dd if=dist/virtuality-0.12.7-ubuntu-26.04-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync
``` ```
Загрузитесь с флешки (меню загрузки — обычно F8, F11, F12 или Esc при включении), выберите **Установить Virtuality**. Загрузитесь с флешки (меню загрузки — обычно F8, F11, F12 или Esc при включении), выберите **Установить Virtuality**.
+1 -1
Просмотреть файл
@@ -102,7 +102,7 @@ def test_dashboard_uses_plain_language(logged_in):
def test_vm_create_preselects_image(logged_in, data_dirs): def test_vm_create_preselects_image(logged_in, data_dirs):
(data_dirs["iso"] / "debian.iso").write_bytes(b"iso") (data_dirs["iso"] / "debian.iso").write_bytes(b"iso")
html = logged_in.get("/vm/create?iso=debian.iso").text html = logged_in.get("/vm/create?iso=debian.iso").text
assert f'value="{data_dirs["iso"] / "debian.iso"}" data-label="debian.iso" selected' in html assert f'value="{data_dirs["iso"] / "debian.iso"}" data-label="debian.iso" data-os="" selected' in html
def test_vm_create_disk_image_without_disk_size(logged_in, data_dirs, monkeypatch): def test_vm_create_disk_image_without_disk_size(logged_in, data_dirs, monkeypatch):
+37
Просмотреть файл
@@ -1,7 +1,10 @@
"""Quick machines from cloud images (cloud-init), OS types for installers, fixed NAT addresses.""" """Quick machines from cloud images (cloud-init), OS types for installers, fixed NAT addresses."""
import shlex import shlex
import pytest
import app import app
import catalog
import cloudinit import cloudinit
import conftest import conftest
import network_core import network_core
@@ -153,3 +156,37 @@ def test_vm_delete_releases_address(logged_in, data_dirs, monkeypatch):
net_calls = recording(monkeypatch, network_core, [(["virsh", "net-dumpxml"], conftest.fake_result((conftest.FIXTURES / "net-dumpxml.xml").read_text()))]) net_calls = recording(monkeypatch, network_core, [(["virsh", "net-dumpxml"], conftest.fake_result((conftest.FIXTURES / "net-dumpxml.xml").read_text()))])
assert logged_in.post("/vm/web01/delete", follow_redirects=False).status_code == 303 assert logged_in.post("/vm/web01/delete", follow_redirects=False).status_code == 303
assert find(calls, "virsh", "undefine") and find(net_calls, "virsh", "net-update", "virtuality-nat", "delete") assert find(calls, "virsh", "undefine") and find(net_calls, "virsh", "net-update", "virtuality-nat", "delete")
# ---------------------------------------------------------------- тип системы по установочному ISO
def fake_iso(path, label):
"""Минимальный ISO 9660: первичный дескриптор тома с меткой по смещению 0x8028."""
descriptor = bytearray(0x800)
descriptor[0], descriptor[1:6], descriptor[6] = 1, b"CD001", 1
descriptor[0x28:0x48] = label.encode().ljust(32)
path.write_bytes(bytes(0x8000) + bytes(descriptor))
return path
@pytest.mark.parametrize("label, name, expected", [
("19044_4046___WIN10_IOT_LTSC", "19044.4046---Win10-IoT-LTSC.iso", "windows10"),
("CCCOMA_X64FRE_RU-RU_DV9", "Win11_24H2_Russian_x64.iso", "windows11"),
("CCCOMA_X64FRE_EN-US_DV9", "windows.iso", "windows11"), # версия неизвестна: UEFI + TPM подходят и для 10
("SSS_X64FREE_EN-US_DV9", "20348.169.210806-2348.fe_release_SERVER_EVAL_x64FRE_en-us.iso", "windows10"),
("SSS_X64FREV_EN-US_DV9", "26100.1742.240906-0331.ge_release_SERVER_EVAL_x64FRE_en-us.iso", "windows11"),
("Ubuntu-Server 26.04 LTS amd64", "ubuntu-26.04-live-server-amd64.iso", ""),
("", "debian-13.1.0-amd64-netinst.iso", ""),
("smk-server", "smk-server-_1.iso", ""),
])
def test_guess_os_type_from_iso(label, name, expected):
assert catalog.guess_os_type(label, name) == expected
def test_windows_iso_preselects_windows_type(logged_in, data_dirs):
"""Машина с Windows 10 создалась как Linux: диск virtio, установщик Windows его не видел."""
fake_iso(data_dirs["iso"] / "19044.4046---Win10-IoT-LTSC.iso", "19044_4046___WIN10_IOT_LTSC")
fake_iso(data_dirs["iso"] / "ubuntu.iso", "Ubuntu-Server 26.04 LTS amd64")
assert app.iso_volume_label(data_dirs["iso"] / "ubuntu.iso") == "Ubuntu-Server 26.04 LTS amd64"
html = logged_in.get("/vm/create?iso=19044.4046---Win10-IoT-LTSC.iso").text
assert 'data-os="windows10" selected' in html and 'data-label="ubuntu.iso" data-os=""' in html
assert 'value="windows10" checked' in html and 'value="linux" checked' not in html
+11
Просмотреть файл
@@ -1,5 +1,16 @@
{ {
"versions": [ "versions": [
{
"version": "0.12.7",
"date": "2026-09-27",
"title": "Тип Windows выбирается сам по установочному образу",
"summary": "Если в форме создания машины выбран установщик Windows, панель узнаёт его по метке образа и имени файла и сама ставит тип Windows. Раньше по умолчанию оставался Linux, диск подключался как virtio, и установщик Windows его не видел.",
"features": [
"Установщик Windows 10, 11 и Server узнаётся по установочному образу, тип системы выбирается сам",
"Выбор можно поменять вручную, панель его больше не перебивает",
"Версия обновлена до 0.12.7"
]
},
{ {
"version": "0.12.6", "version": "0.12.6",
"date": "2026-09-27", "date": "2026-09-27",
+15 -1
Просмотреть файл
@@ -553,6 +553,19 @@ def parse_pool_list() -> list[dict[str, str]]:
return rows return rows
def iso_volume_label(path: Path) -> str:
"""Метка тома ISO 9660: 32 байта в первичном дескрипторе по смещению 0x8028."""
try:
with path.open("rb") as handle:
handle.seek(0x8000)
descriptor = handle.read(0x48)
except OSError:
return ""
if descriptor[1:6] != b"CD001":
return ""
return descriptor[0x28:0x48].decode("ascii", "replace").strip()
def list_iso_files() -> list[dict[str, str]]: def list_iso_files() -> list[dict[str, str]]:
ISO_DIR.mkdir(parents=True, exist_ok=True) ISO_DIR.mkdir(parents=True, exist_ok=True)
files = [] files = []
@@ -564,7 +577,7 @@ def list_iso_files() -> list[dict[str, str]]:
except OSError: except OSError:
size_mb = 0 size_mb = 0
updated = "unknown" updated = "unknown"
files.append({"name": item.name, "path": str(item), "size": f"{size_mb:.1f} MB", "updated": updated}) files.append({"name": item.name, "path": str(item), "size": f"{size_mb:.1f} MB", "updated": updated, "os_type": catalog.guess_os_type(iso_volume_label(item), item.name)})
return files return files
@@ -1819,6 +1832,7 @@ def vm_create_page(request: Request, iso: str = "", image: str = ""):
form = {"memory": 4096, "vcpus": 2, "disk_size": 40, "source_type": "disk_image" if image else "iso", "boot_order": "auto", "network_mode": profile.get("recommended_network", "nat"), "bridge": DEFAULT_BRIDGE} form = {"memory": 4096, "vcpus": 2, "disk_size": 40, "source_type": "disk_image" if image else "iso", "boot_order": "auto", "network_mode": profile.get("recommended_network", "nat"), "bridge": DEFAULT_BRIDGE}
if iso: if iso:
form["iso_path"] = str(ISO_DIR / Path(iso).name) form["iso_path"] = str(ISO_DIR / Path(iso).name)
form["os_type"] = catalog.guess_os_type(iso_volume_label(ISO_DIR / Path(iso).name), Path(iso).name) or "linux"
if image: if image:
form["disk_image_path"] = str(DISK_IMAGES_DIR / Path(image).name) form["disk_image_path"] = str(DISK_IMAGES_DIR / Path(image).name)
return vm_form_context(request, form=form) return vm_form_context(request, form=form)
+19
Просмотреть файл
@@ -3,6 +3,7 @@
Each entry points to the vendor's official cloud image. Images boot in seconds Each entry points to the vendor's official cloud image. Images boot in seconds
and are configured through cloud-init (user, password, SSH key) — no installer. and are configured through cloud-init (user, password, SSH key) — no installer.
""" """
import re
from typing import Any from typing import Any
# osinfo ids must exist in osinfo-db shipped with Ubuntu 24.04+/Debian 13. # osinfo ids must exist in osinfo-db shipped with Ubuntu 24.04+/Debian 13.
@@ -69,6 +70,24 @@ OS_TYPES: list[dict[str, Any]] = [
] ]
# Установщик Windows узнаём по метке тома ISO (у Microsoft вида CCCOMA_X64FRE_RU-RU_DV9) или по имени файла.
WINDOWS_RE = re.compile(r"\bWIN(?:DOWS)?\s?(?:1[01]|7|8|SERVER)\b|\bWINDOWS\b|\b(?:X64|X86|A64|ARM64)FRE[EV]?\b")
WINDOWS11_RE = re.compile(r"\bWIN(?:DOWS)?\s?11\b|\bSERVER\s?2025\b|\b(?:22000|22621|22631|26100|26200)\b")
WINDOWS10_RE = re.compile(r"\bWIN(?:DOWS)?\s?(?:10|7|8)\b|\bLTSC\b|\bSERVER\s?20(?:16|19|22)\b|\b1\d{4}\b|\b20348\b")
def guess_os_type(volume_label: str, filename: str) -> str:
"""Тип системы для установочного ISO: windows11 / windows10, если это установщик Windows, иначе "".
Windows неизвестной версии — windows11: UEFI и TPM подходят и для Windows 10, а без них не встанет 11."""
text = re.sub(r"[^A-Z0-9]+", " ", f"{volume_label} {filename}".upper())
if not WINDOWS_RE.search(text):
return ""
if WINDOWS11_RE.search(text):
return "windows11"
return "windows10" if WINDOWS10_RE.search(text) else "windows11"
def cloud_image(image_id: str) -> dict[str, Any] | None: def cloud_image(image_id: str) -> dict[str, Any] | None:
return next((item for item in CLOUD_IMAGES if item["id"] == image_id), None) return next((item for item in CLOUD_IMAGES if item["id"] == image_id), None)
+17 -1
Просмотреть файл
@@ -478,8 +478,24 @@
$$('[data-create-button]').forEach((btn) => { btn.disabled = !hasSource || !value('name'); }); $$('[data-create-button]').forEach((btn) => { btn.disabled = !hasSource || !value('name'); });
$$('[data-missing-source]').forEach((el) => { el.hidden = hasSource || el.dataset.missingSource !== source; }); $$('[data-missing-source]').forEach((el) => { el.hidden = hasSource || el.dataset.missingSource !== source; });
} }
// Установщик Windows панель узнаёт по ISO (data-os) и сама выбирает тип, пока пользователь не выбрал его руками.
const guessOs = () => {
const select = field('iso_path');
const option = select && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null;
const guess = option?.dataset.os || '';
const hint = $('[data-os-hint]', form);
if (form.dataset.osManual) return;
const radio = $(`input[name=os_type][value="${guess || 'linux'}"]`, form);
if (radio) radio.checked = true;
if (hint) hint.hidden = !guess;
};
form.addEventListener('input', (event) => { if (event.target.name === 'ci_user') event.target.dataset.autoLogin = event.target.value ? '0' : '1'; sync(); }); form.addEventListener('input', (event) => { if (event.target.name === 'ci_user') event.target.dataset.autoLogin = event.target.value ? '0' : '1'; sync(); });
form.addEventListener('change', sync); form.addEventListener('change', (event) => {
if (event.target.name === 'os_type') { form.dataset.osManual = '1'; const hint = $('[data-os-hint]', form); if (hint) hint.hidden = true; }
if (event.target.name === 'iso_path') guessOs();
sync();
});
guessOs();
sync(); sync();
} }
+3 -2
Просмотреть файл
@@ -16,7 +16,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<form method="post" action="/vm/create" class="grid grid-main" data-wizard data-has-isos="{{ '1' if isos else '0' }}" data-has-disks="{{ '1' if disk_images else '0' }}"> <form method="post" action="/vm/create" class="grid grid-main" data-wizard {% if form.name %}data-os-manual="1"{% endif %} data-has-isos="{{ '1' if isos else '0' }}" data-has-disks="{{ '1' if disk_images else '0' }}">
<div class="stack"> <div class="stack">
<article class="card step-card"> <article class="card step-card">
<div class="step-head"><span class="step-num">1</span><div><h2>Название и система</h2><p>Как назовём машину и что на неё установить</p></div></div> <div class="step-head"><span class="step-num">1</span><div><h2>Название и система</h2><p>Как назовём машину и что на неё установить</p></div></div>
@@ -50,7 +50,7 @@
<label for="iso_path">Установочный образ</label> <label for="iso_path">Установочный образ</label>
{% if isos %} {% if isos %}
<select class="select" id="iso_path" name="iso_path"> <select class="select" id="iso_path" name="iso_path">
{% for iso in isos %}<option value="{{ iso.path }}" data-label="{{ iso.name }}" {% if form.iso_path == iso.path %}selected{% endif %}>{{ iso.name }} · {{ iso.size }}</option>{% endfor %} {% for iso in isos %}<option value="{{ iso.path }}" data-label="{{ iso.name }}" data-os="{{ iso.os_type }}" {% if form.iso_path == iso.path %}selected{% endif %}>{{ iso.name }} · {{ iso.size }}</option>{% endfor %}
</select> </select>
<span class="hint">Нужен другой? <a href="/iso">Загрузите образ</a>.</span> <span class="hint">Нужен другой? <a href="/iso">Загрузите образ</a>.</span>
{% endif %} {% endif %}
@@ -65,6 +65,7 @@
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
<span class="hint" data-os-hint hidden>{{ icon('sparkles', 'icon-sm') }} Это установщик Windows, тип выбран сам. Если ошиблись, выберите другой.</span>
<span class="hint">От этого зависят виртуальные устройства: Windows получит совместимые диск и сеть, Windows 11 — UEFI и TPM.</span> <span class="hint">От этого зависят виртуальные устройства: Windows получит совместимые диск и сеть, Windows 11 — UEFI и TPM.</span>
</div> </div>
<div class="field" data-show-when="source_type=disk_image"> <div class="field" data-show-when="source_type=disk_image">