Home | History | Annotate | Download | only in cros
      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