Merge branch 'problame/simplify-placeholder-property' into master
As became clear in #126 (comment) the current zrepl:placeholder user property makes it hard to rename datasets because the placeholder status is determined by computing a hash of the dataset path and comparing it with the property value. This PR - changes zrepl:placeholder to a simple on|off property - derives property status from the property source (local),avoiding the original problem of property inheritance in the original implementation - provides a migration command that, based on the current zrepl configuration, migrates property values to the new on|off schema refs #150
This commit is contained in:
@@ -9,6 +9,16 @@ type DatasetFilter interface {
|
||||
Filter(p *DatasetPath) (pass bool, err error)
|
||||
}
|
||||
|
||||
// Returns a DatasetFilter that does not filter (passes all paths)
|
||||
func NoFilter() DatasetFilter {
|
||||
return noFilter{}
|
||||
}
|
||||
type noFilter struct {}
|
||||
|
||||
var _ DatasetFilter = noFilter{}
|
||||
|
||||
func (noFilter) Filter(p *DatasetPath) (pass bool, err error) { return true, nil }
|
||||
|
||||
func ZFSListMapping(ctx context.Context, filter DatasetFilter) (datasets []*DatasetPath, err error) {
|
||||
res, err := ZFSListMappingProperties(ctx, filter, nil)
|
||||
if err != nil {
|
||||
|
||||
+91
-62
@@ -5,87 +5,81 @@ import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
const ZREPL_PLACEHOLDER_PROPERTY_NAME string = "zrepl:placeholder"
|
||||
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"
|
||||
)
|
||||
|
||||
type FilesystemState struct {
|
||||
Placeholder bool
|
||||
// TODO extend with resume token when that feature is finally added
|
||||
}
|
||||
|
||||
// A somewhat efficient way to determine if a filesystem exists on this host.
|
||||
// Particularly useful if exists is called more than once (will only fork exec once and cache the result)
|
||||
func ZFSListFilesystemState() (localState map[string]FilesystemState, err error) {
|
||||
|
||||
var actual [][]string
|
||||
if actual, err = ZFSList([]string{"name", ZREPL_PLACEHOLDER_PROPERTY_NAME}, "-t", "filesystem,volume"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
localState = make(map[string]FilesystemState, len(actual))
|
||||
for _, e := range actual {
|
||||
dp, err := NewDatasetPath(e[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ZFS does not return parseable dataset path: %s", e[0])
|
||||
}
|
||||
placeholder, _ := IsPlaceholder(dp, e[1])
|
||||
localState[e[0]] = FilesystemState{
|
||||
placeholder,
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Computes the value for the ZREPL_PLACEHOLDER_PROPERTY_NAME ZFS user property
|
||||
// to mark the given DatasetPath p as a placeholder
|
||||
// computeLegacyPlaceholderPropertyValue is a legacy-compatibility function.
|
||||
//
|
||||
// We cannot simply use booleans here since user properties are always
|
||||
// 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.
|
||||
//
|
||||
// We hash the DatasetPath and use it to check for a given path if it is the
|
||||
// one originally marked as placeholder.
|
||||
// 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.
|
||||
//
|
||||
// However, this prohibits moving datasets around via `zfs rename`. The
|
||||
// placeholder attribute must be re-computed for the dataset path after the
|
||||
// move.
|
||||
//
|
||||
// TODO better solution available?
|
||||
func PlaceholderPropertyValue(p *DatasetPath) string {
|
||||
// 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[:])
|
||||
}
|
||||
|
||||
func IsPlaceholder(p *DatasetPath, placeholderPropertyValue string) (isPlaceholder bool, err error) {
|
||||
expected := PlaceholderPropertyValue(p)
|
||||
isPlaceholder = expected == placeholderPropertyValue
|
||||
if !isPlaceholder {
|
||||
err = fmt.Errorf("expected %s, has %s", expected, placeholderPropertyValue)
|
||||
// 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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// for nonexistent FS, isPlaceholder == false && err == nil
|
||||
func ZFSIsPlaceholderFilesystem(p *DatasetPath) (isPlaceholder bool, err error) {
|
||||
props, err := zfsGet(p.ToString(), []string{ZREPL_PLACEHOLDER_PROPERTY_NAME}, sourceAny)
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
// interpret this as an early exit of the zfs binary due to the fs not existing
|
||||
return false, nil
|
||||
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(p *DatasetPath) (state *FilesystemPlaceholderState, err error) {
|
||||
state = &FilesystemPlaceholderState{FS: p.ToString()}
|
||||
state.FS = p.ToString()
|
||||
props, err := zfsGet(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 false, err
|
||||
return state, err
|
||||
}
|
||||
isPlaceholder, _ = IsPlaceholder(p, props.Get(ZREPL_PLACEHOLDER_PROPERTY_NAME))
|
||||
return
|
||||
state.FSExists = true
|
||||
state.RawLocalPropertyValue = props.Get(PlaceholderPropertyName)
|
||||
state.IsPlaceholder = isLocalPlaceholderPropertyValuePlaceholder(p, state.RawLocalPropertyValue)
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func ZFSCreatePlaceholderFilesystem(p *DatasetPath) (err error) {
|
||||
v := PlaceholderPropertyValue(p)
|
||||
cmd := exec.Command(ZFS_BINARY, "create",
|
||||
"-o", fmt.Sprintf("%s=%s", ZREPL_PLACEHOLDER_PROPERTY_NAME, v),
|
||||
"-o", fmt.Sprintf("%s=%s", PlaceholderPropertyName, placeholderPropertyOn),
|
||||
"-o", "mountpoint=none",
|
||||
p.ToString())
|
||||
|
||||
@@ -106,8 +100,43 @@ func ZFSCreatePlaceholderFilesystem(p *DatasetPath) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func ZFSSetNoPlaceholder(p *DatasetPath) error {
|
||||
func ZFSSetPlaceholder(p *DatasetPath, isPlaceholder bool) error {
|
||||
props := NewZFSProperties()
|
||||
props.Set(ZREPL_PLACEHOLDER_PROPERTY_NAME, "off")
|
||||
prop := placeholderPropertyOff
|
||||
if isPlaceholder {
|
||||
prop = placeholderPropertyOn
|
||||
}
|
||||
props.Set(PlaceholderPropertyName, prop)
|
||||
return zfsSet(p.ToString(), props)
|
||||
}
|
||||
}
|
||||
|
||||
type MigrateHashBasedPlaceholderReport struct {
|
||||
OriginalState FilesystemPlaceholderState
|
||||
NeedsModification bool
|
||||
}
|
||||
|
||||
// fs must exist, will panic otherwise
|
||||
func ZFSMigrateHashBasedPlaceholderToCurrent(fs *DatasetPath, dryRun bool) (*MigrateHashBasedPlaceholderReport, error) {
|
||||
st, err := ZFSGetFilesystemPlaceholderState(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(fs, st.IsPlaceholder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error re-writing placeholder property: %s", err)
|
||||
}
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user