Zoo

staging

← back to the zoo

23. Last night's backup works

Everything is backed up nightly, a copy leaves the server, and the backup is opened to prove it.

Add `backup_check = <db> <table>` to ratcloud.conf: after each nightly backup root counts that table in the copy and writes it to RC_FACTS/backup.json.

This is the code serving the zoo right now: read from disk for this request, from commit 77ff74344d (staging). The zoo's own self-check fetches this page and compares it byte for byte with the file it runs.

exhibits/23-last-nights-backup.php sha256 d8089ef9ee51 · raw

1<?php
2// Exhibit 23. Root backs up every app nightly, verifies the copy, sends one off the server, and
3// opens the zoo's guestbook *in the backup copy* to count its rows (`backup_check = zoo.db
4// guestbook` in ratcloud.conf). All of it arrives in RC_FACTS/backup.json.
5return [
6    "n" => 23, "wing" => "Operations",
7    "title" => "Last night's backup works",
8    "promise" => "Everything is backed up nightly, a copy leaves the server, and the backup is opened to prove it.",
9    "block" => "Add `backup_check = <db> <table>` to ratcloud.conf: after each nightly backup root counts that table in the copy and writes it to RC_FACTS/backup.json.",
10    "files" => ["ratcloud.conf"],
11    "show" => function (?array $me): string {
12        $b = fact("backup.json");
13        if (!$b || !$b["latest"]) return '<p class="muted">No verified backup yet.</p>';
14        $l = $b["latest"]; $o = $b["offsite"] ?? null;
15        $rb = $l["readback"][env("RC_ENV")] ?? null;   // root reads back each environment's own copy
16        return '<ul class="list"><li><span class="tag ok">backup</span><span>' . ago(strtotime($l["at"])) . ", " . round($l["bytes"] / 1048576, 1) . " MB, verified</span></li>"
17             . '<li><span class="tag ' . (isset($rb["rows"]) ? "ok" : "bad") . '">read back</span><span>' . (isset($rb["rows"]) ? (int)$rb["rows"] . " guestbook signature(s), counted in the backup copy" : "no guestbook count read from it" . (isset($rb["error"]) ? ": " . h($rb["error"]) : " yet")) . "</span></li>"
18             . '<li><span class="tag ' . (($o["ok"] ?? false) ? "ok" : "bad") . '">offsite</span><span>' . ($o ? ago(strtotime($o["at"])) . ", " . round($o["bytes"] / 1048576, 1) . " MB encrypted" : "none yet") . "</span></li></ul>";
19    },
20    "check" => function (): array {
21        $b = fact("backup.json");
22        $l = $b["latest"] ?? null;
23        if (!$l) return [false, "RC_FACTS/backup.json has no verified backup"];
24        $age = (time() - strtotime($l["at"])) / 3600;
25        if ($age > 26) return [false, sprintf("the newest verified backup is %.0f hours old", $age)];
26        $rb = $l["readback"][env("RC_ENV")] ?? null;
27        $count = $rb["rows"] ?? null;
28        if ($count === null) return [false, "the backup has no guestbook count read back from it" . (isset($rb["error"]) ? ": " . $rb["error"] : "")];
29        $o = $b["offsite"] ?? null;
30        if (!($o["ok"] ?? false) || time() - strtotime($o["at"]) > 26 * 3600) return [false, "no copy left the server in the last day"];
31        return [true, sprintf("backup %.0f hours old, %d guestbook rows read back from it, offsite copy %s", $age, $count, $o["stamp"] ?? "")];
32    },
33];

ratcloud.conf sha256 25c85b08e06f · raw

1# The zoo's settings. rc renders nginx and the jobs from this; a mistake refuses the push.
2name = Zoo
3description = See every building block working
4# 10: anyone may listen to the counter. 11: `auth` makes nginx ask index.php first.
5channels = counter, mine auth
6# 21: the clock. 22: the self-check that turns every self-running exhibit into a light.
7cron = bin/tick.php every 5m, bin/selfcheck.php every 5m
8max_body = 1M
9# 23: after each nightly backup, root counts the guestbook in the backup copy.
10backup_check = zoo.db guestbook