Создание машины: диск больше свободного места создаётся только с подтверждением, в шаге «Мощность» видно свободное место; раньше установка падала с ошибкой virt-install
Этот коммит содержится в:
@@ -171,6 +171,7 @@ def data_dirs(tmp_path, monkeypatch):
|
|||||||
monkeypatch.setattr(update_core, "STATE_FILE", dirs["update"] / "state.json")
|
monkeypatch.setattr(update_core, "STATE_FILE", dirs["update"] / "state.json")
|
||||||
monkeypatch.setattr(update_core, "LOG_FILE", dirs["update"] / "update.log")
|
monkeypatch.setattr(update_core, "LOG_FILE", dirs["update"] / "update.log")
|
||||||
monkeypatch.setattr(update_core, "SOURCE_DIR", ROOT)
|
monkeypatch.setattr(update_core, "SOURCE_DIR", ROOT)
|
||||||
|
monkeypatch.setattr(app, "images_free_bytes", lambda: 500 * 1024 ** 3) # временная папка тестов бывает маленькой (tmpfs)
|
||||||
for module in (app, core, network_core, host_profile, *feature_modules()):
|
for module in (app, core, network_core, host_profile, *feature_modules()):
|
||||||
monkeypatch.setattr(module, "run_cmd", fake_run_cmd)
|
monkeypatch.setattr(module, "run_cmd", fake_run_cmd)
|
||||||
return dirs
|
return dirs
|
||||||
|
|||||||
@@ -196,3 +196,27 @@ def test_uploader_collects_file_before_disabling_input():
|
|||||||
assert "input.disabled = active" in uploader
|
assert "input.disabled = active" in uploader
|
||||||
assert uploader.index("new FormData(form)") < uploader.index("setActive(true)")
|
assert uploader.index("new FormData(form)") < uploader.index("setActive(true)")
|
||||||
assert "xhr.send(data)" in uploader
|
assert "xhr.send(data)" in uploader
|
||||||
|
|
||||||
|
|
||||||
|
def test_vm_create_disk_bigger_than_free_space_needs_confirmation(logged_in, data_dirs, monkeypatch):
|
||||||
|
"""virt-install отказывал с английской ошибкой; теперь панель предупреждает сама и создаёт только с подтверждением."""
|
||||||
|
iso = data_dirs["iso"] / "smk.iso"
|
||||||
|
iso.write_bytes(b"iso")
|
||||||
|
started = []
|
||||||
|
monkeypatch.setattr(app, "start_background_operation", lambda operation, cmd: started.append(cmd))
|
||||||
|
monkeypatch.setattr(app.network_core, "create_nat_network", lambda: {})
|
||||||
|
monkeypatch.setattr(app, "images_free_bytes", lambda: 25 * 1024 ** 3)
|
||||||
|
form = {"name": "smk", "memory": 4096, "vcpus": 2, "disk_size": 40, "iso_path": str(iso), "source_type": "iso", "network_mode": "nat"}
|
||||||
|
|
||||||
|
page = logged_in.get("/vm/create").text
|
||||||
|
assert "Свободно на сервере: 25" in page and 'name="allow_overcommit"' not in page
|
||||||
|
response = logged_in.post("/vm/create", data=form)
|
||||||
|
assert response.status_code == 400 and started == []
|
||||||
|
assert "Диск на 40 ГБ больше, чем свободно на сервере" in response.text and 'name="allow_overcommit"' in response.text
|
||||||
|
|
||||||
|
response = logged_in.post("/vm/create", data={**form, "allow_overcommit": "1"}, follow_redirects=False)
|
||||||
|
assert response.status_code == 303, response.text[:300]
|
||||||
|
assert started[0][-2:] == ["--check", "disk_size=off"]
|
||||||
|
|
||||||
|
response = logged_in.post("/vm/create", data={**form, "name": "small", "disk_size": 20}, follow_redirects=False)
|
||||||
|
assert response.status_code == 303 and "--check" not in started[1]
|
||||||
|
|||||||
+21
-3
@@ -1273,10 +1273,20 @@ def virt_boot_arg(boot_order: str, is_arm: bool) -> str:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def images_free_bytes() -> int:
|
||||||
|
"""Свободное место там, где лежат диски машин."""
|
||||||
|
for path in (IMAGES_DIR, STORAGE_DIR, Path("/")):
|
||||||
|
try:
|
||||||
|
return shutil.disk_usage(path).free
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def vm_form_context(request: Request, error: str | None = None, form: dict[str, Any] | None = None, status_code: int = 200):
|
def vm_form_context(request: Request, error: str | None = None, form: dict[str, Any] | None = None, status_code: int = 200):
|
||||||
profile = host_profile.load_host_profile()
|
profile = host_profile.load_host_profile()
|
||||||
default_mode = profile.get("recommended_network", "nat")
|
default_mode = profile.get("recommended_network", "nat")
|
||||||
return templates.TemplateResponse(request, "vm_create.html", {"app_name": APP_NAME, "user": AUTH_USER, "isos": list_iso_files(), "disk_images": list_disk_image_files(), "os_types": catalog.OS_TYPES, "boot_options": vm_boot_order_options(), "error": error, "profile": profile, "form": form or {"memory": 4096, "vcpus": 2, "disk_size": 40, "source_type": "iso", "boot_order": "auto", "network_mode": default_mode, "bridge": DEFAULT_BRIDGE}}, status_code=status_code)
|
return templates.TemplateResponse(request, "vm_create.html", {"app_name": APP_NAME, "user": AUTH_USER, "images_free": presenters.format_bytes(images_free_bytes()), "isos": list_iso_files(), "disk_images": list_disk_image_files(), "os_types": catalog.OS_TYPES, "boot_options": vm_boot_order_options(), "error": error, "profile": profile, "form": form or {"memory": 4096, "vcpus": 2, "disk_size": 40, "source_type": "iso", "boot_order": "auto", "network_mode": default_mode, "bridge": DEFAULT_BRIDGE}}, status_code=status_code)
|
||||||
|
|
||||||
|
|
||||||
# Virtuality noVNC console patch
|
# Virtuality noVNC console patch
|
||||||
@@ -1785,12 +1795,16 @@ def vm_create_page(request: Request, iso: str = "", image: str = ""):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/vm/create", response_class=HTMLResponse)
|
@app.post("/vm/create", response_class=HTMLResponse)
|
||||||
def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(20), iso_path: str = Form(""), disk_image_path: str = Form(""), source_type: str = Form("iso"), boot_order: str = Form("auto"), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE), os_type: str = Form("linux"), cloud_init: str = Form("0"), ci_user: str = Form(""), ci_password: str = Form(""), ci_ssh_key: str = Form("")):
|
def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(20), iso_path: str = Form(""), disk_image_path: str = Form(""), source_type: str = Form("iso"), boot_order: str = Form("auto"), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE), os_type: str = Form("linux"), cloud_init: str = Form("0"), ci_user: str = Form(""), ci_password: str = Form(""), ci_ssh_key: str = Form(""), allow_overcommit: str = Form("")):
|
||||||
auth_redirect = require_auth(request)
|
auth_redirect = require_auth(request)
|
||||||
if auth_redirect:
|
if auth_redirect:
|
||||||
return auth_redirect
|
return auth_redirect
|
||||||
ci_user = ci_user.strip()
|
ci_user = ci_user.strip()
|
||||||
form = {"name": name, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "iso_path": iso_path, "disk_image_path": disk_image_path, "source_type": source_type, "boot_order": boot_order, "network_mode": network_mode, "bridge": bridge, "os_type": os_type, "cloud_init": cloud_init, "ci_user": ci_user, "ci_ssh_key": ci_ssh_key}
|
# Диск qcow2 занимает место по мере заполнения, поэтому больше свободного места его создать можно,
|
||||||
|
# но только осознанно: если машина его заполнит, место на сервере кончится у всех машин.
|
||||||
|
free = images_free_bytes()
|
||||||
|
too_big = disk_size * 1024 ** 3 > free
|
||||||
|
form = {"name": name, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "iso_path": iso_path, "disk_image_path": disk_image_path, "source_type": source_type, "boot_order": boot_order, "network_mode": network_mode, "bridge": bridge, "os_type": os_type, "cloud_init": cloud_init, "ci_user": ci_user, "ci_ssh_key": ci_ssh_key, "overcommit": too_big, "allow_overcommit": allow_overcommit == "1"}
|
||||||
error = None
|
error = None
|
||||||
use_cloud_init = source_type == "disk_image" and cloud_init == "1"
|
use_cloud_init = source_type == "disk_image" and cloud_init == "1"
|
||||||
ssh_keys = cloudinit.parse_ssh_keys(ci_ssh_key) if use_cloud_init else []
|
ssh_keys = cloudinit.parse_ssh_keys(ci_ssh_key) if use_cloud_init else []
|
||||||
@@ -1804,6 +1818,8 @@ def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form
|
|||||||
error = "CPU должен быть от 1 до 128 vCPU."
|
error = "CPU должен быть от 1 до 128 vCPU."
|
||||||
elif disk_size < 4 or disk_size > 4096:
|
elif disk_size < 4 or disk_size > 4096:
|
||||||
error = "Диск должен быть от 4 GB до 4096 GB."
|
error = "Диск должен быть от 4 GB до 4096 GB."
|
||||||
|
elif too_big and allow_overcommit != "1":
|
||||||
|
error = f"Диск на {disk_size} ГБ больше, чем свободно на сервере ({presenters.format_bytes(free)}). Уменьшите диск или подтвердите создание в шаге «Мощность»."
|
||||||
elif network_mode not in ("nat", "bridge"):
|
elif network_mode not in ("nat", "bridge"):
|
||||||
error = "Некорректный режим сети."
|
error = "Некорректный режим сети."
|
||||||
elif network_mode == "bridge" and (not bridge or not re.fullmatch(r"[a-zA-Z0-9_.:-]+", bridge)):
|
elif network_mode == "bridge" and (not bridge or not re.fullmatch(r"[a-zA-Z0-9_.:-]+", bridge)):
|
||||||
@@ -1903,6 +1919,8 @@ def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form
|
|||||||
cmd = ["bash", "-lc", "set -euo pipefail; " + "; ".join(steps)]
|
cmd = ["bash", "-lc", "set -euo pipefail; " + "; ".join(steps)]
|
||||||
else:
|
else:
|
||||||
cmd += ["--disk", f"path={disk_path},size={disk_size},format=qcow2,bus={disk_bus}", "--cdrom", iso_path, "--osinfo", os_info["osinfo"], "--network", network_arg, *graphics]
|
cmd += ["--disk", f"path={disk_path},size={disk_size},format=qcow2,bus={disk_bus}", "--cdrom", iso_path, "--osinfo", os_info["osinfo"], "--network", network_arg, *graphics]
|
||||||
|
if too_big:
|
||||||
|
cmd += ["--check", "disk_size=off"] # подтверждено пользователем: virt-install иначе откажется
|
||||||
|
|
||||||
success_message = "Машина создана" + (f". {login_hint}" if login_hint else "") + (f". Адрес в сети машин: {nat_ip}" if nat_ip else "")
|
success_message = "Машина создана" + (f". {login_hint}" if login_hint else "") + (f". Адрес в сети машин: {nat_ip}" if nat_ip else "")
|
||||||
operation_id = str(uuid.uuid4())
|
operation_id = str(uuid.uuid4())
|
||||||
|
|||||||
@@ -122,6 +122,13 @@
|
|||||||
<div class="field"><label for="memory">Память, МБ</label><input class="input" id="memory" type="number" name="memory" min="512" max="262144" step="256" value="{{ form.memory }}" required></div>
|
<div class="field"><label for="memory">Память, МБ</label><input class="input" id="memory" type="number" name="memory" min="512" max="262144" step="256" value="{{ form.memory }}" required></div>
|
||||||
<div class="field"><label for="disk_size">Диск, ГБ</label><input class="input" id="disk_size" type="number" name="disk_size" min="4" max="4096" value="{{ form.disk_size }}" required></div>
|
<div class="field"><label for="disk_size">Диск, ГБ</label><input class="input" id="disk_size" type="number" name="disk_size" min="4" max="4096" value="{{ form.disk_size }}" required></div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="subtle small">Свободно на сервере: {{ images_free }}. Диск машины занимает место постепенно, по мере заполнения.</p>
|
||||||
|
{% if form.overcommit %}
|
||||||
|
<label class="check danger">
|
||||||
|
<input type="checkbox" name="allow_overcommit" value="1" {% if form.allow_overcommit %}checked{% endif %}>
|
||||||
|
<span><strong>Всё равно создать диск на {{ form.disk_size }} ГБ</strong><span>На сервере свободно {{ images_free }}. Сразу столько места диск не займёт, поэтому создать его можно. Но если система внутри машины заполнит больше, чем есть на сервере, место закончится у всех машин и они остановятся. Надёжнее уменьшить диск или освободить место.</span></span>
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
{% if profile.virtualization_warning %}{{ alert('warning', 'На этом сервере нет аппаратного ускорения — машины будут заметно медленнее.') }}{% endif %}
|
{% if profile.virtualization_warning %}{{ alert('warning', 'На этом сервере нет аппаратного ускорения — машины будут заметно медленнее.') }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user