diff --git a/internal/jobs/jobs.go b/internal/jobs/jobs.go new file mode 100644 index 0000000..06adf1b --- /dev/null +++ b/internal/jobs/jobs.go @@ -0,0 +1,141 @@ +package jobs + +import ( + "context" + "fmt" + "log" + "os/exec" + "strconv" + "strings" + "time" + + "git.gentoo.party/sam/thanks/internal/zfs" +) + +type BackupJob struct { + Source string // the source dataset (e.g., zroot) + TargetHost string // SSH-compatible host + Target string // the target dataset + Keep int // number of snapshots to keep + Prefix string // name each snapshot with this prefix + Recursive bool // create recursive snapshots +} + +// func (j *BackupJob) getBaseSnap() { +// params := "zfs get -j -d 1 -t snapshot guid,creation %s" +// srcCmd := fmt.Sprintf(params, j.Source) +// dstCmd := fmt.Sprintf(params, j.Target) +// } +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) + if err != nil { + execErr, ok := err.(*exec.ExitError) + if !ok { + log.Printf("%s\n", out) + return nil, err + } + + if execErr.ExitCode() == 1 { + return nil, nil + } + } + + snapList := strings.Split(string(out), "\n") + snaps := make([]zfs.Snapshot, len(snapList)-1) + + for i, snap := range snapList { + params := strings.Split(snap, "\t") // "zroot@foo\tTIME" -> ["zroot@foo", "TIME"] + if len(params) != 2 { + continue + } + log.Printf("%+v\n", params) + 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 +} + +func (j *BackupJob) snapName() string { + // ts := time.Now().Format(time.RFC3339) + ts := time.Now().UnixMicro() + if j.Prefix == "" { + return fmt.Sprintf("%d", ts) + } else { + return fmt.Sprintf("%s%d", j.Prefix, ts) + } +} + +func (j *BackupJob) Snapshot(ctx context.Context) (string, error) { + snapName := j.snapName() + snap := fmt.Sprintf("%s@%s", j.Source, snapName) + + out, err := zfs.Cmd( + ctx, + "zfs snapshot %s", + snap, + ) + if err != nil { + log.Printf("zfs-snapshot: error: %s", out) + return snapName, nil + } + return snap, err +} + +func (j *BackupJob) FullSend(ctx context.Context, snap string) { + out, err := zfs.Cmd(ctx, "zfs send -Lec %s | ssh %s zfs recv -Fu %s", snap, j.TargetHost, j.Target) + if err != nil { + log.Fatalf("zfs-send: error: %s", out) + } +} + +func (j *BackupJob) IncrementalSend(ctx context.Context, prevSnap *zfs.Snapshot, newSnap string) { + out, err := zfs.Cmd( + ctx, + "zfs send -I@%s %s | ssh %s zfs recv %s", + prevSnap.SnapshotName, + newSnap, + j.TargetHost, + j.Target, + ) + if err != nil { + // FIXME: return an error so we can cleanup + log.Fatalf("zfs-send: error: %s", out) + } +} + +func (j *BackupJob) Do(ctx context.Context) { + currentSnaps, err := j.listSnapshots(ctx) + if err != nil { + log.Fatal("failed to list existing snapshots") + } + var prev *zfs.Snapshot + + if len(currentSnaps) > 0 { + prev = ¤tSnaps[len(currentSnaps)-1] + } + + newSnapshot, err := j.Snapshot(ctx) + if err != nil { + log.Fatalf("error creating new snapshot: %s", err.Error()) + } + + if prev == nil { + // Full Send + j.FullSend(ctx, newSnapshot) + } else { + // Inc send + j.IncrementalSend(ctx, prev, newSnapshot) + } +} diff --git a/internal/zfs/zfs.go b/internal/zfs/zfs.go new file mode 100644 index 0000000..2412377 --- /dev/null +++ b/internal/zfs/zfs.go @@ -0,0 +1,31 @@ +package zfs + +import ( + "context" + "fmt" + "os/exec" + "time" +) + +type Snapshot struct { + SnapshotName string // Name sans dataset (the part after the '@') + Name string // Full dataset@snapname + Dataset string + Creation time.Time + GUID int64 +} + +func Cmd(ctx context.Context, arg string, a ...any) ([]byte, error) { + if len(a) > 0 { + arg = fmt.Sprintf(arg, a...) + } + + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", arg) + + fmt.Printf("zfs: %+v\n", arg) + out, err := cmd.CombinedOutput() + if err != nil { + return out, err + } + return out, err +} diff --git a/main.go b/main.go index 909a771..b6f0625 100644 --- a/main.go +++ b/main.go @@ -2,183 +2,12 @@ package main import ( "context" - "fmt" - "log" - "os/exec" - "strconv" - "strings" - "time" + + "git.gentoo.party/sam/thanks/internal/jobs" ) -type Snapshot struct { - SnapshotName string // Name sans dataset (the part after the '@') - Name string // Full dataset@snapname - Dataset string - Creation time.Time - GUID int64 -} - -func zfsCmd(ctx context.Context, arg string, a ...any) ([]byte, error) { - if len(a) > 0 { - arg = fmt.Sprintf(arg, a...) - } - - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", arg) - - fmt.Printf("zfs: %+v\n", arg) - out, err := cmd.CombinedOutput() - if err != nil { - return out, err - } - return out, err -} - -func (j *BackupJob) listSnapshots(ctx context.Context) ([]Snapshot, error) { - out, err := zfsCmd(ctx, "zfs list -Hp -o name,creation -t snapshot -s creation %s | grep '%s'", j.Source, j.Prefix) - if err != nil { - execErr, ok := err.(*exec.ExitError) - if !ok { - log.Printf("%s\n", out) - return nil, err - } - - if execErr.ExitCode() == 1 { - return nil, nil - } - } - - snapList := strings.Split(string(out), "\n") - snaps := make([]Snapshot, len(snapList)-1) - - for i, snap := range snapList { - params := strings.Split(snap, "\t") // "zroot@foo\tTIME" -> ["zroot@foo", "TIME"] - if len(params) != 2 { - continue - } - log.Printf("%+v\n", params) - 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] = Snapshot{ - SnapshotName: identifier[1], - Dataset: identifier[0], - Name: params[0], - Creation: time.Unix(t, 0), - } - } - - return snaps, nil -} - -func (j *BackupJob) snapName() string { - // ts := time.Now().Format(time.RFC3339) - ts := time.Now().UnixMicro() - if j.Prefix == "" { - return fmt.Sprintf("%d", ts) - } else { - return fmt.Sprintf("%s%d", j.Prefix, ts) - } -} - -func (j *BackupJob) Snapshot(ctx context.Context) (string, error) { - snapName := j.snapName() - snap := fmt.Sprintf("%s@%s", j.Source, snapName) - - out, err := zfsCmd( - ctx, - "zfs snapshot %s", - snap, - ) - if err != nil { - log.Printf("zfs-snapshot: error: %s", out) - return snapName, nil - } - return snap, err -} - -func (j *BackupJob) FullSend(ctx context.Context, snap string) { - out, err := zfsCmd(ctx, "zfs send -Lec %s | ssh %s zfs recv -Fu %s", snap, j.TargetHost, j.Target) - if err != nil { - log.Fatalf("zfs-send: error: %s", out) - } -} - -func (j *BackupJob) IncrementalSend(ctx context.Context, prevSnap *Snapshot, newSnap string) { - out, err := zfsCmd( - ctx, - "zfs send -I@%s %s | ssh %s zfs recv %s", - prevSnap.SnapshotName, - newSnap, - j.TargetHost, - j.Target, - ) - if err != nil { - // FIXME: return an error so we can cleanup - log.Fatalf("zfs-send: error: %s", out) - } -} - -type BackupJob struct { - Source string // the source dataset (e.g., zroot) - TargetHost string // SSH-compatible host - Target string // the target dataset - Keep int // number of snapshots to keep - Prefix string // name each snapshot with this prefix - Recursive bool // create recursive snapshots -} - -func (j *BackupJob) Do(ctx context.Context) { - currentSnaps, err := j.listSnapshots(ctx) - if err != nil { - log.Fatal("failed to list existing snapshots") - } - var prev *Snapshot - - if len(currentSnaps) > 0 { - prev = ¤tSnaps[len(currentSnaps)-1] - } - - newSnapshot, err := j.Snapshot(ctx) - if err != nil { - log.Fatalf("error creating new snapshot: %s", err.Error()) - } - - if prev == nil { - // Full Send - j.FullSend(ctx, newSnapshot) - } else { - // Inc send - j.IncrementalSend(ctx, prev, newSnapshot) - } -} - -type ZProperty struct { - Value string -} - -type ZDataset struct { - Name string - Type string - SnapshotName string - Properties map[string]ZProperty -} - -type ZJSON struct { - Datasets map[string]ZDataset -} - -// func (j *BackupJob) getBaseSnap() { -// params := "zfs get -j -d 1 -t snapshot guid,creation %s" -// srcCmd := fmt.Sprintf(params, j.Source) -// dstCmd := fmt.Sprintf(params, j.Target) -// } - func main() { - myJob := BackupJob{ + myJob := jobs.BackupJob{ Source: "zroot/home/sam/thanks", Target: "zrust/backup/weller/thanks", TargetHost: "backup@woodford.gentoo.party",