github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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 blockDeviceMap := make(map[string]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.Errorf( 115 "error checking if %q is in use: %v", dev.DeviceName, err, 116 ) 117 // We cannot detect, so err on the side of caution and default to 118 // "in use" so the device cannot be used. 119 dev.InUse = true 120 } 121 122 // Add additional information from sysfs. 123 if err := addHardwareInfo(&dev); err != nil { 124 logger.Errorf( 125 "error getting hardware info for %q from sysfs: %v", 126 dev.DeviceName, err, 127 ) 128 } 129 blockDeviceMap[dev.DeviceName] = dev 130 } 131 if err := s.Err(); err != nil { 132 return nil, errors.Annotate(err, "cannot parse lsblk output") 133 } 134 135 blockDevices := make([]storage.BlockDevice, 0, len(blockDeviceMap)) 136 for _, dev := range blockDeviceMap { 137 blockDevices = append(blockDevices, dev) 138 } 139 return blockDevices, nil 140 } 141 142 // blockDeviceInUse checks if the specified block device 143 // is in use by attempting to open the device exclusively. 144 // 145 // If the error returned satisfies os.IsNotExists, then 146 // the device will be ignored altogether. 147 var blockDeviceInUse = func(dev storage.BlockDevice) (bool, error) { 148 f, err := os.OpenFile("/dev/"+dev.DeviceName, os.O_EXCL, 0) 149 if err == nil { 150 f.Close() 151 return false, nil 152 } 153 perr, ok := err.(*os.PathError) 154 if !ok { 155 return false, err 156 } 157 // open(2): "In general, the behavior of O_EXCL is undefined if 158 // it is used without O_CREAT. There is one exception: on Linux 159 // 2.6 and later, O_EXCL can be used without O_CREAT if pathname 160 // refers to a block device. If the block device is in use by the 161 // system (e.g., mounted), open() fails with the error EBUSY." 162 if errno, _ := perr.Err.(syscall.Errno); errno == syscall.EBUSY { 163 return true, nil 164 } 165 return false, err 166 } 167 168 // addHardwareInfo adds additional information about the hardware, and how it is 169 // attached to the machine, to the given BlockDevice. 170 func addHardwareInfo(dev *storage.BlockDevice) error { 171 logger.Tracef(`executing "udevadm info" for %s`, dev.DeviceName) 172 output, err := exec.Command( 173 "udevadm", "info", 174 "-q", "property", 175 "--path", "/block/"+dev.DeviceName, 176 ).Output() 177 if err != nil { 178 msg := "udevadm failed" 179 if output := bytes.TrimSpace(output); len(output) > 0 { 180 msg += fmt.Sprintf(" (%s)", output) 181 } 182 return errors.Annotate(err, msg) 183 } 184 185 var devpath, idBus, idSerial string 186 187 s := bufio.NewScanner(bytes.NewReader(output)) 188 for s.Scan() { 189 line := s.Text() 190 sep := strings.IndexRune(line, '=') 191 if sep == -1 { 192 logger.Debugf("unexpected udevadm output line: %q", line) 193 continue 194 } 195 key, value := line[:sep], line[sep+1:] 196 switch key { 197 case "DEVPATH": 198 devpath = value 199 case "ID_BUS": 200 idBus = value 201 case "ID_SERIAL": 202 idSerial = value 203 default: 204 logger.Tracef("ignoring line: %q", line) 205 } 206 } 207 if err := s.Err(); err != nil { 208 return errors.Annotate(err, "cannot parse udevadm output") 209 } 210 211 if idBus != "" && idSerial != "" { 212 // ID_BUS will be something like "scsi" or "ata"; 213 // ID_SERIAL will be soemthing like ${MODEL}_${SERIALNO}; 214 // and together they make up the symlink in /dev/disk/by-id. 215 dev.HardwareId = idBus + "-" + idSerial 216 } 217 218 // For devices on the SCSI bus, we include the address. This is to 219 // support storage providers where the SCSI address may be specified, 220 // but the device name can not (and may change, depending on timing). 221 if idBus == "scsi" && devpath != "" { 222 // DEVPATH will be "<uninteresting stuff>/<SCSI address>/block/<device>". 223 re := regexp.MustCompile(fmt.Sprintf( 224 `^.*/(\d+):(\d+):(\d+):(\d+)/block/%s$`, 225 regexp.QuoteMeta(dev.DeviceName), 226 )) 227 submatch := re.FindStringSubmatch(devpath) 228 if submatch != nil { 229 // We use the address scheme used by lshw: bus@address. We don't use 230 // lshw because it does things we don't need, and that slows it down. 231 // 232 // In DEVPATH, the address format is "H:C:T:L" ([H]ost, [C]hannel, 233 // [T]arget, [L]un); the lshw address format is "H:C.T.L" 234 dev.BusAddress = fmt.Sprintf( 235 "scsi@%s:%s.%s.%s", 236 submatch[1], submatch[2], submatch[3], submatch[4], 237 ) 238 } else { 239 logger.Debugf( 240 "non matching DEVPATH for %q: %q", 241 dev.DeviceName, devpath, 242 ) 243 } 244 } 245 246 return nil 247 }