zrepl/zfs/placeholder.go
Christian Schwarz fb6a9be954 fix encrypt-on-receive with placeholders
fixes https://github.com/zrepl/zrepl/issues/504

Problem:
  plain send + recv with root_fs encrypted + placeholders causes plain recvs
  whereas user would expect encrypt-on-recv
Reason:
  We create placeholder filesytems with -o encryption=off.
  Thus, children received below those placeholders won't inherit
  encryption of root_fs.
Fix:
  We'll have three values for `recv.placeholders.encryption: unspecified (default) | off | inherit`.
  When we create a placeholder, we will fail the operation if  `recv.placeholders.encryption = unspecified`.
  The exception is if the placeholder filesystem is to encode the client identity ($root_fs/$client_identity) in a pull job.
  Those are created in `inherit` mode if the config field is `unspecified` so that users who don't need
  placeholders are not bothered by these details.

Future Work:
  Automatically warn existing users of encrypt-on-recv about the problem
  if they are affected.
  The problem that I hit during implementation of this is that the
  `encryption` prop's `source` doesn't quite behave like other props:
  `source` is `default` for `encryption=off` and `-` when `encryption=on`.
  Hence, we can't use `source` to distinguish the following 2x2 cases:
  (1) placeholder created with explicit -o encryption=off
  (2) placeholder created without specifying -o encryption
  with
  (A) an encrypted parent at creation time
  (B) an unencrypted parent at creation time
2021-12-18 15:12:47 +01:00

198 lines
6.3 KiB
Go

package zfs
import (
"context"
"crypto/sha512"
"encoding/hex"
"fmt"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/zfs/zfscmd"
)
const (
// For a placeholder filesystem to be a placeholder, the property source must be local,
// i.e. not inherited.
PlaceholderPropertyName string = "zrepl:placeholder"
placeholderPropertyOn string = "on"
placeholderPropertyOff string = "off"
)
// computeLegacyPlaceholderPropertyValue is a legacy-compatibility function.
//
// In the 0.0.x series, the value stored in the PlaceholderPropertyName user property
// was a hash value of the dataset path.
// A simple `on|off` value could not be used at the time because `zfs list` was used to
// list all filesystems and their placeholder state with a single command: due to property
// inheritance, `zfs list` would print the placeholder state for all (non-placeholder) children
// of a dataset, so the hash value was used to distinguish whether the property was local or
// inherited.
//
// One of the drawbacks of the above approach is that `zfs rename` renders a placeholder filesystem
// a non-placeholder filesystem if any of the parent path components change.
//
// We `zfs get` nowadays, which returns the property source, making the hash value no longer
// necessary. However, we want to keep legacy compatibility.
func computeLegacyHashBasedPlaceholderPropertyValue(p *DatasetPath) string {
ps := []byte(p.ToString())
sum := sha512.Sum512_256(ps)
return hex.EncodeToString(sum[:])
}
// the caller asserts that placeholderPropertyValue is sourceLocal
func isLocalPlaceholderPropertyValuePlaceholder(p *DatasetPath, placeholderPropertyValue string) (isPlaceholder bool) {
legacy := computeLegacyHashBasedPlaceholderPropertyValue(p)
switch placeholderPropertyValue {
case legacy:
return true
case placeholderPropertyOn:
return true
default:
return false
}
}
type FilesystemPlaceholderState struct {
FS string
FSExists bool
IsPlaceholder bool
RawLocalPropertyValue string
}
// ZFSGetFilesystemPlaceholderState is the authoritative way to determine whether a filesystem
// is a placeholder. Note that the property source must be `local` for the returned value to be valid.
//
// For nonexistent FS, err == nil and state.FSExists == false
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)
var _ error = (*DatasetDoesNotExist)(nil) // weak assertion on zfsGet's interface
if _, ok := err.(*DatasetDoesNotExist); ok {
return state, nil
} else if err != nil {
return state, err
}
state.FSExists = true
state.RawLocalPropertyValue = props.Get(PlaceholderPropertyName)
state.IsPlaceholder = isLocalPlaceholderPropertyValuePlaceholder(p, state.RawLocalPropertyValue)
return state, nil
}
//go:generate enumer -type=FilesystemPlaceholderCreateEncryptionValue -trimprefix=FilesystemPlaceholderCreateEncryption
type FilesystemPlaceholderCreateEncryptionValue int
const (
FilesystemPlaceholderCreateEncryptionInherit FilesystemPlaceholderCreateEncryptionValue = 1 << iota
FilesystemPlaceholderCreateEncryptionOff
)
func ZFSCreatePlaceholderFilesystem(ctx context.Context, fs *DatasetPath, parent *DatasetPath, encryption FilesystemPlaceholderCreateEncryptionValue) (err error) {
if fs.Length() == 1 {
return fmt.Errorf("cannot create %q: pools cannot be created with zfs create", fs.ToString())
}
cmdline := []string{
"create",
"-o", fmt.Sprintf("%s=%s", PlaceholderPropertyName, placeholderPropertyOn),
"-o", "mountpoint=none",
}
if !encryption.IsAFilesystemPlaceholderCreateEncryptionValue() {
panic(encryption)
}
switch encryption {
case FilesystemPlaceholderCreateEncryptionInherit:
// no-op
case FilesystemPlaceholderCreateEncryptionOff:
cmdline = append(cmdline, "-o", "encryption=off")
default:
panic(encryption)
}
cmdline = append(cmdline, fs.ToString())
cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, cmdline...)
stdio, err := cmd.CombinedOutput()
if err != nil {
err = &ZFSError{
Stderr: stdio,
WaitErr: err,
}
}
return
}
func ZFSSetPlaceholder(ctx context.Context, p *DatasetPath, isPlaceholder bool) error {
prop := placeholderPropertyOff
if isPlaceholder {
prop = placeholderPropertyOn
}
props := map[string]string{PlaceholderPropertyName: prop}
return zfsSet(ctx, p.ToString(), props)
}
type MigrateHashBasedPlaceholderReport struct {
OriginalState FilesystemPlaceholderState
NeedsModification bool
}
// fs must exist, will panic otherwise
func ZFSMigrateHashBasedPlaceholderToCurrent(ctx context.Context, fs *DatasetPath, dryRun bool) (*MigrateHashBasedPlaceholderReport, error) {
st, err := ZFSGetFilesystemPlaceholderState(ctx, fs)
if err != nil {
return nil, fmt.Errorf("error getting placeholder state: %s", err)
}
if !st.FSExists {
panic("inconsistent placeholder state returned: fs must exist")
}
report := MigrateHashBasedPlaceholderReport{
OriginalState: *st,
}
report.NeedsModification = st.IsPlaceholder && st.RawLocalPropertyValue != placeholderPropertyOn
if dryRun || !report.NeedsModification {
return &report, nil
}
err = ZFSSetPlaceholder(ctx, fs, st.IsPlaceholder)
if err != nil {
return nil, fmt.Errorf("error re-writing placeholder property: %s", err)
}
return &report, nil
}
func ZFSListPlaceholderFilesystemsWithAdditionalProps(ctx context.Context, root string, additionalProps []string) (map[string]*ZFSProperties, error) {
props := []string{PlaceholderPropertyName}
if len(additionalProps) > 0 {
props = append(props, additionalProps...)
}
propsByFS, err := zfsGetRecursive(ctx, root, -1, []string{"filesystem", "volume"}, props, SourceAny)
if err != nil {
return nil, errors.Wrapf(err, "cannot get placeholder filesystems under %q", root)
}
filtered := make(map[string]*ZFSProperties)
for fs, props := range propsByFS {
details := props.GetDetails(PlaceholderPropertyName)
if details.Source != SourceLocal {
continue
}
fsp, err := NewDatasetPath(fs)
if err != nil {
return nil, errors.Wrapf(err, "zfs get returned invalid dataset path %q", fs)
}
if !isLocalPlaceholderPropertyValuePlaceholder(fsp, details.Value) {
continue
}
filtered[fs] = props
}
return filtered, nil
}