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