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
0c1c124a02 (live). 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