Первый модуль-фича на общем 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
83 строки
5.0 KiB
HTML
83 строки
5.0 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_ui.html" import icon, badge, meter, empty, alert %}
|
|
{% set page_title = 'Резервные копии' %}
|
|
|
|
{% block page_header %}
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>Резервные копии</h1>
|
|
<p class="lead">Полная копия машины: настройки и все её диски. Из копии можно восстановить машину на этом сервере — под тем же или новым названием.</p>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block alerts %}
|
|
{% if backup_message %}{{ alert('success', backup_message) }}{% endif %}
|
|
{% if backup_error %}{{ alert('danger', backup_error) }}{% endif %}
|
|
{% for op in running %}
|
|
<div class="alert info">{{ icon('activity') }}<div><strong>{{ op.title }} — {{ op.progress|default(0) }}%</strong>{{ op.message }}</div><a class="btn btn-sm alert-action" href="/operations/{{ op.id }}">Следить</a></div>
|
|
{% endfor %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="grid grid-3">
|
|
<article class="card stat">
|
|
<div class="stat-head">Копий на сервере<span class="stat-icon">{{ icon('database', 'icon-sm') }}</span></div>
|
|
<div class="stat-value">{{ storage.count }}<small>{{ plural(storage.count, 'копия', 'копии', 'копий') }}</small></div>
|
|
<div class="stat-foot">{{ groups|length }} {{ plural(groups|length, 'машина', 'машины', 'машин') }}</div>
|
|
</article>
|
|
<article class="card stat">
|
|
<div class="stat-head">Занято копиями<span class="stat-icon">{{ icon('hard-drive', 'icon-sm') }}</span></div>
|
|
<div class="stat-value">{{ storage.used }}</div>
|
|
{{ meter(storage.used_pct, 'brand') }}
|
|
<div class="stat-foot">{{ storage.used_pct }}% места на диске сервера</div>
|
|
</article>
|
|
<article class="card stat">
|
|
<div class="stat-head">Свободно для новых копий<span class="stat-icon">{{ icon('layers', 'icon-sm') }}</span></div>
|
|
<div class="stat-value">{{ storage.free }}</div>
|
|
<div class="stat-foot">Перед копированием место проверяется автоматически</div>
|
|
</article>
|
|
</section>
|
|
|
|
{% if groups %}
|
|
<div class="stack mt-3">
|
|
{% for g in groups %}
|
|
<article class="card">
|
|
<div class="card-header">
|
|
<div class="row">
|
|
<span class="vm-avatar">{{ icon('monitor') }}</span>
|
|
<div>
|
|
<h2>{% if g.exists %}<a href="/vm/{{ g.vm }}" style="color:inherit">{{ g.vm }}</a>{% else %}{{ g.vm }}{% endif %}{% if not g.exists %} {{ badge('машина удалена', 'warning', cls='hide-sm') }}{% endif %}</h2>
|
|
<p>{{ g.count }} {{ plural(g.count, 'копия', 'копии', 'копий') }} · {{ g.size }}{% if not g.exists %} · машины больше нет на сервере, но её можно восстановить{% endif %}</p>
|
|
</div>
|
|
</div>
|
|
{% if g.exists %}<a class="btn btn-sm" href="/vm/{{ g.vm }}/backups">{{ icon('plus', 'icon-sm') }}Создать копию</a>{% endif %}
|
|
</div>
|
|
<div class="list">
|
|
{% with rows = g.backups, back = '/backups' %}{% include "_backup_rows.html" %}{% endwith %}
|
|
</div>
|
|
</article>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<article class="card mt-3">
|
|
{{ empty('database', 'Копий пока нет', 'Откройте страницу машины, перейдите на вкладку «Резервные копии» и нажмите «Создать копию». Копирование идёт в фоне — страницу можно закрыть.', '/', 'К списку машин', 'monitor') }}
|
|
</article>
|
|
{% endif %}
|
|
|
|
<section class="grid grid-2 mt-3">
|
|
<article class="card">
|
|
<div class="card-body stack-sm">
|
|
<div class="row">{{ icon('shield-check') }}<h3>Что входит в копию</h3></div>
|
|
<p class="muted small">Настройки машины (процессор, память, сеть, порядок загрузки) и все её диски целиком. Установочные образы (ISO) не копируются — они лежат отдельно в разделе «Образы ОС». Диски сжимаются, поэтому копия обычно меньше самой машины.</p>
|
|
</div>
|
|
</article>
|
|
<article class="card">
|
|
<div class="card-body stack-sm">
|
|
<div class="row">{{ icon('info') }}<h3>Как это работает</h3></div>
|
|
<p class="muted small">Работающая машина на время копирования аккуратно выключается и потом запускается снова. При восстановлении машина создаётся выключенной и получает новые сетевые адреса, чтобы не мешать оригиналу. Копии хранятся в папке <code>/var/lib/virtuality/backups</code>.</p>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
{% endblock %}
|