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:
+3
-178
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user