devmapper: add dmsetup
Signed-off-by: Maksym Pavlenko <makpav@amazon.com>
This commit is contained in:
parent
fe05e4d1a9
commit
809e5fd3b8
341
snapshots/devmapper/dmsetup/dmsetup.go
Normal file
341
snapshots/devmapper/dmsetup/dmsetup.go
Normal file
@ -0,0 +1,341 @@
|
||||
/*
|
||||
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...)
|
||||
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")
|
||||
}
|
||||
|
||||
// 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 ""
|
||||
}
|
||||
|
||||
const failedSubstr = "failed: "
|
||||
|
||||
line := lines[0]
|
||||
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
|
||||
}
|
194
snapshots/devmapper/dmsetup/dmsetup_test.go
Normal file
194
snapshots/devmapper/dmsetup/dmsetup_test.go
Normal file
@ -0,0 +1,194 @@
|
||||
/*
|
||||
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 (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/pkg/testutil"
|
||||
"github.com/containerd/containerd/snapshots/devmapper/losetup"
|
||||
"github.com/docker/go-units"
|
||||
"golang.org/x/sys/unix"
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
)
|
||||
|
||||
const (
|
||||
testPoolName = "test-pool"
|
||||
testDeviceName = "test-device"
|
||||
deviceID = 1
|
||||
snapshotID = 2
|
||||
)
|
||||
|
||||
func TestDMSetup(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
tempDir, err := ioutil.TempDir("", "dmsetup-tests-")
|
||||
assert.NilError(t, err, "failed to make temp dir for tests")
|
||||
|
||||
defer func() {
|
||||
err := os.RemoveAll(tempDir)
|
||||
assert.NilError(t, err)
|
||||
}()
|
||||
|
||||
dataImage, loopDataDevice := createLoopbackDevice(t, tempDir)
|
||||
metaImage, loopMetaDevice := createLoopbackDevice(t, tempDir)
|
||||
|
||||
defer func() {
|
||||
err = losetup.RemoveLoopDevicesAssociatedWithImage(dataImage)
|
||||
assert.NilError(t, err, "failed to detach loop devices for data image: %s", dataImage)
|
||||
|
||||
err = losetup.RemoveLoopDevicesAssociatedWithImage(metaImage)
|
||||
assert.NilError(t, err, "failed to detach loop devices for meta image: %s", metaImage)
|
||||
}()
|
||||
|
||||
t.Run("CreatePool", func(t *testing.T) {
|
||||
err := CreatePool(testPoolName, loopDataDevice, loopMetaDevice, 128)
|
||||
assert.NilError(t, err, "failed to create thin-pool")
|
||||
|
||||
table, err := Table(testPoolName)
|
||||
t.Logf("table: %s", table)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, strings.HasPrefix(table, "0 32768 thin-pool"))
|
||||
assert.Assert(t, strings.HasSuffix(table, "128 32768 1 skip_block_zeroing"))
|
||||
})
|
||||
|
||||
t.Run("ReloadPool", func(t *testing.T) {
|
||||
err := ReloadPool(testPoolName, loopDataDevice, loopMetaDevice, 256)
|
||||
assert.NilError(t, err, "failed to reload thin-pool")
|
||||
})
|
||||
|
||||
t.Run("CreateDevice", testCreateDevice)
|
||||
|
||||
t.Run("CreateSnapshot", testCreateSnapshot)
|
||||
t.Run("DeleteSnapshot", testDeleteSnapshot)
|
||||
|
||||
t.Run("ActivateDevice", testActivateDevice)
|
||||
t.Run("SuspendResumeDevice", testSuspendResumeDevice)
|
||||
t.Run("RemoveDevice", testRemoveDevice)
|
||||
|
||||
t.Run("RemovePool", func(t *testing.T) {
|
||||
err = RemoveDevice(testPoolName, RemoveWithForce, RemoveWithRetries)
|
||||
assert.NilError(t, err, "failed to remove thin-pool")
|
||||
})
|
||||
|
||||
t.Run("Version", testVersion)
|
||||
}
|
||||
|
||||
func testCreateDevice(t *testing.T) {
|
||||
err := CreateDevice(testPoolName, deviceID)
|
||||
assert.NilError(t, err, "failed to create test device")
|
||||
|
||||
err = CreateDevice(testPoolName, deviceID)
|
||||
assert.Assert(t, err == unix.EEXIST)
|
||||
|
||||
infos, err := Info(testPoolName)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Len(infos, 1), "got unexpected number of device infos")
|
||||
}
|
||||
|
||||
func testCreateSnapshot(t *testing.T) {
|
||||
err := CreateSnapshot(testPoolName, snapshotID, deviceID)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func testDeleteSnapshot(t *testing.T) {
|
||||
err := DeleteDevice(testPoolName, snapshotID)
|
||||
assert.NilError(t, err, "failed to send delete message")
|
||||
|
||||
err = DeleteDevice(testPoolName, snapshotID)
|
||||
assert.Assert(t, err == unix.ENODATA)
|
||||
}
|
||||
|
||||
func testActivateDevice(t *testing.T) {
|
||||
err := ActivateDevice(testPoolName, testDeviceName, 1, 1024, "")
|
||||
assert.NilError(t, err, "failed to activate device")
|
||||
|
||||
err = ActivateDevice(testPoolName, testDeviceName, 1, 1024, "")
|
||||
assert.Equal(t, err, unix.EBUSY)
|
||||
|
||||
if _, err := os.Stat("/dev/mapper/" + testDeviceName); err != nil && !os.IsExist(err) {
|
||||
assert.NilError(t, err, "failed to stat device")
|
||||
}
|
||||
|
||||
list, err := Info(testPoolName)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Len(list, 1))
|
||||
|
||||
info := list[0]
|
||||
assert.Equal(t, testPoolName, info.Name)
|
||||
assert.Assert(t, info.TableLive)
|
||||
}
|
||||
|
||||
func testSuspendResumeDevice(t *testing.T) {
|
||||
err := SuspendDevice(testDeviceName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = SuspendDevice(testDeviceName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
list, err := Info(testDeviceName)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Len(list, 1))
|
||||
|
||||
info := list[0]
|
||||
assert.Assert(t, info.Suspended)
|
||||
|
||||
err = ResumeDevice(testDeviceName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = ResumeDevice(testDeviceName)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func testRemoveDevice(t *testing.T) {
|
||||
err := RemoveDevice(testPoolName)
|
||||
assert.Assert(t, err == unix.EBUSY, "removing thin-pool with dependencies shouldn't be allowed")
|
||||
|
||||
err = RemoveDevice(testDeviceName, RemoveWithRetries)
|
||||
assert.NilError(t, err, "failed to remove thin-device")
|
||||
}
|
||||
|
||||
func testVersion(t *testing.T) {
|
||||
version, err := Version()
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, version != "")
|
||||
}
|
||||
|
||||
func createLoopbackDevice(t *testing.T, dir string) (string, string) {
|
||||
file, err := ioutil.TempFile(dir, "dmsetup-tests-")
|
||||
assert.NilError(t, err)
|
||||
|
||||
size, err := units.RAMInBytes("16Mb")
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = file.Truncate(size)
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = file.Close()
|
||||
assert.NilError(t, err)
|
||||
|
||||
imagePath := file.Name()
|
||||
|
||||
loopDevice, err := losetup.AttachLoopDevice(imagePath)
|
||||
assert.NilError(t, err)
|
||||
|
||||
return imagePath, loopDevice
|
||||
}
|
Loading…
Reference in New Issue
Block a user