Move snapshots/devmapper to plugins/snapshots/devmapper
Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
446
plugins/snapshots/devmapper/dmsetup/dmsetup.go
Normal file
446
plugins/snapshots/devmapper/dmsetup/dmsetup.go
Normal file
@@ -0,0 +1,446 @@
|
||||
//go: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.
|
||||
*/
|
||||
|
||||
// Copyright 2012-2017 Docker, Inc.
|
||||
|
||||
package dmsetup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
blkdiscard "github.com/containerd/containerd/v2/plugins/snapshots/devmapper/blkdiscard"
|
||||
"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
|
||||
)
|
||||
|
||||
// ErrInUse represents an error mutating a device because it is in use elsewhere
|
||||
var ErrInUse = errors.New("device is in use")
|
||||
|
||||
// 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 "", fmt.Errorf("failed to get block device size: %s: %w", dataFile, err)
|
||||
}
|
||||
|
||||
// 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, fmt.Errorf("failed to parse line %q: %w", line, err)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
RawOutput string
|
||||
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.RawOutput = output
|
||||
|
||||
// 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, fmt.Errorf("failed to parse output: %q", output)
|
||||
}
|
||||
|
||||
status.Offset, err = strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse offset: %q: %w", parts[0], err)
|
||||
}
|
||||
|
||||
status.Length, err = strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse length: %q: %w", parts[1], err)
|
||||
}
|
||||
|
||||
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(path string) (int64, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
size, err := f.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to seek on %q: %w", path, err)
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
// DiscardBlocks discards all blocks for the given thin device
|
||||
//
|
||||
// ported from https://github.com/moby/moby/blob/7b9275c0da707b030e62c96b679a976f31f929d3/pkg/devicemapper/devmapper.go#L416
|
||||
func DiscardBlocks(deviceName string) error {
|
||||
inUse, err := isInUse(deviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inUse {
|
||||
return ErrInUse
|
||||
}
|
||||
path := GetFullDevicePath(deviceName)
|
||||
_, err = blkdiscard.BlkDiscard(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 "", fmt.Errorf("dmsetup %s\nerror: %s\n: %w", strings.Join(args, " "), output, err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func isInUse(deviceName string) (bool, error) {
|
||||
info, err := Info(deviceName)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if len(info) != 1 {
|
||||
return true, errors.New("could not get device info")
|
||||
}
|
||||
return info[0].OpenCount != 0, nil
|
||||
}
|
||||
202
plugins/snapshots/devmapper/dmsetup/dmsetup_test.go
Normal file
202
plugins/snapshots/devmapper/dmsetup/dmsetup_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
//go: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 (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
testPoolName = "test-pool"
|
||||
testDeviceName = "test-device"
|
||||
deviceID = 1
|
||||
snapshotID = 2
|
||||
)
|
||||
|
||||
func TestDMSetup(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
dataImage, loopDataDevice := createLoopbackDevice(t, tempDir)
|
||||
metaImage, loopMetaDevice := createLoopbackDevice(t, tempDir)
|
||||
|
||||
defer func() {
|
||||
err := mount.DetachLoopDevice(loopDataDevice, loopMetaDevice)
|
||||
assert.Nil(t, err, "failed to detach loop devices for data image: %s and meta image: %s", dataImage, metaImage)
|
||||
}()
|
||||
|
||||
t.Run("CreatePool", func(t *testing.T) {
|
||||
err := CreatePool(testPoolName, loopDataDevice, loopMetaDevice, 128)
|
||||
assert.Nil(t, err, "failed to create thin-pool with %s %s", loopDataDevice, loopMetaDevice)
|
||||
|
||||
table, err := Table(testPoolName)
|
||||
t.Logf("table: %s", table)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(table, "0 32768 thin-pool"))
|
||||
assert.True(t, strings.HasSuffix(table, "128 32768 1 skip_block_zeroing"))
|
||||
})
|
||||
|
||||
t.Run("ReloadPool", func(t *testing.T) {
|
||||
err := ReloadPool(testPoolName, loopDataDevice, loopMetaDevice, 256)
|
||||
assert.Nil(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("DeviceStatus", testDeviceStatus)
|
||||
t.Run("SuspendResumeDevice", testSuspendResumeDevice)
|
||||
t.Run("DiscardBlocks", testDiscardBlocks)
|
||||
t.Run("RemoveDevice", testRemoveDevice)
|
||||
|
||||
t.Run("RemovePool", func(t *testing.T) {
|
||||
err := RemoveDevice(testPoolName, RemoveWithForce, RemoveWithRetries)
|
||||
assert.Nil(t, err, "failed to remove thin-pool")
|
||||
})
|
||||
|
||||
t.Run("Version", testVersion)
|
||||
}
|
||||
|
||||
func testCreateDevice(t *testing.T) {
|
||||
err := CreateDevice(testPoolName, deviceID)
|
||||
assert.Nil(t, err, "failed to create test device")
|
||||
|
||||
err = CreateDevice(testPoolName, deviceID)
|
||||
assert.True(t, err == unix.EEXIST)
|
||||
|
||||
infos, err := Info(testPoolName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, infos, 1, "got unexpected number of device infos")
|
||||
}
|
||||
|
||||
func testCreateSnapshot(t *testing.T) {
|
||||
err := CreateSnapshot(testPoolName, snapshotID, deviceID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func testDeleteSnapshot(t *testing.T) {
|
||||
err := DeleteDevice(testPoolName, snapshotID)
|
||||
assert.Nil(t, err, "failed to send delete message")
|
||||
|
||||
err = DeleteDevice(testPoolName, snapshotID)
|
||||
assert.Equal(t, err, unix.ENODATA)
|
||||
}
|
||||
|
||||
func testActivateDevice(t *testing.T) {
|
||||
err := ActivateDevice(testPoolName, testDeviceName, 1, 1024, "")
|
||||
assert.Nil(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.Nil(t, err, "failed to stat device")
|
||||
}
|
||||
|
||||
list, err := Info(testPoolName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, list, 1)
|
||||
|
||||
info := list[0]
|
||||
assert.Equal(t, testPoolName, info.Name)
|
||||
assert.True(t, info.TableLive)
|
||||
}
|
||||
|
||||
func testDeviceStatus(t *testing.T) {
|
||||
status, err := Status(testDeviceName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(0), status.Offset)
|
||||
assert.Equal(t, int64(2), status.Length)
|
||||
assert.Equal(t, "thin", status.Target)
|
||||
assert.Equal(t, status.Params, []string{"0", "-"})
|
||||
}
|
||||
|
||||
func testSuspendResumeDevice(t *testing.T) {
|
||||
err := SuspendDevice(testDeviceName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = SuspendDevice(testDeviceName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
list, err := Info(testDeviceName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, list, 1)
|
||||
|
||||
info := list[0]
|
||||
assert.True(t, info.Suspended)
|
||||
|
||||
err = ResumeDevice(testDeviceName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = ResumeDevice(testDeviceName)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func testDiscardBlocks(t *testing.T) {
|
||||
err := DiscardBlocks(testDeviceName)
|
||||
assert.Nil(t, err, "failed to discard blocks")
|
||||
}
|
||||
|
||||
func testRemoveDevice(t *testing.T) {
|
||||
err := RemoveDevice(testPoolName)
|
||||
assert.Equal(t, err, unix.EBUSY, "removing thin-pool with dependencies shouldn't be allowed")
|
||||
|
||||
err = RemoveDevice(testDeviceName, RemoveWithRetries)
|
||||
assert.Nil(t, err, "failed to remove thin-device")
|
||||
}
|
||||
|
||||
func testVersion(t *testing.T) {
|
||||
version, err := Version()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, version)
|
||||
}
|
||||
|
||||
func createLoopbackDevice(t *testing.T, dir string) (string, string) {
|
||||
file, err := os.CreateTemp(dir, "dmsetup-tests-")
|
||||
assert.NoError(t, err)
|
||||
|
||||
size, err := units.RAMInBytes("16Mb")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = file.Truncate(size)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = file.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
imagePath := file.Name()
|
||||
|
||||
loopDevice, err := mount.AttachLoopDevice(imagePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return imagePath, loopDevice
|
||||
}
|
||||
Reference in New Issue
Block a user