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.TestFail(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                          disable_sysinfo=True,
163                          filesize=fio_file_size,
164                          requirements=[('4k_read_qd32', [])],
165                          tag='before_trim')
166
167        # Generate random order of chunk to trim
168        trim_order = list(range(0, chunk_count))
169        random.shuffle(trim_order)
170        trim_status = [False] * chunk_count
171
172        # Init stat variable
173        data_verify_count = 0
174        data_verify_match = 0
175        trim_verify_count = 0
176        trim_verify_zero = 0
177        trim_verify_one = 0
178        trim_verify_non_delete = 0
179        trim_deterministic = True
180
181        last_ratio = 0
182        for ratio in trim_ratio:
183
184            # Do trim
185            begin_trim_chunk = int(last_ratio * chunk_count)
186            end_trim_chunk = int(ratio * chunk_count)
187            fd = os.open(self._filename, os.O_RDWR, 0666)
188            for chunk in trim_order[begin_trim_chunk:end_trim_chunk]:
189                self._do_trim(fd, chunk * chunk_size, chunk_size)
190                trim_status[chunk] = True
191            os.close(fd)
192            last_ratio = ratio
193
194            cur_hash = self._get_hash(chunk_count, chunk_size)
195
196            trim_verify_count += int(ratio * chunk_count)
197            data_verify_count += chunk_count - int(ratio * chunk_count)
198
199            # Verify hash
200            for cur, ref, trim in zip(cur_hash, ref_hash, trim_status):
201                if trim:
202                    if not trim_hash:
203                        trim_hash = cur
204                    elif cur != trim_hash:
205                        trim_deterministic = False
206
207                    if cur == zero_hash:
208                        trim_verify_zero += 1
209                    elif cur == one_hash:
210                        trim_verify_one += 1
211                    elif cur == ref:
212                        trim_verify_non_delete += 1
213                else:
214                    if cur == ref:
215                        data_verify_match += 1
216
217        keyval = dict()
218        keyval['data_verify_count'] = data_verify_count
219        keyval['data_verify_match'] = data_verify_match
220        keyval['trim_verify_count'] = trim_verify_count
221        keyval['trim_verify_zero'] = trim_verify_zero
222        keyval['trim_verify_one'] = trim_verify_one
223        keyval['trim_verify_non_delete'] = trim_verify_non_delete
224        keyval['trim_deterministic'] = trim_deterministic
225        self.write_perf_keyval(keyval)
226
227        # Check read speed/latency when reading from trimmed data.
228        self.job.run_test('hardware_StorageFio',
229                          disable_sysinfo=True,
230                          filesize=fio_file_size,
231                          requirements=[('4k_read_qd32', [])],
232                          tag='after_trim')
233
234        if data_verify_match < data_verify_count:
235            reason = 'Fail to verify untrimmed data.'
236            msg = utils.get_storage_error_msg(self._diskname, reason)
237            raise error.TestFail(msg)
238
239        if trim_verify_zero <  trim_verify_count:
240            reason = 'Trimmed data are not zeroed.'
241            msg = utils.get_storage_error_msg(self._diskname, reason)
242            if utils.is_disk_scsi(self._diskname):
243                if utils.verify_hdparm_feature(self._diskname,
244                                               self.hdparm_rzat):
245                    msg += ' Disk claim deterministic read zero after trim.'
246                    raise error.TestFail(msg)
247            raise error.TestNAError(msg)
248