webui: add automatic .cfg migration when restoring a backup

Also adjust some card layout in both Backup and Restore cards.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-15 19:45:29 +02:00
parent 67ec3faa54
commit 89392c56be
3 changed files with 71 additions and 3 deletions
+54 -1
View File
@@ -273,6 +273,49 @@ func (h *SystemHandler) SupportBundle(w http.ResponseWriter, r *http.Request) {
// target="running" (default): PUT to running so changes take effect immediately;
// sets the cfg-unsaved cookie so the persistent notification prompts a save.
// target="startup": PUT to startup only; reboot required to apply.
// migrateConfig runs an uploaded .cfg backup through the on-target migrate(1)
// tool so a config saved by an older release is brought up to the running
// syntax before it is applied. The config is written to a temp file and
// migrated in place — migrate only reads stdin from a TTY, so the file path is
// the reliable interface. A config already at the current version comes back
// unchanged. An error carrying migrate's stderr is returned for a config newer
// than the system supports (downgrade) or any other migrate failure.
func migrateConfig(ctx context.Context, raw []byte) ([]byte, error) {
f, err := os.CreateTemp("", "restore-*.cfg")
if err != nil {
return nil, err
}
tmp := f.Name()
defer os.Remove(tmp)
if _, err := f.Write(raw); err != nil {
f.Close()
return nil, err
}
if err := f.Close(); err != nil {
return nil, err
}
var stderr bytes.Buffer
cmd := exec.CommandContext(ctx, "/usr/sbin/migrate", "-i", "-e", tmp)
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
msg := strings.TrimSpace(stderr.String())
msg = strings.ReplaceAll(msg, tmp+": ", "")
msg = strings.TrimPrefix(msg, "Error: ")
if msg == "" {
msg = err.Error()
}
return nil, fmt.Errorf("%s", msg)
}
if notes := strings.TrimSpace(stderr.String()); notes != "" {
log.Printf("restore: migrated uploaded config:\n%s", notes)
}
return os.ReadFile(tmp)
}
func (h *SystemHandler) RestoreConfig(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
@@ -304,12 +347,22 @@ func (h *SystemHandler) RestoreConfig(w http.ResponseWriter, r *http.Request) {
return
}
// Bring an older backup up to the running syntax before applying it.
migrated, err := migrateConfig(r.Context(), raw)
if err != nil {
log.Printf("restore: migrate: %v", err)
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `<span class="sc-fd-err">Restore failed: %s</span>`,
template.HTMLEscapeString(err.Error()))
return
}
target := "running"
if r.FormValue("save-to-startup") == "on" {
target = "startup"
}
if err := h.RC.PutDatastore(r.Context(), target, check); err != nil {
if err := h.RC.PutDatastore(r.Context(), target, json.RawMessage(migrated)); err != nil {
log.Printf("restore(%s): %v", target, err)
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `<span class="sc-fd-err">Restore failed: %s</span>`,
+13
View File
@@ -2813,6 +2813,19 @@ details.cfg-fold[open] > summary::before { transform: rotate(90deg); }
margin-top: 0.75rem;
cursor: pointer;
}
/* Restore card: let the form fill the body so its file selector and checkbox
sit under the leading paragraph, with only the submit button anchored at the
bottom (aligned with the other action cards). */
#restore-form {
display: flex;
flex-direction: column;
flex: 1;
}
.sc-form-actions {
margin-top: auto;
padding-top: 0.75rem;
}
.sc-restore-warn-text {
color: var(--warning-fg);
font-size: 0.875rem;
+4 -2
View File
@@ -9,7 +9,7 @@
<div class="card-header">Backup</div>
<div class="card-body sc-action-body">
<p class="sc-desc">Download the current startup configuration as a JSON file.
The file format is <pre>startup-config-$hostname-$date-$time.cfg</pre>
The file format is <code>startup-config-$hostname-$date-$time.cfg</code>
</p>
<div>
<a href="/config" hx-boost="false" class="btn btn-primary">Download config</a>
@@ -22,6 +22,8 @@
<div class="card-body sc-action-body">
<p class="sc-desc">Restore a previously backed-up configuration. By default
the restore operation is made to running config, this is the safe default.</p>
<p class="sc-desc">If the file was saved by an older release, it is migrated to
the current configuration syntax before being applied.</p>
<form id="restore-form"
hx-post="/maintenance/backup/restore"
hx-target="#restore-result"
@@ -37,7 +39,7 @@
<span class="sc-restore-warn-text">Save to startup — may break system boot!</span>
</label>
<div id="restore-result"></div>
<div style="margin-top:0.75rem">
<div class="sc-form-actions">
<button type="submit" class="btn btn-primary">Restore</button>
</div>
</form>