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 }