Первый модуль-фича на общем 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
76 строки
5.0 KiB
HTML
76 строки
5.0 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_ui.html" import icon, badge, alert %}
|
|
{% set page_title = 'Восстановить ' ~ backup.vm %}
|
|
|
|
{% block page_header %}
|
|
<div class="page-header">
|
|
<div>
|
|
<div class="eyebrow"><a href="/backups">{{ icon('arrow-left', 'icon-sm') }} Резервные копии</a></div>
|
|
<h1>Восстановить машину из копии</h1>
|
|
<p class="lead">Копия машины <strong>{{ backup.vm }}</strong> от {{ backup.created_label }}{% if backup.note %} — «{{ backup.note }}»{% endif %}.</p>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block alerts %}
|
|
{% if error %}{{ alert('danger', error) }}{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="grid grid-main">
|
|
<article class="card">
|
|
<form method="post" action="/backups/{{ backup.vm }}/{{ backup.id }}/restore" data-busy="Запускаем…" {% if exists %}data-confirm="Машина {{ name }} будет удалена вместе со всеми её дисками и заменена копией от {{ backup.created_label }}. Отменить это будет нельзя." data-confirm-title="Заменить машину {{ name }}?" data-confirm-button="Заменить и восстановить"{% endif %}>
|
|
<div class="card-body form-grid">
|
|
<div class="field">
|
|
<label for="name">Название восстановленной машины</label>
|
|
<input class="input" id="name" type="text" name="name" value="{{ name }}" pattern="[a-zA-Z0-9][a-zA-Z0-9_.\-]{1,62}" autocomplete="off" required>
|
|
<span class="hint">Оставьте как есть, чтобы вернуть машину {{ backup.vm }}, или введите новое название — получится вторая независимая машина.</span>
|
|
</div>
|
|
|
|
{% if exists %}
|
|
<div class="alert warning">{{ icon('triangle-alert') }}<div><strong>Машина {{ name }} уже есть на сервере</strong>Чтобы восстановить копию под этим названием, существующую машину придётся удалить вместе с её дисками. Если хотите сохранить обе — укажите другое название.</div></div>
|
|
<label class="check danger">
|
|
<input type="checkbox" name="replace" value="1" required>
|
|
<span><strong>Заменить существующую машину {{ name }}</strong><span>Она будет выключена и удалена вместе с дисками, а на её место встанет копия.</span></span>
|
|
</label>
|
|
{% else %}
|
|
<input type="hidden" name="replace" value="">
|
|
{% endif %}
|
|
|
|
<div class="kv" style="grid-template-columns:repeat(3,minmax(0,1fr))">
|
|
<div><span>Дисков в копии</span><strong>{{ backup.disk_count }} · {{ backup.size }}</strong></div>
|
|
<div><span>Понадобится места</span><strong>около {{ need }}</strong></div>
|
|
<div><span>Свободно</span><strong>{{ free }}</strong></div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer">
|
|
<a class="btn" href="/backups">Отмена</a>
|
|
<button type="submit" class="btn {{ 'btn-danger' if exists else 'btn-primary' }}">{{ icon('rotate-cw') }}{{ 'Заменить и восстановить' if exists else 'Восстановить' }}</button>
|
|
</div>
|
|
</form>
|
|
</article>
|
|
|
|
<aside class="stack">
|
|
<article class="card">
|
|
<div class="card-header"><h2>Что будет сделано</h2></div>
|
|
<div class="checklist">
|
|
{% for title, text in [
|
|
('Диски копируются обратно', 'В папку ' ~ images_dir ~ ' — распаковка занимает несколько минут.'),
|
|
('Настройки применяются заново', 'Процессор, память, сеть и порядок загрузки — как на момент копии.'),
|
|
('Сетевые адреса выдаются новые', 'Чтобы восстановленная машина не конфликтовала с оригиналом. IP-адрес внутри может измениться.'),
|
|
('Машина остаётся выключенной', 'Проверьте настройки и запустите её сами.'),
|
|
] %}
|
|
<div class="check-item success">{{ icon('circle-check') }}<div><strong>{{ title }}</strong><span>{{ text }}</span></div></div>
|
|
{% endfor %}
|
|
</div>
|
|
</article>
|
|
<article class="card">
|
|
<div class="card-body stack-sm">
|
|
<div class="row">{{ icon('info') }}<h3>Копия не пострадает</h3></div>
|
|
<p class="muted small">Восстанавливать из одной копии можно сколько угодно раз — файлы копии остаются на месте.</p>
|
|
</div>
|
|
</article>
|
|
</aside>
|
|
</section>
|
|
{% endblock %}
|