Стабильный канал обновлений был добавлен; клон и увеличение диска защищены от спящих машин

- stable (по умолчанию): обновление до последнего тега vX.Y.Z; пока релизов нет — ветка main
- main: ранний доступ, все изменения; канал берётся из web.env (Настройки → Обновления)
- scripts/update_target.sh общий для автообновления и ручного обновления, ZIP-запасной путь для тегов
- страница «Обновления» показывает канал и целевую версию
- клонирование и увеличение диска недоступны, пока машина в спящем режиме

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
Claude
2026-09-25 15:19:19 +00:00
родитель 7ec3dc6082
Коммит c5b576dbe7
8 изменённых файлов: 146 добавлений и 24 удалений
+9
Просмотреть файл
@@ -174,6 +174,11 @@ def parse_desc_output(result: dict[str, Any]) -> str:
return text
def saved_state(name: str) -> bool:
"""The machine is hibernated (virsh managedsave): its disks must not change until it is resumed."""
return has_managed_save(run(["virsh", "dominfo", name], timeout=10)["stdout"])
def has_managed_save(dominfo: str) -> bool:
"""Строка «Managed save: yes» в `virsh dominfo` — машина в спящем режиме."""
match = re.search(r"^Managed save:\s*(\S+)", dominfo or "", re.MULTILINE | re.IGNORECASE)
@@ -372,6 +377,8 @@ def disk_resize(request: Request, name: str, target: str, size_gb: int = Form(..
free = free_space(disk["source"])
if growth > free:
return redirect_tab(name, "storage", "disk_error", f"На сервере свободно только {presenters.format_bytes(free)}: столько добавить диску не получится")
if saved_state(name):
return redirect_tab(name, "storage", "disk_error", "Машина в спящем режиме: сохранённая память рассчитана на старый размер диска. Сначала запустите её и выключите обычным образом.")
if is_shut_off(vm_state(name)):
result = run(["qemu-img", "resize", disk["source"], f"{size_gb}G"], timeout=120)
else:
@@ -413,6 +420,8 @@ def clone_context(name: str) -> dict[str, Any]:
blockers = []
if not is_shut_off(state):
blockers.append("running")
elif saved_state(name):
blockers.append("saved")
if not cloneable:
blockers.append("no_disks")
if needed > free: