replace use of shell w/ direct calls

This commit is contained in:
Sam Hoffman
2026-01-31 01:55:24 -05:00
parent 297c499bba
commit 7774a5b956
2 changed files with 92 additions and 46 deletions

View File

@@ -7,8 +7,6 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"os/exec"
"strconv"
"strings" "strings"
"time" "time"
@@ -16,12 +14,13 @@ import (
) )
type BackupJob struct { type BackupJob struct {
Source string `yaml:"source"` // the source dataset (e.g., zroot) Source string `yaml:"source"` // the source dataset (e.g., zroot)
TargetHost string `yaml:"targetHost"` // SSH-compatible host TargetHost string `yaml:"targetHost"` // SSH-compatible host
Target string `yaml:"target"` // the target dataset Target string `yaml:"target"` // the target dataset
Keep int `yaml:"keep"` // number of snapshots to keep Keep int `yaml:"keep"` // number of snapshots to keep
Recursive bool `yaml:"recursive"` // create recursive snapshots MaxAge time.Duration `yaml:"maxAge"` // age at which to delete snapshot (if Keep is met)
Prefix string `yaml:"prefix"` // name each snapshot with this prefix Recursive bool `yaml:"recursive"` // create recursive snapshots
Prefix string `yaml:"prefix"` // name each snapshot with this prefix
} }
// func (j *BackupJob) getBaseSnap() { // func (j *BackupJob) getBaseSnap() {
@@ -30,44 +29,20 @@ type BackupJob struct {
// dstCmd := fmt.Sprintf(params, j.Target) // dstCmd := fmt.Sprintf(params, j.Target)
// } // }
func (j *BackupJob) listSnapshots(ctx context.Context) ([]zfs.Snapshot, error) { func (j *BackupJob) listSnapshots(ctx context.Context) ([]zfs.Snapshot, error) {
out, err := zfs.Cmd(ctx, "zfs list -Hp -o name,creation -t snapshot -s creation %s | grep '%s'", j.Source, j.Prefix) allSnaps, err := zfs.Snapshots(ctx, j.Source)
if err != nil { if err != nil {
execErr, ok := err.(*exec.ExitError) return nil, err
if !ok {
log.Printf("%s\n", out)
return nil, err
}
if execErr.ExitCode() == 1 {
return nil, nil
}
} }
snapList := strings.Split(string(out), "\n") filtered := make([]zfs.Snapshot, 0)
snaps := make([]zfs.Snapshot, len(snapList)-1) for _, snap := range allSnaps {
if !strings.Contains(snap.Name, j.Prefix) {
for i, snap := range snapList {
params := strings.Split(snap, "\t") // "zroot@foo\tTIME" -> ["zroot@foo", "TIME"]
if len(params) != 2 {
continue continue
} }
log.Printf("%+v\n", params) filtered = append(filtered, snap)
t, err := strconv.ParseInt(params[1], 10, 64)
if err != nil {
log.Fatalf("invalid time %s", params[1])
}
identifier := strings.Split(params[0], "@") // zroot@foo -> ["zroot", "foo"]
snaps[i] = zfs.Snapshot{
SnapshotName: identifier[1],
Dataset: identifier[0],
Name: params[0],
Creation: time.Unix(t, 0),
}
} }
return snaps, nil return filtered, nil
} }
func (j *BackupJob) snapName() string { func (j *BackupJob) snapName() string {
@@ -97,9 +72,8 @@ func (j *BackupJob) Snapshot(ctx context.Context) (string, error) {
} }
func (j *BackupJob) FullSend(ctx context.Context, snap string) { func (j *BackupJob) FullSend(ctx context.Context, snap string) {
out, err := zfs.Cmd( out, err := zfs.FullSend(
ctx, ctx,
"zfs send -Lec %s | ssh %s zfs recv -Fu %s",
snap, snap,
j.TargetHost, j.TargetHost,
j.Target, j.Target,
@@ -109,19 +83,43 @@ func (j *BackupJob) FullSend(ctx context.Context, snap string) {
} }
} }
func (j *BackupJob) IncrementalSend(ctx context.Context, prevSnap *zfs.Snapshot, newSnap string) { func (j *BackupJob) IncrementalSend(ctx context.Context, prevSnap *zfs.Snapshot, newSnap string) ([]byte, error) {
out, err := zfs.Cmd( return zfs.IncrementalSend(
ctx, ctx,
"zfs send -I@%s %s | ssh %s zfs recv %s",
prevSnap.SnapshotName, prevSnap.SnapshotName,
newSnap, newSnap,
j.TargetHost, j.TargetHost,
j.Target, j.Target,
) )
}
func (j *BackupJob) Retain(ctx context.Context) error {
snaps, err := j.listSnapshots(ctx)
if err != nil { if err != nil {
// FIXME: return an error so we can cleanup return fmt.Errorf("retain: failure listing snapshots: %s", err.Error())
log.Fatalf("zfs-send: error: %s", out)
} }
forDeletion := make([]zfs.Snapshot, 0, len(snaps))
now := time.Now()
if len(snaps) < j.Keep {
return nil
}
deleteCount := len(snaps) - j.Keep
for i, snap := range snaps {
age := now.Sub(snap.Creation)
if age >= j.MaxAge {
forDeletion[i] = snap
}
if len(forDeletion) == deleteCount {
break
}
}
return nil
} }
func (j *BackupJob) Do(ctx context.Context) { func (j *BackupJob) Do(ctx context.Context) {

View File

@@ -10,6 +10,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"git.gentoo.party/sam/thanks/internal/runner"
) )
type Snapshot struct { type Snapshot struct {
@@ -113,3 +115,49 @@ func Snapshots(ctx context.Context, target string) ([]Snapshot, error) {
return snaps, err return snaps, err
} }
func FullSend(ctx context.Context, snap, sshTarget, target string) ([]byte, error) {
return runner.Pipeline(
exec.CommandContext(
ctx,
"zfs",
"send",
"-Lec",
snap,
),
exec.CommandContext(
ctx,
"ssh",
sshTarget,
"zfs",
"recv",
"-Fu",
target,
),
)
}
func IncrementalSend(ctx context.Context, prevSnapName, newSnapName, sshTarget, target string) ([]byte, error) {
return runner.Pipeline(
exec.CommandContext(
ctx,
"zfs",
"send",
fmt.Sprintf("-I@%s", prevSnapName),
newSnapName,
),
exec.CommandContext(
ctx,
"ssh",
sshTarget,
"zfs",
"recv",
target,
),
)
}
func Destroy(ctx context.Context, target string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "zfs", "destroy", target)
return cmd.CombinedOutput()
}