1 #!/usr/bin/python 2 3 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 """This is a module to scan /sys/block/ virtual FS, query udev 8 9 It provides a list of all removable or USB devices connected to the machine on 10 which the module is running. 11 It can be used from command line or from a python script. 12 13 To use it as python module it's enough to call the get_all() function. 14 @see |get_all| documentation for the output format 15 |get_all()| output is human readable (as oppposite to python's data structures) 16 """ 17 18 import logging, os, re 19 20 # this script can be run at command line on DUT (ie /usr/local/autotest 21 # contains only the client/ subtree), on a normal autotest 22 # installation/repository or as a python module used on a client-side test. 23 import common 24 from autotest_lib.client.common_lib import utils 25 26 INFO_PATH = "/sys/block" 27 UDEV_CMD_FOR_SERIAL_NUMBER = "udevadm info -a -n %s | grep -iE 'ATTRS{" \ 28 "serial}' | head -n 1" 29 LSUSB_CMD = "lsusb -v | grep -iE '^Device Desc|bcdUSB|iSerial'" 30 DESC_PATTERN = r'Device Descriptor:' 31 BCDUSB_PATTERN = r'bcdUSB\s+(\d+\.\d+)' 32 ISERIAL_PATTERN = r'iSerial\s+\d\s(\S*)' 33 UDEV_SERIAL_PATTERN = r'=="(.*)"' 34 35 def get_udev_info(blockdev, method='udev'): 36 """Get information about |blockdev| 37 38 @param blockdev: a block device, e.g., /dev/sda1 or /dev/sda 39 @param method: either 'udev' (default) or 'blkid' 40 41 @return a dictionary with two or more of the followig keys: 42 "ID_BUS", "ID_MODEL": always present 43 "ID_FS_UUID", "ID_FS_TYPE", "ID_FS_LABEL": present only if those info 44 are meaningul and present for the queried device 45 """ 46 ret = {} 47 cmd = None 48 ignore_status = False 49 50 if method == "udev": 51 cmd = "udevadm info --name %s --query=property" % blockdev 52 elif method == "blkid": 53 # this script is run as root in a normal autotest run, 54 # so this works: It doesn't have access to the necessary info 55 # when run as a non-privileged user 56 cmd = "blkid -c /dev/null -o udev %s" % blockdev 57 ignore_status = True 58 59 if cmd: 60 output = utils.system_output(cmd, ignore_status=ignore_status) 61 62 udev_keys = ("ID_BUS", "ID_MODEL", "ID_FS_UUID", "ID_FS_TYPE", 63 "ID_FS_LABEL") 64 for line in output.splitlines(): 65 udev_key, udev_val = line.split('=') 66 67 if udev_key in udev_keys: 68 ret[udev_key] = udev_val 69 70 return ret 71 72 73 def get_usbdevice_type_and_serial(device): 74 """Get USB device type and Serial number 75 76 @param device: USB device mount point Example: /dev/sda or /dev/sdb 77 @return: Returns the information about USB type and the serial number 78 of the device 79 """ 80 usb_info_list = [] 81 # Getting the USB type and Serial number info using 'lsusb -v'. Sample 82 # output is shown in below 83 # Device Descriptor: 84 # bcdUSB 2.00 85 # iSerial 3 131BC7 86 # bcdUSB 2.00 87 # Device Descriptor: 88 # bcdUSB 2.10 89 # iSerial 3 001A4D5E8634B03169273995 90 91 lsusb_output = utils.system_output(LSUSB_CMD) 92 # we are parsing each line and getting the usb info 93 for line in lsusb_output.splitlines(): 94 desc_matched = re.search(DESC_PATTERN, line) 95 bcdusb_matched = re.search(BCDUSB_PATTERN, line) 96 iserial_matched = re.search(ISERIAL_PATTERN, line) 97 if desc_matched: 98 usb_info = {} 99 elif bcdusb_matched: 100 # bcdUSB may appear multiple time. Drop the remaining. 101 usb_info['bcdUSB'] = bcdusb_matched.group(1) 102 elif iserial_matched: 103 usb_info['iSerial'] = iserial_matched.group(1) 104 usb_info_list.append(usb_info) 105 logging.debug('lsusb output is %s', usb_info_list) 106 # Comparing the lsusb serial number with udev output serial number 107 # Both serial numbers should be same. Sample udev command output is 108 # shown in below. 109 # ATTRS{serial}=="001A4D5E8634B03169273995" 110 udev_serial_output = utils.system_output(UDEV_CMD_FOR_SERIAL_NUMBER % 111 device) 112 udev_serial_matched = re.search(UDEV_SERIAL_PATTERN, udev_serial_output) 113 if udev_serial_matched: 114 udev_serial = udev_serial_matched.group(1) 115 logging.debug("udev serial number is %s", udev_serial) 116 for usb_details in usb_info_list: 117 if usb_details['iSerial'] == udev_serial: 118 return usb_details.get('bcdUSB'), udev_serial 119 return None, None 120 121 def get_partition_info(part_path, bus, model, partid=None, fstype=None, 122 label=None, block_size=0, is_removable=False): 123 """Return information about a device as a list of dictionaries 124 125 Normally a single device described by the passed parameters will match a 126 single device on the system, and thus a single element list as return 127 value; although it's possible that a single block device is associated with 128 several mountpoints, this scenario will lead to a dictionary for each 129 mountpoint. 130 131 @param part_path: full partition path under |INFO_PATH| 132 e.g., /sys/block/sda or /sys/block/sda/sda1 133 @param bus: bus, e.g., 'usb' or 'ata', according to udev 134 @param model: device moduel, e.g., according to udev 135 @param partid: partition id, if present 136 @param fstype: filesystem type, if present 137 @param label: filesystem label, if present 138 @param block_size: filesystem block size 139 @param is_removable: whether it is a removable device 140 141 @return a list of dictionaries contaning each a partition info. 142 An empty list can be returned if no matching device is found 143 """ 144 ret = [] 145 # take the partitioned device name from the /sys/block/ path name 146 part = part_path.split('/')[-1] 147 device = "/dev/%s" % part 148 149 if not partid: 150 info = get_udev_info(device, "blkid") 151 partid = info.get('ID_FS_UUID', None) 152 if not fstype: 153 fstype = info.get('ID_FS_TYPE', None) 154 if not label: 155 label = partid 156 157 readonly = open("%s/ro" % part_path).read() 158 if not int(readonly): 159 partition_blocks = open("%s/size" % part_path).read() 160 size = block_size * int(partition_blocks) 161 162 stub = {} 163 stub['device'] = device 164 stub['bus'] = bus 165 stub['model'] = model 166 stub['size'] = size 167 168 # look for it among the mounted devices first 169 mounts = open("/proc/mounts").readlines() 170 seen = False 171 for line in mounts: 172 dev, mount, proc_fstype, flags = line.split(' ', 3) 173 174 if device == dev: 175 if 'rw' in flags.split(','): 176 seen = True # at least one match occurred 177 178 # Sorround mountpoint with quotes, to make it parsable in 179 # case of spaces. Also information retrieved from 180 # /proc/mount override the udev passed ones (e.g., 181 # proc_fstype instead of fstype) 182 dev = stub.copy() 183 dev['fs_uuid'] = partid 184 dev['fstype'] = proc_fstype 185 dev['is_mounted'] = True 186 dev['mountpoint'] = mount 187 dev['usb_type'], dev['serial'] = \ 188 get_usbdevice_type_and_serial(dev['device']) 189 ret.append(dev) 190 191 # If not among mounted devices, it's just attached, print about the 192 # same information but suggest a place where the user can mount the 193 # device instead 194 if not seen: 195 # we consider it if it's removable and and a partition id 196 # OR it's on the USB bus or ATA bus. 197 # Some USB HD do not get announced as removable, but they should be 198 # showed. 199 # There are good changes that if it's on a USB bus it's removable 200 # and thus interesting for us, independently whether it's declared 201 # removable 202 if (is_removable and partid) or bus in ['usb', 'ata']: 203 if not label: 204 info = get_udev_info(device, 'blkid') 205 label = info.get('ID_FS_LABEL', partid) 206 207 dev = stub.copy() 208 dev['fs_uuid'] = partid 209 dev['fstype'] = fstype 210 dev['is_mounted'] = False 211 dev['mountpoint'] = "/media/removable/%s" % label 212 dev['usb_type'], dev['serial'] = \ 213 get_usbdevice_type_and_serial(dev['device']) 214 ret.append(dev) 215 return ret 216 217 218 def get_device_info(blockdev): 219 """Retrieve information about |blockdev| 220 221 @see |get_partition_info()| doc for the dictionary format 222 223 @param blockdev: a block device name, e.g., "sda". 224 225 @return a list of dictionary, with each item representing a found device 226 """ 227 ret = [] 228 229 spath = "%s/%s" % (INFO_PATH, blockdev) 230 block_size = int(open("%s/queue/physical_block_size" % spath).read()) 231 is_removable = bool(int(open("%s/removable" % spath).read())) 232 233 info = get_udev_info(blockdev, "udev") 234 dev_bus = info['ID_BUS'] 235 dev_model = info['ID_MODEL'] 236 dev_fs = info.get('ID_FS_TYPE', None) 237 dev_uuid = info.get('ID_FS_UUID', None) 238 dev_label = info.get('ID_FS_LABEL', dev_uuid) 239 240 has_partitions = False 241 for basename in os.listdir(spath): 242 partition_path = "%s/%s" % (spath, basename) 243 # we want to check if within |spath| there are subdevices with 244 # partitions 245 # e.g., if within /sys/block/sda sda1 and other partition are present 246 if not re.match("%s[0-9]+" % blockdev, basename): 247 continue # ignore what is not a subdevice 248 249 # |blockdev| has subdevices: get info for them 250 has_partitions = True 251 devs = get_partition_info(partition_path, dev_bus, dev_model, 252 block_size=block_size, 253 is_removable=is_removable) 254 ret.extend(devs) 255 256 if not has_partitions: 257 devs = get_partition_info(spath, dev_bus, dev_model, dev_uuid, dev_fs, 258 dev_label, block_size=block_size, 259 is_removable=is_removable) 260 ret.extend(devs) 261 262 return ret 263 264 265 def get_all(): 266 """Return all removable or USB storage devices attached 267 268 @return a list of dictionaries, each list element describing a device 269 """ 270 ret = [] 271 for dev in os.listdir(INFO_PATH): 272 # Among block devices we need to filter out what are virtual 273 if re.match("s[a-z]+", dev): 274 # for each of them try to obtain some info 275 ret.extend(get_device_info(dev)) 276 return ret 277 278 279 def main(): 280 for device in get_all(): 281 print ("%(device)s %(bus)s %(model)s %(size)d %(fs_uuid)s %(fstype)s " 282 "%(is_mounted)d %(mountpoint)s %(usb_type)s %(serial)s" % 283 device) 284 285 286 if __name__ == "__main__": 287 main() 288