endpoint: refactor, fix stale holds on initial replication, holds release subcmds

- endpoint abstractions now share an interface `Abstraction`
- pkg endpoint now has a query facitilty (`ListAbstractions`) which is
  used to find on-disk
    - step holds and bookmarks
    - replication cursors (v1, v2)
    - last-received-holds
- the `zrepl holds list` command consumes endpoint.ListAbstractions
- the new `zrepl holds release-{all,stale}` commands can be used
  to remove abstractions of package endpoint

Co-authored-by: InsanePrawn <insane.prawny@gmail.com>

supersedes #282
fixes #280
fixes #278
This commit is contained in:
Christian Schwarz
2020-03-26 23:43:17 +01:00
parent 44bd354eae
commit f3734ed0d4
28 changed files with 1854 additions and 1168 deletions
+3 -178
View File
@@ -7,8 +7,6 @@ import (
"fmt"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"syscall"
@@ -36,12 +34,9 @@ func ValidHoldTag(tag string) error {
}
// Idemptotent: does not return an error if the tag already exists
func ZFSHold(ctx context.Context, fs string, v ZFSSendArgVersion, tag string) error {
if err := v.ValidateInMemory(fs); err != nil {
return errors.Wrap(err, "invalid version")
}
func ZFSHold(ctx context.Context, fs string, v FilesystemVersion, tag string) error {
if !v.IsSnapshot() {
return errors.Errorf("can only hold snapshots, got %s", v.RelName)
return errors.Errorf("can only hold snapshots, got %s", v.RelName())
}
if err := validateNotEmpty("tag", tag); err != nil {
@@ -131,177 +126,7 @@ func ZFSRelease(ctx context.Context, tag string, snaps ...string) error {
debug("zfs release: no such tag lines=%v otherLines=%v", noSuchTagLines, otherLines)
}
if len(otherLines) > 0 {
return fmt.Errorf("unknown zfs error while releasing hold with tag %q: unidentified stderr lines\n%s", tag, strings.Join(otherLines, "\n"))
return fmt.Errorf("unknown zfs error while releasing hold with tag %q:\n%s", tag, strings.Join(otherLines, "\n"))
}
return nil
}
// Idempotent: if the hold doesn't exist, this is not an error
func ZFSReleaseAllOlderAndIncludingGUID(ctx context.Context, fs string, snapOrBookmarkGuid uint64, tag string) error {
return doZFSReleaseAllOlderAndIncOrExcludingGUID(ctx, fs, snapOrBookmarkGuid, tag, true)
}
// Idempotent: if the hold doesn't exist, this is not an error
func ZFSReleaseAllOlderThanGUID(ctx context.Context, fs string, snapOrBookmarkGuid uint64, tag string) error {
return doZFSReleaseAllOlderAndIncOrExcludingGUID(ctx, fs, snapOrBookmarkGuid, tag, false)
}
type zfsReleaseAllOlderAndIncOrExcludingGUIDZFSListLine struct {
entityType EntityType
name string
createtxg uint64
guid uint64
userrefs uint64 // always 0 for bookmarks
}
func doZFSReleaseAllOlderAndIncOrExcludingGUID(ctx context.Context, fs string, snapOrBookmarkGuid uint64, tag string, includeGuid bool) error {
// TODO channel program support still unreleased but
// might be a huge performance improvement
// https://github.com/zfsonlinux/zfs/pull/7902/files
if err := validateZFSFilesystem(fs); err != nil {
return errors.Wrap(err, "`fs` is not a valid filesystem path")
}
if tag == "" {
return fmt.Errorf("`tag` must not be empty`")
}
output, err := exec.CommandContext(ctx,
"zfs", "list", "-o", "type,name,createtxg,guid,userrefs",
"-H", "-t", "snapshot,bookmark", "-r", "-d", "1", fs).CombinedOutput()
if err != nil {
return &ZFSError{output, errors.Wrap(err, "cannot list snapshots and their userrefs")}
}
lines, err := doZFSReleaseAllOlderAndIncOrExcludingGUIDParseListOutput(output)
if err != nil {
return errors.Wrap(err, "unexpected ZFS output")
}
releaseSnaps, err := doZFSReleaseAllOlderAndIncOrExcludingGUIDFindSnapshots(snapOrBookmarkGuid, includeGuid, lines)
if err != nil {
return err
}
if len(releaseSnaps) == 0 {
return nil
}
return ZFSRelease(ctx, tag, releaseSnaps...)
}
func doZFSReleaseAllOlderAndIncOrExcludingGUIDParseListOutput(output []byte) ([]zfsReleaseAllOlderAndIncOrExcludingGUIDZFSListLine, error) {
scan := bufio.NewScanner(bytes.NewReader(output))
var lines []zfsReleaseAllOlderAndIncOrExcludingGUIDZFSListLine
for scan.Scan() {
const numCols = 5
comps := strings.SplitN(scan.Text(), "\t", numCols)
if len(comps) != numCols {
return nil, fmt.Errorf("not %d columns\n%s", numCols, output)
}
dstype := comps[0]
name := comps[1]
var entityType EntityType
switch dstype {
case "snapshot":
entityType = EntityTypeSnapshot
case "bookmark":
entityType = EntityTypeBookmark
default:
return nil, fmt.Errorf("column 0 is %q, expecting \"snapshot\" or \"bookmark\"", dstype)
}
createtxg, err := strconv.ParseUint(comps[2], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse createtxg %q: %s\n%s", comps[2], err, output)
}
guid, err := strconv.ParseUint(comps[3], 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse guid %q: %s\n%s", comps[3], err, output)
}
var userrefs uint64
switch entityType {
case EntityTypeBookmark:
if comps[4] != "-" {
return nil, fmt.Errorf("entity type \"bookmark\" should have userrefs=\"-\", got %q", comps[4])
}
userrefs = 0
case EntityTypeSnapshot:
userrefs, err = strconv.ParseUint(comps[4], 10, 64) // shadow
if err != nil {
return nil, fmt.Errorf("cannot parse userrefs %q: %s\n%s", comps[4], err, output)
}
default:
panic(entityType)
}
lines = append(lines, zfsReleaseAllOlderAndIncOrExcludingGUIDZFSListLine{
entityType: entityType,
name: name,
createtxg: createtxg,
guid: guid,
userrefs: userrefs,
})
}
return lines, nil
}
func doZFSReleaseAllOlderAndIncOrExcludingGUIDFindSnapshots(snapOrBookmarkGuid uint64, includeGuid bool, lines []zfsReleaseAllOlderAndIncOrExcludingGUIDZFSListLine) (releaseSnaps []string, err error) {
// sort lines by createtxg,(snap < bookmark)
// we cannot do this using zfs list -s because `type` is not a
sort.Slice(lines, func(i, j int) (less bool) {
if lines[i].createtxg == lines[j].createtxg {
iET := func(t EntityType) int {
switch t {
case EntityTypeSnapshot:
return 0
case EntityTypeBookmark:
return 1
default:
panic("unexpected entity type " + t.String())
}
}
return iET(lines[i].entityType) < iET(lines[j].entityType)
}
return lines[i].createtxg < lines[j].createtxg
})
// iterate over snapshots oldest to newest and collect snapshots that have holds and
// are older than (inclusive or exclusive, depends on includeGuid) a snapshot or bookmark
// with snapOrBookmarkGuid
foundGuid := false
for _, line := range lines {
if line.guid == snapOrBookmarkGuid {
foundGuid = true
}
if line.userrefs > 0 {
if !foundGuid || (foundGuid && includeGuid) {
// only snapshots have userrefs > 0, no need to check entityType
releaseSnaps = append(releaseSnaps, line.name)
}
}
if foundGuid {
// The secondary key in sorting (snap < bookmark) guarantees that we
// A) either found the snapshot with snapOrBookmarkGuid
// B) or no snapshot with snapGuid exists, but one or more bookmarks of it exists
// In the case of A, we already added the snapshot to releaseSnaps if includeGuid requests it,
// and can ignore possible subsequent bookmarks of the snapshot.
// In the case of B, there is nothing to add to releaseSnaps.
break
}
}
if !foundGuid {
return nil, fmt.Errorf("cannot find snapshot or bookmark with guid %v", snapOrBookmarkGuid)
}
return releaseSnaps, nil
}
-38
View File
@@ -1,38 +0,0 @@
package zfs
import (
"testing"
"github.com/kr/pretty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDoZFSReleaseAllOlderAndIncOrExcludingGUIDFindSnapshots(t *testing.T) {
// what we test here: sort bookmark #3 before @3
// => assert that the function doesn't stop at the first guid match
// (which might be a bookmark, depending on zfs list ordering)
// but instead considers the entire stride of bookmarks and snapshots with that guid
//
// also, throw in unordered createtxg for good measure
list, err := doZFSReleaseAllOlderAndIncOrExcludingGUIDParseListOutput(
[]byte("snapshot\tfoo@1\t1\t1013001\t1\n" +
"snapshot\tfoo@2\t2\t2013002\t1\n" +
"bookmark\tfoo#3\t3\t7013003\t-\n" +
"snapshot\tfoo@6\t6\t5013006\t1\n" +
"snapshot\tfoo@3\t3\t7013003\t1\n" +
"snapshot\tfoo@4\t3\t6013004\t1\n" +
""),
)
require.NoError(t, err)
t.Log(pretty.Sprint(list))
require.Equal(t, 6, len(list))
require.Equal(t, EntityTypeBookmark, list[2].entityType)
releaseSnaps, err := doZFSReleaseAllOlderAndIncOrExcludingGUIDFindSnapshots(7013003, true, list)
t.Logf("releasedSnaps = %#v", releaseSnaps)
assert.NoError(t, err)
assert.Equal(t, []string{"foo@1", "foo@2", "foo@3"}, releaseSnaps)
}
+72 -23
View File
@@ -55,6 +55,7 @@ func DecomposeVersionString(v string) (fs string, versionType VersionType, name
}
}
// The data in a FilesystemVersion is guaranteed to stem from a ZFS CLI invocation.
type FilesystemVersion struct {
Type VersionType
@@ -72,9 +73,16 @@ type FilesystemVersion struct {
Creation time.Time
}
func (v FilesystemVersion) String() string {
func (v FilesystemVersion) GetCreateTXG() uint64 { return v.CreateTXG }
func (v FilesystemVersion) GetGUID() uint64 { return v.Guid }
func (v FilesystemVersion) GetGuid() uint64 { return v.Guid }
func (v FilesystemVersion) GetName() string { return v.Name }
func (v FilesystemVersion) IsSnapshot() bool { return v.Type == Snapshot }
func (v FilesystemVersion) IsBookmark() bool { return v.Type == Bookmark }
func (v FilesystemVersion) RelName() string {
return fmt.Sprintf("%s%s", v.Type.DelimiterChar(), v.Name)
}
func (v FilesystemVersion) String() string { return v.RelName() }
func (v FilesystemVersion) ToAbsPath(p *DatasetPath) string {
var b bytes.Buffer
@@ -84,6 +92,49 @@ func (v FilesystemVersion) ToAbsPath(p *DatasetPath) string {
return b.String()
}
func (v FilesystemVersion) FullPath(fs string) string {
return fmt.Sprintf("%s%s", fs, v.RelName())
}
func (v FilesystemVersion) ToSendArgVersion() ZFSSendArgVersion {
return ZFSSendArgVersion{
RelName: v.RelName(),
GUID: v.Guid,
}
}
type ParseFilesystemVersionArgs struct {
fullname string
guid, createtxg, creation string
}
func ParseFilesystemVersion(args ParseFilesystemVersionArgs) (v FilesystemVersion, err error) {
_, v.Type, v.Name, err = DecomposeVersionString(args.fullname)
if err != nil {
return v, err
}
if v.Guid, err = strconv.ParseUint(args.guid, 10, 64); err != nil {
err = errors.Wrapf(err, "cannot parse GUID %q", args.guid)
return v, err
}
if v.CreateTXG, err = strconv.ParseUint(args.createtxg, 10, 64); err != nil {
err = errors.Wrapf(err, "cannot parse CreateTXG %q", args.createtxg)
return v, err
}
creationUnix, err := strconv.ParseInt(args.creation, 10, 64)
if err != nil {
err = errors.Wrapf(err, "cannot parse creation date %q", args.creation)
return v, err
} else {
v.Creation = time.Unix(creationUnix, 0)
}
return v, nil
}
type FilesystemVersionFilter interface {
Filter(t VersionType, name string) (accept bool, err error)
}
@@ -126,32 +177,17 @@ func ZFSListFilesystemVersions(fs *DatasetPath, filter FilesystemVersionFilter)
}
line := listResult.Fields
var v FilesystemVersion
_, v.Type, v.Name, err = DecomposeVersionString(line[0])
args := ParseFilesystemVersionArgs{
fullname: line[0],
guid: line[1],
createtxg: line[2],
creation: line[3],
}
v, err := ParseFilesystemVersion(args)
if err != nil {
return nil, err
}
if v.Guid, err = strconv.ParseUint(line[1], 10, 64); err != nil {
err = errors.Wrap(err, "cannot parse GUID")
return
}
if v.CreateTXG, err = strconv.ParseUint(line[2], 10, 64); err != nil {
err = errors.Wrap(err, "cannot parse CreateTXG")
return
}
creationUnix, err := strconv.ParseInt(line[3], 10, 64)
if err != nil {
err = fmt.Errorf("cannot parse creation date '%s': %s", line[3], err)
return nil, err
} else {
v.Creation = time.Unix(creationUnix, 0)
}
accept := true
if filter != nil {
accept, err = filter.Filter(v.Type, v.Name)
@@ -167,3 +203,16 @@ func ZFSListFilesystemVersions(fs *DatasetPath, filter FilesystemVersionFilter)
}
return
}
func ZFSGetFilesystemVersion(ds string) (v FilesystemVersion, _ error) {
props, err := zfsGet(ds, []string{"createtxg", "guid", "creation"}, sourceAny)
if err != nil {
return v, err
}
return ParseFilesystemVersion(ParseFilesystemVersionArgs{
fullname: ds,
createtxg: props.Get("createtxg"),
guid: props.Get("guid"),
creation: props.Get("creation"),
})
}
+45 -71
View File
@@ -314,7 +314,7 @@ func absVersion(fs string, v *ZFSSendArgVersion) (full string, err error) {
// a must already be validated
//
// SECURITY SENSITIVE because Raw must be handled correctly
func (a ZFSSendArgs) buildCommonSendArgs() ([]string, error) {
func (a ZFSSendArgsUnvalidated) buildCommonSendArgs() ([]string, error) {
args := make([]string, 0, 3)
// ResumeToken takes precedence, we assume that it has been validated to reflect
@@ -525,6 +525,9 @@ type ZFSSendArgVersion struct {
GUID uint64
}
func (v ZFSSendArgVersion) GetGuid() uint64 { return v.GUID }
func (v ZFSSendArgVersion) ToSendArgVersion() ZFSSendArgVersion { return v }
func (v ZFSSendArgVersion) ValidateInMemory(fs string) error {
if fs == "" {
panic(fs)
@@ -558,26 +561,26 @@ func (v ZFSSendArgVersion) mustValidateInMemory(fs string) {
}
// fs must be not empty
func (a ZFSSendArgVersion) ValidateExistsAndGetCheckedProps(ctx context.Context, fs string) (ZFSPropCreateTxgAndGuidProps, error) {
func (a ZFSSendArgVersion) ValidateExistsAndGetVersion(ctx context.Context, fs string) (v FilesystemVersion, _ error) {
if err := a.ValidateInMemory(fs); err != nil {
return ZFSPropCreateTxgAndGuidProps{}, nil
return v, nil
}
realProps, err := ZFSGetCreateTXGAndGuid(a.FullPath(fs))
realVersion, err := ZFSGetFilesystemVersion(a.FullPath(fs))
if err != nil {
return ZFSPropCreateTxgAndGuidProps{}, err
return v, err
}
if realProps.Guid != a.GUID {
return ZFSPropCreateTxgAndGuidProps{}, fmt.Errorf("`GUID` field does not match real dataset's GUID: %q != %q", realProps.Guid, a.GUID)
if realVersion.Guid != a.GUID {
return v, fmt.Errorf("`GUID` field does not match real dataset's GUID: %q != %q", realVersion.Guid, a.GUID)
}
return realProps, nil
return realVersion, nil
}
func (a ZFSSendArgVersion) ValidateExists(ctx context.Context, fs string) error {
_, err := a.ValidateExistsAndGetCheckedProps(ctx, fs)
_, err := a.ValidateExistsAndGetVersion(ctx, fs)
return err
}
@@ -619,7 +622,7 @@ func (n *NilBool) String() string {
}
// When updating this struct, check Validate and ValidateCorrespondsToResumeToken (POTENTIALLY SECURITY SENSITIVE)
type ZFSSendArgs struct {
type ZFSSendArgsUnvalidated struct {
FS string
From, To *ZFSSendArgVersion // From may be nil
Encrypted *NilBool
@@ -628,6 +631,12 @@ type ZFSSendArgs struct {
ResumeToken string // if not nil, must match what is specified in From, To (covered by ValidateCorrespondsToResumeToken)
}
type ZFSSendArgsValidated struct {
ZFSSendArgsUnvalidated
FromVersion *FilesystemVersion
ToVersion FilesystemVersion
}
type zfsSendArgsValidationContext struct {
encEnabled *NilBool
}
@@ -642,16 +651,16 @@ const (
)
type ZFSSendArgsValidationError struct {
Args ZFSSendArgs
Args ZFSSendArgsUnvalidated
What ZFSSendArgsValidationErrorCode
Msg error
}
func newValidationError(sendArgs ZFSSendArgs, what ZFSSendArgsValidationErrorCode, cause error) *ZFSSendArgsValidationError {
func newValidationError(sendArgs ZFSSendArgsUnvalidated, what ZFSSendArgsValidationErrorCode, cause error) *ZFSSendArgsValidationError {
return &ZFSSendArgsValidationError{sendArgs, what, cause}
}
func newGenericValidationError(sendArgs ZFSSendArgs, cause error) *ZFSSendArgsValidationError {
func newGenericValidationError(sendArgs ZFSSendArgsUnvalidated, cause error) *ZFSSendArgsValidationError {
return &ZFSSendArgsValidationError{sendArgs, ZFSSendArgsGenericValidationError, cause}
}
@@ -663,49 +672,57 @@ func (e ZFSSendArgsValidationError) Error() string {
// - Make sure that if ResumeToken != "", it reflects the same operation as the other parameters would.
//
// This function is not pure because GUIDs are checked against the local host's datasets.
func (a ZFSSendArgs) Validate(ctx context.Context) error {
func (a ZFSSendArgsUnvalidated) Validate(ctx context.Context) (v ZFSSendArgsValidated, _ error) {
if dp, err := NewDatasetPath(a.FS); err != nil || dp.Length() == 0 {
return newGenericValidationError(a, fmt.Errorf("`FS` must be a valid non-zero dataset path"))
return v, newGenericValidationError(a, fmt.Errorf("`FS` must be a valid non-zero dataset path"))
}
if a.To == nil {
return newGenericValidationError(a, fmt.Errorf("`To` must not be nil"))
return v, newGenericValidationError(a, fmt.Errorf("`To` must not be nil"))
}
if err := a.To.ValidateExists(ctx, a.FS); err != nil {
return newGenericValidationError(a, errors.Wrap(err, "`To` invalid"))
toVersion, err := a.To.ValidateExistsAndGetVersion(ctx, a.FS)
if err != nil {
return v, newGenericValidationError(a, errors.Wrap(err, "`To` invalid"))
}
var fromVersion *FilesystemVersion
if a.From != nil {
if err := a.From.ValidateExists(ctx, a.FS); err != nil {
return newGenericValidationError(a, errors.Wrap(err, "`From` invalid"))
fromV, err := a.From.ValidateExistsAndGetVersion(ctx, a.FS)
if err != nil {
return v, newGenericValidationError(a, errors.Wrap(err, "`From` invalid"))
}
fromVersion = &fromV
// fallthrough
}
if err := a.Encrypted.Validate(); err != nil {
return newGenericValidationError(a, errors.Wrap(err, "`Raw` invalid"))
return v, newGenericValidationError(a, errors.Wrap(err, "`Raw` invalid"))
}
valCtx := &zfsSendArgsValidationContext{}
fsEncrypted, err := ZFSGetEncryptionEnabled(ctx, a.FS)
if err != nil {
return newValidationError(a, ZFSSendArgsFSEncryptionCheckFail,
return v, newValidationError(a, ZFSSendArgsFSEncryptionCheckFail,
errors.Wrapf(err, "cannot check whether filesystem %q is encrypted", a.FS))
}
valCtx.encEnabled = &NilBool{fsEncrypted}
if a.Encrypted.B && !fsEncrypted {
return newValidationError(a, ZFSSendArgsEncryptedSendRequestedButFSUnencrypted,
return v, newValidationError(a, ZFSSendArgsEncryptedSendRequestedButFSUnencrypted,
errors.Errorf("encrypted send requested, but filesystem %q is not encrypted", a.FS))
}
if a.ResumeToken != "" {
if err := a.validateCorrespondsToResumeToken(ctx, valCtx); err != nil {
return newValidationError(a, ZFSSendArgsResumeTokenMismatch, err)
return v, newValidationError(a, ZFSSendArgsResumeTokenMismatch, err)
}
}
return nil
return ZFSSendArgsValidated{
ZFSSendArgsUnvalidated: a,
FromVersion: fromVersion,
ToVersion: toVersion,
}, nil
}
type ZFSSendArgsResumeTokenMismatchError struct {
@@ -735,7 +752,7 @@ func (c ZFSSendArgsResumeTokenMismatchErrorCode) fmt(format string, args ...inte
// This is SECURITY SENSITIVE and requires exhaustive checking of both side's values
// An attacker requesting a Send with a crafted ResumeToken may encode different parameters in the resume token than expected:
// for example, they may specify another file system (e.g. the filesystem with secret data) or request unencrypted send instead of encrypted raw send.
func (a ZFSSendArgs) validateCorrespondsToResumeToken(ctx context.Context, valCtx *zfsSendArgsValidationContext) error {
func (a ZFSSendArgsUnvalidated) validateCorrespondsToResumeToken(ctx context.Context, valCtx *zfsSendArgsValidationContext) error {
if a.ResumeToken == "" {
return nil // nothing to do
@@ -808,7 +825,7 @@ var ErrEncryptedSendNotSupported = fmt.Errorf("raw sends which are required for
// (if from is "" a full ZFS send is done)
//
// Returns ErrEncryptedSendNotSupported if encrypted send is requested but not supported by CLI
func ZFSSend(ctx context.Context, sendArgs ZFSSendArgs) (*ReadCloserCopier, error) {
func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*ReadCloserCopier, error) {
args := make([]string, 0)
args = append(args, "send")
@@ -825,10 +842,6 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgs) (*ReadCloserCopier, erro
}
}
if err := sendArgs.Validate(ctx); err != nil {
return nil, err // do not wrap, part of API, tested by platformtest
}
sargs, err := sendArgs.buildCommonSendArgs()
if err != nil {
return nil, err
@@ -959,11 +972,7 @@ func (s *DrySendInfo) unmarshalInfoLine(l string) (regexMatched bool, err error)
// to may be "", in which case a full ZFS send is done
// May return BookmarkSizeEstimationNotSupported as err if from is a bookmark.
func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgs) (_ *DrySendInfo, err error) {
if err := sendArgs.Validate(ctx); err != nil {
return nil, errors.Wrap(err, "cannot validate send args")
}
func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgsValidated) (_ *DrySendInfo, err error) {
if sendArgs.From != nil && strings.Contains(sendArgs.From.RelName, "#") {
/* TODO:
@@ -1458,41 +1467,6 @@ func zfsGet(path string, props []string, allowedSources zfsPropertySource) (*ZFS
return res, nil
}
type ZFSPropCreateTxgAndGuidProps struct {
CreateTXG, Guid uint64
}
func ZFSGetCreateTXGAndGuid(ds string) (ZFSPropCreateTxgAndGuidProps, error) {
props, err := zfsGetNumberProps(ds, []string{"createtxg", "guid"}, sourceAny)
if err != nil {
return ZFSPropCreateTxgAndGuidProps{}, err
}
return ZFSPropCreateTxgAndGuidProps{
CreateTXG: props["createtxg"],
Guid: props["guid"],
}, nil
}
// returns *DatasetDoesNotExist if the dataset does not exist
func zfsGetNumberProps(ds string, props []string, src zfsPropertySource) (map[string]uint64, error) {
sps, err := zfsGet(ds, props, sourceAny)
if err != nil {
if _, ok := err.(*DatasetDoesNotExist); ok {
return nil, err // pass through as is
}
return nil, errors.Wrap(err, "zfs: set replication cursor: get snapshot createtxg")
}
r := make(map[string]uint64, len(props))
for _, p := range props {
v, err := strconv.ParseUint(sps.Get(p), 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "zfs get: parse number property %q", p)
}
r[p] = v
}
return r, nil
}
type DestroySnapshotsError struct {
RawLines []string
Filesystem string