package zfs import ( "bufio" "context" "fmt" "io" "log" "os/exec" "strconv" "strings" "time" "git.gentoo.party/sam/thanks/internal/runner" ) type Snapshot struct { SnapshotName string // Name sans dataset (the part after the '@') Name string // Full dataset@snapname Dataset string Creation time.Time GUID uint64 } 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 } func ParseSnapshots(reader io.Reader) ([]Snapshot, error) { scanner := bufio.NewScanner(reader) snaps := make([]Snapshot, 0) for scanner.Scan() { line := scanner.Text() params := strings.Split(line, "\t") // "zroot@foo\tTIME" -> ["zroot@foo", "TIME"] if len(params) != 3 { continue } 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"] GUID, err := strconv.ParseUint(params[2], 10, 64) if err != nil { log.Fatalf("invalid GUID: %s - %s", params[2], err.Error()) } snaps = append( snaps, Snapshot{ SnapshotName: identifier[1], Dataset: identifier[0], Name: params[0], Creation: time.Unix(t, 0), GUID: GUID, }, ) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("error parsing zfs-list output: %s", err.Error()) } return snaps, nil } type Host struct { SSH string ZFSPath string } func (h *Host) CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd { var args []string if h.SSH != "" { name = "ssh" args = append([]string{"ssh", h.SSH}, arg...) } else { args = arg } cmd := exec.CommandContext( ctx, name, args..., ) return cmd } // h.Comman func Snapshots(ctx context.Context, target string) ([]Snapshot, error) { cmd := exec.CommandContext( ctx, "zfs", "list", "-Hp", "-o", "name,creation,guid", "-t", "snapshot", "-s", "creation", target, ) stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } defer stdout.Close() err = cmd.Start() if err != nil { return nil, err } snaps, err := ParseSnapshots(stdout) if err != nil { return nil, err } err = cmd.Wait() if err != nil { return nil, 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() }