summaryrefslogtreecommitdiff
path: root/stdlib/backups/borg
diff options
context:
space:
mode:
Diffstat (limited to 'stdlib/backups/borg')
-rw-r--r--stdlib/backups/borg/borg-script-template27
-rw-r--r--stdlib/backups/borg/client.go123
-rw-r--r--stdlib/backups/borg/server.go1
-rw-r--r--stdlib/backups/borg/systemd-service-template15
-rw-r--r--stdlib/backups/borg/systemd-timer-template9
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