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