func (s *ListBlockDevicesSuite) testListBlockDevicesExtended( c *gc.C, udevadmInfo string, expect storage.BlockDevice, ) { testing.PatchExecutable(c, s, "lsblk", `#!/bin/bash --norc cat <<EOF KNAME="sda" SIZE="240057409536" LABEL="" UUID="" TYPE="disk" EOF`) testing.PatchExecutable(c, s, "udevadm", `#!/bin/bash --norc cat <<EOF `+strings.TrimSpace(udevadmInfo)+` EOF`) expect.DeviceName = "sda" expect.Size = 228936 devices, err := diskmanager.ListBlockDevices() c.Assert(err, jc.ErrorIsNil) c.Assert(devices, jc.DeepEquals, []storage.BlockDevice{expect}) }
func listBlockDevices() ([]storage.BlockDevice, error) { columns := []string{ "KNAME", // kernel name "SIZE", // size "LABEL", // filesystem label "UUID", // filesystem UUID "FSTYPE", // filesystem type "TYPE", // device type "MOUNTPOINT", // moint point } logger.Tracef("executing lsblk") output, err := exec.Command( "lsblk", "-b", // output size in bytes "-P", // output fields as key=value pairs "-o", strings.Join(columns, ","), ).Output() if err != nil { return nil, errors.Annotate( err, "cannot list block devices: lsblk failed", ) } var devices []storage.BlockDevice s := bufio.NewScanner(bytes.NewReader(output)) for s.Scan() { pairs := pairsRE.FindAllStringSubmatch(s.Text(), -1) var dev storage.BlockDevice var deviceType string for _, pair := range pairs { switch pair[1] { case "KNAME": dev.DeviceName = pair[2] case "SIZE": size, err := strconv.ParseUint(pair[2], 10, 64) if err != nil { logger.Errorf( "invalid size %q from lsblk: %v", pair[2], err, ) } else { dev.Size = size / bytesInMiB } case "LABEL": dev.Label = pair[2] case "UUID": dev.UUID = pair[2] case "FSTYPE": dev.FilesystemType = pair[2] case "TYPE": deviceType = pair[2] case "MOUNTPOINT": dev.MountPoint = pair[2] default: logger.Debugf("unexpected field from lsblk: %q", pair[1]) } } // We may later want to expand this, e.g. to handle lvm, // dmraid, crypt, etc., but this is enough to cover bases // for now. switch deviceType { case typeDisk, typeLoop: default: logger.Tracef("ignoring %q type device: %+v", deviceType, dev) continue } // Check if the block device is in use. We need to know this so we can // issue an error if the user attempts to allocate an in-use disk to a // unit. dev.InUse, err = blockDeviceInUse(dev) if os.IsNotExist(err) { // In LXC containers, lsblk will show the block devices of the // host, but the devices will typically not be present. continue } else if err != nil { logger.Infof("could not check if %q is in use: %v", dev.DeviceName, err) // We cannot detect, so err on the side of caution and default to // "in use" so the device cannot be used. dev.InUse = true } // Add additional information from sysfs. if err := addHardwareInfo(&dev); err != nil { logger.Errorf( "error getting hardware info for %q from sysfs: %v", dev.DeviceName, err, ) } devices = append(devices, dev) } if err := s.Err(); err != nil { return nil, errors.Annotate(err, "cannot parse lsblk output") } return devices, nil }
// addHardwareInfo adds additional information about the hardware, and how it is // attached to the machine, to the given BlockDevice. func addHardwareInfo(dev *storage.BlockDevice) error { logger.Tracef(`executing "udevadm info" for %s`, dev.DeviceName) output, err := exec.Command( "udevadm", "info", "-q", "property", "--path", "/block/"+dev.DeviceName, ).Output() if err != nil { msg := "udevadm failed" if output := bytes.TrimSpace(output); len(output) > 0 { msg += fmt.Sprintf(" (%s)", output) } return errors.Annotate(err, msg) } var devpath, idBus, idSerial string s := bufio.NewScanner(bytes.NewReader(output)) for s.Scan() { line := s.Text() sep := strings.IndexRune(line, '=') if sep == -1 { logger.Debugf("unexpected udevadm output line: %q", line) continue } key, value := line[:sep], line[sep+1:] switch key { case "DEVPATH": devpath = value case "DEVLINKS": dev.DeviceLinks = strings.Split(value, " ") case "ID_BUS": idBus = value case "ID_SERIAL": idSerial = value default: logger.Tracef("ignoring line: %q", line) } } if err := s.Err(); err != nil { return errors.Annotate(err, "cannot parse udevadm output") } if idBus != "" && idSerial != "" { // ID_BUS will be something like "scsi" or "ata"; // ID_SERIAL will be soemthing like ${MODEL}_${SERIALNO}; // and together they make up the symlink in /dev/disk/by-id. dev.HardwareId = idBus + "-" + idSerial } // For devices on the SCSI bus, we include the address. This is to // support storage providers where the SCSI address may be specified, // but the device name can not (and may change, depending on timing). if idBus == "scsi" && devpath != "" { // DEVPATH will be "<uninteresting stuff>/<SCSI address>/block/<device>". re := regexp.MustCompile(fmt.Sprintf( `^.*/(\d+):(\d+):(\d+):(\d+)/block/%s$`, regexp.QuoteMeta(dev.DeviceName), )) submatch := re.FindStringSubmatch(devpath) if submatch != nil { // We use the address scheme used by lshw: bus@address. We don't use // lshw because it does things we don't need, and that slows it down. // // In DEVPATH, the address format is "H:C:T:L" ([H]ost, [C]hannel, // [T]arget, [L]un); the lshw address format is "H:C.T.L" dev.BusAddress = fmt.Sprintf( "scsi@%s:%s.%s.%s", submatch[1], submatch[2], submatch[3], submatch[4], ) } else { logger.Debugf( "non matching DEVPATH for %q: %q", dev.DeviceName, devpath, ) } } return nil }