diff options
Diffstat (limited to 'stdlib/backups/borg')
-rw-r--r-- | stdlib/backups/borg/borg-script-template | 27 | ||||
-rw-r--r-- | stdlib/backups/borg/client.go | 123 | ||||
-rw-r--r-- | stdlib/backups/borg/server.go | 1 | ||||
-rw-r--r-- | stdlib/backups/borg/systemd-service-template | 15 | ||||
-rw-r--r-- | stdlib/backups/borg/systemd-timer-template | 9 |
5 files changed, 174 insertions, 1 deletions
diff --git a/stdlib/backups/borg/borg-script-template b/stdlib/backups/borg/borg-script-template new file mode 100644 index 0000000..c09c297 --- /dev/null +++ b/stdlib/backups/borg/borg-script-template @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +on_exit() { + exit $? +} +trap on_exit EXIT + +archiveName="%s-%s-$(date +%%Y-%%m-%%dT%%H:%%M:%%S)" +archiveSuffix=".failed" + +# Run borg init if the repo doesn't exist yet +if ! borg list > /dev/null; then + borg init --encryption none +fi +( + borg create \ + --compression auto,zstd \ + "::${archiveName}${archiveSuffix}" \ + '%s' +) +borg rename "::${archiveName}${archiveSuffix}" "${archiveName}" + +borg prune \ + --keep-daily=14 --keep-monthly=3 --keep-weekly=4 \ + --glob-archives '%s-%s*' +borg compact diff --git a/stdlib/backups/borg/client.go b/stdlib/backups/borg/client.go new file mode 100644 index 0000000..a203ae2 --- /dev/null +++ b/stdlib/backups/borg/client.go @@ -0,0 +1,123 @@ +package borg + +import ( + _ "embed" + "fmt" + "os" + + "log/slog" + "path/filepath" + + gonf "git.adyxax.org/adyxax/gonf/v2/pkg" +) + +//go:embed borg-script-template +var borg_script_template string + +//go:embed systemd-service-template +var systemd_service_template string + +//go:embed systemd-timer-template +var systemd_timer_template string + +type Job struct { + hostname string + path string + privateKey []byte +} + +type BorgClient struct { + chain []gonf.Promise + jobs map[string]*Job // name -> privateKey + path string + status gonf.Status +} + +func (b *BorgClient) IfRepaired(p ...gonf.Promise) gonf.Promise { + b.chain = append(b.chain, p...) + return b +} + +func (b *BorgClient) Promise() *BorgClient { + gonf.MakeCustomPromise(b).Promise() + return b +} + +func (b *BorgClient) Resolve() { + b.status = gonf.KEPT + // borg package + switch installBorgPackage() { + case gonf.BROKEN: + b.status = gonf.BROKEN + return + case gonf.REPAIRED: + b.status = gonf.REPAIRED + } + // private key + rootDir := gonf.ModeUserGroup(0700, "root", "root") + rootRO := gonf.ModeUserGroup(0400, "root", "root") + rootRX := gonf.ModeUserGroup(0500, "root", "root") + gonf.Directory("/root/.cache/borg").DirectoriesPermissions(rootDir).Resolve() + gonf.Directory("/root/.config/borg").DirectoriesPermissions(rootDir).Resolve() + systemdSystemPath := "/etc/systemd/system/" + hostname, err := os.Hostname() + if err != nil { + slog.Error("Unable to find current hostname", "err", err) + hostname = "unknown" + } + for name, job := range b.jobs { + gonf.File(filepath.Join(b.path, fmt.Sprintf("%s.key", name))). + DirectoriesPermissions(rootDir). + Permissions(rootRO).Contents(job.privateKey). + Resolve() + gonf.File(filepath.Join(b.path, fmt.Sprintf("%s.sh", name))). + DirectoriesPermissions(rootDir). + Permissions(rootRX).Contents(fmt.Sprintf(borg_script_template, + hostname, + name, + job.path, + job.hostname, name)). + Resolve() + service_name := fmt.Sprintf("borgbackup-job-%s.service", name) + gonf.File(filepath.Join(systemdSystemPath, service_name)). + DirectoriesPermissions(rootDir). + Permissions(rootRO).Contents(fmt.Sprintf(systemd_service_template, + name, + job.hostname, name, + name, + name)). + Resolve() + timer_name := fmt.Sprintf("borgbackup-job-%s.timer", name) + gonf.File(filepath.Join(systemdSystemPath, timer_name)). + DirectoriesPermissions(rootDir). + Permissions(rootRO).Contents(fmt.Sprintf(systemd_timer_template, name)). + Resolve() + gonf.Service(timer_name).State("enabled", "started").Resolve() + } +} + +func (b BorgClient) Status() gonf.Status { + return b.status +} + +func Client() *BorgClient { + return &BorgClient{ + chain: nil, + jobs: make(map[string]*Job), + path: "/etc/borg/", + status: gonf.PROMISED, + } +} + +func (b *BorgClient) Add(name string, path string, privateKey []byte, hostname string) *BorgClient { + if _, ok := b.jobs[name]; ok { + slog.Debug("Duplicate name for BorgClient", "name", name) + panic("Duplicate name for BorgClient") + } + b.jobs[name] = &Job{ + hostname: hostname, + path: path, + privateKey: privateKey, + } + return b +} diff --git a/stdlib/backups/borg/server.go b/stdlib/backups/borg/server.go index 2ef1d7e..4fe0e74 100644 --- a/stdlib/backups/borg/server.go +++ b/stdlib/backups/borg/server.go @@ -73,7 +73,6 @@ func (b *BorgServer) Resolve() { case gonf.REPAIRED: b.status = gonf.REPAIRED } - // TODO init repositories? or let the borg client do it? } func (b BorgServer) Status() gonf.Status { diff --git a/stdlib/backups/borg/systemd-service-template b/stdlib/backups/borg/systemd-service-template new file mode 100644 index 0000000..336f6d2 --- /dev/null +++ b/stdlib/backups/borg/systemd-service-template @@ -0,0 +1,15 @@ +[Unit] +Description=BorgBackup job %s + +[Service] +Environment="BORG_REPO=ssh://borg@%s/srv/borg/%s" +Environment="BORG_RSH=ssh -i /etc/borg/%s.key" +CPUSchedulingPolicy=idle +ExecStart=/etc/borg/%s.sh +Group=root +IOSchedulingClass=idle +PrivateTmp=true +ProtectSystem=strict +ReadWritePaths=/root/.config/borg +ReadWritePaths=/root/.cache/borg +User=root diff --git a/stdlib/backups/borg/systemd-timer-template b/stdlib/backups/borg/systemd-timer-template new file mode 100644 index 0000000..218e5cc --- /dev/null +++ b/stdlib/backups/borg/systemd-timer-template @@ -0,0 +1,9 @@ +[Unit] +Description=BorgBackup job %s timer + +[Timer] +OnCalendar=daily +Persistent=false + +[Install] +WantedBy=timers.target |