Keep backups where the control plane can see them
The first backup on the live control plane was written, and the control plane reported none: /var/backups/llmgw already existed, made by an earlier move to PostgreSQL for its own dumps, owned by postgres and 0700. MkdirAll leaves an existing directory as it is, so the archive landed where the llmgw user could not list it, and llmgw_backup_last_success_timestamp_seconds said 0. Opening that directory up would have exposed the dumps already in it, so backups get their own, /var/backups/llmgw-server. And `llmgw server backup` now warns when the directory it wrote to is one the control plane cannot list, rather than leaving that to be found from an alert. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1da4a2e1ef
commit
c3a4150eab
6 files changed
+40
-7
No files matched your search
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=llmgw backup: the store and /etc/llmgw, to /var/backups/llmgw
|
||||
Description=llmgw backup: the store and /etc/llmgw, to /var/backups/llmgw-server
|
||||
After=llmgw-server.service postgresql.service
|
||||
|
||||
[Service]
|
||||
|
||||
+3
-3
@@ -973,7 +973,7 @@ cannot be rebuilt without:
|
||||
certificate authority's key (`pki/`). Nothing in llmgw can reconstruct that
|
||||
key; losing it means reissuing every node's identity.
|
||||
|
||||
Archives go to `/var/backups/llmgw`, readable by root only because of the CA
|
||||
Archives go to `/var/backups/llmgw-server`, readable by root only because of the CA
|
||||
key, and the newest 14 are kept. Run it nightly with the timer:
|
||||
|
||||
```
|
||||
@@ -981,7 +981,7 @@ sudo install -m 0644 deploy/systemd/llmgw-backup.service deploy/systemd/llmgw-ba
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now llmgw-backup.timer
|
||||
sudo systemctl start llmgw-backup.service # one now, to see it work
|
||||
ls -l /var/backups/llmgw
|
||||
ls -l /var/backups/llmgw-server
|
||||
```
|
||||
|
||||
The control plane publishes when the newest archive was written as
|
||||
@@ -1006,7 +1006,7 @@ them:
|
||||
|
||||
```
|
||||
sudo systemctl stop llmgw-server
|
||||
mkdir /tmp/restore && sudo tar -xzf /var/backups/llmgw/llmgw-backup-<host>-<time>.tar.gz -C /tmp/restore
|
||||
mkdir /tmp/restore && sudo tar -xzf /var/backups/llmgw-server/llmgw-backup-<host>-<time>.tar.gz -C /tmp/restore
|
||||
cat /tmp/restore/MANIFEST # version and time it was taken
|
||||
|
||||
# /etc/llmgw: configuration, secrets, certificates and the CA
|
||||
|
||||
+1
-1
@@ -1272,7 +1272,7 @@ one leaves that engine to be installed by hand.
|
||||
|
||||
BackupDir is where `llmgw server backup` writes, and where the newest
|
||||
archive's age is read for llmgw_backup_last_success_timestamp_seconds.
|
||||
Empty means /var/backups/llmgw.
|
||||
Empty means /var/backups/llmgw-server.
|
||||
|
||||
### `router`
|
||||
|
||||
|
||||
+22
-1
@@ -25,7 +25,11 @@ import (
|
||||
|
||||
// DefaultBackupDir is where backups are written and where the control plane
|
||||
// looks for the newest one.
|
||||
const DefaultBackupDir = "/var/backups/llmgw"
|
||||
//
|
||||
// Its own directory rather than /var/backups/llmgw, which a PostgreSQL move
|
||||
// had already made for its own dumps, owned by postgres and closed to
|
||||
// everyone else — so the control plane could not see a backup landing in it.
|
||||
const DefaultBackupDir = "/var/backups/llmgw-server"
|
||||
|
||||
func newBackupCmd() *cobra.Command {
|
||||
var (
|
||||
@@ -56,6 +60,10 @@ func newBackupCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if why := unlistable(dir); why != "" {
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "warning: %s; the control plane will report no backup "+
|
||||
"and BackupTooOld will fire\n", why)
|
||||
}
|
||||
removed, err := pruneBackups(dir, keep)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -287,3 +295,16 @@ func NewestBackup(dir string) time.Time {
|
||||
}
|
||||
return newest
|
||||
}
|
||||
|
||||
// unlistable says why the control plane, which does not run as root, could
|
||||
// not see the archives in dir, or "" when it can.
|
||||
func unlistable(dir string) string {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if info.Mode().Perm()&0o005 != 0o005 {
|
||||
return fmt.Sprintf("%s is %v, so only its owner can list it", dir, info.Mode().Perm())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -104,3 +104,15 @@ func TestOldBackupsArePrunedAndTheNewestIsFound(t *testing.T) {
|
||||
t.Error("a missing directory reported a backup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestABackupDirectoryTheControlPlaneCannotListIsSaid(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_ = os.Chmod(dir, 0o700)
|
||||
if unlistable(dir) == "" {
|
||||
t.Error("a directory only its owner can list was taken as visible")
|
||||
}
|
||||
_ = os.Chmod(dir, 0o755)
|
||||
if why := unlistable(dir); why != "" {
|
||||
t.Errorf("a listable directory: %s", why)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ type Server struct {
|
||||
|
||||
// BackupDir is where `llmgw server backup` writes, and where the newest
|
||||
// archive's age is read for llmgw_backup_last_success_timestamp_seconds.
|
||||
// Empty means /var/backups/llmgw.
|
||||
// Empty means /var/backups/llmgw-server.
|
||||
BackupDir string `koanf:"backup_dir"`
|
||||
|
||||
Router Router `koanf:"router"`
|
||||
|
||||
Reference in new issue
Block a user