Резервные копии машин были добавлены: создание, восстановление, удаление

Первый модуль-фича на общем core.py — web/features/backups.py,
подключён в app.py одной строкой include_router.

- копия = папка /var/lib/virtuality/backups/<машина>/<ГГГГММДД-ЧЧММ>/
  с vm.xml (virsh dumpxml --migratable), <диск>.qcow2 на каждый диск
  и meta.json (машина, дата, диски, размер, версия, заметка);
- создание — фоновая операция: свободное место проверяется заранее по
  qemu-img info, работающая машина мягко выключается (ждём до 3 минут,
  без принудительного выключения), диски копируются qemu-img convert -c
  с прогрессом из строк «(NN.NN/100%)», после чего машина запускается;
- восстановление — операция: диски распаковываются в images/<имя>[-<диск>]
  .qcow2, в XML меняется имя, убираются uuid, MAC и nvram, затем
  virsh define; восстановление под занятым именем требует галочку
  «заменить существующую» — старая машина удаляется с дисками, а ISO
  перед этим извлекается из привода, чтобы --remove-all-storage его не
  стёр;
- страницы: «Резервные копии» в меню (все копии по машинам, занятое и
  свободное место), вкладка «Резервные копии» на странице машины с
  формой «Создать копию» и заметкой, страница восстановления с
  понятным списком шагов;
- create_backup(vm, note, wait=True) можно вызывать без HTTP —
  задел под расписание (python3 web/features/backups.py web01 «заметка»);
- tests/conftest.py: каталог backups, патчи core.*, вывод virsh
  domblklist --details, dumpxml --migratable и qemu-img info из fixtures;
  tests/test_backups.py — 33 теста парсеров, воркеров и маршрутов;
  tests/dev_server.py показывает демо-копии.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
Claude
2026-09-25 14:32:05 +00:00
родитель 1e29134fb3
Коммит 2450278d93
13 изменённых файлов: 1433 добавлений и 8 удалений
+9
Просмотреть файл
@@ -298,6 +298,15 @@ hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }
.switch[aria-checked="true"] { background: var(--success); }
.switch[aria-checked="true"]::after { transform: translateX(18px); }
/* checkbox row (consent for destructive actions) */
.check { display: flex; gap: 12px; align-items: flex-start; padding: 13px 16px; border: 1.5px solid var(--border); border-radius: var(--radius); background: var(--surface-2); cursor: pointer; }
.check input { width: 18px; height: 18px; flex: none; margin: 2px 0 0; accent-color: var(--brand); cursor: pointer; }
.check > span { display: grid; gap: 2px; }
.check > span > span { color: var(--text-3); font-size: 12.5px; }
.check.danger { border-color: color-mix(in srgb, var(--danger) 30%, transparent); background: var(--danger-soft); }
.check.danger input { accent-color: var(--danger); }
.check.danger strong { color: var(--danger); }
/* option cards (radio) */
.options { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.options-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }