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 }