• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
9It provides a list of all removable or USB devices connected to the machine on
10which the module is running.
11It can be used from command line or from a python script.
12
13To 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
18import 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.
23import common
24from autotest_lib.client.common_lib import utils
25
26INFO_PATH = "/sys/block"
27UDEV_CMD_FOR_SERIAL_NUMBER = "udevadm info -a -n %s | grep -iE 'ATTRS{" \
28                             "serial}' | head -n 1"
29LSUSB_CMD = "lsusb -v | grep -iE '^Device Desc|bcdUSB|iSerial'"
30DESC_PATTERN = r'Device Descriptor:'
31BCDUSB_PATTERN = r'bcdUSB\s+(\d+\.\d+)'
32ISERIAL_PATTERN = r'iSerial\s+\d\s(\S*)'
33UDEV_SERIAL_PATTERN = r'=="(.*)"'
34
35def 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
73def 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
121def 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
218def 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
265def 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
279def 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
286if __name__ == "__main__":
287    main()
288