1# Lint as: python2, python3 2# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import os, fcntl, logging, struct, random 7 8from autotest_lib.client.bin import test, utils 9from autotest_lib.client.common_lib import error 10 11 12class hardware_TrimIntegrity(test.test): 13 """ 14 Performs data integrity trim test on an unmounted partition. 15 16 This test will write 1 GB of data and verify that trimmed data are gone and 17 untrimmed data are unaffected. The verification will be run in 5 passes with 18 0%, 25%, 50%, 75%, and 100% of data trimmed. 19 20 Also, perform 4K random read QD32 before and after trim. We should see some 21 speed / latency difference if the device firmware trim data properly. 22 23 Condition for test result: 24 - Trim command is not supported 25 -> Target disk is a harddisk : TestNA 26 -> Target disk is SCSI disk w/o trim : TestNA 27 -> Otherwise : TestFail 28 - Can not verify integrity of untrimmed data 29 -> All case : TestFail 30 - Trim data is not Zero 31 -> SSD with RZAT : TestFail 32 -> NVMe with dlfeat:1 : TestFail 33 -> Otherwise : TestNA 34 """ 35 36 version = 1 37 FILE_SIZE = 1024 * 1024 * 1024 38 CHUNK_SIZE = 192 * 1024 39 TRIM_RATIO = [0, 0.25, 0.5, 0.75, 1] 40 41 hdparm_trim = 'Data Set Management TRIM supported' 42 hdparm_rzat = 'Deterministic read ZEROs after TRIM' 43 nvme_dlfeat = 'dlfeat' 44 45 # Use hash value to check integrity of the random data. 46 HASH_CMD = 'sha256sum | cut -d" " -f 1' 47 # 0x1277 is ioctl BLKDISCARD command 48 IOCTL_TRIM_CMD = 0x1277 49 IOCTL_NOT_SUPPORT_ERRNO = 95 50 51 def _get_hash(self, chunk_count, chunk_size): 52 """ 53 Get hash for every chunk of data. 54 """ 55 cmd = str('for i in $(seq 0 %d); do dd if=%s of=/dev/stdout bs=%d' 56 ' count=1 skip=$i iflag=direct | %s; done' % 57 (chunk_count - 1, self._filename, chunk_size, self.HASH_CMD)) 58 return utils.run(cmd).stdout.split() 59 60 def _do_trim(self, fd, offset, size): 61 """ 62 Invoke ioctl to trim command. 63 """ 64 fcntl.ioctl(fd, self.IOCTL_TRIM_CMD, struct.pack('QQ', offset, size)) 65 66 def _verify_trim_support(self, size): 67 """ 68 Check for trim support in ioctl. Raise TestNAError if not support. 69 70 @param size: size to try the trim command 71 """ 72 try: 73 fd = os.open(self._filename, os.O_RDWR, 0o666) 74 self._do_trim(fd, 0, size) 75 except IOError as err: 76 if err.errno == self.IOCTL_NOT_SUPPORT_ERRNO: 77 reason = 'IOCTL Does not support trim.' 78 msg = utils.get_storage_error_msg(self._diskname, reason) 79 80 if utils.is_disk_scsi(self._diskname): 81 if utils.is_disk_harddisk(self._diskname): 82 msg += ' Disk is a hard disk.' 83 raise error.TestNAError(msg) 84 if utils.verify_hdparm_feature(self._diskname, 85 self.hdparm_trim): 86 msg += ' Disk claims trim supported.' 87 else: 88 msg += ' Disk does not claim trim supported.' 89 raise error.TestNAError(msg) 90 # SSD with trim support / mmc / sd card 91 raise error.TestFail(msg) 92 else: 93 raise 94 finally: 95 os.close(fd) 96 97 def initialize(self): 98 self.job.use_sequence_number = True 99 100 def run_once(self, filename=None, file_size=FILE_SIZE, 101 chunk_size=CHUNK_SIZE, trim_ratio=TRIM_RATIO): 102 """ 103 Executes the test and logs the output. 104 @param file_name: file/disk name to test 105 default: spare partition of internal disk 106 @param file_size: size of data to test. default: 1GB 107 @param chunk_size: size of chunk to calculate hash/trim. default: 64KB 108 @param trim_ratio: list of ratio of file size to trim data 109 default: [0, 0.25, 0.5, 0.75, 1] 110 """ 111 112 if not filename: 113 self._diskname = utils.get_fixed_dst_drive() 114 if self._diskname == utils.get_root_device(): 115 self._filename = utils.get_free_root_partition() 116 else: 117 self._filename = self._diskname 118 else: 119 self._filename = filename 120 self._diskname = utils.get_disk_from_filename(filename) 121 122 if file_size == 0: 123 fulldisk = True 124 file_size = utils.get_disk_size(self._filename) 125 if file_size == 0: 126 cmd = ('%s seem to have 0 storage block. Is the media present?' 127 % filename) 128 raise error.TestError(cmd) 129 else: 130 fulldisk = False 131 132 # Make file size multiple of 4 * chunk size 133 file_size -= file_size % (4 * chunk_size) 134 135 logging.info('filename: %s, filesize: %d', self._filename, file_size) 136 137 self._verify_trim_support(chunk_size) 138 139 # Calculate hash value for zero'ed and one'ed data 140 cmd = str('dd if=/dev/zero bs=%d count=1 | %s' % 141 (chunk_size, self.HASH_CMD)) 142 zero_hash = utils.run(cmd).stdout.strip() 143 144 cmd = str("dd if=/dev/zero bs=%d count=1 | tr '\\0' '\\xff' | %s" % 145 (chunk_size, self.HASH_CMD)) 146 one_hash = utils.run(cmd).stdout.strip() 147 148 trim_hash = "" 149 150 # Write random data to disk 151 chunk_count = file_size // chunk_size 152 cmd = str('dd if=/dev/urandom of=%s bs=%d count=%d oflag=direct' % 153 (self._filename, chunk_size, chunk_count)) 154 utils.run(cmd) 155 156 ref_hash = self._get_hash(chunk_count, chunk_size) 157 158 # Check read speed/latency when reading real data. 159 self.job.run_test('hardware_StorageFio', 160 disable_sysinfo=True, 161 filesize=file_size, 162 blkdiscard=False, 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, 0o666) 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 disable_sysinfo=True, 229 filesize=file_size, 230 blkdiscard=False, 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 elif utils.is_disk_nvme(self._diskname): 248 dlfeat = utils.get_nvme_id_ns_feature(self._diskname, 249 self.nvme_dlfeat) 250 if dlfeat == "None": 251 msg += ' Expected values for trimmed data not reported.' 252 raise error.TestNAError(msg) 253 elif int(dlfeat, 16) & 7 == 1: 254 msg += ' Disk indicates values should be zero after trim.' 255 raise error.TestFail(msg) 256 # TODO(asavery): NVMe 1.3 specification allows all bytes set 257 # to FF from a deallocated logical block 258 elif int(dlfeat, 16) & 7 == 2: 259 msg += ' Unexpected values, test does not check for ones.' 260 raise error.TestFail(msg) 261 else: 262 msg += ' Expected values for trimmed data not specified.' 263 raise error.TestNAError(msg) 264 raise error.TestNAError(msg) 265