diff --git a/client/migrate.go b/client/migrate.go index 6e85a04..c7fbbf9 100644 --- a/client/migrate.go +++ b/client/migrate.go @@ -211,17 +211,15 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f } fmt.Printf("identified owning job %q\n", owningJob.Name()) - versions, err := zfs.ZFSListFilesystemVersions(fs, nil) + bookmarks, err := zfs.ZFSListFilesystemVersions(fs, zfs.ListFilesystemVersionsOptions{ + Types: zfs.Bookmarks, + }) if err != nil { return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString()) } var oldCursor *zfs.FilesystemVersion - for i, fsv := range versions { - if fsv.Type != zfs.Bookmark { - continue - } - + for i, fsv := range bookmarks { _, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs)) if err != endpoint.ErrV1ReplicationCursor { continue @@ -232,7 +230,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors") } - oldCursor = &versions[i] + oldCursor = &bookmarks[i] } diff --git a/daemon/filters/fsvfilter.go b/daemon/filters/fsvfilter.go deleted file mode 100644 index 812d258..0000000 --- a/daemon/filters/fsvfilter.go +++ /dev/null @@ -1,41 +0,0 @@ -package filters - -import ( - "strings" - - "github.com/zrepl/zrepl/zfs" -) - -type AnyFSVFilter struct{} - -func NewAnyFSVFilter() AnyFSVFilter { - return AnyFSVFilter{} -} - -var _ zfs.FilesystemVersionFilter = AnyFSVFilter{} - -func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) { - return true, nil -} - -type PrefixFilter struct { - prefix string - fstype zfs.VersionType - fstypeSet bool // optionals anyone? -} - -var _ zfs.FilesystemVersionFilter = &PrefixFilter{} - -func NewPrefixFilter(prefix string) *PrefixFilter { - return &PrefixFilter{prefix: prefix} -} - -func NewTypedPrefixFilter(prefix string, versionType zfs.VersionType) *PrefixFilter { - return &PrefixFilter{prefix, versionType, true} -} - -func (f *PrefixFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) { - fstypeMatches := (!f.fstypeSet || t == f.fstype) - prefixMatches := strings.HasPrefix(name, f.prefix) - return fstypeMatches && prefixMatches, nil -} diff --git a/daemon/snapper/snapper.go b/daemon/snapper/snapper.go index ec24785..44affad 100644 --- a/daemon/snapper/snapper.go +++ b/daemon/snapper/snapper.go @@ -485,7 +485,10 @@ var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions" func findSyncPointFSNextOptimalSnapshotTime(l Logger, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) { - fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot)) + fsvs, err := zfs.ZFSListFilesystemVersions(d, zfs.ListFilesystemVersionsOptions{ + Types: zfs.Snapshots, + ShortnamePrefix: prefix, + }) if err != nil { return time.Time{}, errors.Wrap(err, "list filesystem versions") } diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 49e7d08..46805f8 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -96,7 +96,7 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst if err != nil { return nil, err } - fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil) + fsvs, err := zfs.ZFSListFilesystemVersions(lp, zfs.ListFilesystemVersionsOptions{}) if err != nil { return nil, err } @@ -130,6 +130,7 @@ func (p *Sender) HintMostRecentCommonAncestor(ctx context.Context, r *pdu.HintMo TryReleaseStepStaleFS(ctx, fs, p.jobId) return &pdu.HintMostRecentCommonAncestorRes{}, nil } + // we were hinted a specific common ancestor mostRecentVersion, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, r.GetSenderVersion()) @@ -588,8 +589,9 @@ func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFile if err != nil { return nil, err } + // TODO share following code with sender - fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil) + fsvs, err := zfs.ZFSListFilesystemVersions(lp, zfs.ListFilesystemVersionsOptions{}) if err != nil { return nil, err } diff --git a/platformtest/platformtest_ops.go b/platformtest/platformtest_ops.go index 258efaa..3b4415a 100644 --- a/platformtest/platformtest_ops.go +++ b/platformtest/platformtest_ops.go @@ -24,6 +24,7 @@ type Stmt interface { type Op string const ( + Comment Op = "#" AssertExists Op = "!E" AssertNotExists Op = "!N" Add Op = "+" @@ -102,6 +103,26 @@ func (o *SnapOp) Run(ctx context.Context, e Execer) error { } } +type BookmarkOp struct { + Op Op + Existing string + Bookmark string +} + +func (o *BookmarkOp) Run(ctx context.Context, e Execer) error { + switch o.Op { + case Add: + return e.RunExpectSuccessNoOutput(ctx, "zfs", "bookmark", o.Existing, o.Bookmark) + case Del: + if o.Existing != "" { + panic("existing must be empty for destroy, got " + o.Existing) + } + return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Bookmark) + default: + panic(o.Op) + } +} + type RunOp struct { RootDS string Script string @@ -255,16 +276,26 @@ nextLine: op = AssertExists case string(AssertNotExists): op = AssertNotExists + case string(Comment): + op = Comment + continue default: return nil, &LineError{scan.Text(), fmt.Sprintf("invalid op %q", comps.Text())} } - // FS / SNAP + // FS / SNAP / BOOKMARK if err := expectMoreTokens(); err != nil { return nil, err } if strings.ContainsAny(comps.Text(), "@") { stmts = append(stmts, &SnapOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, comps.Text())}) + } else if strings.ContainsAny(comps.Text(), "#") { + bookmark := fmt.Sprintf("%s/%s", rootds, comps.Text()) + if err := expectMoreTokens(); err != nil { + return nil, err + } + existing := fmt.Sprintf("%s/%s", rootds, comps.Text()) + stmts = append(stmts, &BookmarkOp{Op: op, Existing: existing, Bookmark: bookmark}) } else { // FS fs := comps.Text() diff --git a/platformtest/tests/helpers.go b/platformtest/tests/helpers.go index 7f7b612..72c940a 100644 --- a/platformtest/tests/helpers.go +++ b/platformtest/tests/helpers.go @@ -5,6 +5,7 @@ import ( "math/rand" "os" "path" + "sort" "strings" "github.com/stretchr/testify/require" @@ -150,3 +151,26 @@ func makeResumeSituation(ctx *platformtest.Context, src dummySnapshotSituation, return situation } + +func versionRelnamesSorted(versions []zfs.FilesystemVersion) []string { + var vstrs []string + for _, v := range versions { + vstrs = append(vstrs, v.RelName()) + } + sort.Sort(sort.StringSlice(vstrs)) + return vstrs +} + +func datasetToStringSortedTrimPrefix(prefix *zfs.DatasetPath, paths []*zfs.DatasetPath) []string { + var pstrs []string + for _, p := range paths { + trimmed := p.Copy() + trimmed.TrimPrefix(prefix) + if trimmed.Length() == 0 { + continue + } + pstrs = append(pstrs, trimmed.ToString()) + } + sort.Sort(sort.StringSlice(pstrs)) + return pstrs +} diff --git a/platformtest/tests/listFilesystemVersions.go b/platformtest/tests/listFilesystemVersions.go new file mode 100644 index 0000000..e8d3528 --- /dev/null +++ b/platformtest/tests/listFilesystemVersions.go @@ -0,0 +1,178 @@ +package tests + +import ( + "fmt" + "sort" + "strings" + + "github.com/stretchr/testify/require" + "github.com/zrepl/zrepl/platformtest" + "github.com/zrepl/zrepl/zfs" +) + +func ListFilesystemVersionsTypeFilteringAndPrefix(t *platformtest.Context) { + platformtest.Run(t, platformtest.PanicErr, t.RootDataset, ` + DESTROYROOT + CREATEROOT + + "foo bar" + + "foo bar@foo 1" + + "foo bar#foo 1" "foo bar@foo 1" + + "foo bar#bookfoo 1" "foo bar@foo 1" + + "foo bar@foo 2" + + "foo bar#foo 2" "foo bar@foo 2" + + "foo bar#bookfoo 2" "foo bar@foo 2" + + "foo bar@blup 1" + + "foo bar#blup 1" "foo bar@blup 1" + + "foo bar@ foo with leading whitespace" + + # repeat the whole thing for a child dataset to make sure we disable recursion + + + "foo bar/child dataset" + + "foo bar/child dataset@foo 1" + + "foo bar/child dataset#foo 1" "foo bar/child dataset@foo 1" + + "foo bar/child dataset#bookfoo 1" "foo bar/child dataset@foo 1" + + "foo bar/child dataset@foo 2" + + "foo bar/child dataset#foo 2" "foo bar/child dataset@foo 2" + + "foo bar/child dataset#bookfoo 2" "foo bar/child dataset@foo 2" + + "foo bar/child dataset@blup 1" + + "foo bar/child dataset#blup 1" "foo bar/child dataset@blup 1" + + "foo bar/child dataset@ foo with leading whitespace" + `) + + fs := fmt.Sprintf("%s/foo bar", t.RootDataset) + + // no options := all types + vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{}) + require.NoError(t, err) + require.Equal(t, []string{ + "#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2", + "@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2", + }, versionRelnamesSorted(vs)) + + // just snapshots + vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{ + Types: zfs.Snapshots, + }) + require.NoError(t, err) + require.Equal(t, []string{"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs)) + + // just bookmarks + vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{ + Types: zfs.Bookmarks, + }) + require.NoError(t, err) + require.Equal(t, []string{"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2"}, versionRelnamesSorted(vs)) + + // just with prefix foo + vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{ + ShortnamePrefix: "foo", + }) + require.NoError(t, err) + require.Equal(t, []string{"#foo 1", "#foo 2", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs)) + +} + +func ListFilesystemVersionsZeroExistIsNotAnError(t *platformtest.Context) { + platformtest.Run(t, platformtest.PanicErr, t.RootDataset, ` + DESTROYROOT + CREATEROOT + + "foo bar" + `) + + fs := fmt.Sprintf("%s/foo bar", t.RootDataset) + + vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{}) + require.Empty(t, vs) + require.NoError(t, err) + dsne, ok := err.(*zfs.DatasetDoesNotExist) + require.True(t, ok) + require.Equal(t, fs, dsne.Path) +} + +func ListFilesystemVersionsFilesystemNotExist(t *platformtest.Context) { + platformtest.Run(t, platformtest.PanicErr, t.RootDataset, ` + DESTROYROOT + CREATEROOT + `) + + nonexistentFS := fmt.Sprintf("%s/not existent", t.RootDataset) + + vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(nonexistentFS), zfs.ListFilesystemVersionsOptions{}) + require.Empty(t, vs) + require.Error(t, err) + t.Logf("err = %T\n%s", err, err) + dsne, ok := err.(*zfs.DatasetDoesNotExist) + require.True(t, ok) + require.Equal(t, nonexistentFS, dsne.Path) +} + +func ListFilesystemVersionsUserrefs(t *platformtest.Context) { + platformtest.Run(t, platformtest.PanicErr, t.RootDataset, ` + DESTROYROOT + CREATEROOT + + "foo bar" + + "foo bar@snap 1" + + "foo bar#snap 1" "foo bar@snap 1" + + "foo bar@snap 2" + + "foo bar#snap 2" "foo bar@snap 2" + R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@snap 2" + + "foo bar@snap 3" + + "foo bar#snap 3" "foo bar@snap 3" + R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@snap 3" + R zfs hold zrepl_platformtest_second_hold "${ROOTDS}/foo bar@snap 3" + + "foo bar@snap 4" + + "foo bar#snap 4" "foo bar@snap 4" + + + + "foo bar/child datset" + + "foo bar/child datset@snap 1" + + "foo bar/child datset#snap 1" "foo bar/child datset@snap 1" + + "foo bar/child datset@snap 2" + + "foo bar/child datset#snap 2" "foo bar/child datset@snap 2" + R zfs hold zrepl_platformtest "${ROOTDS}/foo bar/child datset@snap 2" + + "foo bar/child datset@snap 3" + + "foo bar/child datset#snap 3" "foo bar/child datset@snap 3" + R zfs hold zrepl_platformtest "${ROOTDS}/foo bar/child datset@snap 3" + R zfs hold zrepl_platformtest_second_hold "${ROOTDS}/foo bar/child datset@snap 3" + + "foo bar/child datset@snap 4" + + "foo bar/child datset#snap 4" "foo bar/child datset@snap 4" + `) + + fs := fmt.Sprintf("%s/foo bar", t.RootDataset) + + vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{}) + require.NoError(t, err) + + type expectation struct { + relName string + userrefs zfs.OptionUint64 + } + + expect := []expectation{ + {"#snap 1", zfs.OptionUint64{Valid: false}}, + {"#snap 2", zfs.OptionUint64{Valid: false}}, + {"#snap 3", zfs.OptionUint64{Valid: false}}, + {"#snap 4", zfs.OptionUint64{Valid: false}}, + {"@snap 1", zfs.OptionUint64{Value: 0, Valid: true}}, + {"@snap 2", zfs.OptionUint64{Value: 1, Valid: true}}, + {"@snap 3", zfs.OptionUint64{Value: 2, Valid: true}}, + {"@snap 4", zfs.OptionUint64{Value: 0, Valid: true}}, + } + + sort.Slice(vs, func(i, j int) bool { + return strings.Compare(vs[i].RelName(), vs[j].RelName()) < 0 + }) + + var expectRelNames []string + for _, e := range expect { + expectRelNames = append(expectRelNames, e.relName) + } + + require.Equal(t, expectRelNames, versionRelnamesSorted(vs)) + + for i, e := range expect { + require.Equal(t, e.relName, vs[i].RelName()) + require.Equal(t, e.userrefs, vs[i].UserRefs) + } + +} diff --git a/platformtest/tests/listFilesystems.go b/platformtest/tests/listFilesystems.go new file mode 100644 index 0000000..05f5167 --- /dev/null +++ b/platformtest/tests/listFilesystems.go @@ -0,0 +1,33 @@ +package tests + +import ( + "strings" + + "github.com/stretchr/testify/require" + "github.com/zrepl/zrepl/platformtest" + "github.com/zrepl/zrepl/zfs" +) + +func ListFilesystemsNoFilter(t *platformtest.Context) { + platformtest.Run(t, platformtest.PanicErr, t.RootDataset, ` + DESTROYROOT + CREATEROOT + R zfs create -V 10M "${ROOTDS}/bar baz" + + "foo bar" + + "foo bar/bar blup" + + "foo bar/blah" + R zfs create -V 10M "${ROOTDS}/foo bar/blah/a volume" + `) + + fss, err := zfs.ZFSListMapping(t, zfs.NoFilter()) + require.NoError(t, err) + var onlyTestPool []*zfs.DatasetPath + for _, fs := range fss { + if strings.HasPrefix(fs.ToString(), t.RootDataset) { + onlyTestPool = append(onlyTestPool, fs) + } + } + onlyTestPoolStr := datasetToStringSortedTrimPrefix(mustDatasetPath(t.RootDataset), onlyTestPool) + require.Equal(t, []string{"bar baz", "foo bar", "foo bar/bar blup", "foo bar/blah", "foo bar/blah/a volume"}, onlyTestPoolStr) + +} diff --git a/platformtest/tests/tests.go b/platformtest/tests/tests.go index aacdb8e..1aee976 100644 --- a/platformtest/tests/tests.go +++ b/platformtest/tests/tests.go @@ -26,4 +26,9 @@ var Cases = []Case{ SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden, SendArgsValidationResumeTokenEncryptionMismatchForbidden, SendArgsValidationResumeTokenDifferentFilesystemForbidden, + ListFilesystemVersionsTypeFilteringAndPrefix, + ListFilesystemVersionsFilesystemNotExist, + ListFilesystemVersionsFilesystemNotExist, + ListFilesystemVersionsUserrefs, + ListFilesystemsNoFilter, } diff --git a/zfs/mapping.go b/zfs/mapping.go index ca2c794..f414c9d 100644 --- a/zfs/mapping.go +++ b/zfs/mapping.go @@ -59,7 +59,7 @@ func ZFSListMappingProperties(ctx context.Context, filter DatasetFilter, propert defer cancel() rchan := make(chan ZFSListResult) - go ZFSListChan(ctx, rchan, properties, "-r", "-t", "filesystem,volume") + go ZFSListChan(ctx, rchan, properties, nil, "-r", "-t", "filesystem,volume") datasets = make([]ZFSListMappingPropertiesResult, 0) for r := range rchan { diff --git a/zfs/versions.go b/zfs/versions.go index 89a0f8c..846e9a9 100644 --- a/zfs/versions.go +++ b/zfs/versions.go @@ -20,6 +20,30 @@ const ( Snapshot VersionType = "snapshot" ) +type VersionTypeSet map[VersionType]bool + +var ( + AllVersionTypes = VersionTypeSet{ + Bookmark: true, + Snapshot: true, + } + Bookmarks = VersionTypeSet{ + Bookmark: true, + } + Snapshots = VersionTypeSet{ + Snapshot: true, + } +) + +func (s VersionTypeSet) zfsListTFlagRepr() string { + var types []string + for t, _ := range s { + types = append(types, t.String()) + } + return strings.Join(types, ",") +} +func (s VersionTypeSet) String() string { return s.zfsListTFlagRepr() } + func (t VersionType) DelimiterChar() string { switch t { case Bookmark: @@ -71,6 +95,14 @@ type FilesystemVersion struct { // The time the dataset was created Creation time.Time + + // userrefs field (snapshots only) + UserRefs OptionUint64 +} + +type OptionUint64 struct { + Value uint64 + Valid bool } func (v FilesystemVersion) GetCreateTXG() uint64 { return v.CreateTXG } @@ -104,8 +136,8 @@ func (v FilesystemVersion) ToSendArgVersion() ZFSSendArgVersion { } type ParseFilesystemVersionArgs struct { - fullname string - guid, createtxg, creation string + fullname string + guid, createtxg, creation, userrefs string } func ParseFilesystemVersion(args ParseFilesystemVersionArgs) (v FilesystemVersion, err error) { @@ -132,27 +164,49 @@ func ParseFilesystemVersion(args ParseFilesystemVersionArgs) (v FilesystemVersio v.Creation = time.Unix(creationUnix, 0) } + switch v.Type { + case Bookmark: + if args.userrefs != "-" { + return v, errors.Errorf("expecting %q for bookmark property userrefs, got %q", "-", args.userrefs) + } + v.UserRefs = OptionUint64{Valid: false} + case Snapshot: + if v.UserRefs.Value, err = strconv.ParseUint(args.userrefs, 10, 64); err != nil { + err = errors.Wrapf(err, "cannot parse userrefs %q", args.userrefs) + return v, err + } + v.UserRefs.Valid = true + default: + panic(v.Type) + } + return v, nil } -type FilesystemVersionFilter interface { - Filter(t VersionType, name string) (accept bool, err error) +type ListFilesystemVersionsOptions struct { + // the prefix of the version name, without the delimiter char + // empty means any prefix matches + ShortnamePrefix string + + // which types should be returned + // nil or len(0) means any prefix matches + Types VersionTypeSet } -type closureFilesystemVersionFilter struct { - cb func(t VersionType, name string) (accept bool, err error) +func (o *ListFilesystemVersionsOptions) typesFlagArgs() string { + if len(o.Types) == 0 { + return AllVersionTypes.zfsListTFlagRepr() + } else { + return o.Types.zfsListTFlagRepr() + } } -func (f *closureFilesystemVersionFilter) Filter(t VersionType, name string) (accept bool, err error) { - return f.cb(t, name) +func (o *ListFilesystemVersionsOptions) matches(v FilesystemVersion) bool { + return (len(o.Types) == 0 || o.Types[v.Type]) && strings.HasPrefix(v.Name, o.ShortnamePrefix) } -func FilterFromClosure(cb func(t VersionType, name string) (accept bool, err error)) FilesystemVersionFilter { - return &closureFilesystemVersionFilter{cb} -} - -// returned versions are sorted by createtxg -func ZFSListFilesystemVersions(fs *DatasetPath, filter FilesystemVersionFilter) (res []FilesystemVersion, err error) { +// returned versions are sorted by createtxg FIXME drop sort by createtxg requirement +func ZFSListFilesystemVersions(fs *DatasetPath, options ListFilesystemVersionsOptions) (res []FilesystemVersion, err error) { listResults := make(chan ZFSListResult) promTimer := prometheus.NewTimer(prom.ZFSListFilesystemVersionDuration.WithLabelValues(fs.ToString())) @@ -161,9 +215,10 @@ func ZFSListFilesystemVersions(fs *DatasetPath, filter FilesystemVersionFilter) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go ZFSListChan(ctx, listResults, - []string{"name", "guid", "createtxg", "creation"}, + []string{"name", "guid", "createtxg", "creation", "userrefs"}, + fs, "-r", "-d", "1", - "-t", "bookmark,snapshot", + "-t", options.typesFlagArgs(), "-s", "createtxg", fs.ToString()) res = make([]FilesystemVersion, 0) @@ -182,21 +237,14 @@ func ZFSListFilesystemVersions(fs *DatasetPath, filter FilesystemVersionFilter) guid: line[1], createtxg: line[2], creation: line[3], + userrefs: line[4], } v, err := ParseFilesystemVersion(args) if err != nil { return nil, err } - accept := true - if filter != nil { - accept, err = filter.Filter(v.Type, v.Name) - if err != nil { - err = fmt.Errorf("error executing filter: %s", err) - return nil, err - } - } - if accept { + if options.matches(v) { res = append(res, v) } diff --git a/zfs/zfs.go b/zfs/zfs.go index 531b38b..8e7c842 100644 --- a/zfs/zfs.go +++ b/zfs/zfs.go @@ -221,9 +221,12 @@ type ZFSListResult struct { // If no error occurs, it is just closed. // If the operation is cancelled via context, the channel is just closed. // +// If notExistHint is not nil and zfs exits with an error, +// the stderr is attempted to be interpreted as a *DatasetDoesNotExist error. +// // However, if callers do not drain `out` or cancel via `ctx`, the process will leak either running because // IO is pending or as a zombie. -func ZFSListChan(ctx context.Context, out chan ZFSListResult, properties []string, zfsArgs ...string) { +func ZFSListChan(ctx context.Context, out chan ZFSListResult, properties []string, notExistHint *DatasetPath, zfsArgs ...string) { defer close(out) args := make([]string, 0, 4+len(zfsArgs)) @@ -272,11 +275,21 @@ func ZFSListChan(ctx context.Context, out chan ZFSListResult, properties []strin } } if err := cmd.Wait(); err != nil { - if err, ok := err.(*exec.ExitError); ok { - sendResult(nil, &ZFSError{ - Stderr: stderrBuf.Bytes(), - WaitErr: err, - }) + if _, ok := err.(*exec.ExitError); ok { + enotexist := func() error { + if notExistHint == nil { + return nil + } + return tryDatasetDoesNotExist(notExistHint.ToString(), stderrBuf.Bytes()) + } + if err := enotexist(); err != nil { + sendResult(nil, err) + } else { + sendResult(nil, &ZFSError{ + Stderr: stderrBuf.Bytes(), + WaitErr: err, + }) + } } else { sendResult(nil, &ZFSError{WaitErr: err}) } @@ -1075,15 +1088,12 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, streamCopier // does not perform a rollback unless `send -R` was used (which we assume hasn't been the case) var snaps []FilesystemVersion { - vs, err := ZFSListFilesystemVersions(fsdp, nil) + snaps, err := ZFSListFilesystemVersions(fsdp, ListFilesystemVersionsOptions{ + Types: Snapshots, + }) if err != nil { return fmt.Errorf("cannot list versions for rollback for forced receive: %s", err) } - for _, v := range vs { - if v.Type == Snapshot { - snaps = append(snaps, v) - } - } sort.Slice(snaps, func(i, j int) bool { return snaps[i].CreateTXG < snaps[j].CreateTXG })