Skip to content

Commit 809e5fd

Browse files
committed
devmapper: add dmsetup
Signed-off-by: Maksym Pavlenko <[email protected]>
1 parent fe05e4d commit 809e5fd

2 files changed

Lines changed: 535 additions & 0 deletions

File tree

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package dmsetup
18+
19+
import (
20+
"fmt"
21+
"os/exec"
22+
"strconv"
23+
"strings"
24+
25+
"github.com/pkg/errors"
26+
"golang.org/x/sys/unix"
27+
)
28+
29+
const (
30+
// DevMapperDir represents devmapper devices location
31+
DevMapperDir = "/dev/mapper/"
32+
// SectorSize represents the number of bytes in one sector on devmapper devices
33+
SectorSize = 512
34+
)
35+
36+
// DeviceInfo represents device info returned by "dmsetup info".
37+
// dmsetup(8) provides more information on each of these fields.
38+
type DeviceInfo struct {
39+
Name string
40+
BlockDeviceName string
41+
TableLive bool
42+
TableInactive bool
43+
Suspended bool
44+
ReadOnly bool
45+
Major uint32
46+
Minor uint32
47+
OpenCount uint32 // Open reference count
48+
TargetCount uint32 // Number of targets in the live table
49+
EventNumber uint32 // Last event sequence number (used by wait)
50+
}
51+
52+
var errTable map[string]unix.Errno
53+
54+
func init() {
55+
// Precompute map of <text>=<errno> for optimal lookup
56+
errTable = make(map[string]unix.Errno)
57+
for errno := unix.EPERM; errno <= unix.EHWPOISON; errno++ {
58+
errTable[errno.Error()] = errno
59+
}
60+
}
61+
62+
// CreatePool creates a device with the given name, data and metadata file and block size (see "dmsetup create")
63+
func CreatePool(poolName, dataFile, metaFile string, blockSizeSectors uint32) error {
64+
thinPool, err := makeThinPoolMapping(dataFile, metaFile, blockSizeSectors)
65+
if err != nil {
66+
return err
67+
}
68+
69+
_, err = dmsetup("create", poolName, "--table", thinPool)
70+
return err
71+
}
72+
73+
// ReloadPool reloads existing thin-pool (see "dmsetup reload")
74+
func ReloadPool(deviceName, dataFile, metaFile string, blockSizeSectors uint32) error {
75+
thinPool, err := makeThinPoolMapping(dataFile, metaFile, blockSizeSectors)
76+
if err != nil {
77+
return err
78+
}
79+
80+
_, err = dmsetup("reload", deviceName, "--table", thinPool)
81+
return err
82+
}
83+
84+
const (
85+
lowWaterMark = 32768 // Picked arbitrary, might need tuning
86+
skipZeroing = "skip_block_zeroing" // Skipping zeroing to reduce latency for device creation
87+
)
88+
89+
// makeThinPoolMapping makes thin-pool table entry
90+
func makeThinPoolMapping(dataFile, metaFile string, blockSizeSectors uint32) (string, error) {
91+
dataDeviceSizeBytes, err := BlockDeviceSize(dataFile)
92+
if err != nil {
93+
return "", errors.Wrapf(err, "failed to get block device size: %s", dataFile)
94+
}
95+
96+
// Thin-pool mapping target has the following format:
97+
// start - starting block in virtual device
98+
// length - length of this segment
99+
// metadata_dev - the metadata device
100+
// data_dev - the data device
101+
// data_block_size - the data block size in sectors
102+
// low_water_mark - the low water mark, expressed in blocks of size data_block_size
103+
// feature_args - the number of feature arguments
104+
// args
105+
lengthSectors := dataDeviceSizeBytes / SectorSize
106+
target := fmt.Sprintf("0 %d thin-pool %s %s %d %d 1 %s",
107+
lengthSectors,
108+
metaFile,
109+
dataFile,
110+
blockSizeSectors,
111+
lowWaterMark,
112+
skipZeroing)
113+
114+
return target, nil
115+
}
116+
117+
// CreateDevice sends "create_thin <deviceID>" message to the given thin-pool
118+
func CreateDevice(poolName string, deviceID uint32) error {
119+
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("create_thin %d", deviceID))
120+
return err
121+
}
122+
123+
// ActivateDevice activates the given thin-device using the 'thin' target
124+
func ActivateDevice(poolName string, deviceName string, deviceID uint32, size uint64, external string) error {
125+
mapping := makeThinMapping(poolName, deviceID, size, external)
126+
_, err := dmsetup("create", deviceName, "--table", mapping)
127+
return err
128+
}
129+
130+
// makeThinMapping makes thin target table entry
131+
func makeThinMapping(poolName string, deviceID uint32, sizeBytes uint64, externalOriginDevice string) string {
132+
lengthSectors := sizeBytes / SectorSize
133+
134+
// Thin target has the following format:
135+
// start - starting block in virtual device
136+
// length - length of this segment
137+
// pool_dev - the thin-pool device, can be /dev/mapper/pool_name or 253:0
138+
// dev_id - the internal device id of the device to be activated
139+
// external_origin_dev - an optional block device outside the pool to be treated as a read-only snapshot origin.
140+
target := fmt.Sprintf("0 %d thin %s %d %s", lengthSectors, GetFullDevicePath(poolName), deviceID, externalOriginDevice)
141+
return strings.TrimSpace(target)
142+
}
143+
144+
// SuspendDevice suspends the given device (see "dmsetup suspend")
145+
func SuspendDevice(deviceName string) error {
146+
_, err := dmsetup("suspend", deviceName)
147+
return err
148+
}
149+
150+
// ResumeDevice resumes the given device (see "dmsetup resume")
151+
func ResumeDevice(deviceName string) error {
152+
_, err := dmsetup("resume", deviceName)
153+
return err
154+
}
155+
156+
// Table returns the current table for the device
157+
func Table(deviceName string) (string, error) {
158+
return dmsetup("table", deviceName)
159+
}
160+
161+
// CreateSnapshot sends "create_snap" message to the given thin-pool.
162+
// Caller needs to suspend and resume device if it is active.
163+
func CreateSnapshot(poolName string, deviceID uint32, baseDeviceID uint32) error {
164+
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("create_snap %d %d", deviceID, baseDeviceID))
165+
return err
166+
}
167+
168+
// DeleteDevice sends "delete <deviceID>" message to the given thin-pool
169+
func DeleteDevice(poolName string, deviceID uint32) error {
170+
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("delete %d", deviceID))
171+
return err
172+
}
173+
174+
// RemoveDeviceOpt represents command line arguments for "dmsetup remove" command
175+
type RemoveDeviceOpt string
176+
177+
const (
178+
// RemoveWithForce flag replaces the table with one that fails all I/O if
179+
// open device can't be removed
180+
RemoveWithForce RemoveDeviceOpt = "--force"
181+
// RemoveWithRetries option will cause the operation to be retried
182+
// for a few seconds before failing
183+
RemoveWithRetries RemoveDeviceOpt = "--retry"
184+
// RemoveDeferred flag will enable deferred removal of open devices,
185+
// the device will be removed when the last user closes it
186+
RemoveDeferred RemoveDeviceOpt = "--deferred"
187+
)
188+
189+
// RemoveDevice removes a device (see "dmsetup remove")
190+
func RemoveDevice(deviceName string, opts ...RemoveDeviceOpt) error {
191+
args := []string{
192+
"remove",
193+
}
194+
195+
for _, opt := range opts {
196+
args = append(args, string(opt))
197+
}
198+
199+
args = append(args, GetFullDevicePath(deviceName))
200+
201+
_, err := dmsetup(args...)
202+
return err
203+
}
204+
205+
// Info outputs device information (see "dmsetup info").
206+
// If device name is empty, all device infos will be returned.
207+
func Info(deviceName string) ([]*DeviceInfo, error) {
208+
output, err := dmsetup(
209+
"info",
210+
"--columns",
211+
"--noheadings",
212+
"-o",
213+
"name,blkdevname,attr,major,minor,open,segments,events",
214+
"--separator",
215+
" ",
216+
deviceName)
217+
218+
if err != nil {
219+
return nil, err
220+
}
221+
222+
var (
223+
lines = strings.Split(output, "\n")
224+
devices = make([]*DeviceInfo, len(lines))
225+
)
226+
227+
for i, line := range lines {
228+
var (
229+
attr = ""
230+
info = &DeviceInfo{}
231+
)
232+
233+
_, err := fmt.Sscan(line,
234+
&info.Name,
235+
&info.BlockDeviceName,
236+
&attr,
237+
&info.Major,
238+
&info.Minor,
239+
&info.OpenCount,
240+
&info.TargetCount,
241+
&info.EventNumber)
242+
243+
if err != nil {
244+
return nil, errors.Wrapf(err, "failed to parse line %q", line)
245+
}
246+
247+
// Parse attributes (see "man 8 dmsetup" for details)
248+
info.Suspended = strings.Contains(attr, "s")
249+
info.ReadOnly = strings.Contains(attr, "r")
250+
info.TableLive = strings.Contains(attr, "L")
251+
info.TableInactive = strings.Contains(attr, "I")
252+
253+
devices[i] = info
254+
}
255+
256+
return devices, nil
257+
}
258+
259+
// Version returns "dmsetup version" output
260+
func Version() (string, error) {
261+
return dmsetup("version")
262+
}
263+
264+
// GetFullDevicePath returns full path for the given device name (like "/dev/mapper/name")
265+
func GetFullDevicePath(deviceName string) string {
266+
if strings.HasPrefix(deviceName, DevMapperDir) {
267+
return deviceName
268+
}
269+
270+
return DevMapperDir + deviceName
271+
}
272+
273+
// BlockDeviceSize returns size of block device in bytes
274+
func BlockDeviceSize(devicePath string) (uint64, error) {
275+
data, err := exec.Command("blockdev", "--getsize64", "-q", devicePath).CombinedOutput()
276+
output := string(data)
277+
if err != nil {
278+
return 0, errors.Wrapf(err, output)
279+
}
280+
281+
output = strings.TrimSuffix(output, "\n")
282+
return strconv.ParseUint(output, 10, 64)
283+
}
284+
285+
func dmsetup(args ...string) (string, error) {
286+
data, err := exec.Command("dmsetup", args...).CombinedOutput()
287+
output := string(data)
288+
if err != nil {
289+
// Try find Linux error code otherwise return generic error with dmsetup output
290+
if errno, ok := tryGetUnixError(output); ok {
291+
return "", errno
292+
}
293+
294+
return "", errors.Wrapf(err, "dmsetup %s\nerror: %s\n", strings.Join(args, " "), output)
295+
}
296+
297+
output = strings.TrimSuffix(output, "\n")
298+
output = strings.TrimSpace(output)
299+
300+
return output, nil
301+
}
302+
303+
// tryGetUnixError tries to find Linux error code from dmsetup output
304+
func tryGetUnixError(output string) (unix.Errno, bool) {
305+
// It's useful to have Linux error codes like EBUSY, EPERM, ..., instead of just text.
306+
// Unfortunately there is no better way than extracting/comparing error text.
307+
text := parseDmsetupError(output)
308+
if text == "" {
309+
return 0, false
310+
}
311+
312+
err, ok := errTable[text]
313+
return err, ok
314+
}
315+
316+
// dmsetup returns error messages in format:
317+
// device-mapper: message ioctl on <name> failed: File exists\n
318+
// Command failed\n
319+
// parseDmsetupError extracts text between "failed: " and "\n"
320+
func parseDmsetupError(output string) string {
321+
lines := strings.SplitN(output, "\n", 2)
322+
if len(lines) < 2 {
323+
return ""
324+
}
325+
326+
const failedSubstr = "failed: "
327+
328+
line := lines[0]
329+
idx := strings.LastIndex(line, failedSubstr)
330+
if idx == -1 {
331+
return ""
332+
}
333+
334+
str := line[idx:]
335+
336+
// Strip "failed: " prefix
337+
str = strings.TrimPrefix(str, failedSubstr)
338+
339+
str = strings.ToLower(str)
340+
return str
341+
}

0 commit comments

Comments
 (0)