xref: /aosp_15_r20/external/autotest/client/cros/storage.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Li"""Storage device utilities to be used in storage device based tests
7*9c5db199SXin Li"""
8*9c5db199SXin Li
9*9c5db199SXin Liimport logging, re, os, time, hashlib
10*9c5db199SXin Li
11*9c5db199SXin Lifrom autotest_lib.client.bin import test, utils
12*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
13*9c5db199SXin Lifrom autotest_lib.client.cros import liststorage
14*9c5db199SXin Li
15*9c5db199SXin Li
16*9c5db199SXin Liclass StorageException(error.TestError):
17*9c5db199SXin Li    """Indicates that a storage/volume operation failed.
18*9c5db199SXin Li    It is fatal to the test unless caught.
19*9c5db199SXin Li    """
20*9c5db199SXin Li    pass
21*9c5db199SXin Li
22*9c5db199SXin Li
23*9c5db199SXin Liclass StorageScanner(object):
24*9c5db199SXin Li    """Scan device for storage points.
25*9c5db199SXin Li
26*9c5db199SXin Li    It also performs basic operations on found storage devices as mount/umount,
27*9c5db199SXin Li    creating file with randomized content or checksum file content.
28*9c5db199SXin Li
29*9c5db199SXin Li    Each storage device is defined by a dictionary containing the following
30*9c5db199SXin Li    keys:
31*9c5db199SXin Li
32*9c5db199SXin Li    device: the device path (e.g. /dev/sdb1)
33*9c5db199SXin Li    bus: the bus name (e.g. usb, ata, etc)
34*9c5db199SXin Li    model: the kind of device (e.g. Multi-Card, USB_DISK_2.0, SanDisk)
35*9c5db199SXin Li    size: the size of the volume/partition ib bytes (int)
36*9c5db199SXin Li    fs_uuid: the UUID for the filesystem (str)
37*9c5db199SXin Li    fstype: filesystem type
38*9c5db199SXin Li    is_mounted: wether the FS is mounted (0=False,1=True)
39*9c5db199SXin Li    mountpoint: where the FS is mounted (if mounted=1) or a suggestion where to
40*9c5db199SXin Li                mount it (if mounted=0)
41*9c5db199SXin Li
42*9c5db199SXin Li    Also |filter()| and |scan()| will use the same dictionary keys associated
43*9c5db199SXin Li    with regular expression in order to filter a result set.
44*9c5db199SXin Li    Multiple keys act in an AND-fashion way. The absence of a key in the filter
45*9c5db199SXin Li    make the filter matching all the values for said key in the storage
46*9c5db199SXin Li    dictionary.
47*9c5db199SXin Li
48*9c5db199SXin Li    Example: {'device':'/dev/sd[ab]1', 'is_mounted':'0'} will match all the
49*9c5db199SXin Li    found devices which block device file is either /dev/sda1 or /dev/sdb1, AND
50*9c5db199SXin Li    are not mounted, excluding all other devices from the matched result.
51*9c5db199SXin Li    """
52*9c5db199SXin Li    storages = None
53*9c5db199SXin Li
54*9c5db199SXin Li
55*9c5db199SXin Li    def __init__(self):
56*9c5db199SXin Li        self.__mounted = {}
57*9c5db199SXin Li
58*9c5db199SXin Li
59*9c5db199SXin Li    def filter(self, storage_filter={}):
60*9c5db199SXin Li        """Filters a stored result returning a list of matching devices.
61*9c5db199SXin Li
62*9c5db199SXin Li        The passed dictionary represent the filter and its values are regular
63*9c5db199SXin Li        expressions (str). If an element of self.storage matches the regex
64*9c5db199SXin Li        defined in all the keys for a filter, the item will be part of the
65*9c5db199SXin Li        returning value.
66*9c5db199SXin Li
67*9c5db199SXin Li        Calling this method does not change self.storages, thus can be called
68*9c5db199SXin Li        several times against the same result set.
69*9c5db199SXin Li
70*9c5db199SXin Li        @param storage_filter: a dictionary representing the filter.
71*9c5db199SXin Li
72*9c5db199SXin Li        @return a list of dictionaries representing the found devices after the
73*9c5db199SXin Li                application of the filter. The list can be empty if no device
74*9c5db199SXin Li                has been found.
75*9c5db199SXin Li        """
76*9c5db199SXin Li        ret = []
77*9c5db199SXin Li
78*9c5db199SXin Li        for storage in self.storages:
79*9c5db199SXin Li            matches = True
80*9c5db199SXin Li            for key in storage_filter:
81*9c5db199SXin Li                if not re.match(storage_filter[key], storage[key]):
82*9c5db199SXin Li                    matches = False
83*9c5db199SXin Li                    break
84*9c5db199SXin Li            if matches:
85*9c5db199SXin Li                ret.append(storage.copy())
86*9c5db199SXin Li
87*9c5db199SXin Li        return ret
88*9c5db199SXin Li
89*9c5db199SXin Li
90*9c5db199SXin Li    def scan(self, storage_filter={}):
91*9c5db199SXin Li        """Scan the current storage devices.
92*9c5db199SXin Li
93*9c5db199SXin Li        If no parameter is given, it will return all the storage devices found.
94*9c5db199SXin Li        Otherwise it will internally call self.filter() with the passed
95*9c5db199SXin Li        filter.
96*9c5db199SXin Li        The result (being it filtered or not) will be saved in self.storages.
97*9c5db199SXin Li
98*9c5db199SXin Li        Such list can be (re)-filtered using self.filter().
99*9c5db199SXin Li
100*9c5db199SXin Li        @param storage_filter: a dict representing the filter, default is
101*9c5db199SXin Li                matching anything.
102*9c5db199SXin Li
103*9c5db199SXin Li        @return a list of found dictionaries representing the found devices.
104*9c5db199SXin Li                 The list can be empty if no device has been found.
105*9c5db199SXin Li        """
106*9c5db199SXin Li        self.storages = liststorage.get_all()
107*9c5db199SXin Li
108*9c5db199SXin Li        if storage_filter:
109*9c5db199SXin Li            self.storages = self.filter(storage_filter)
110*9c5db199SXin Li
111*9c5db199SXin Li        return self.storages
112*9c5db199SXin Li
113*9c5db199SXin Li
114*9c5db199SXin Li    def mount_volume(self, index=None, storage_dict=None, args=''):
115*9c5db199SXin Li        """Mount the passed volume.
116*9c5db199SXin Li
117*9c5db199SXin Li        Either index or storage_dict can be set, but not both at the same time.
118*9c5db199SXin Li        If neither is passed, it will mount the first volume found in
119*9c5db199SXin Li        self.storage.
120*9c5db199SXin Li
121*9c5db199SXin Li        @param index: (int) the index in self.storages for the storage
122*9c5db199SXin Li                device/volume to be mounted.
123*9c5db199SXin Li        @param storage_dict: (dict) the storage dictionary representing the
124*9c5db199SXin Li                storage device, the dictionary should be obtained from
125*9c5db199SXin Li                self.storage or using self.scan() or self.filter().
126*9c5db199SXin Li        @param args: (str) args to be passed to the mount command, if needed.
127*9c5db199SXin Li                     e.g., "-o foo,bar -t ext3".
128*9c5db199SXin Li        """
129*9c5db199SXin Li        if index is None and storage_dict is None:
130*9c5db199SXin Li            storage_dict = self.storages[0]
131*9c5db199SXin Li        elif isinstance(index, int):
132*9c5db199SXin Li            storage_dict = self.storages[index]
133*9c5db199SXin Li        elif not isinstance(storage_dict, dict):
134*9c5db199SXin Li            raise TypeError('Either index or storage_dict passed '
135*9c5db199SXin Li                            'with the wrong type')
136*9c5db199SXin Li
137*9c5db199SXin Li        if storage_dict['is_mounted']:
138*9c5db199SXin Li            logging.debug('Volume "%s" is already mounted, skipping '
139*9c5db199SXin Li                          'mount_volume().')
140*9c5db199SXin Li            return
141*9c5db199SXin Li
142*9c5db199SXin Li        logging.info('Mounting %(device)s in %(mountpoint)s.', storage_dict)
143*9c5db199SXin Li
144*9c5db199SXin Li        try:
145*9c5db199SXin Li            # Create the dir in case it does not exist.
146*9c5db199SXin Li            os.mkdir(storage_dict['mountpoint'])
147*9c5db199SXin Li        except OSError as e:
148*9c5db199SXin Li            # If it's not "file exists", report the exception.
149*9c5db199SXin Li            if e.errno != 17:
150*9c5db199SXin Li                raise e
151*9c5db199SXin Li        cmd = 'mount %s' % args
152*9c5db199SXin Li        cmd += ' %(device)s %(mountpoint)s' % storage_dict
153*9c5db199SXin Li        utils.system(cmd)
154*9c5db199SXin Li        storage_dict['is_mounted'] = True
155*9c5db199SXin Li        self.__mounted[storage_dict['mountpoint']] = storage_dict
156*9c5db199SXin Li
157*9c5db199SXin Li
158*9c5db199SXin Li    def umount_volume(self, index=None, storage_dict=None, args=''):
159*9c5db199SXin Li        """Un-mount the passed volume, by index or storage dictionary.
160*9c5db199SXin Li
161*9c5db199SXin Li        Either index or storage_dict can be set, but not both at the same time.
162*9c5db199SXin Li        If neither is passed, it will mount the first volume found in
163*9c5db199SXin Li        self.storage.
164*9c5db199SXin Li
165*9c5db199SXin Li        @param index: (int) the index in self.storages for the storage
166*9c5db199SXin Li                device/volume to be mounted.
167*9c5db199SXin Li        @param storage_dict: (dict) the storage dictionary representing the
168*9c5db199SXin Li                storage device, the dictionary should be obtained from
169*9c5db199SXin Li                self.storage or using self.scan() or self.filter().
170*9c5db199SXin Li        @param args: (str) args to be passed to the umount command, if needed.
171*9c5db199SXin Li                     e.g., '-f -t' for force+lazy umount.
172*9c5db199SXin Li        """
173*9c5db199SXin Li        if index is None and storage_dict is None:
174*9c5db199SXin Li            storage_dict = self.storages[0]
175*9c5db199SXin Li        elif isinstance(index, int):
176*9c5db199SXin Li            storage_dict = self.storages[index]
177*9c5db199SXin Li        elif not isinstance(storage_dict, dict):
178*9c5db199SXin Li            raise TypeError('Either index or storage_dict passed '
179*9c5db199SXin Li                            'with the wrong type')
180*9c5db199SXin Li
181*9c5db199SXin Li
182*9c5db199SXin Li        if not storage_dict['is_mounted']:
183*9c5db199SXin Li            logging.debug('Volume "%s" is already unmounted: skipping '
184*9c5db199SXin Li                          'umount_volume().')
185*9c5db199SXin Li            return
186*9c5db199SXin Li
187*9c5db199SXin Li        logging.info('Unmounting %(device)s from %(mountpoint)s.',
188*9c5db199SXin Li                     storage_dict)
189*9c5db199SXin Li        cmd = 'umount %s' % args
190*9c5db199SXin Li        cmd += ' %(device)s' % storage_dict
191*9c5db199SXin Li        utils.system(cmd)
192*9c5db199SXin Li        # We don't care if it fails, it might be busy for a /proc/mounts issue.
193*9c5db199SXin Li        # See BUG=chromium-os:32105
194*9c5db199SXin Li        try:
195*9c5db199SXin Li            os.rmdir(storage_dict['mountpoint'])
196*9c5db199SXin Li        except OSError as e:
197*9c5db199SXin Li            logging.debug('Removing %s failed: %s: ignoring.',
198*9c5db199SXin Li                          storage_dict['mountpoint'], e)
199*9c5db199SXin Li        storage_dict['is_mounted'] = False
200*9c5db199SXin Li        # If we previously mounted it, remove it from our internal list.
201*9c5db199SXin Li        if storage_dict['mountpoint'] in self.__mounted:
202*9c5db199SXin Li            del self.__mounted[storage_dict['mountpoint']]
203*9c5db199SXin Li
204*9c5db199SXin Li
205*9c5db199SXin Li    def unmount_all(self):
206*9c5db199SXin Li        """Unmount all volumes mounted by self.mount_volume().
207*9c5db199SXin Li        """
208*9c5db199SXin Li        # We need to copy it since we are iterating over a dict which will
209*9c5db199SXin Li        # change size.
210*9c5db199SXin Li        for volume in self.__mounted.copy():
211*9c5db199SXin Li            self.umount_volume(storage_dict=self.__mounted[volume])
212*9c5db199SXin Li
213*9c5db199SXin Li
214*9c5db199SXin Liclass StorageTester(test.test):
215*9c5db199SXin Li    """This is a class all tests about Storage can use.
216*9c5db199SXin Li
217*9c5db199SXin Li    It has methods to
218*9c5db199SXin Li    - create random files
219*9c5db199SXin Li    - compute a file's md5 checksum
220*9c5db199SXin Li    - look/wait for a specific device (specified using StorageScanner
221*9c5db199SXin Li      dictionary format)
222*9c5db199SXin Li
223*9c5db199SXin Li    Subclasses can override the _prepare_volume() method in order to disable
224*9c5db199SXin Li    them or change their behaviours.
225*9c5db199SXin Li
226*9c5db199SXin Li    Subclasses should take care of unmount all the mounted filesystems when
227*9c5db199SXin Li    needed (e.g. on cleanup phase), calling self.umount_volume() or
228*9c5db199SXin Li    self.unmount_all().
229*9c5db199SXin Li    """
230*9c5db199SXin Li    scanner = None
231*9c5db199SXin Li
232*9c5db199SXin Li
233*9c5db199SXin Li    def initialize(self, filter_dict={'bus':'usb'}, filesystem='ext2'):
234*9c5db199SXin Li        """Initialize the test.
235*9c5db199SXin Li
236*9c5db199SXin Li        Instantiate a StorageScanner instance to be used by tests and prepare
237*9c5db199SXin Li        any volume matched by |filter_dict|.
238*9c5db199SXin Li        Volume preparation is done by the _prepare_volume() method, which can be
239*9c5db199SXin Li        overriden by subclasses.
240*9c5db199SXin Li
241*9c5db199SXin Li        @param filter_dict: a dictionary to filter attached USB devices to be
242*9c5db199SXin Li                            initialized.
243*9c5db199SXin Li        @param filesystem: the filesystem name to format the attached device.
244*9c5db199SXin Li        """
245*9c5db199SXin Li        super(StorageTester, self).initialize()
246*9c5db199SXin Li
247*9c5db199SXin Li        self.scanner = StorageScanner()
248*9c5db199SXin Li
249*9c5db199SXin Li        self._prepare_volume(filter_dict, filesystem=filesystem)
250*9c5db199SXin Li
251*9c5db199SXin Li        # Be sure that if any operation above uses self.scanner related
252*9c5db199SXin Li        # methods, its result is cleaned after use.
253*9c5db199SXin Li        self.storages = None
254*9c5db199SXin Li
255*9c5db199SXin Li
256*9c5db199SXin Li    def _prepare_volume(self, filter_dict, filesystem='ext2'):
257*9c5db199SXin Li        """Prepare matching volumes for test.
258*9c5db199SXin Li
259*9c5db199SXin Li        Prepare all the volumes matching |filter_dict| for test by formatting
260*9c5db199SXin Li        the matching storages with |filesystem|.
261*9c5db199SXin Li
262*9c5db199SXin Li        This method is called by StorageTester.initialize(), a subclass can
263*9c5db199SXin Li        override this method to change its behaviour.
264*9c5db199SXin Li        Setting it to None (or a not callable) will disable it.
265*9c5db199SXin Li
266*9c5db199SXin Li        @param filter_dict: a filter for the storages to be prepared.
267*9c5db199SXin Li        @param filesystem: filesystem with which volumes will be formatted.
268*9c5db199SXin Li        """
269*9c5db199SXin Li        if not os.path.isfile('/sbin/mkfs.%s' % filesystem):
270*9c5db199SXin Li            raise error.TestError('filesystem not supported by mkfs installed '
271*9c5db199SXin Li                                  'on this device')
272*9c5db199SXin Li
273*9c5db199SXin Li        try:
274*9c5db199SXin Li            storages = self.wait_for_devices(filter_dict, cycles=1,
275*9c5db199SXin Li                                             mount_volume=False)[0]
276*9c5db199SXin Li
277*9c5db199SXin Li            for storage in storages:
278*9c5db199SXin Li                logging.debug('Preparing volume on %s.', storage['device'])
279*9c5db199SXin Li                cmd = 'mkfs.%s %s' % (filesystem, storage['device'])
280*9c5db199SXin Li                utils.system(cmd)
281*9c5db199SXin Li        except StorageException as e:
282*9c5db199SXin Li            logging.warning("%s._prepare_volume() didn't find any device "
283*9c5db199SXin Li                            "attached: skipping volume preparation: %s",
284*9c5db199SXin Li                            self.__class__.__name__, e)
285*9c5db199SXin Li        except error.CmdError as e:
286*9c5db199SXin Li            logging.warning("%s._prepare_volume() couldn't format volume: %s",
287*9c5db199SXin Li                            self.__class__.__name__, e)
288*9c5db199SXin Li
289*9c5db199SXin Li        logging.debug('Volume preparation finished.')
290*9c5db199SXin Li
291*9c5db199SXin Li
292*9c5db199SXin Li    def wait_for_devices(self, storage_filter, time_to_sleep=1, cycles=10,
293*9c5db199SXin Li                         mount_volume=True):
294*9c5db199SXin Li        """Cycles |cycles| times waiting |time_to_sleep| seconds each cycle,
295*9c5db199SXin Li        looking for a device matching |storage_filter|
296*9c5db199SXin Li
297*9c5db199SXin Li        @param storage_filter: a dictionary holding a set of  storage device's
298*9c5db199SXin Li                keys which are used as filter, to look for devices.
299*9c5db199SXin Li                @see StorageDevice class documentation.
300*9c5db199SXin Li        @param time_to_sleep: time (int) to wait after each |cycles|.
301*9c5db199SXin Li        @param cycles: number of tentatives. Use -1 for infinite.
302*9c5db199SXin Li
303*9c5db199SXin Li        @raises StorageException if no device can be found.
304*9c5db199SXin Li
305*9c5db199SXin Li        @return (storage_dict, waited_time) tuple. storage_dict is the found
306*9c5db199SXin Li                 device list and waited_time is the time spent waiting for the
307*9c5db199SXin Li                 device to be found.
308*9c5db199SXin Li        """
309*9c5db199SXin Li        msg = ('Scanning for %s for %d times, waiting each time '
310*9c5db199SXin Li               '%d secs' % (storage_filter, cycles, time_to_sleep))
311*9c5db199SXin Li        if mount_volume:
312*9c5db199SXin Li            logging.debug('%s and mounting each matched volume.', msg)
313*9c5db199SXin Li        else:
314*9c5db199SXin Li            logging.debug('%s, but not mounting each matched volume.', msg)
315*9c5db199SXin Li
316*9c5db199SXin Li        if cycles == -1:
317*9c5db199SXin Li            logging.info('Waiting until device is inserted, '
318*9c5db199SXin Li                         'no timeout has been set.')
319*9c5db199SXin Li
320*9c5db199SXin Li        cycle = 0
321*9c5db199SXin Li        while cycles == -1 or cycle < cycles:
322*9c5db199SXin Li            ret = self.scanner.scan(storage_filter)
323*9c5db199SXin Li            if ret:
324*9c5db199SXin Li                logging.debug('Found %s (mount_volume=%d).', ret, mount_volume)
325*9c5db199SXin Li                if mount_volume:
326*9c5db199SXin Li                    for storage in ret:
327*9c5db199SXin Li                        self.scanner.mount_volume(storage_dict=storage)
328*9c5db199SXin Li
329*9c5db199SXin Li                return (ret, cycle*time_to_sleep)
330*9c5db199SXin Li            else:
331*9c5db199SXin Li                logging.debug('Storage %s not found, wait and rescan '
332*9c5db199SXin Li                              '(cycle %d).', storage_filter, cycle)
333*9c5db199SXin Li                # Wait a bit and rescan storage list.
334*9c5db199SXin Li                time.sleep(time_to_sleep)
335*9c5db199SXin Li                cycle += 1
336*9c5db199SXin Li
337*9c5db199SXin Li        # Device still not found.
338*9c5db199SXin Li        msg = ('Could not find anything matching "%s" after %d seconds' %
339*9c5db199SXin Li                (storage_filter, time_to_sleep*cycles))
340*9c5db199SXin Li        raise StorageException(msg)
341*9c5db199SXin Li
342*9c5db199SXin Li
343*9c5db199SXin Li    def wait_for_device(self, storage_filter, time_to_sleep=1, cycles=10,
344*9c5db199SXin Li                        mount_volume=True):
345*9c5db199SXin Li        """Cycles |cycles| times waiting |time_to_sleep| seconds each cycle,
346*9c5db199SXin Li        looking for a device matching |storage_filter|.
347*9c5db199SXin Li
348*9c5db199SXin Li        This method needs to match one and only one device.
349*9c5db199SXin Li        @raises StorageException if no device can be found or more than one is
350*9c5db199SXin Li                 found.
351*9c5db199SXin Li
352*9c5db199SXin Li        @param storage_filter: a dictionary holding a set of  storage device's
353*9c5db199SXin Li                keys which are used as filter, to look for devices
354*9c5db199SXin Li                The filter has to be match a single device, a multiple matching
355*9c5db199SXin Li                filter will lead to StorageException to e risen. Use
356*9c5db199SXin Li                self.wait_for_devices() if more than one device is allowed to
357*9c5db199SXin Li                be found.
358*9c5db199SXin Li                @see StorageDevice class documentation.
359*9c5db199SXin Li        @param time_to_sleep: time (int) to wait after each |cycles|.
360*9c5db199SXin Li        @param cycles: number of tentatives. Use -1 for infinite.
361*9c5db199SXin Li
362*9c5db199SXin Li        @return (storage_dict, waited_time) tuple. storage_dict is the found
363*9c5db199SXin Li                 device list and waited_time is the time spent waiting for the
364*9c5db199SXin Li                 device to be found.
365*9c5db199SXin Li        """
366*9c5db199SXin Li        storages, waited_time = self.wait_for_devices(storage_filter,
367*9c5db199SXin Li            time_to_sleep=time_to_sleep,
368*9c5db199SXin Li            cycles=cycles,
369*9c5db199SXin Li            mount_volume=mount_volume)
370*9c5db199SXin Li        if len(storages) > 1:
371*9c5db199SXin Li            msg = ('filter matched more than one storage volume, use '
372*9c5db199SXin Li                '%s.wait_for_devices() if you need more than one match' %
373*9c5db199SXin Li                self.__class__)
374*9c5db199SXin Li            raise StorageException(msg)
375*9c5db199SXin Li
376*9c5db199SXin Li        # Return the first element if only this one has been matched.
377*9c5db199SXin Li        return (storages[0], waited_time)
378*9c5db199SXin Li
379*9c5db199SXin Li
380*9c5db199SXin Li# Some helpers not present in utils.py to abstract normal file operations.
381*9c5db199SXin Li
382*9c5db199SXin Lidef create_file(path, size):
383*9c5db199SXin Li    """Create a file using /dev/urandom.
384*9c5db199SXin Li
385*9c5db199SXin Li    @param path: the path of the file.
386*9c5db199SXin Li    @param size: the file size in bytes.
387*9c5db199SXin Li    """
388*9c5db199SXin Li    logging.debug('Creating %s (size %d) from /dev/urandom.', path, size)
389*9c5db199SXin Li    with open('/dev/urandom', 'rb') as urandom:
390*9c5db199SXin Li        utils.open_write_close(path, urandom.read(size))
391*9c5db199SXin Li
392*9c5db199SXin Li
393*9c5db199SXin Lidef checksum_file(path):
394*9c5db199SXin Li    """Compute the MD5 Checksum for a file.
395*9c5db199SXin Li
396*9c5db199SXin Li    @param path: the path of the file.
397*9c5db199SXin Li
398*9c5db199SXin Li    @return a string with the checksum.
399*9c5db199SXin Li    """
400*9c5db199SXin Li    chunk_size = 1024
401*9c5db199SXin Li
402*9c5db199SXin Li    m = hashlib.md5()
403*9c5db199SXin Li    with open(path, 'rb') as f:
404*9c5db199SXin Li        for chunk in f.read(chunk_size):
405*9c5db199SXin Li            m.update(chunk)
406*9c5db199SXin Li
407*9c5db199SXin Li    logging.debug("MD5 checksum for %s is %s.", path, m.hexdigest())
408*9c5db199SXin Li
409*9c5db199SXin Li    return m.hexdigest()
410*9c5db199SXin Li
411*9c5db199SXin Li
412*9c5db199SXin Lidef args_to_storage_dict(args):
413*9c5db199SXin Li    """Map args into storage dictionaries.
414*9c5db199SXin Li
415*9c5db199SXin Li    This function is to be used (likely) in control files to obtain a storage
416*9c5db199SXin Li    dictionary from command line arguments.
417*9c5db199SXin Li
418*9c5db199SXin Li    @param args: a list of arguments as passed to control file.
419*9c5db199SXin Li
420*9c5db199SXin Li    @return a tuple (storage_dict, rest_of_args) where storage_dict is a
421*9c5db199SXin Li            dictionary for storage filtering and rest_of_args is a dictionary
422*9c5db199SXin Li            of keys which do not match storage dict keys.
423*9c5db199SXin Li    """
424*9c5db199SXin Li    args_dict = utils.args_to_dict(args)
425*9c5db199SXin Li    storage_dict = {}
426*9c5db199SXin Li
427*9c5db199SXin Li    # A list of all allowed keys and their type.
428*9c5db199SXin Li    key_list = ('device', 'bus', 'model', 'size', 'fs_uuid', 'fstype',
429*9c5db199SXin Li                'is_mounted', 'mountpoint')
430*9c5db199SXin Li
431*9c5db199SXin Li    def set_if_exists(src, dst, key):
432*9c5db199SXin Li        """If |src| has |key| copies its value to |dst|.
433*9c5db199SXin Li
434*9c5db199SXin Li        @return True if |key| exists in |src|, False otherwise.
435*9c5db199SXin Li        """
436*9c5db199SXin Li        if key in src:
437*9c5db199SXin Li            dst[key] = src[key]
438*9c5db199SXin Li            return True
439*9c5db199SXin Li        else:
440*9c5db199SXin Li            return False
441*9c5db199SXin Li
442*9c5db199SXin Li    for key in key_list:
443*9c5db199SXin Li        if set_if_exists(args_dict, storage_dict, key):
444*9c5db199SXin Li            del args_dict[key]
445*9c5db199SXin Li
446*9c5db199SXin Li    # Return the storage dict and the leftovers of the args to be evaluated
447*9c5db199SXin Li    # later.
448*9c5db199SXin Li    return storage_dict, args_dict
449