github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/snapshots/devmapper/dmsetup/dmsetup.go (about)

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