diff --git a/internal/zfs/zfs-list_snapshot.txt b/internal/zfs/zfs-list_snapshot.txt new file mode 100644 index 0000000..b7d83b8 --- /dev/null +++ b/internal/zfs/zfs-list_snapshot.txt @@ -0,0 +1,6 @@ +zroot/home/sam/thanks@thanks-1769803874756987 1769803874 9856814317153087085 +zroot/home/sam/thanks@thanks-1769804009334385 1769804009 10898692868281532431 +zroot/home/sam/thanks@thanks-1769810365460466 1769810365 17939462811459040773 +zroot/home/sam/thanks@thanks-1769810764161053 1769810764 17021914338902266865 +zroot/home/sam/thanks@thanks-1769813076492805 1769813076 17716303459843516357 +zroot/home/sam/thanks@thanks-1769813083041347 1769813083 905910106048059171 diff --git a/internal/zfs/zfs.go b/internal/zfs/zfs.go index 2412377..c642863 100644 --- a/internal/zfs/zfs.go +++ b/internal/zfs/zfs.go @@ -1,9 +1,14 @@ package zfs import ( + "bufio" "context" "fmt" + "io" + "log" "os/exec" + "strconv" + "strings" "time" ) @@ -12,7 +17,7 @@ type Snapshot struct { Name string // Full dataset@snapname Dataset string Creation time.Time - GUID int64 + GUID uint64 } func Cmd(ctx context.Context, arg string, a ...any) ([]byte, error) { @@ -29,3 +34,82 @@ func Cmd(ctx context.Context, arg string, a ...any) ([]byte, error) { } 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 +} + +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 +} diff --git a/internal/zfs/zfs_test.go b/internal/zfs/zfs_test.go index ddaa201..37889e2 100644 --- a/internal/zfs/zfs_test.go +++ b/internal/zfs/zfs_test.go @@ -2,8 +2,12 @@ package zfs_test import ( "fmt" + "os" "os/exec" "testing" + + "git.gentoo.party/sam/thanks/internal/zfs" + "github.com/stretchr/testify/assert" ) type zfscmd struct { @@ -43,3 +47,14 @@ func Test_ZFSCommand(t *testing.T) { t.Errorf("SSH zrunner failed: %s \n\n%s", err.Error(), out) } } + +func Test_Snapshots(t *testing.T) { + f, err := os.Open("./zfs-list_snapshot.txt") + assert.Nil(t, err, "failed to open test data") + out, err := zfs.ParseSnapshots(f) + assert.Nil(t, err, "ParseSnapshots returned non-nil error") + + for _, snap := range out { + assert.Contains(t, snap.SnapshotName, "thanks") + } +}