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