[#285] support setting zfs send / recv flags in the config (send: -wLcepbS, recv: -ox)
Co-authored-by: Christian Schwarz <me@cschwarz.com> Signed-off-by: InsanePrawn <insane.prawny@gmail.com> closes #285 closes #276 closes #24
This commit is contained in:
committed by
Christian Schwarz
parent
1c937e58f7
commit
393fc10a69
+1
-1
@@ -51,7 +51,7 @@ func ZFSGetEncryptionEnabled(ctx context.Context, fs string) (enabled bool, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
props, err := zfsGet(ctx, fs, []string{"encryption"}, sourceAny)
|
||||
props, err := zfsGet(ctx, fs, []string{"encryption"}, SourceAny)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "cannot get `encryption` property")
|
||||
}
|
||||
|
||||
+2
-3
@@ -67,7 +67,7 @@ type FilesystemPlaceholderState struct {
|
||||
func ZFSGetFilesystemPlaceholderState(ctx context.Context, p *DatasetPath) (state *FilesystemPlaceholderState, err error) {
|
||||
state = &FilesystemPlaceholderState{FS: p.ToString()}
|
||||
state.FS = p.ToString()
|
||||
props, err := zfsGet(ctx, p.ToString(), []string{PlaceholderPropertyName}, sourceLocal)
|
||||
props, err := zfsGet(ctx, p.ToString(), []string{PlaceholderPropertyName}, SourceLocal)
|
||||
var _ error = (*DatasetDoesNotExist)(nil) // weak assertion on zfsGet's interface
|
||||
if _, ok := err.(*DatasetDoesNotExist); ok {
|
||||
return state, nil
|
||||
@@ -110,12 +110,11 @@ func ZFSCreatePlaceholderFilesystem(ctx context.Context, fs *DatasetPath, parent
|
||||
}
|
||||
|
||||
func ZFSSetPlaceholder(ctx context.Context, p *DatasetPath, isPlaceholder bool) error {
|
||||
props := NewZFSProperties()
|
||||
prop := placeholderPropertyOff
|
||||
if isPlaceholder {
|
||||
prop = placeholderPropertyOn
|
||||
}
|
||||
props.Set(PlaceholderPropertyName, prop)
|
||||
props := map[string]string{PlaceholderPropertyName: prop}
|
||||
return zfsSet(ctx, p.ToString(), props)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package property
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Property string
|
||||
|
||||
// Check property name conforms to zfsprops(8), section "User Properties"
|
||||
// Keep regex and error message in sync!
|
||||
var (
|
||||
propertyValidNameChars = regexp.MustCompile(`^[0-9a-zA-Z-_\.:]+$`)
|
||||
propertyValidNameCharsErr = fmt.Errorf("property name must only contain alphanumeric chars and any in %q", "-_.:")
|
||||
)
|
||||
|
||||
func (p Property) Validate() error {
|
||||
const PROPERTYNAMEMAXLEN int = 256
|
||||
|
||||
if len(p) < 1 {
|
||||
return fmt.Errorf("property name cannot be empty")
|
||||
}
|
||||
if len(p) > PROPERTYNAMEMAXLEN {
|
||||
return fmt.Errorf("property name longer than %d characters", PROPERTYNAMEMAXLEN)
|
||||
}
|
||||
if p[0] == '-' {
|
||||
return fmt.Errorf("property name cannot start with '-'")
|
||||
}
|
||||
if !propertyValidNameChars.MatchString(string(p)) {
|
||||
return propertyValidNameCharsErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Code generated by "enumer -type=PropertySource -trimprefix=Source"; DO NOT EDIT.
|
||||
|
||||
//
|
||||
package zfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
_PropertySourceName_0 = "LocalDefault"
|
||||
_PropertySourceName_1 = "Inherited"
|
||||
_PropertySourceName_2 = "None"
|
||||
_PropertySourceName_3 = "Temporary"
|
||||
_PropertySourceName_4 = "Received"
|
||||
_PropertySourceName_5 = "Any"
|
||||
)
|
||||
|
||||
var (
|
||||
_PropertySourceIndex_0 = [...]uint8{0, 5, 12}
|
||||
_PropertySourceIndex_1 = [...]uint8{0, 9}
|
||||
_PropertySourceIndex_2 = [...]uint8{0, 4}
|
||||
_PropertySourceIndex_3 = [...]uint8{0, 9}
|
||||
_PropertySourceIndex_4 = [...]uint8{0, 8}
|
||||
_PropertySourceIndex_5 = [...]uint8{0, 3}
|
||||
)
|
||||
|
||||
func (i PropertySource) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 2:
|
||||
i -= 1
|
||||
return _PropertySourceName_0[_PropertySourceIndex_0[i]:_PropertySourceIndex_0[i+1]]
|
||||
case i == 4:
|
||||
return _PropertySourceName_1
|
||||
case i == 8:
|
||||
return _PropertySourceName_2
|
||||
case i == 16:
|
||||
return _PropertySourceName_3
|
||||
case i == 32:
|
||||
return _PropertySourceName_4
|
||||
case i == 18446744073709551615:
|
||||
return _PropertySourceName_5
|
||||
default:
|
||||
return fmt.Sprintf("PropertySource(%d)", i)
|
||||
}
|
||||
}
|
||||
|
||||
var _PropertySourceValues = []PropertySource{1, 2, 4, 8, 16, 32, 18446744073709551615}
|
||||
|
||||
var _PropertySourceNameToValueMap = map[string]PropertySource{
|
||||
_PropertySourceName_0[0:5]: 1,
|
||||
_PropertySourceName_0[5:12]: 2,
|
||||
_PropertySourceName_1[0:9]: 4,
|
||||
_PropertySourceName_2[0:4]: 8,
|
||||
_PropertySourceName_3[0:9]: 16,
|
||||
_PropertySourceName_4[0:8]: 32,
|
||||
_PropertySourceName_5[0:3]: 18446744073709551615,
|
||||
}
|
||||
|
||||
// PropertySourceString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func PropertySourceString(s string) (PropertySource, error) {
|
||||
if val, ok := _PropertySourceNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to PropertySource values", s)
|
||||
}
|
||||
|
||||
// PropertySourceValues returns all values of the enum
|
||||
func PropertySourceValues() []PropertySource {
|
||||
return _PropertySourceValues
|
||||
}
|
||||
|
||||
// IsAPropertySource returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i PropertySource) IsAPropertySource() bool {
|
||||
for _, v := range _PropertySourceValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+1
-1
@@ -267,7 +267,7 @@ func ZFSListFilesystemVersions(ctx context.Context, fs *DatasetPath, options Lis
|
||||
}
|
||||
|
||||
func ZFSGetFilesystemVersion(ctx context.Context, ds string) (v FilesystemVersion, _ error) {
|
||||
props, err := zfsGet(ctx, ds, []string{"createtxg", "guid", "creation", "userrefs"}, sourceAny)
|
||||
props, err := zfsGet(ctx, ds, []string{"createtxg", "guid", "creation", "userrefs"}, SourceAny)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
|
||||
+235
-120
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/zrepl/zrepl/util/circlog"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||
)
|
||||
|
||||
@@ -326,45 +327,6 @@ func absVersion(fs string, v *ZFSSendArgVersion) (full string, err error) {
|
||||
return fmt.Sprintf("%s%s", fs, v.RelName), nil
|
||||
}
|
||||
|
||||
// tok is allowed to be nil
|
||||
// a must already be validated
|
||||
//
|
||||
// SECURITY SENSITIVE because Raw must be handled correctly
|
||||
func (a ZFSSendArgsUnvalidated) buildCommonSendArgs() ([]string, error) {
|
||||
|
||||
args := make([]string, 0, 3)
|
||||
// ResumeToken takes precedence, we assume that it has been validated to reflect
|
||||
// what is described by the other fields in ZFSSendArgs
|
||||
if a.ResumeToken != "" {
|
||||
args = append(args, "-t", a.ResumeToken)
|
||||
return args, nil
|
||||
}
|
||||
|
||||
if a.Encrypted.B {
|
||||
args = append(args, "-w")
|
||||
}
|
||||
|
||||
toV, err := absVersion(a.FS, a.To)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fromV := ""
|
||||
if a.From != nil {
|
||||
fromV, err = absVersion(a.FS, a.From)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if fromV == "" { // Initial
|
||||
args = append(args, toV)
|
||||
} else {
|
||||
args = append(args, "-i", fromV, toV)
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
|
||||
if capacity <= 0 {
|
||||
panic(fmt.Sprintf("capacity must be positive %v", capacity))
|
||||
@@ -566,12 +528,9 @@ func (v ZFSSendArgVersion) MustBeBookmark() {
|
||||
|
||||
// When updating this struct, check Validate and ValidateCorrespondsToResumeToken (POTENTIALLY SECURITY SENSITIVE)
|
||||
type ZFSSendArgsUnvalidated struct {
|
||||
FS string
|
||||
From, To *ZFSSendArgVersion // From may be nil
|
||||
Encrypted *nodefault.Bool
|
||||
|
||||
// Preferred if not empty
|
||||
ResumeToken string // if not nil, must match what is specified in From, To (covered by ValidateCorrespondsToResumeToken)
|
||||
FS string
|
||||
From, To *ZFSSendArgVersion // From may be nil
|
||||
ZFSSendFlags
|
||||
}
|
||||
|
||||
type ZFSSendArgsValidated struct {
|
||||
@@ -580,6 +539,20 @@ type ZFSSendArgsValidated struct {
|
||||
ToVersion FilesystemVersion
|
||||
}
|
||||
|
||||
type ZFSSendFlags struct {
|
||||
Encrypted *nodefault.Bool
|
||||
Properties bool
|
||||
BackupProperties bool
|
||||
Raw bool
|
||||
LargeBlocks bool
|
||||
Compressed bool
|
||||
EmbeddedData bool
|
||||
Saved bool
|
||||
|
||||
// Preferred if not empty
|
||||
ResumeToken string // if not nil, must match what is specified in From, To (covered by ValidateCorrespondsToResumeToken)
|
||||
}
|
||||
|
||||
type zfsSendArgsValidationContext struct {
|
||||
encEnabled *nodefault.Bool
|
||||
}
|
||||
@@ -638,8 +611,8 @@ func (a ZFSSendArgsUnvalidated) Validate(ctx context.Context) (v ZFSSendArgsVali
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
if err := a.Encrypted.ValidateNoDefault(); err != nil {
|
||||
return v, newGenericValidationError(a, errors.Wrap(err, "`Raw` invalid"))
|
||||
if err := a.ZFSSendFlags.Validate(); err != nil {
|
||||
return v, newGenericValidationError(a, errors.Wrap(err, "send flags invalid"))
|
||||
}
|
||||
|
||||
valCtx := &zfsSendArgsValidationContext{}
|
||||
@@ -655,10 +628,8 @@ func (a ZFSSendArgsUnvalidated) Validate(ctx context.Context) (v ZFSSendArgsVali
|
||||
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 v, newValidationError(a, ZFSSendArgsResumeTokenMismatch, err)
|
||||
}
|
||||
if err := a.validateEncryptionFlagsCorrespondToResumeToken(ctx, valCtx); err != nil {
|
||||
return v, newValidationError(a, ZFSSendArgsResumeTokenMismatch, err)
|
||||
}
|
||||
|
||||
return ZFSSendArgsValidated{
|
||||
@@ -668,6 +639,89 @@ func (a ZFSSendArgsUnvalidated) Validate(ctx context.Context) (v ZFSSendArgsVali
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f ZFSSendFlags) Validate() error {
|
||||
if err := f.Encrypted.ValidateNoDefault(); err != nil {
|
||||
return errors.Wrap(err, "flag `Encrypted` invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If ResumeToken is empty, builds a command line with the flags specified.
|
||||
// If ResumeToken is not empty, build a command line with just `-t {{.ResumeToken}}`.
|
||||
//
|
||||
// SECURITY SENSITIVE it is the caller's responsibility to ensure that a.Encrypted semantics
|
||||
// hold for the file system that will be sent with the send flags returned by this function
|
||||
func (a ZFSSendFlags) buildSendFlagsUnchecked() []string {
|
||||
|
||||
args := make([]string, 0)
|
||||
|
||||
// ResumeToken takes precedence, we assume that it has been validated
|
||||
// to reflect what is described by the other fields.
|
||||
if a.ResumeToken != "" {
|
||||
args = append(args, "-t", a.ResumeToken)
|
||||
return args
|
||||
}
|
||||
|
||||
if a.Encrypted.B || a.Raw {
|
||||
args = append(args, "-w")
|
||||
}
|
||||
|
||||
if a.Properties {
|
||||
args = append(args, "-p")
|
||||
}
|
||||
|
||||
if a.BackupProperties {
|
||||
args = append(args, "-b")
|
||||
}
|
||||
|
||||
if a.LargeBlocks {
|
||||
args = append(args, "-L")
|
||||
}
|
||||
|
||||
if a.Compressed {
|
||||
args = append(args, "-c")
|
||||
}
|
||||
|
||||
if a.EmbeddedData {
|
||||
args = append(args, "-e")
|
||||
}
|
||||
|
||||
if a.Saved {
|
||||
args = append(args, "-S")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (a ZFSSendArgsValidated) buildSendCommandLine() ([]string, error) {
|
||||
|
||||
flags := a.buildSendFlagsUnchecked()
|
||||
|
||||
if a.ZFSSendFlags.ResumeToken != "" {
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
toV, err := absVersion(a.FS, a.To)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fromV := ""
|
||||
if a.From != nil {
|
||||
fromV, err = absVersion(a.FS, a.From)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if fromV == "" { // Initial
|
||||
flags = append(flags, toV)
|
||||
} else {
|
||||
flags = append(flags, "-i", fromV, toV)
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
type ZFSSendArgsResumeTokenMismatchError struct {
|
||||
What ZFSSendArgsResumeTokenMismatchErrorCode
|
||||
Err error
|
||||
@@ -692,10 +746,17 @@ 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:
|
||||
// Validate that the encryption settings specified in `a` correspond to the encryption settings encoded in the resume token.
|
||||
//
|
||||
// This is SECURITY SENSITIVE:
|
||||
// It is possible for an attacker to craft arbitrary resume tokens.
|
||||
// Those malicious resume tokens could 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 ZFSSendArgsUnvalidated) validateCorrespondsToResumeToken(ctx context.Context, valCtx *zfsSendArgsValidationContext) error {
|
||||
//
|
||||
// Note that we don't check correspondence of all other send flags because
|
||||
// a) the resume token does not capture all send flags (e.g. send -p is implemented in libzfs and thus not represented in the resume token)
|
||||
// b) it would force us to either reject resume tokens with unknown flags.
|
||||
func (a ZFSSendArgsUnvalidated) validateEncryptionFlagsCorrespondToResumeToken(ctx context.Context, valCtx *zfsSendArgsValidationContext) error {
|
||||
|
||||
if a.ResumeToken == "" {
|
||||
return nil // nothing to do
|
||||
@@ -720,6 +781,7 @@ func (a ZFSSendArgsUnvalidated) validateCorrespondsToResumeToken(ctx context.Con
|
||||
"filesystem in resume token field `toname` = %q does not match expected value %q", tokenFS.ToString(), a.FS)
|
||||
}
|
||||
|
||||
// If From is set, it must match.
|
||||
if (a.From != nil) != t.HasFromGUID { // existence must be same
|
||||
if t.HasFromGUID {
|
||||
return gen.fmt("resume token not expected to be incremental, but `fromguid` = %v", t.FromGUID)
|
||||
@@ -774,18 +836,20 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, e
|
||||
args = append(args, "send")
|
||||
|
||||
// pre-validation of sendArgs for plain ErrEncryptedSendNotSupported error
|
||||
// TODO go1.13: push this down to sendArgs.Validate
|
||||
if encryptedSendValid := sendArgs.Encrypted.ValidateNoDefault(); encryptedSendValid == nil && sendArgs.Encrypted.B {
|
||||
supported, err := EncryptionCLISupported(ctx)
|
||||
// we tie BackupProperties (send -b) and SendRaw (-w, same as with Encrypted) to this
|
||||
// since these were released together.
|
||||
if sendArgs.Encrypted.B || sendArgs.Raw || sendArgs.BackupProperties {
|
||||
encryptionSupported, err := EncryptionCLISupported(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot determine CLI native encryption support")
|
||||
}
|
||||
if !supported {
|
||||
|
||||
if !encryptionSupported {
|
||||
return nil, ErrEncryptedSendNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
sargs, err := sendArgs.buildCommonSendArgs()
|
||||
sargs, err := sendArgs.buildSendCommandLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -959,7 +1023,7 @@ func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgsValidated) (_ *DrySendI
|
||||
|
||||
args := make([]string, 0)
|
||||
args = append(args, "send", "-n", "-v", "-P")
|
||||
sargs, err := sendArgs.buildCommonSendArgs()
|
||||
sargs, err := sendArgs.buildSendCommandLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -977,14 +1041,6 @@ func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgsValidated) (_ *DrySendI
|
||||
return &si, nil
|
||||
}
|
||||
|
||||
type RecvOptions struct {
|
||||
// Rollback to the oldest snapshot, destroy it, then perform `recv -F`.
|
||||
// Note that this doesn't change property values, i.e. an existing local property value will be kept.
|
||||
RollbackAndForceRecv bool
|
||||
// Set -s flag used for resumable send & recv
|
||||
SavePartialRecvState bool
|
||||
}
|
||||
|
||||
type ErrRecvResumeNotSupported struct {
|
||||
FS string
|
||||
CheckErr error
|
||||
@@ -1001,6 +1057,39 @@ func (e *ErrRecvResumeNotSupported) Error() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type RecvOptions struct {
|
||||
// Rollback to the oldest snapshot, destroy it, then perform `recv -F`.
|
||||
// Note that this doesn't change property values, i.e. an existing local property value will be kept.
|
||||
RollbackAndForceRecv bool
|
||||
// Set -s flag used for resumable send & recv
|
||||
SavePartialRecvState bool
|
||||
|
||||
InheritProperties []zfsprop.Property
|
||||
OverrideProperties map[zfsprop.Property]string
|
||||
}
|
||||
|
||||
func (opts RecvOptions) buildRecvFlags() []string {
|
||||
args := make([]string, 0)
|
||||
if opts.RollbackAndForceRecv {
|
||||
args = append(args, "-F")
|
||||
}
|
||||
if opts.SavePartialRecvState {
|
||||
args = append(args, "-s")
|
||||
}
|
||||
if opts.InheritProperties != nil {
|
||||
for _, prop := range opts.InheritProperties {
|
||||
args = append(args, "-x", string(prop))
|
||||
}
|
||||
}
|
||||
if opts.OverrideProperties != nil {
|
||||
for prop, value := range opts.OverrideProperties {
|
||||
args = append(args, "-o", fmt.Sprintf("%s=%s", prop, value))
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
const RecvStderrBufSiz = 1 << 15
|
||||
|
||||
func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.ReadCloser, opts RecvOptions) (err error) {
|
||||
@@ -1049,17 +1138,14 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.Rea
|
||||
}
|
||||
}
|
||||
|
||||
args := make([]string, 0)
|
||||
args = append(args, "recv")
|
||||
if opts.RollbackAndForceRecv {
|
||||
args = append(args, "-F")
|
||||
}
|
||||
if opts.SavePartialRecvState {
|
||||
if supported, err := ResumeRecvSupported(ctx, fsdp); err != nil || !supported {
|
||||
return &ErrRecvResumeNotSupported{FS: fs, CheckErr: err}
|
||||
}
|
||||
args = append(args, "-s")
|
||||
}
|
||||
|
||||
args := []string{"recv"}
|
||||
args = append(args, opts.buildRecvFlags()...)
|
||||
args = append(args, v.FullPath(fs))
|
||||
|
||||
ctx, cancelCmd := context.WithCancel(ctx)
|
||||
@@ -1235,43 +1321,38 @@ func ZFSRecvClearResumeToken(ctx context.Context, fs string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
type PropertyValue struct {
|
||||
Value string
|
||||
Source PropertySource
|
||||
}
|
||||
|
||||
type ZFSProperties struct {
|
||||
m map[string]string
|
||||
m map[string]PropertyValue
|
||||
}
|
||||
|
||||
func NewZFSProperties() *ZFSProperties {
|
||||
return &ZFSProperties{make(map[string]string, 4)}
|
||||
}
|
||||
|
||||
func (p *ZFSProperties) Set(key, val string) {
|
||||
p.m[key] = val
|
||||
return &ZFSProperties{make(map[string]PropertyValue, 4)}
|
||||
}
|
||||
|
||||
func (p *ZFSProperties) Get(key string) string {
|
||||
return p.m[key].Value
|
||||
}
|
||||
|
||||
func (p *ZFSProperties) GetDetails(key string) PropertyValue {
|
||||
return p.m[key]
|
||||
}
|
||||
|
||||
func (p *ZFSProperties) appendArgs(args *[]string) (err error) {
|
||||
for prop, val := range p.m {
|
||||
func zfsSet(ctx context.Context, path string, props map[string]string) error {
|
||||
args := make([]string, 0)
|
||||
args = append(args, "set")
|
||||
|
||||
for prop, val := range props {
|
||||
if strings.Contains(prop, "=") {
|
||||
return errors.New("prop contains rune '=' which is the delimiter between property name and value")
|
||||
}
|
||||
*args = append(*args, fmt.Sprintf("%s=%s", prop, val))
|
||||
args = append(args, fmt.Sprintf("%s=%s", prop, val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ZFSSet(ctx context.Context, fs *DatasetPath, props *ZFSProperties) (err error) {
|
||||
return zfsSet(ctx, fs.ToString(), props)
|
||||
}
|
||||
|
||||
func zfsSet(ctx context.Context, path string, props *ZFSProperties) (err error) {
|
||||
args := make([]string, 0)
|
||||
args = append(args, "set")
|
||||
err = props.appendArgs(&args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = append(args, path)
|
||||
|
||||
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, args...)
|
||||
@@ -1283,11 +1364,15 @@ func zfsSet(ctx context.Context, path string, props *ZFSProperties) (err error)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
func ZFSSet(ctx context.Context, fs *DatasetPath, props map[string]string) error {
|
||||
return zfsSet(ctx, fs.ToString(), props)
|
||||
}
|
||||
|
||||
func ZFSGet(ctx context.Context, fs *DatasetPath, props []string) (*ZFSProperties, error) {
|
||||
return zfsGet(ctx, fs.ToString(), props, sourceAny)
|
||||
return zfsGet(ctx, fs.ToString(), props, SourceAny)
|
||||
}
|
||||
|
||||
// The returned error includes requested filesystem and version as quoted strings in its error message
|
||||
@@ -1307,7 +1392,7 @@ func ZFSGetGUID(ctx context.Context, fs string, version string) (g uint64, err e
|
||||
return 0, errors.New("version does not start with @ or #")
|
||||
}
|
||||
path := fmt.Sprintf("%s%s", fs, version)
|
||||
props, err := zfsGet(ctx, path, []string{"guid"}, sourceAny) // always local
|
||||
props, err := zfsGet(ctx, path, []string{"guid"}, SourceAny) // always local
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -1323,7 +1408,7 @@ func ZFSGetMountpoint(ctx context.Context, fs string) (*GetMountpointOutput, err
|
||||
if err := EntityNamecheck(fs, EntityTypeFilesystem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
props, err := zfsGet(ctx, fs, []string{"mountpoint", "mounted"}, sourceAny)
|
||||
props, err := zfsGet(ctx, fs, []string{"mountpoint", "mounted"}, SourceAny)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1340,7 +1425,7 @@ func ZFSGetMountpoint(ctx context.Context, fs string) (*GetMountpointOutput, err
|
||||
}
|
||||
|
||||
func ZFSGetRawAnySource(ctx context.Context, path string, props []string) (*ZFSProperties, error) {
|
||||
return zfsGet(ctx, path, props, sourceAny)
|
||||
return zfsGet(ctx, path, props, SourceAny)
|
||||
}
|
||||
|
||||
var zfsGetDatasetDoesNotExistRegexp = regexp.MustCompile(`^cannot open '([^)]+)': (dataset does not exist|no such pool or dataset)`) // verified in platformtest
|
||||
@@ -1360,46 +1445,68 @@ func tryDatasetDoesNotExist(expectPath string, stderr []byte) *DatasetDoesNotExi
|
||||
return nil
|
||||
}
|
||||
|
||||
type zfsPropertySource uint
|
||||
//go:generate enumer -type=PropertySource -trimprefix=Source
|
||||
type PropertySource uint
|
||||
|
||||
const (
|
||||
sourceLocal zfsPropertySource = 1 << iota
|
||||
sourceDefault
|
||||
sourceInherited
|
||||
sourceNone
|
||||
sourceTemporary
|
||||
sourceReceived
|
||||
SourceLocal PropertySource = 1 << iota
|
||||
SourceDefault
|
||||
SourceInherited
|
||||
SourceNone
|
||||
SourceTemporary
|
||||
SourceReceived
|
||||
|
||||
sourceAny zfsPropertySource = ^zfsPropertySource(0)
|
||||
SourceAny PropertySource = ^PropertySource(0)
|
||||
)
|
||||
|
||||
func (s zfsPropertySource) zfsGetSourceFieldPrefixes() []string {
|
||||
var propertySourceParseLUT = map[string]PropertySource{
|
||||
"local": SourceLocal,
|
||||
"default": SourceDefault,
|
||||
"inherited": SourceInherited,
|
||||
"-": SourceNone,
|
||||
"temporary": SourceTemporary,
|
||||
"received": SourceReceived,
|
||||
}
|
||||
|
||||
func parsePropertySource(s string) (PropertySource, error) {
|
||||
fields := strings.Fields(s)
|
||||
if len(fields) > 0 {
|
||||
v, ok := propertySourceParseLUT[fields[0]]
|
||||
if ok {
|
||||
return v, nil
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
return 0, fmt.Errorf("unknown property source %q", s)
|
||||
}
|
||||
|
||||
func (s PropertySource) zfsGetSourceFieldPrefixes() []string {
|
||||
prefixes := make([]string, 0, 7)
|
||||
if s&sourceLocal != 0 {
|
||||
if s&SourceLocal != 0 {
|
||||
prefixes = append(prefixes, "local")
|
||||
}
|
||||
if s&sourceDefault != 0 {
|
||||
if s&SourceDefault != 0 {
|
||||
prefixes = append(prefixes, "default")
|
||||
}
|
||||
if s&sourceInherited != 0 {
|
||||
if s&SourceInherited != 0 {
|
||||
prefixes = append(prefixes, "inherited")
|
||||
}
|
||||
if s&sourceNone != 0 {
|
||||
if s&SourceNone != 0 {
|
||||
prefixes = append(prefixes, "-")
|
||||
}
|
||||
if s&sourceTemporary != 0 {
|
||||
if s&SourceTemporary != 0 {
|
||||
prefixes = append(prefixes, "temporary")
|
||||
}
|
||||
if s&sourceReceived != 0 {
|
||||
if s&SourceReceived != 0 {
|
||||
prefixes = append(prefixes, "received")
|
||||
}
|
||||
if s == sourceAny {
|
||||
if s == SourceAny {
|
||||
prefixes = append(prefixes, "")
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
func zfsGet(ctx context.Context, path string, props []string, allowedSources zfsPropertySource) (*ZFSProperties, error) {
|
||||
func zfsGet(ctx context.Context, path string, props []string, allowedSources PropertySource) (*ZFSProperties, error) {
|
||||
args := []string{"get", "-Hp", "-o", "property,value,source", strings.Join(props, ","), path}
|
||||
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, args...)
|
||||
stdout, err := cmd.Output()
|
||||
@@ -1425,7 +1532,7 @@ func zfsGet(ctx context.Context, path string, props []string, allowedSources zfs
|
||||
return nil, fmt.Errorf("zfs get did not return the number of expected property values")
|
||||
}
|
||||
res := &ZFSProperties{
|
||||
make(map[string]string, len(lines)),
|
||||
make(map[string]PropertyValue, len(lines)),
|
||||
}
|
||||
allowedPrefixes := allowedSources.zfsGetSourceFieldPrefixes()
|
||||
for _, line := range lines[:len(lines)-1] {
|
||||
@@ -1436,8 +1543,16 @@ func zfsGet(ctx context.Context, path string, props []string, allowedSources zfs
|
||||
return nil, fmt.Errorf("zfs get did not return property,value,source tuples")
|
||||
}
|
||||
for _, p := range allowedPrefixes {
|
||||
// prefix-match so that SourceAny (= "") works
|
||||
if strings.HasPrefix(fields[2], p) {
|
||||
res.m[fields[0]] = fields[1]
|
||||
source, err := parsePropertySource(fields[2])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parse property source")
|
||||
}
|
||||
res.m[fields[0]] = PropertyValue{
|
||||
Value: fields[1],
|
||||
Source: source,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
+150
-4
@@ -5,6 +5,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -41,20 +44,20 @@ func TestDatasetPathTrimNPrefixComps(t *testing.T) {
|
||||
func TestZFSPropertySource(t *testing.T) {
|
||||
|
||||
tcs := []struct {
|
||||
in zfsPropertySource
|
||||
in PropertySource
|
||||
exp []string
|
||||
}{
|
||||
{
|
||||
in: sourceAny,
|
||||
in: SourceAny,
|
||||
// although empty prefix matches any source
|
||||
exp: []string{"local", "default", "inherited", "-", "temporary", "received", ""},
|
||||
},
|
||||
{
|
||||
in: sourceTemporary,
|
||||
in: SourceTemporary,
|
||||
exp: []string{"temporary"},
|
||||
},
|
||||
{
|
||||
in: sourceLocal | sourceInherited,
|
||||
in: SourceLocal | SourceInherited,
|
||||
exp: []string{"local", "inherited"},
|
||||
},
|
||||
}
|
||||
@@ -69,6 +72,21 @@ func TestZFSPropertySource(t *testing.T) {
|
||||
|
||||
for _, tc := range tcs {
|
||||
|
||||
t.Logf("TEST CASE %v", tc)
|
||||
|
||||
// give the parsing code some coverage
|
||||
for _, e := range tc.exp {
|
||||
if e == "" {
|
||||
continue // "" is the prefix that matches SourceAny
|
||||
}
|
||||
s, err := parsePropertySource(e)
|
||||
assert.NoError(t, err)
|
||||
t.Logf("s: %x %s", s, s)
|
||||
t.Logf("in: %x %s", tc.in, tc.in)
|
||||
assert.True(t, s&tc.in != 0)
|
||||
}
|
||||
|
||||
// prefix matching
|
||||
res := tc.in.zfsGetSourceFieldPrefixes()
|
||||
resSet := toSet(res)
|
||||
expSet := toSet(tc.exp)
|
||||
@@ -304,3 +322,131 @@ func TestTryRecvDestroyOrOverwriteEncryptedErr(t *testing.T) {
|
||||
require.NotNil(t, err)
|
||||
assert.EqualError(t, err, strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func TestZFSSendArgsBuildSendFlags(t *testing.T) {
|
||||
|
||||
type args = ZFSSendFlags
|
||||
type SendTest struct {
|
||||
conf args
|
||||
exactMatch bool
|
||||
flagsInclude []string
|
||||
flagsExclude []string
|
||||
}
|
||||
|
||||
withEncrypted := func(justFlags ZFSSendFlags) ZFSSendFlags {
|
||||
if justFlags.Encrypted == nil {
|
||||
justFlags.Encrypted = &nodefault.Bool{B: false}
|
||||
}
|
||||
return justFlags
|
||||
}
|
||||
|
||||
var sendTests = map[string]SendTest{
|
||||
"Empty Args": {
|
||||
conf: withEncrypted(args{}),
|
||||
flagsInclude: []string{},
|
||||
flagsExclude: []string{"-w", "-p", "-b"},
|
||||
},
|
||||
"Raw": {
|
||||
conf: withEncrypted(args{Raw: true}),
|
||||
flagsInclude: []string{"-w"},
|
||||
flagsExclude: []string{},
|
||||
},
|
||||
"Encrypted": {
|
||||
conf: withEncrypted(args{Encrypted: &nodefault.Bool{B: true}}),
|
||||
flagsInclude: []string{"-w"},
|
||||
flagsExclude: []string{},
|
||||
},
|
||||
"Unencrypted_And_Raw": {
|
||||
conf: withEncrypted(args{Encrypted: &nodefault.Bool{B: false}, Raw: true}),
|
||||
flagsInclude: []string{"-w"},
|
||||
flagsExclude: []string{},
|
||||
},
|
||||
"Encrypted_And_Raw": {
|
||||
conf: withEncrypted(args{Encrypted: &nodefault.Bool{B: true}, Raw: true}),
|
||||
flagsInclude: []string{"-w"},
|
||||
flagsExclude: []string{},
|
||||
},
|
||||
"Send properties": {
|
||||
conf: withEncrypted(args{Properties: true}),
|
||||
flagsInclude: []string{"-p"},
|
||||
flagsExclude: []string{},
|
||||
},
|
||||
"Send backup properties": {
|
||||
conf: withEncrypted(args{BackupProperties: true}),
|
||||
flagsInclude: []string{"-b"},
|
||||
flagsExclude: []string{},
|
||||
},
|
||||
"Send -b and -p": {
|
||||
conf: withEncrypted(args{Properties: true, BackupProperties: true}),
|
||||
flagsInclude: []string{"-p", "-b"},
|
||||
flagsExclude: []string{},
|
||||
},
|
||||
"Send resume state": {
|
||||
conf: withEncrypted(args{Saved: true}),
|
||||
flagsInclude: []string{"-S"},
|
||||
},
|
||||
"Resume token wins if not empty": {
|
||||
conf: withEncrypted(args{ResumeToken: "$theresumetoken$", Compressed: true}),
|
||||
flagsInclude: []string{"-t", "$theresumetoken$"},
|
||||
exactMatch: true,
|
||||
},
|
||||
}
|
||||
|
||||
for testName, test := range sendTests {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
flags := test.conf.buildSendFlagsUnchecked()
|
||||
assert.GreaterOrEqual(t, len(flags), len(test.flagsInclude))
|
||||
assert.Subset(t, flags, test.flagsInclude)
|
||||
if test.exactMatch {
|
||||
assert.Equal(t, flags, test.flagsInclude)
|
||||
}
|
||||
for flag := range flags {
|
||||
assert.NotContains(t, test.flagsExclude, flag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSCommonRecvArgsBuild(t *testing.T) {
|
||||
type RecvTest struct {
|
||||
conf RecvOptions
|
||||
flagsInclude []string
|
||||
flagsExclude []string
|
||||
}
|
||||
var recvTests = map[string]RecvTest{
|
||||
"Empty Args": {
|
||||
conf: RecvOptions{},
|
||||
flagsInclude: []string{},
|
||||
flagsExclude: []string{"-x", "-o", "-F", "-s"},
|
||||
},
|
||||
"ForceRollback": {
|
||||
conf: RecvOptions{RollbackAndForceRecv: true},
|
||||
flagsInclude: []string{"-F"},
|
||||
flagsExclude: []string{"-x", "-o", "-s"},
|
||||
},
|
||||
"PartialSend": {
|
||||
conf: RecvOptions{SavePartialRecvState: true},
|
||||
flagsInclude: []string{"-s"},
|
||||
flagsExclude: []string{"-x", "-o", "-F"},
|
||||
},
|
||||
"Override properties": {
|
||||
conf: RecvOptions{OverrideProperties: map[zfsprop.Property]string{zfsprop.Property("abc"): "123"}},
|
||||
flagsInclude: []string{"-o", "abc=123"},
|
||||
flagsExclude: []string{"-x", "-F", "-s"},
|
||||
},
|
||||
"Exclude/inherit properties": {
|
||||
conf: RecvOptions{InheritProperties: []zfsprop.Property{"abc", "123"}},
|
||||
flagsInclude: []string{"-x", "abc", "123"}, flagsExclude: []string{"-o", "-F", "-s"},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, test := range recvTests {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
flags := test.conf.buildRecvFlags()
|
||||
assert.Subset(t, flags, test.flagsInclude)
|
||||
for flag := range flags {
|
||||
assert.NotContains(t, test.flagsExclude, flag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user