forked from containerd/containerd
/
dmsetup.go
401 lines (332 loc) · 11.3 KB
/
dmsetup.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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
// +build linux
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dmsetup
import (
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
const (
// DevMapperDir represents devmapper devices location
DevMapperDir = "/dev/mapper/"
// SectorSize represents the number of bytes in one sector on devmapper devices
SectorSize = 512
)
// DeviceInfo represents device info returned by "dmsetup info".
// dmsetup(8) provides more information on each of these fields.
type DeviceInfo struct {
Name string
BlockDeviceName string
TableLive bool
TableInactive bool
Suspended bool
ReadOnly bool
Major uint32
Minor uint32
OpenCount uint32 // Open reference count
TargetCount uint32 // Number of targets in the live table
EventNumber uint32 // Last event sequence number (used by wait)
}
var errTable map[string]unix.Errno
func init() {
// Precompute map of <text>=<errno> for optimal lookup
errTable = make(map[string]unix.Errno)
for errno := unix.EPERM; errno <= unix.EHWPOISON; errno++ {
errTable[errno.Error()] = errno
}
}
// CreatePool creates a device with the given name, data and metadata file and block size (see "dmsetup create")
func CreatePool(poolName, dataFile, metaFile string, blockSizeSectors uint32) error {
thinPool, err := makeThinPoolMapping(dataFile, metaFile, blockSizeSectors)
if err != nil {
return err
}
_, err = dmsetup("create", poolName, "--table", thinPool)
return err
}
// ReloadPool reloads existing thin-pool (see "dmsetup reload")
func ReloadPool(deviceName, dataFile, metaFile string, blockSizeSectors uint32) error {
thinPool, err := makeThinPoolMapping(dataFile, metaFile, blockSizeSectors)
if err != nil {
return err
}
_, err = dmsetup("reload", deviceName, "--table", thinPool)
return err
}
const (
lowWaterMark = 32768 // Picked arbitrary, might need tuning
skipZeroing = "skip_block_zeroing" // Skipping zeroing to reduce latency for device creation
)
// makeThinPoolMapping makes thin-pool table entry
func makeThinPoolMapping(dataFile, metaFile string, blockSizeSectors uint32) (string, error) {
dataDeviceSizeBytes, err := BlockDeviceSize(dataFile)
if err != nil {
return "", errors.Wrapf(err, "failed to get block device size: %s", dataFile)
}
// Thin-pool mapping target has the following format:
// start - starting block in virtual device
// length - length of this segment
// metadata_dev - the metadata device
// data_dev - the data device
// data_block_size - the data block size in sectors
// low_water_mark - the low water mark, expressed in blocks of size data_block_size
// feature_args - the number of feature arguments
// args
lengthSectors := dataDeviceSizeBytes / SectorSize
target := fmt.Sprintf("0 %d thin-pool %s %s %d %d 1 %s",
lengthSectors,
metaFile,
dataFile,
blockSizeSectors,
lowWaterMark,
skipZeroing)
return target, nil
}
// CreateDevice sends "create_thin <deviceID>" message to the given thin-pool
func CreateDevice(poolName string, deviceID uint32) error {
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("create_thin %d", deviceID))
return err
}
// ActivateDevice activates the given thin-device using the 'thin' target
func ActivateDevice(poolName string, deviceName string, deviceID uint32, size uint64, external string) error {
mapping := makeThinMapping(poolName, deviceID, size, external)
_, err := dmsetup("create", deviceName, "--table", mapping)
return err
}
// makeThinMapping makes thin target table entry
func makeThinMapping(poolName string, deviceID uint32, sizeBytes uint64, externalOriginDevice string) string {
lengthSectors := sizeBytes / SectorSize
// Thin target has the following format:
// start - starting block in virtual device
// length - length of this segment
// pool_dev - the thin-pool device, can be /dev/mapper/pool_name or 253:0
// dev_id - the internal device id of the device to be activated
// external_origin_dev - an optional block device outside the pool to be treated as a read-only snapshot origin.
target := fmt.Sprintf("0 %d thin %s %d %s", lengthSectors, GetFullDevicePath(poolName), deviceID, externalOriginDevice)
return strings.TrimSpace(target)
}
// SuspendDevice suspends the given device (see "dmsetup suspend")
func SuspendDevice(deviceName string) error {
_, err := dmsetup("suspend", deviceName)
return err
}
// ResumeDevice resumes the given device (see "dmsetup resume")
func ResumeDevice(deviceName string) error {
_, err := dmsetup("resume", deviceName)
return err
}
// Table returns the current table for the device
func Table(deviceName string) (string, error) {
return dmsetup("table", deviceName)
}
// CreateSnapshot sends "create_snap" message to the given thin-pool.
// Caller needs to suspend and resume device if it is active.
func CreateSnapshot(poolName string, deviceID uint32, baseDeviceID uint32) error {
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("create_snap %d %d", deviceID, baseDeviceID))
return err
}
// DeleteDevice sends "delete <deviceID>" message to the given thin-pool
func DeleteDevice(poolName string, deviceID uint32) error {
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("delete %d", deviceID))
return err
}
// RemoveDeviceOpt represents command line arguments for "dmsetup remove" command
type RemoveDeviceOpt string
const (
// RemoveWithForce flag replaces the table with one that fails all I/O if
// open device can't be removed
RemoveWithForce RemoveDeviceOpt = "--force"
// RemoveWithRetries option will cause the operation to be retried
// for a few seconds before failing
RemoveWithRetries RemoveDeviceOpt = "--retry"
// RemoveDeferred flag will enable deferred removal of open devices,
// the device will be removed when the last user closes it
RemoveDeferred RemoveDeviceOpt = "--deferred"
)
// RemoveDevice removes a device (see "dmsetup remove")
func RemoveDevice(deviceName string, opts ...RemoveDeviceOpt) error {
args := []string{
"remove",
}
for _, opt := range opts {
args = append(args, string(opt))
}
args = append(args, GetFullDevicePath(deviceName))
_, err := dmsetup(args...)
if err == unix.ENXIO {
// Ignore "No such device or address" error because we dmsetup
// remove with "deferred" option, there is chance for the device
// having been removed.
return nil
}
return err
}
// Info outputs device information (see "dmsetup info").
// If device name is empty, all device infos will be returned.
func Info(deviceName string) ([]*DeviceInfo, error) {
output, err := dmsetup(
"info",
"--columns",
"--noheadings",
"-o",
"name,blkdevname,attr,major,minor,open,segments,events",
"--separator",
" ",
deviceName)
if err != nil {
return nil, err
}
var (
lines = strings.Split(output, "\n")
devices = make([]*DeviceInfo, len(lines))
)
for i, line := range lines {
var (
attr = ""
info = &DeviceInfo{}
)
_, err := fmt.Sscan(line,
&info.Name,
&info.BlockDeviceName,
&attr,
&info.Major,
&info.Minor,
&info.OpenCount,
&info.TargetCount,
&info.EventNumber)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse line %q", line)
}
// Parse attributes (see "man 8 dmsetup" for details)
info.Suspended = strings.Contains(attr, "s")
info.ReadOnly = strings.Contains(attr, "r")
info.TableLive = strings.Contains(attr, "L")
info.TableInactive = strings.Contains(attr, "I")
devices[i] = info
}
return devices, nil
}
// Version returns "dmsetup version" output
func Version() (string, error) {
return dmsetup("version")
}
// DeviceStatus represents devmapper device status information
type DeviceStatus struct {
Offset int64
Length int64
Target string
Params []string
}
// Status provides status information for devmapper device
func Status(deviceName string) (*DeviceStatus, error) {
var (
err error
status DeviceStatus
)
output, err := dmsetup("status", deviceName)
if err != nil {
return nil, err
}
// Status output format:
// Offset (int64)
// Length (int64)
// Target type (string)
// Params (Array of strings)
const MinParseCount = 4
parts := strings.Split(output, " ")
if len(parts) < MinParseCount {
return nil, errors.Errorf("failed to parse output: %q", output)
}
status.Offset, err = strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse offset: %q", parts[0])
}
status.Length, err = strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse length: %q", parts[1])
}
status.Target = parts[2]
status.Params = parts[3:]
return &status, nil
}
// GetFullDevicePath returns full path for the given device name (like "/dev/mapper/name")
func GetFullDevicePath(deviceName string) string {
if strings.HasPrefix(deviceName, DevMapperDir) {
return deviceName
}
return DevMapperDir + deviceName
}
// BlockDeviceSize returns size of block device in bytes
func BlockDeviceSize(devicePath string) (uint64, error) {
data, err := exec.Command("blockdev", "--getsize64", "-q", devicePath).CombinedOutput()
output := string(data)
if err != nil {
return 0, errors.Wrapf(err, output)
}
output = strings.TrimSuffix(output, "\n")
return strconv.ParseUint(output, 10, 64)
}
func dmsetup(args ...string) (string, error) {
data, err := exec.Command("dmsetup", args...).CombinedOutput()
output := string(data)
if err != nil {
// Try find Linux error code otherwise return generic error with dmsetup output
if errno, ok := tryGetUnixError(output); ok {
return "", errno
}
return "", errors.Wrapf(err, "dmsetup %s\nerror: %s\n", strings.Join(args, " "), output)
}
output = strings.TrimSuffix(output, "\n")
output = strings.TrimSpace(output)
return output, nil
}
// tryGetUnixError tries to find Linux error code from dmsetup output
func tryGetUnixError(output string) (unix.Errno, bool) {
// It's useful to have Linux error codes like EBUSY, EPERM, ..., instead of just text.
// Unfortunately there is no better way than extracting/comparing error text.
text := parseDmsetupError(output)
if text == "" {
return 0, false
}
err, ok := errTable[text]
return err, ok
}
// dmsetup returns error messages in format:
// device-mapper: message ioctl on <name> failed: File exists\n
// Command failed\n
// parseDmsetupError extracts text between "failed: " and "\n"
func parseDmsetupError(output string) string {
lines := strings.SplitN(output, "\n", 2)
if len(lines) < 2 {
return ""
}
line := lines[0]
// Handle output like "Device /dev/mapper/snapshotter-suite-pool-snap-1 not found"
if strings.HasSuffix(line, "not found") {
return unix.ENXIO.Error()
}
const failedSubstr = "failed: "
idx := strings.LastIndex(line, failedSubstr)
if idx == -1 {
return ""
}
str := line[idx:]
// Strip "failed: " prefix
str = strings.TrimPrefix(str, failedSubstr)
str = strings.ToLower(str)
return str
}