#5816 "Restoring mode": while a backup goes back in, the scheduler holds, ingest and HTMX answer 503 with Retry-After (agents keep their samples), and every page says what is happening. A safety backup of Steward as it stands is made first, to /data/backups and the bucket. The backup's key is put in place, the restore audited, and Steward exits with code 3 so Swarm, compose or systemd starts it fresh. A failed in-place restore changes nothing and says why on Backups; a full replace that fails after the wipe restarts into first-run setup. #5817 "Settings → Backups: Restore": a Restore button on each sent backup and on the file card. The chosen backup is checked (the same comparison as Check, now a shared partial) and held server-side for an hour under a token; it starts only once the admin types "restore", and an older backup also needs "Replace everything" ticked. A newer backup, or one whose key can't open its secrets, gets no button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
172 lines
9.2 KiB
HTML
172 lines
9.2 KiB
HTML
{# settings/backups.html — Steward's own backups (#5673): a verdict, the bucket
|
|
they go to (Fabled Archives over S3) and the daily hour, then the recent
|
|
runs, each with its own check and Check and Restore buttons (#5823, #5817),
|
|
and a file to check or restore. #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs %}
|
|
{% from "settings/_tabs.html" import settings_tabs %}
|
|
{% block title %}Settings — Backups — Steward{% endblock %}
|
|
{% block section_tabs %}{{ settings_tabs("backups") }}{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Backups", "")]) }}{% endblock %}
|
|
{% block content %}
|
|
{% set tone = {"ok": "tone-ok", "warn": "tone-warn", "bad": "tone-crit", "off": "tone-dim"}[verdict.tone] %}
|
|
<p class="page-verdict"><span class="{{ tone }}">{{ verdict.text }}
|
|
{%- if verdict.moment %} {{ verdict.moment|ago }}{% endif %}.
|
|
{%- if verdict.failed %} {{ verdict.error }}
|
|
{% if verdict.good %}The newest good backup was sent {{ verdict.good|ago }}.{% else %}No backup has been sent yet.{% endif %}
|
|
{%- endif %}</span></p>
|
|
|
|
{% if restore_failed %}
|
|
<p class="tone-crit" style="margin:-0.5rem 0 1.25rem;">The restore of {{ restore_failed.name }}
|
|
{%- if restore_failed.since %} {{ restore_failed.since|ago }}{% endif %} failed, and nothing changed:
|
|
{{ restore_failed.error }}</p>
|
|
{% endif %}
|
|
|
|
{% if flash %}
|
|
<div style="padding:0.75rem 1rem;border-radius:6px;margin-bottom:1.25rem;
|
|
background:{% if flash.ok %}var(--green-dim){% else %}var(--red-dim){% endif %};
|
|
border:1px solid {% if flash.ok %}var(--green){% else %}var(--red){% endif %};
|
|
color:{% if flash.ok %}var(--green){% else %}var(--red){% endif %};
|
|
font-size:0.88rem;">
|
|
{{ flash.message }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div style="max-width:640px;">
|
|
<form method="post" action="/settings/backups/">{{ csrf_field() }}
|
|
<div class="card" style="margin-bottom:1.25rem;">
|
|
<h2 class="section-title" style="margin-bottom:0.5rem;">Bucket</h2>
|
|
<p class="field-hint" style="margin-bottom:1rem;">
|
|
Each backup is one file, <code>steward-<time>.tar</code>, for getting Steward running again:
|
|
its settings, users, hosts, monitors, alert rules and dashboards, and its secret key. Logs and
|
|
history are left out: the audit log, alert history, metrics and check results. Anyone holding a
|
|
copy can read every password and key Steward keeps, so keep the bucket private.
|
|
</p>
|
|
|
|
<div class="form-group">
|
|
<label for="b-endpoint">Endpoint</label>
|
|
<input type="url" id="b-endpoint" name="backup.s3_endpoint" value="{{ settings['backup.s3_endpoint'] }}"
|
|
placeholder="http://archives:8081">
|
|
<p class="field-hint">The S3 address, without the bucket. Requests are path-style: endpoint/bucket/file.</p>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:2fr 1fr;gap:1rem;">
|
|
<div class="form-group">
|
|
<label for="b-bucket">Bucket</label>
|
|
<input type="text" id="b-bucket" name="backup.s3_bucket" value="{{ settings['backup.s3_bucket'] }}"
|
|
placeholder="steward">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="b-region">Region</label>
|
|
<input type="text" id="b-region" name="backup.s3_region" value="{{ settings['backup.s3_region'] }}">
|
|
</div>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
|
|
<div class="form-group">
|
|
<label for="b-access">Access key</label>
|
|
<input type="text" id="b-access" name="backup.s3_access_key" autocomplete="off"
|
|
value="{{ settings['backup.s3_access_key'] }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="b-secret">Secret key</label>
|
|
<input type="password" id="b-secret" name="backup.s3_secret_key" autocomplete="new-password"
|
|
placeholder="{% if has_secret %}kept; type to change{% endif %}">
|
|
{% if has_secret %}
|
|
<label style="display:flex;align-items:center;gap:0.4rem;font-weight:normal;margin-top:0.35rem;">
|
|
<input type="checkbox" name="backup.clear_secret"> Remove the stored secret key
|
|
</label>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<p class="field-hint">An append-only key is enough: Steward only uploads and never deletes. How long
|
|
backups are kept is the bucket's own setting.</p>
|
|
<div class="form-group" style="display:flex;align-items:center;gap:0.75rem;">
|
|
<input type="checkbox" name="backup.s3_verify_tls" id="b-tls"
|
|
{% if settings['backup.s3_verify_tls'] %}checked{% endif %}>
|
|
<label for="b-tls" style="margin:0;">Check the endpoint's TLS certificate</label>
|
|
</div>
|
|
|
|
<h2 class="section-title" style="margin:1.5rem 0 0.75rem;">Schedule</h2>
|
|
<div class="form-group" style="display:flex;align-items:center;gap:0.75rem;">
|
|
<input type="checkbox" name="backup.enabled" id="b-enabled"
|
|
{% if settings['backup.enabled'] %}checked{% endif %}>
|
|
<label for="b-enabled" style="margin:0;">Back up every day</label>
|
|
</div>
|
|
<div class="form-group" style="max-width:220px;">
|
|
<label for="b-hour">At hour ({{ tz_label() }})</label>
|
|
<select id="b-hour" name="backup.schedule_hour">
|
|
{% for h in range(24) %}
|
|
<option value="{{ h }}" {% if shown_hour == h %}selected{% endif %}>{{ "%02d:00"|format(h) }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<p class="field-hint">A day missed while Steward was down is made up when it is back; a failed backup is
|
|
tried again an hour later. Watch it with an <a href="/alerts/rules/new">alert rule</a> on
|
|
"Steward itself", resource <code>backup</code>, metric "Newest backup's age".</p>
|
|
|
|
<div style="margin-top:1rem;"><button type="submit" class="btn">Save</button></div>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="card" style="margin-bottom:1.25rem;display:flex;gap:0.75rem;flex-wrap:wrap;align-items:center;">
|
|
<form method="post" action="/settings/backups/test/">{{ csrf_field() }}
|
|
<button type="submit" class="btn btn-ghost">Test the bucket</button>
|
|
</form>
|
|
<form method="post" action="/settings/backups/now/">{{ csrf_field() }}
|
|
<button type="submit" class="btn btn-ghost">Back up now</button>
|
|
</form>
|
|
<p class="field-hint" style="margin:0;">Both use the saved settings.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="section-title" style="margin:1.5rem 0 1rem;">Recent backups</h2>
|
|
{% if history %}
|
|
<div class="card-flush">
|
|
<table class="table">
|
|
<thead><tr><th>When</th><th>Outcome</th><th>File</th><th>Size</th><th>Took</th><th>Started by</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for r in history %}
|
|
<tr>
|
|
<td class="nowrap">{{ r.at|when if r.at else "—" }}</td>
|
|
<td>{% if r.ok %}<span class="tone-ok">Sent</span>
|
|
{%- if r.check %}<div class="cell-sub {{ 'tone-crit' if not r.check.ok else ('tone-warn' if r.check.changes else '') }}">
|
|
{%- if r.check.ok and not r.check.changes %}Checked: reads back whole{% else %}{{ r.check.summary }}{% endif %}</div>{% endif %}
|
|
{%- else %}<span class="tone-crit">Failed</span>
|
|
<div class="cell-sub">{{ r.error }}</div>{% endif %}</td>
|
|
<td class="cell-name"><span title="{{ r.name }}">{{ r.name or "—" }}</span></td>
|
|
<td class="nowrap">{{ r.size|bytes if r.ok else "—" }}</td>
|
|
<td class="nowrap">{{ r.secs|duration }}</td>
|
|
<td>{{ "the schedule" if r.by == "schedule" else r.by }}</td>
|
|
<td>{% if r.ok and r.name %}<div style="display:flex;gap:0.35rem;">
|
|
<form method="post" action="/settings/backups/check/">{{ csrf_field() }}
|
|
<input type="hidden" name="name" value="{{ r.name }}">
|
|
<button type="submit" class="btn btn-ghost btn-sm" title="Compare {{ r.name }} with what is in place; nothing changes">Check</button>
|
|
</form>
|
|
<form method="post" action="/settings/backups/restore/">{{ csrf_field() }}
|
|
<input type="hidden" name="name" value="{{ r.name }}">
|
|
<button type="submit" class="btn btn-ghost btn-sm" title="See what restoring {{ r.name }} would change, then confirm">Restore</button>
|
|
</form></div>{% endif %}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="tone-dim">None yet.</p>
|
|
{% endif %}
|
|
<p class="field-hint" style="margin-top:0.75rem;">Check compares a backup with what is in place: what
|
|
a restore would bring back, change or remove, area by area. It changes nothing. Restore shows the same
|
|
comparison and asks you to confirm before anything changes.</p>
|
|
|
|
<div class="card" style="max-width:640px;margin-top:1.25rem;">
|
|
<h2 class="section-title" style="margin-bottom:0.5rem;">Check or restore a file</h2>
|
|
<form method="post" action="/settings/backups/check/file/" enctype="multipart/form-data"
|
|
style="display:flex;gap:0.75rem;flex-wrap:wrap;align-items:center;">{{ csrf_field() }}
|
|
<input type="file" name="file" accept=".tar" required>
|
|
<button type="submit" class="btn btn-ghost">Check</button>
|
|
<button type="submit" class="btn btn-ghost" formaction="/settings/backups/restore/file/">Restore</button>
|
|
</form>
|
|
<p class="field-hint" style="margin-top:0.5rem;">A <code>steward-<time>.tar</code> downloaded from
|
|
the bucket, or made with <code>steward backup</code>.</p>
|
|
</div>
|
|
{% endblock %}
|