hardware_TrimIntegrity.py revision 8ba770dc9bcf2e60bf2db1b7128d7502df6e8e60
1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import os, fcntl, logging, struct, random, re
6
7from autotest_lib.client.bin import test, utils
8from autotest_lib.client.common_lib import error
9
10
11class hardware_TrimIntegrity(test.test):
12    """
13    Performs data integrity trim test on an unmounted partition.
14
15    This test will write 1 GB of data and verify that trimmed data are gone and
16    untrimmed data are unaffected. The verification will be run in 5 passes with
17    0%, 25%, 50%, 75%, and 100% of data trimmed.
18
19    Also, perform 4K random read QD32 before and after trim. We should see some
20    speed / latency difference if the device firmware trim data properly.
21
22    Condition for test result:
23    - Trim command is not supported
24      -> Target disk is a harddisk           : TestNA
25      -> Target disk is SCSI disk w/o trim   : TestNA
26      -> Otherwise                           : TestFail
27    - Can not verify integrity of untrimmed data
28      -> All case                            : TestFail
29    - Trim data is not Zero
30      -> SSD with RZAT                       : TestFail
31      -> Otherwise                           : TestNA
32    """
33
34    version = 1
35    FILE_SIZE = 1024 * 1024 * 1024
36    CHUNK_SIZE = 192 * 1024
37    TRIM_RATIO = [0, 0.25, 0.5, 0.75, 1]
38
39    hdparm_trim = 'Data Set Management TRIM supported'
40    hdparm_rzat = 'Deterministic read ZEROs after TRIM'
41
42    # Use hash value to check integrity of the random data.
43    HASH_CMD = 'sha256sum | cut -d" " -f 1'
44    # 0x1277 is ioctl BLKDISCARD command
45    IOCTL_TRIM_CMD = 0x1277
46    IOCTL_NOT_SUPPORT_ERRNO = 95
47
48    def _get_hash(self, chunk_count, chunk_size):
49        """
50        Get hash for every chunk of data.
51        """
52        cmd = str('for i in $(seq 0 %d); do dd if=%s of=/dev/stdout bs=%d'
53                  ' count=1 skip=$i iflag=direct | %s; done' %
54                  (chunk_count - 1, self._filename, chunk_size, self.HASH_CMD))
55        return utils.run(cmd).stdout.split()
56
57    def _do_trim(self, fd, offset, size):
58        """
59        Invoke ioctl to trim command.
60        """
61        fcntl.ioctl(fd, self.IOCTL_TRIM_CMD, struct.pack('QQ', offset, size))
62
63    def _verify_trim_support(self, size):
64        """
65        Check for trim support in ioctl. Raise TestNAError if not support.
66
67        @param size: size to try the trim command
68        """
69        try:
70            fd = os.open(self._filename, os.O_RDWR, 0666)
71            self._do_trim(fd, 0, size)
72        except IOError, err:
73            if err.errno == self.IOCTL_NOT_SUPPORT_ERRNO:
74                reason = 'IOCTL Does not support trim.'
75                msg = utils.get_storage_error_msg(self._diskname, reason)
76
77                if utils.is_disk_scsi(self._diskname):
78                    if utils.is_disk_harddisk(self._diskname):
79                        msg += ' Disk is a hard disk.'
80                        raise error.TestNAError(msg)
81                    if utils.verify_hdparm_feature(self._diskname,
82                                                   self.hdparm_trim):
83                        msg += ' Disk claims trim supported.'
84                    else:
85                        msg += ' Disk does not claim trim supported.'
86                        raise error.TestNAError(msg)
87                # SSD with trim support / mmc / sd card
88                raise error.TestFailError(msg)
89            else:
90                raise
91        finally:
92            os.close(fd)
93
94    def run_once(self, filename=None, file_size=FILE_SIZE,
95                 chunk_size=CHUNK_SIZE, trim_ratio=TRIM_RATIO):
96        """
97        Executes the test and logs the output.
98        @param file_name:  file/disk name to test
99                           default: spare partition of internal disk
100        @param file_size:  size of data to test. default: 1GB
101        @param chunk_size: size of chunk to calculate hash/trim. default: 64KB
102        @param trim_ratio: list of ratio of file size to trim data
103                           default: [0, 0.25, 0.5, 0.75, 1]
104        """
105
106        if not filename:
107            self._diskname = utils.get_fixed_dst_drive()
108            if self._diskname == utils.get_root_device():
109                self._filename = utils.get_free_root_partition()
110            else:
111                self._filename = self._diskname
112        else:
113            self._filename = filename
114            self._diskname = utils.get_disk_from_filename(filename)
115
116        if file_size == 0:
117            fulldisk = True
118            file_size = utils.get_disk_size(self._filename)
119            if file_size == 0:
120                cmd = ('%s seem to have 0 storage block. Is the media present?'
121                        % filename)
122                raise error.TestError(cmd)
123        else:
124            fulldisk = False
125
126        # Make file size multiple of 4 * chunk size
127        file_size -= file_size % (4 * chunk_size)
128
129        if fulldisk:
130            fio_file_size = 0
131        else:
132            fio_file_size = file_size
133
134        logging.info('filename: %s, filesize: %d', self._filename, file_size)
135
136        self._verify_trim_support(chunk_size)
137
138        # Calculate hash value for zero'ed and one'ed data
139        cmd = str('dd if=/dev/zero bs=%d count=1 | %s' %
140                  (chunk_size, self.HASH_CMD))
141        zero_hash = utils.run(cmd).stdout.strip()
142
143        cmd = str("dd if=/dev/zero bs=%d count=1 | tr '\\0' '\\xff' | %s" %
144                  (chunk_size, self.HASH_CMD))
145        one_hash = utils.run(cmd).stdout.strip()
146
147        trim_hash = ""
148
149        # Write random data to disk
150        chunk_count = file_size / chunk_size
151        cmd = str('dd if=/dev/urandom of=%s bs=%d count=%d oflag=direct' %
152                  (self._filename, chunk_size, chunk_count))
153        utils.run(cmd)
154
155        ref_hash = self._get_hash(chunk_count, chunk_size)
156
157        # Check read speed/latency when reading real data.
158        self.job.run_test('hardware_StorageFio',
159                          filesize=fio_file_size,
160                          requirements=[('4k_read_qd32', [])],
161                          tag='before_trim')
162
163        # Generate random order of chunk to trim
164        trim_order = list(range(0, chunk_count))
165        random.shuffle(trim_order)
166        trim_status = [False] * chunk_count
167
168        # Init stat variable
169        data_verify_count = 0
170        data_verify_match = 0
171        trim_verify_count = 0
172        trim_verify_zero = 0
173        trim_verify_one = 0
174        trim_verify_non_delete = 0
175        trim_deterministic = True
176
177        last_ratio = 0
178        for ratio in trim_ratio:
179
180            # Do trim
181            begin_trim_chunk = int(last_ratio * chunk_count)
182            end_trim_chunk = int(ratio * chunk_count)
183            fd = os.open(self._filename, os.O_RDWR, 0666)
184            for chunk in trim_order[begin_trim_chunk:end_trim_chunk]:
185                self._do_trim(fd, chunk * chunk_size, chunk_size)
186                trim_status[chunk] = True
187            os.close(fd)
188            last_ratio = ratio
189
190            cur_hash = self._get_hash(chunk_count, chunk_size)
191
192            trim_verify_count += int(ratio * chunk_count)
193            data_verify_count += chunk_count - int(ratio * chunk_count)
194
195            # Verify hash
196            for cur, ref, trim in zip(cur_hash, ref_hash, trim_status):
197                if trim:
198                    if not trim_hash:
199                        trim_hash = cur
200                    elif cur != trim_hash:
201                        trim_deterministic = False
202
203                    if cur == zero_hash:
204                        trim_verify_zero += 1
205                    elif cur == one_hash:
206                        trim_verify_one += 1
207                    elif cur == ref:
208                        trim_verify_non_delete += 1
209                else:
210                    if cur == ref:
211                        data_verify_match += 1
212
213        keyval = dict()
214        keyval['data_verify_count'] = data_verify_count
215        keyval['data_verify_match'] = data_verify_match
216        keyval['trim_verify_count'] = trim_verify_count
217        keyval['trim_verify_zero'] = trim_verify_zero
218        keyval['trim_verify_one'] = trim_verify_one
219        keyval['trim_verify_non_delete'] = trim_verify_non_delete
220        keyval['trim_deterministic'] = trim_deterministic
221        self.write_perf_keyval(keyval)
222
223        # Check read speed/latency when reading from trimmed data.
224        self.job.run_test('hardware_StorageFio',
225                          filesize=fio_file_size,
226                          requirements=[('4k_read_qd32', [])],
227                          tag='after_trim')
228
229        if data_verify_match < data_verify_count:
230            reason = 'Fail to verify untrimmed data.'
231            msg = utils.get_storage_error_msg(self._diskname, reason)
232            raise error.TestFail(msg)
233
234        if trim_verify_zero <  trim_verify_count:
235            reason = 'Trimmed data are not zeroed.'
236            msg = utils.get_storage_error_msg(self._diskname, reason)
237            if utils.is_disk_scsi(self._diskname):
238                if utils.verify_hdparm_feature(self._diskname,
239                                               self.hdparm_rzat):
240                    msg += ' Disk claim deterministic read zero after trim.'
241                    raise error.TestFail(msg)
242            raise error.TestNAError(msg)
243