hardware_TrimIntegrity.py revision 58fde35f14d0b6101f517c6e45185e35230736fc
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
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 initialize(self):
95        self.job.use_sequence_number = True
96
97    def run_once(self, filename=None, file_size=FILE_SIZE,
98                 chunk_size=CHUNK_SIZE, trim_ratio=TRIM_RATIO):
99        """
100        Executes the test and logs the output.
101        @param file_name:  file/disk name to test
102                           default: spare partition of internal disk
103        @param file_size:  size of data to test. default: 1GB
104        @param chunk_size: size of chunk to calculate hash/trim. default: 64KB
105        @param trim_ratio: list of ratio of file size to trim data
106                           default: [0, 0.25, 0.5, 0.75, 1]
107        """
108
109        if not filename:
110            self._diskname = utils.get_fixed_dst_drive()
111            if self._diskname == utils.get_root_device():
112                self._filename = utils.get_free_root_partition()
113            else:
114                self._filename = self._diskname
115        else:
116            self._filename = filename
117            self._diskname = utils.get_disk_from_filename(filename)
118
119        if file_size == 0:
120            fulldisk = True
121            file_size = utils.get_disk_size(self._filename)
122            if file_size == 0:
123                cmd = ('%s seem to have 0 storage block. Is the media present?'
124                        % filename)
125                raise error.TestError(cmd)
126        else:
127            fulldisk = False
128
129        # Make file size multiple of 4 * chunk size
130        file_size -= file_size % (4 * chunk_size)
131
132        if fulldisk:
133            fio_file_size = 0
134        else:
135            fio_file_size = file_size
136
137        logging.info('filename: %s, filesize: %d', self._filename, file_size)
138
139        self._verify_trim_support(chunk_size)
140
141        # Calculate hash value for zero'ed and one'ed data
142        cmd = str('dd if=/dev/zero bs=%d count=1 | %s' %
143                  (chunk_size, self.HASH_CMD))
144        zero_hash = utils.run(cmd).stdout.strip()
145
146        cmd = str("dd if=/dev/zero bs=%d count=1 | tr '\\0' '\\xff' | %s" %
147                  (chunk_size, self.HASH_CMD))
148        one_hash = utils.run(cmd).stdout.strip()
149
150        trim_hash = ""
151
152        # Write random data to disk
153        chunk_count = file_size / chunk_size
154        cmd = str('dd if=/dev/urandom of=%s bs=%d count=%d oflag=direct' %
155                  (self._filename, chunk_size, chunk_count))
156        utils.run(cmd)
157
158        ref_hash = self._get_hash(chunk_count, chunk_size)
159
160        # Check read speed/latency when reading real data.
161        self.job.run_test('hardware_StorageFio',
162                          filesize=fio_file_size,
163                          requirements=[('4k_read_qd32', [])],
164                          tag='before_trim')
165
166        # Generate random order of chunk to trim
167        trim_order = list(range(0, chunk_count))
168        random.shuffle(trim_order)
169        trim_status = [False] * chunk_count
170
171        # Init stat variable
172        data_verify_count = 0
173        data_verify_match = 0
174        trim_verify_count = 0
175        trim_verify_zero = 0
176        trim_verify_one = 0
177        trim_verify_non_delete = 0
178        trim_deterministic = True
179
180        last_ratio = 0
181        for ratio in trim_ratio:
182
183            # Do trim
184            begin_trim_chunk = int(last_ratio * chunk_count)
185            end_trim_chunk = int(ratio * chunk_count)
186            fd = os.open(self._filename, os.O_RDWR, 0666)
187            for chunk in trim_order[begin_trim_chunk:end_trim_chunk]:
188                self._do_trim(fd, chunk * chunk_size, chunk_size)
189                trim_status[chunk] = True
190            os.close(fd)
191            last_ratio = ratio
192
193            cur_hash = self._get_hash(chunk_count, chunk_size)
194
195            trim_verify_count += int(ratio * chunk_count)
196            data_verify_count += chunk_count - int(ratio * chunk_count)
197
198            # Verify hash
199            for cur, ref, trim in zip(cur_hash, ref_hash, trim_status):
200                if trim:
201                    if not trim_hash:
202                        trim_hash = cur
203                    elif cur != trim_hash:
204                        trim_deterministic = False
205
206                    if cur == zero_hash:
207                        trim_verify_zero += 1
208                    elif cur == one_hash:
209                        trim_verify_one += 1
210                    elif cur == ref:
211                        trim_verify_non_delete += 1
212                else:
213                    if cur == ref:
214                        data_verify_match += 1
215
216        keyval = dict()
217        keyval['data_verify_count'] = data_verify_count
218        keyval['data_verify_match'] = data_verify_match
219        keyval['trim_verify_count'] = trim_verify_count
220        keyval['trim_verify_zero'] = trim_verify_zero
221        keyval['trim_verify_one'] = trim_verify_one
222        keyval['trim_verify_non_delete'] = trim_verify_non_delete
223        keyval['trim_deterministic'] = trim_deterministic
224        self.write_perf_keyval(keyval)
225
226        # Check read speed/latency when reading from trimmed data.
227        self.job.run_test('hardware_StorageFio',
228                          filesize=fio_file_size,
229                          requirements=[('4k_read_qd32', [])],
230                          tag='after_trim')
231
232        if data_verify_match < data_verify_count:
233            reason = 'Fail to verify untrimmed data.'
234            msg = utils.get_storage_error_msg(self._diskname, reason)
235            raise error.TestFail(msg)
236
237        if trim_verify_zero <  trim_verify_count:
238            reason = 'Trimmed data are not zeroed.'
239            msg = utils.get_storage_error_msg(self._diskname, reason)
240            if utils.is_disk_scsi(self._diskname):
241                if utils.verify_hdparm_feature(self._diskname,
242                                               self.hdparm_rzat):
243                    msg += ' Disk claim deterministic read zero after trim.'
244                    raise error.TestFail(msg)
245            raise error.TestNAError(msg)
246