Резервные копии по расписанию: каждый день или раз в неделю, все машины или отмеченные, хранятся последние N автокопий; копии делает служба virtuality-backup, а не панель

Этот коммит содержится в:
2026-09-27 15:58:31 +09:00
родитель 1ad557d1ab
Коммит fc76f3f1fa
18 изменённых файлов: 787 добавлений и 25 удалений
+109
Просмотреть файл
@@ -0,0 +1,109 @@
{% extends "base.html" %}
{% from "_ui.html" import icon, badge, alert %}
{% 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">Сервер сам делает копии машин в выбранное время и хранит только последние, чтобы копии не заняли весь диск.</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/schedule" class="schedule-form">
<div class="card-body form-grid">
<label class="switch-row" style="cursor:pointer">
<span class="switch-text"><strong>Делать копии автоматически</strong><span>Выключите, чтобы приостановить расписание. Уже сделанные копии останутся.</span></span>
<input type="checkbox" name="enabled" value="1" {% if form.enabled %}checked{% endif %} style="width:20px;height:20px;accent-color:var(--brand)">
</label>
<div class="field">
<span class="label">Как часто</span>
<div class="options" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr))">
<label class="option">
<input type="radio" name="frequency" value="daily" {% if form.frequency == 'daily' %}checked{% endif %}>
<span class="option-body"><span class="option-top"><span class="option-icon">{{ icon('calendar') }}</span></span><span class="option-title">Каждый день</span><span class="option-text">Для машин, в которых что-то меняется каждый день: сайты, базы, файлы.</span></span>
</label>
<label class="option">
<input type="radio" name="frequency" value="weekly" {% if form.frequency == 'weekly' %}checked{% endif %}>
<span class="option-body"><span class="option-top"><span class="option-icon">{{ icon('calendar-days') }}</span></span><span class="option-title">Раз в неделю</span><span class="option-text">Если машины меняются редко или места под копии немного.</span></span>
</label>
</div>
</div>
<div class="grid grid-2" style="align-items:start">
<div class="field" data-when="weekly">
<label for="weekday">День недели</label>
<select class="select" id="weekday" name="weekday">
{% for day in weekdays %}<option value="{{ loop.index0 }}" {% if form.weekday == loop.index0 %}selected{% endif %}>{{ day }}</option>{% endfor %}
</select>
</div>
<div class="field">
<label for="time">Время</label>
<input class="input" id="time" type="time" name="time" value="{{ form.time }}" required>
<span class="hint">По часам сервера. Лучше ночью, когда машинами никто не пользуется.</span>
</div>
</div>
<div class="field">
<span class="label">Какие машины копировать</span>
<div class="options" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr))">
<label class="option">
<input type="radio" name="machines" value="all" {% if form.all_vms %}checked{% endif %}>
<span class="option-body"><span class="option-title">Все машины</span><span class="option-text">Новые машины тоже попадут в копии сами.</span></span>
</label>
<label class="option">
<input type="radio" name="machines" value="chosen" {% if not form.all_vms %}checked{% endif %}>
<span class="option-body"><span class="option-title">Только отмеченные</span><span class="option-text">Например, без тестовых машин, которые не жалко.</span></span>
</label>
</div>
<div class="stack-sm" data-when="chosen">
{% for name in vm_names %}
<label class="check"><input type="checkbox" name="vms" value="{{ name }}" {% if name in form.vms %}checked{% endif %}><span><strong>{{ name }}</strong></span></label>
{% else %}
<p class="subtle small">Машин на сервере пока нет.</p>
{% endfor %}
</div>
</div>
<div class="field" style="max-width:280px">
<label for="keep">Сколько копий хранить</label>
<input class="input" id="keep" type="number" name="keep" value="{{ form.keep }}" min="1" max="{{ keep_max }}" required>
<span class="hint">Для каждой машины. Более старые автоматические копии удаляются сами.</span>
</div>
{{ alert('info', 'На время копирования работающая машина аккуратно выключается, а потом запускается снова. Машины копируются по очереди, так что каждая стоит только пока копируется она сама.') }}
</div>
<div class="card-footer">
<a class="btn" href="/backups">Отмена</a>
<button type="submit" class="btn btn-primary">{{ icon('check') }}Сохранить</button>
</div>
</form>
</article>
<aside class="stack">
<article class="card">
<div class="card-header"><h2>Как это работает</h2></div>
<div class="checklist">
{% set steps = [
('Старые копии удаляются сами', 'После удачной новой копии у машины остаются последние копии по расписанию, сколько вы указали. Копии, сделанные вручную, не удаляются.'),
('Неудачная копия ничего не удаляет', 'Если места не хватило или машина не выключилась, прежние копии остаются. Итог виден на странице «Резервные копии».'),
('Если сервер был выключен', 'Пропущенную копию сервер сделает в течение двух часов после включения. Если включили позже, копия будет в следующий раз.'),
('Обновления не мешают', 'Копии делает отдельная служба сервера, а не панель. Перезапуск панели их не прерывает.'),
] %}
{% for title, text in steps %}
<div class="check-item success">{{ icon('circle-check') }}<div><strong>{{ title }}</strong><span>{{ text }}</span></div></div>
{% endfor %}
</div>
</article>
</aside>
</section>
{% endblock %}
+48 -1
Просмотреть файл
@@ -39,6 +39,53 @@
</article>
</section>
<article class="card mt-3" id="schedule">
<div class="card-header">
<div class="row">
<span class="vm-avatar">{{ icon('clock') }}</span>
<div>
<h2>Автоматические копии {{ badge('включены', 'success', cls='hide-sm') if schedule.enabled else badge('выключены', 'neutral', False, cls='hide-sm') }}</h2>
{% if schedule.enabled %}
<p>{{ schedule.when|capitalize }} · {{ 'все машины' if schedule.all_vms else schedule.vms|join(', ') }} · хранить {{ schedule.keep }} {{ plural(schedule.keep, 'последнюю копию', 'последние копии', 'последних копий') }}</p>
{% else %}
<p>Сервер может сам копировать машины каждую ночь или раз в неделю и удалять старые копии.</p>
{% endif %}
</div>
</div>
<a class="btn btn-sm" href="/backups/schedule">{{ icon('settings', 'icon-sm') }}{{ 'Изменить' if schedule.enabled else 'Настроить' }}</a>
</div>
{% if schedule.enabled or schedule.last %}
<div class="list">
{% if schedule.enabled %}
<div class="list-item">
<span class="list-icon brand">{{ icon('clock') }}</span>
<div class="grow"><div class="title">Следующая копия: {{ schedule.next }}</div><div class="meta">Работающие машины на время копирования выключаются и потом запускаются снова</div></div>
</div>
{% endif %}
{% if schedule.missed %}
<div class="list-item">
<span class="list-icon info">{{ icon('info') }}</span>
<div class="grow"><div class="title">Копия {{ schedule.missed }} пропущена</div><div class="meta">В это время сервер был выключен, копия будет сделана в следующий раз</div></div>
</div>
{% endif %}
{% if schedule.last %}
{% set last = schedule.last %}
<div class="list-item">
<span class="list-icon {{ 'info' if last.running else 'danger' if last.failed else 'success' }}">{{ icon('activity' if last.running else 'triangle-alert' if last.failed else 'circle-check') }}</span>
<div class="grow">
<div class="title">
{% if last.running %}Идёт копирование по расписанию {{ last.when }}: готово {{ last.count }}
{% elif not last.count %}Запуск {{ last.when }}: машин для копирования не было
{% else %}Запуск {{ last.when }}: скопировано {{ last.ok }} из {{ last.count }} {{ plural(last.count, 'машины', 'машин', 'машин') }}{% endif %}
</div>
{% for item in last.failed %}<div class="meta"><strong>{{ item.vm }}</strong>: {{ item.message }}</div>{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}
</article>
{% if groups %}
<div class="stack mt-3">
{% for g in groups %}
@@ -75,7 +122,7 @@
<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>
<p class="muted small">Работающая машина на время копирования аккуратно выключается и потом запускается снова. При восстановлении машина создаётся выключенной: под новым названием — с новыми сетевыми адресами, чтобы не мешать оригиналу; при замене — с прежними. Копии хранятся в папке <code>/var/lib/virtuality/backups</code>. Копии по расписанию сервер удаляет сам, оставляя последние. Копии, сделанные вручную, лежат, пока вы их не удалите.</p>
</div>
</article>
</section>
+11
Просмотреть файл
@@ -70,6 +70,17 @@
</div>
</form>
</article>
<article class="card">
<div class="card-body stack-sm">
<div class="row">{{ icon('history') }}<h3>Автоматические копии</h3></div>
{% if scheduled %}
<p class="muted small">Машина копируется {{ schedule.when }}, сервер хранит {{ schedule.keep }} {{ plural(schedule.keep, 'последнюю копию', 'последние копии', 'последних копий') }}. Следующая: {{ schedule.next }}.</p>
{% else %}
<p class="muted small">Эта машина не копируется автоматически.</p>
{% endif %}
<a class="btn btn-sm" href="/backups/schedule">{{ icon('settings', 'icon-sm') }}Расписание копий</a>
</div>
</article>
<article class="card">
<div class="card-body stack-sm">
<div class="row">{{ icon('clock') }}<h3>Сколько это займёт</h3></div>