-
-
Notifications
You must be signed in to change notification settings - Fork 89
/
placeholder.go
197 lines (170 loc) · 6.31 KB
/
placeholder.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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
}