| //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 |
| } |