github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/diskmanager/lsblk.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // +build linux
     5  
     6  package diskmanager
     7  
     8  import (
     9  	"bufio"
    10  	"bytes"
    11  	"fmt"
    12  	"os"
    13  	"os/exec"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  	"syscall"
    18  
    19  	"github.com/juju/errors"
    20  
    21  	"github.com/juju/juju/storage"
    22  )
    23  
    24  var pairsRE = regexp.MustCompile(`([A-Z:]+)=(?:"(.*?)")`)
    25  
    26  const (
    27  	// values for the TYPE column that we care about
    28  
    29  	typeDisk = "disk"
    30  	typeLoop = "loop"
    31  	typePart = "part"
    32  )
    33  
    34  func init() {
    35  	DefaultListBlockDevices = listBlockDevices
    36  }
    37  
    38  func listBlockDevices() ([]storage.BlockDevice, error) {
    39  	columns := []string{
    40  		"KNAME",      // kernel name
    41  		"SIZE",       // size
    42  		"LABEL",      // filesystem label
    43  		"UUID",       // filesystem UUID
    44  		"FSTYPE",     // filesystem type
    45  		"TYPE",       // device type
    46  		"MOUNTPOINT", // moint point
    47  		"MAJ:MIN",    // major/minor device numbers
    48  	}
    49  
    50  	logger.Tracef("executing lsblk")
    51  	output, err := exec.Command(
    52  		"lsblk",
    53  		"-b", // output size in bytes
    54  		"-P", // output fields as key=value pairs
    55  		"-o", strings.Join(columns, ","),
    56  	).Output()
    57  	if err != nil {
    58  		return nil, errors.Annotate(
    59  			err, "cannot list block devices: lsblk failed",
    60  		)
    61  	}
    62  
    63  	var devices []storage.BlockDevice
    64  	s := bufio.NewScanner(bytes.NewReader(output))
    65  	for s.Scan() {
    66  		pairs := pairsRE.FindAllStringSubmatch(s.Text(), -1)
    67  		var dev storage.BlockDevice
    68  		var deviceType string
    69  		var majorMinor string
    70  		for _, pair := range pairs {
    71  			switch pair[1] {
    72  			case "KNAME":
    73  				dev.DeviceName = pair[2]
    74  			case "SIZE":
    75  				size, err := strconv.ParseUint(pair[2], 10, 64)
    76  				if err != nil {
    77  					logger.Errorf(
    78  						"invalid size %q from lsblk: %v", pair[2], err,
    79  					)
    80  				} else {
    81  					dev.Size = size / bytesInMiB
    82  				}
    83  			case "LABEL":
    84  				dev.Label = pair[2]
    85  			case "UUID":
    86  				dev.UUID = pair[2]
    87  			case "FSTYPE":
    88  				dev.FilesystemType = pair[2]
    89  			case "TYPE":
    90  				deviceType = pair[2]
    91  			case "MOUNTPOINT":
    92  				dev.MountPoint = pair[2]
    93  			case "MAJ:MIN":
    94  				majorMinor = pair[2]
    95  			default:
    96  				logger.Debugf("unexpected field from lsblk: %q", pair[1])
    97  			}
    98  		}
    99  
   100  		// We may later want to expand this, e.g. to handle lvm,
   101  		// dmraid, crypt, etc., but this is enough to cover bases
   102  		// for now.
   103  		switch deviceType {
   104  		case typeLoop:
   105  		case typePart:
   106  		case typeDisk:
   107  			// Floppy disks, which have major device number 2,
   108  			// should be ignored.
   109  			if strings.HasPrefix(majorMinor, "2:") {
   110  				logger.Tracef("ignoring flopping disk device: %+v", dev)
   111  				continue
   112  			}
   113  		default:
   114  			logger.Tracef("ignoring %q type device: %+v", deviceType, dev)
   115  			continue
   116  		}
   117  
   118  		// Check if the block device is in use. We need to know this so we can
   119  		// issue an error if the user attempts to allocate an in-use disk to a
   120  		// unit.
   121  		dev.InUse, err = blockDeviceInUse(dev)
   122  		if os.IsNotExist(err) {
   123  			// In LXC containers, lsblk will show the block devices of the
   124  			// host, but the devices will typically not be present.
   125  			continue
   126  		} else if err != nil {
   127  			logger.Debugf("could not check if %q is in use: %v", dev.DeviceName, err)
   128  			// We cannot detect, so err on the side of caution and default to
   129  			// "in use" so the device cannot be used.
   130  			dev.InUse = true
   131  		}
   132  
   133  		// Add additional information from sysfs.
   134  		if err := addHardwareInfo(&dev); err != nil {
   135  			logger.Errorf(
   136  				"error getting hardware info for %q from sysfs: %v",
   137  				dev.DeviceName, err,
   138  			)
   139  		}
   140  		devices = append(devices, dev)
   141  	}
   142  	if err := s.Err(); err != nil {
   143  		return nil, errors.Annotate(err, "cannot parse lsblk output")
   144  	}
   145  	return devices, nil
   146  }
   147  
   148  // blockDeviceInUse checks if the specified block device
   149  // is in use by attempting to open the device exclusively.
   150  //
   151  // If the error returned satisfies os.IsNotExists, then
   152  // the device will be ignored altogether.
   153  var blockDeviceInUse = func(dev storage.BlockDevice) (bool, error) {
   154  	f, err := os.OpenFile("/dev/"+dev.DeviceName, os.O_EXCL, 0)
   155  	if err == nil {
   156  		f.Close()
   157  		return false, nil
   158  	}
   159  	perr, ok := err.(*os.PathError)
   160  	if !ok {
   161  		return false, err
   162  	}
   163  	// open(2): "In general, the behavior of O_EXCL is undefined if
   164  	// it is used without O_CREAT. There is one exception: on Linux
   165  	// 2.6 and later, O_EXCL can be used without O_CREAT if pathname
   166  	// refers to a block device. If the block device is in use by the
   167  	// system  (e.g., mounted), open() fails with the error EBUSY."
   168  	if errno, _ := perr.Err.(syscall.Errno); errno == syscall.EBUSY {
   169  		return true, nil
   170  	}
   171  	return false, err
   172  }
   173  
   174  // addHardwareInfo adds additional information about the hardware, and how it is
   175  // attached to the machine, to the given BlockDevice.
   176  func addHardwareInfo(dev *storage.BlockDevice) error {
   177  	logger.Tracef(`executing "udevadm info" for %s`, dev.DeviceName)
   178  	output, err := exec.Command(
   179  		"udevadm", "info",
   180  		"-q", "property",
   181  		"--name", dev.DeviceName,
   182  	).CombinedOutput()
   183  	if err != nil {
   184  		msg := "udevadm failed"
   185  		if output := bytes.TrimSpace(output); len(output) > 0 {
   186  			msg += fmt.Sprintf(" (%s)", output)
   187  		}
   188  		return errors.Annotate(err, msg)
   189  	}
   190  
   191  	var devpath, idBus, idSerial, wwnWithExtension string
   192  
   193  	s := bufio.NewScanner(bytes.NewReader(output))
   194  	for s.Scan() {
   195  		line := s.Text()
   196  		sep := strings.IndexRune(line, '=')
   197  		if sep == -1 {
   198  			logger.Debugf("unexpected udevadm output line: %q", line)
   199  			continue
   200  		}
   201  		key, value := line[:sep], line[sep+1:]
   202  		switch key {
   203  		case "DEVPATH":
   204  			devpath = value
   205  		case "DEVLINKS":
   206  			dev.DeviceLinks = strings.Split(value, " ")
   207  		case "ID_BUS":
   208  			idBus = value
   209  		case "ID_SERIAL":
   210  			idSerial = value
   211  		case "ID_WWN":
   212  			dev.WWN = value
   213  		case "ID_WWN_WITH_EXTENSION":
   214  			wwnWithExtension = value
   215  		default:
   216  			logger.Tracef("ignoring line: %q", line)
   217  		}
   218  	}
   219  	if err := s.Err(); err != nil {
   220  		return errors.Annotate(err, "cannot parse udevadm output")
   221  	}
   222  
   223  	// For cases where there are logical disks attached to a
   224  	// controller (eg RAID), the controller itself has a WWN
   225  	// which is the ID_WWN value and each disk has a WWN with
   226  	// a vendor extension ID_WWN_WITH_EXTENSION added to
   227  	// identify the disk via the /dev/disk/by-id path.
   228  	if wwnWithExtension != "" {
   229  		dev.WWN = wwnWithExtension
   230  	}
   231  	if idBus != "" && idSerial != "" {
   232  		// ID_BUS will be something like "scsi" or "ata";
   233  		// ID_SERIAL will be something like ${MODEL}_${SERIALNO};
   234  		// and together they make up the symlink in /dev/disk/by-id.
   235  		dev.HardwareId = idBus + "-" + idSerial
   236  	}
   237  
   238  	// For devices on the SCSI bus, we include the address. This is to
   239  	// support storage providers where the SCSI address may be specified,
   240  	// but the device name can not (and may change, depending on timing).
   241  	if idBus == "scsi" && devpath != "" {
   242  		// DEVPATH will be "<uninteresting stuff>/<SCSI address>/block/<device>".
   243  		re := regexp.MustCompile(fmt.Sprintf(
   244  			`^.*/(\d+):(\d+):(\d+):(\d+)/block/%s$`,
   245  			regexp.QuoteMeta(dev.DeviceName),
   246  		))
   247  		submatch := re.FindStringSubmatch(devpath)
   248  		if submatch != nil {
   249  			// We use the address scheme used by lshw: bus@address. We don't use
   250  			// lshw because it does things we don't need, and that slows it down.
   251  			//
   252  			// In DEVPATH, the address format is "H:C:T:L" ([H]ost, [C]hannel,
   253  			// [T]arget, [L]un); the lshw address format is "H:C.T.L"
   254  			dev.BusAddress = fmt.Sprintf(
   255  				"scsi@%s:%s.%s.%s",
   256  				submatch[1], submatch[2], submatch[3], submatch[4],
   257  			)
   258  		} else {
   259  			logger.Debugf(
   260  				"non matching DEVPATH for %q: %q",
   261  				dev.DeviceName, devpath,
   262  			)
   263  		}
   264  	}
   265  
   266  	return nil
   267  }