Сравнить коммиты
2
Коммитов
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
c04e5285e7 | ||
|
|
5d02cc81ff |
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
**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
|
||||
git clone https://git.stackworks.ru/viktor/virtuality.git
|
||||
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).
|
||||
@@ -166,7 +166,7 @@ sudo VIRTUALITY_AUTH_USER=admin bash scripts/install_web_panel.sh # панел
|
||||
**Машины**
|
||||
|
||||
- Создание из установочного 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-ключ. Пароль хранится только как хеш. При первой загрузке в машину ставится гостевой агент, поэтому панель видит её адрес в любой сети и выключает её корректно.
|
||||
- Запуск, выключение, перезагрузка, принудительное выключение, автозапуск, удаление вместе с дисками и пробросами портов (работающая машина сначала выключается, установочные ISO остаются).
|
||||
- Пауза и **спящий режим** (память сохраняется на диск сервера, после запуска машина продолжает с того же места).
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.12.6
|
||||
0.12.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/`:
|
||||
|
||||
```text
|
||||
dist/virtuality-0.12.6-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
|
||||
dist/virtuality-0.12.7-ubuntu-26.04-amd64.iso.sha256
|
||||
```
|
||||
|
||||
Скачанный 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:
|
||||
|
||||
@@ -161,14 +161,14 @@ cat /var/log/virtuality/firstboot.log
|
||||
Сверьте контрольную сумму скачанного или собранного образа:
|
||||
|
||||
```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):
|
||||
|
||||
```bash
|
||||
xorriso -indev virtuality-0.12.6-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 -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.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`):
|
||||
|
||||
```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**.
|
||||
|
||||
@@ -102,7 +102,7 @@ def test_dashboard_uses_plain_language(logged_in):
|
||||
def test_vm_create_preselects_image(logged_in, data_dirs):
|
||||
(data_dirs["iso"] / "debian.iso").write_bytes(b"iso")
|
||||
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):
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Quick machines from cloud images (cloud-init), OS types for installers, fixed NAT addresses."""
|
||||
import shlex
|
||||
|
||||
import pytest
|
||||
|
||||
import app
|
||||
import catalog
|
||||
import cloudinit
|
||||
import conftest
|
||||
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()))])
|
||||
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")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- тип системы по установочному 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
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
{
|
||||
"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",
|
||||
"date": "2026-09-27",
|
||||
|
||||
+15
-1
@@ -553,6 +553,19 @@ def parse_pool_list() -> list[dict[str, str]]:
|
||||
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]]:
|
||||
ISO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
files = []
|
||||
@@ -564,7 +577,7 @@ def list_iso_files() -> list[dict[str, str]]:
|
||||
except OSError:
|
||||
size_mb = 0
|
||||
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
|
||||
|
||||
|
||||
@@ -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}
|
||||
if iso:
|
||||
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:
|
||||
form["disk_image_path"] = str(DISK_IMAGES_DIR / Path(image).name)
|
||||
return vm_form_context(request, form=form)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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.
|
||||
"""
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
# 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:
|
||||
return next((item for item in CLOUD_IMAGES if item["id"] == image_id), None)
|
||||
|
||||
|
||||
@@ -478,8 +478,24 @@
|
||||
$$('[data-create-button]').forEach((btn) => { btn.disabled = !hasSource || !value('name'); });
|
||||
$$('[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('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();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
<article class="card step-card">
|
||||
<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>
|
||||
{% if isos %}
|
||||
<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>
|
||||
<span class="hint">Нужен другой? <a href="/iso">Загрузите образ</a>.</span>
|
||||
{% endif %}
|
||||
@@ -65,6 +65,7 @@
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="hint" data-os-hint hidden>{{ icon('sparkles', 'icon-sm') }} Это установщик Windows, тип выбран сам. Если ошиблись, выберите другой.</span>
|
||||
<span class="hint">От этого зависят виртуальные устройства: Windows получит совместимые диск и сеть, Windows 11 — UEFI и TPM.</span>
|
||||
</div>
|
||||
<div class="field" data-show-when="source_type=disk_image">
|
||||
|
||||
Ссылка в новой задаче
Block a user