5 # Test fio's io_uring_cmd ioengine support for DIF/DIX end-to-end data
9 # see python3 nvmept_pi.py --help
11 # EXAMPLES (THIS IS A DESTRUCTIVE TEST!!)
12 # python3 t/nvmept_pi.py --dut /dev/ng0n1 -f ./fio
13 # python3 t/nvmept_pi.py --dut /dev/ng0n1 -f ./fio --lbaf 1
28 from pathlib import Path
29 from fiotestlib import FioJobCmdTest, run_fio_tests
30 from fiotestcommon import SUCCESS_NONZERO
36 class DifDixTest(FioJobCmdTest):
38 NVMe DIF/DIX test class.
41 def setup(self, parameters):
46 f"--ioengine={self.fio_opts['ioengine']}",
47 f"--filename={self.fio_opts['filename']}",
48 f"--rw={self.fio_opts['rw']}",
49 f"--bsrange={self.fio_opts['bsrange']}",
50 f"--output={self.filenames['output']}",
51 f"--md_per_io_size={self.fio_opts['md_per_io_size']}",
52 f"--pi_act={self.fio_opts['pi_act']}",
53 f"--pi_chk={self.fio_opts['pi_chk']}",
54 f"--apptag={self.fio_opts['apptag']}",
55 f"--apptag_mask={self.fio_opts['apptag_mask']}",
57 for opt in ['fixedbufs', 'nonvectored', 'force_async', 'registerfiles',
58 'sqthread_poll', 'sqthread_poll_cpu', 'hipri', 'nowait',
59 'time_based', 'runtime', 'verify', 'io_size', 'offset', 'number_ios',
61 if opt in self.fio_opts:
62 option = f"--{opt}={self.fio_opts[opt]}"
63 fio_args.append(option)
65 if self.fio_opts['ioengine'] == 'io_uring_cmd':
66 fio_args.append('--cmd_type=nvme')
67 elif self.fio_opts['ioengine'] == 'xnvme':
68 fio_args.append('--thread=1')
69 fio_args.append('--xnvme_async=io_uring_cmd')
71 super().setup(fio_args)
76 # Write data with pi_act=1 and then read the data back (with both
80 # Write workload with variable IO sizes
85 "number_ios": NUMBER_IOS,
86 "output-format": "json",
88 "apptag_mask": "0xFFFF",
91 "pi_chk": "APPTAG,GUARD,REFTAG",
94 "test_class": DifDixTest,
97 # Read workload with fixed small IO size
102 "number_ios": NUMBER_IOS,
103 "output-format": "json",
106 "apptag_mask": "0xFFFF",
108 "pi_chk": "APPTAG,GUARD,REFTAG",
111 "test_class": DifDixTest,
114 # Read workload with fixed small IO size
119 "number_ios": NUMBER_IOS,
120 "output-format": "json",
123 "apptag_mask": "0xFFFF",
125 "pi_chk": "APPTAG,GUARD,REFTAG",
128 "test_class": DifDixTest,
131 # Write workload with fixed large IO size
132 # Precondition for read workloads to follow
137 "number_ios": NUMBER_IOS,
138 "output-format": "json",
140 "apptag_mask": "0xFFFF",
143 "pi_chk": "APPTAG,GUARD,REFTAG",
146 "test_class": DifDixTest,
149 # Read workload with variable IO sizes
154 "number_ios": NUMBER_IOS,
155 "output-format": "json",
158 "apptag_mask": "0xFFFF",
160 "pi_chk": "APPTAG,GUARD,REFTAG",
163 "test_class": DifDixTest,
166 # Read workload with variable IO sizes
171 "number_ios": NUMBER_IOS,
172 "output-format": "json",
175 "apptag_mask": "0xFFFF",
177 "pi_chk": "APPTAG,GUARD,REFTAG",
180 "test_class": DifDixTest,
183 # Write data with pi_act=0 and then read the data back (with both
187 # Write workload with variable IO sizes
192 "number_ios": NUMBER_IOS,
193 "output-format": "json",
195 "apptag_mask": "0xFFFF",
198 "pi_chk": "APPTAG,GUARD,REFTAG",
201 "test_class": DifDixTest,
204 # Read workload with fixed small IO size
209 "number_ios": NUMBER_IOS,
210 "output-format": "json",
213 "apptag_mask": "0xFFFF",
215 "pi_chk": "APPTAG,GUARD,REFTAG",
218 "test_class": DifDixTest,
221 # Read workload with fixed small IO size
226 "number_ios": NUMBER_IOS,
227 "output-format": "json",
230 "apptag_mask": "0xFFFF",
232 "pi_chk": "APPTAG,GUARD,REFTAG",
235 "test_class": DifDixTest,
238 # Write workload with fixed large IO sizes
243 "number_ios": NUMBER_IOS,
244 "output-format": "json",
246 "apptag_mask": "0xFFFF",
249 "pi_chk": "APPTAG,GUARD,REFTAG",
252 "test_class": DifDixTest,
255 # Read workload with variable IO sizes
260 "number_ios": NUMBER_IOS,
261 "output-format": "json",
264 "apptag_mask": "0xFFFF",
266 "pi_chk": "APPTAG,GUARD,REFTAG",
269 "test_class": DifDixTest,
272 # Read workload with variable IO sizes
277 "number_ios": NUMBER_IOS,
278 "output-format": "json",
281 "apptag_mask": "0xFFFF",
283 "pi_chk": "APPTAG,GUARD,REFTAG",
286 "test_class": DifDixTest,
289 # Test apptag errors.
292 # Read workload with variable IO sizes
294 # trigger an apptag error
298 "number_ios": NUMBER_IOS,
299 "output-format": "json",
302 "apptag_mask": "0xFFFF",
304 "pi_chk": "APPTAG,GUARD,REFTAG",
307 "success": SUCCESS_NONZERO,
308 "test_class": DifDixTest,
311 # Read workload with variable IO sizes
313 # trigger an apptag error
317 "number_ios": NUMBER_IOS,
318 "output-format": "json",
321 "apptag_mask": "0xFFFF",
323 "pi_chk": "APPTAG,GUARD,REFTAG",
326 "success": SUCCESS_NONZERO,
327 "test_class": DifDixTest,
330 # Read workload with variable IO sizes
332 # trigger an apptag error
333 # same as above but with pi_chk=APPTAG only
337 "number_ios": NUMBER_IOS,
338 "output-format": "json",
341 "apptag_mask": "0xFFFF",
346 "success": SUCCESS_NONZERO,
347 "test_class": DifDixTest,
350 # Read workload with variable IO sizes
352 # trigger an apptag error
353 # same as above but with pi_chk=APPTAG only
357 "number_ios": NUMBER_IOS,
358 "output-format": "json",
361 "apptag_mask": "0xFFFF",
366 "success": SUCCESS_NONZERO,
367 "test_class": DifDixTest,
370 # Read workload with variable IO sizes
372 # this case would trigger an apptag error, but pi_chk says to check
373 # only the Guard PI and reftag, so there should be no error
377 "number_ios": NUMBER_IOS,
378 "output-format": "json",
381 "apptag_mask": "0xFFFF",
383 "pi_chk": "GUARD,REFTAG",
386 "test_class": DifDixTest,
389 # Read workload with variable IO sizes
391 # this case would trigger an apptag error, but pi_chk says to check
392 # only the Guard PI and reftag, so there should be no error
396 "number_ios": NUMBER_IOS,
397 "output-format": "json",
400 "apptag_mask": "0xFFFF",
402 "pi_chk": "GUARD,REFTAG",
405 "test_class": DifDixTest,
408 # Read workload with variable IO sizes
410 # this case would trigger an apptag error, but pi_chk says to check
411 # only the Guard PI, so there should be no error
415 "number_ios": NUMBER_IOS,
416 "output-format": "json",
419 "apptag_mask": "0xFFFF",
424 "test_class": DifDixTest,
427 # Read workload with variable IO sizes
429 # this case would trigger an apptag error, but pi_chk says to check
430 # only the Guard PI, so there should be no error
434 "number_ios": NUMBER_IOS,
435 "output-format": "json",
438 "apptag_mask": "0xFFFF",
443 "test_class": DifDixTest,
446 # Read workload with variable IO sizes
448 # this case would trigger an apptag error, but pi_chk says to check
449 # only the reftag, so there should be no error
450 # This case will be skipped when the device is formatted with Type 3 PI
451 # since Type 3 PI ignores the reftag
455 "number_ios": NUMBER_IOS,
456 "output-format": "json",
459 "apptag_mask": "0xFFFF",
465 "test_class": DifDixTest,
468 # Read workload with variable IO sizes
470 # this case would trigger an apptag error, but pi_chk says to check
471 # only the reftag, so there should be no error
472 # This case will be skipped when the device is formatted with Type 3 PI
473 # since Type 3 PI ignores the reftag
477 "number_ios": NUMBER_IOS,
478 "output-format": "json",
481 "apptag_mask": "0xFFFF",
487 "test_class": DifDixTest,
490 # Read workload with variable IO sizes
492 # use apptag mask to ignore apptag mismatch
496 "number_ios": NUMBER_IOS,
497 "output-format": "json",
500 "apptag_mask": "0x0FFF",
502 "pi_chk": "APPTAG,GUARD,REFTAG",
505 "test_class": DifDixTest,
508 # Read workload with variable IO sizes
510 # use apptag mask to ignore apptag mismatch
514 "number_ios": NUMBER_IOS,
515 "output-format": "json",
518 "apptag_mask": "0x0FFF",
520 "pi_chk": "APPTAG,GUARD,REFTAG",
523 "test_class": DifDixTest,
526 # Read workload with variable IO sizes
528 # use apptag mask to ignore apptag mismatch
532 "number_ios": NUMBER_IOS,
533 "output-format": "json",
536 "apptag_mask": "0x0FFF",
538 "pi_chk": "APPTAG,GUARD,REFTAG",
541 "test_class": DifDixTest,
544 # Read workload with variable IO sizes
546 # use apptag mask to ignore apptag mismatch
550 "number_ios": NUMBER_IOS,
551 "output-format": "json",
554 "apptag_mask": "0x0FFF",
556 "pi_chk": "APPTAG,GUARD,REFTAG",
559 "test_class": DifDixTest,
562 # Write workload with fixed large IO sizes
563 # Set apptag=0xFFFF to disable all checking for Type 1 and 2
568 "number_ios": NUMBER_IOS,
569 "output-format": "json",
571 "apptag_mask": "0xFFFF",
574 "pi_chk": "APPTAG,GUARD,REFTAG",
578 "test_class": DifDixTest,
581 # Read workload with variable IO sizes
583 # Data was written with apptag=0xFFFF
584 # Reading the data back should disable all checking for Type 1 and 2
588 "number_ios": NUMBER_IOS,
589 "output-format": "json",
592 "apptag_mask": "0xFFFF",
594 "pi_chk": "APPTAG,GUARD,REFTAG",
598 "test_class": DifDixTest,
601 # Read workload with variable IO sizes
603 # Data was written with apptag=0xFFFF
604 # Reading the data back should disable all checking for Type 1 and 2
608 "number_ios": NUMBER_IOS,
609 "output-format": "json",
612 "apptag_mask": "0xFFFF",
614 "pi_chk": "APPTAG,GUARD,REFTAG",
618 "test_class": DifDixTest,
621 # Error cases related to block size and metadata size
624 # Use a min block size that is not a multiple of lba/elba size to
629 "number_ios": NUMBER_IOS,
632 "apptag_mask": "0x0FFF",
634 "pi_chk": "APPTAG,GUARD,REFTAG",
635 "bs_low": BS_LOW+0.5,
637 "success": SUCCESS_NONZERO,
638 "test_class": DifDixTest,
641 # Use metadata size that is too small
645 "number_ios": NUMBER_IOS,
648 "apptag_mask": "0x0FFF",
650 "pi_chk": "APPTAG,GUARD,REFTAG",
653 "mdsize_adjustment": -1,
654 "success": SUCCESS_NONZERO,
656 "test_class": DifDixTest,
659 # Read workload with variable IO sizes
661 # Should still work even if metadata size is too large
665 "number_ios": NUMBER_IOS,
668 "apptag_mask": "0x0FFF",
670 "pi_chk": "APPTAG,GUARD,REFTAG",
673 "mdsize_adjustment": 1,
674 "test_class": DifDixTest,
680 """Parse command-line arguments."""
682 parser = argparse.ArgumentParser()
683 parser.add_argument('-d', '--debug', help='Enable debug messages', action='store_true')
684 parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)')
685 parser.add_argument('-a', '--artifact-root', help='artifact root directory')
686 parser.add_argument('-s', '--skip', nargs='+', type=int,
687 help='list of test(s) to skip')
688 parser.add_argument('-o', '--run-only', nargs='+', type=int,
689 help='list of test(s) to run, skipping all others')
690 parser.add_argument('--dut', help='target NVMe character device to test '
691 '(e.g., /dev/ng0n1). WARNING: THIS IS A DESTRUCTIVE TEST', required=True)
692 parser.add_argument('-l', '--lbaf', nargs='+', type=int,
693 help='list of lba formats to test')
694 parser.add_argument('-i', '--ioengine', default='io_uring_cmd')
695 args = parser.parse_args()
702 Determine which LBA formats to use. Use either the ones specified on the
703 command line or if none are specified query the device and use all lba
704 formats with metadata.
707 id_ns_cmd = f"sudo nvme id-ns --output-format=json {args.dut}".split(' ')
708 id_ns_output = subprocess.check_output(id_ns_cmd)
709 lbafs = json.loads(id_ns_output)['lbafs']
711 for lbaf in args.lbaf:
712 lbaf_list.append({'lbaf': lbaf, 'ds': 2 ** lbafs[lbaf]['ds'],
713 'ms': lbafs[lbaf]['ms'], })
714 if lbafs[lbaf]['ms'] == 0:
715 print(f'Error: lbaf {lbaf} has metadata size zero')
718 for lbaf_num, lbaf in enumerate(lbafs):
720 lbaf_list.append({'lbaf': lbaf_num, 'ds': 2 ** lbaf['ds'],
726 def get_guard_pi(lbaf_list, args):
728 Find out how many bits of guard protection information are associated with
729 each lbaf to be used. If this is not available assume 16-bit guard pi.
730 Also record the bytes of protection information associated with the number
733 nvm_id_ns_cmd = f"sudo nvme nvm-id-ns --output-format=json {args.dut}".split(' ')
735 nvm_id_ns_output = subprocess.check_output(nvm_id_ns_cmd)
736 except subprocess.CalledProcessError:
737 print(f"Non-zero return code from {' '.join(nvm_id_ns_cmd)}; " \
738 "assuming all lbafs use 16b Guard Protection Information")
739 for lbaf in lbaf_list:
740 lbaf['guard_pi_bits'] = 16
742 elbafs = json.loads(nvm_id_ns_output)['elbafs']
743 for elbaf_num, elbaf in enumerate(elbafs):
744 for lbaf in lbaf_list:
745 if lbaf['lbaf'] == elbaf_num:
746 lbaf['guard_pi_bits'] = 16 << elbaf['pif']
748 # For 16b Guard Protection Information, the PI requires 8 bytes
749 # For 32b and 64b Guard PI, the PI requires 16 bytes
750 for lbaf in lbaf_list:
751 if lbaf['guard_pi_bits'] == 16:
754 lbaf['pi_bytes'] = 16
757 def get_capabilities(args):
759 Determine what end-to-end data protection features the device supports.
761 caps = { 'pil': [], 'pitype': [], 'elba': [] }
762 id_ns_cmd = f"sudo nvme id-ns --output-format=json {args.dut}".split(' ')
763 id_ns_output = subprocess.check_output(id_ns_cmd)
764 id_ns_json = json.loads(id_ns_output)
766 mc = id_ns_json['mc']
768 caps['elba'].append(1)
770 caps['elba'].append(0)
772 dpc = id_ns_json['dpc']
774 caps['pitype'].append(1)
776 caps['pitype'].append(2)
778 caps['pitype'].append(3)
780 caps['pil'].append(1)
782 caps['pil'].append(0)
784 for _, value in caps.items():
786 logging.error("One or more end-to-end data protection features unsupported: %s", caps)
792 def format_device(args, lbaf, pitype, pil, elba):
794 Format device using specified lba format with specified pitype, pil, and
798 format_cmd = f"sudo nvme format {args.dut} --lbaf={lbaf['lbaf']} " \
799 f"--pi={pitype} --pil={pil} --ms={elba} --force"
800 logging.debug("Format command: %s", format_cmd)
801 format_cmd = format_cmd.split(' ')
802 format_cmd_result = subprocess.run(format_cmd, capture_output=True, check=False,
803 encoding=locale.getpreferredencoding())
805 # Sometimes nvme-cli may format the device successfully but fail to
806 # rescan the namespaces after the format. Continue if this happens but
807 # abort if some other error occurs.
808 if format_cmd_result.returncode != 0:
809 if 'failed to rescan namespaces' not in format_cmd_result.stderr \
810 or 'Success formatting namespace' not in format_cmd_result.stdout:
811 logging.error(format_cmd_result.stdout)
812 logging.error(format_cmd_result.stderr)
813 print("Unable to format device; skipping this configuration")
816 logging.debug(format_cmd_result.stdout)
820 def difdix_test(test_env, args, lbaf, pitype, elba):
822 Adjust test arguments based on values of lbaf, pitype, and elba. Then run
825 for test in TEST_LIST:
826 test['force_skip'] = False
828 blocksize = lbaf['ds']
829 # Set fio blocksize parameter at runtime
830 # If we formatted the device in extended LBA mode (e.g., 520-byte
831 # sectors), we usually need to add the lba data size and metadata size
832 # together for fio's bs parameter. However, if pi_act == 1 and the
833 # device is formatted so that the metadata is the same size as the PI,
834 # then the device will take care of everything and the application
835 # should just use regular power of 2 lba data size even when the device
836 # is in extended lba mode.
838 if not test['fio_opts']['pi_act'] or lbaf['ms'] != lbaf['pi_bytes']:
839 blocksize += lbaf['ms']
840 test['fio_opts']['md_per_io_size'] = 0
842 # If we are using a separate buffer for metadata, fio doesn't need to
843 # do anything when pi_act==1 and protection information size is equal to
844 # metadata size since the device is taking care of it all. If either of
845 # the two conditions do not hold, then we do need to allocate a
846 # separate metadata buffer.
847 if test['fio_opts']['pi_act'] and lbaf['ms'] == lbaf['pi_bytes']:
848 test['fio_opts']['md_per_io_size'] = 0
850 test['fio_opts']['md_per_io_size'] = lbaf['ms'] * test['bs_high']
852 test['fio_opts']['bsrange'] = f"{blocksize * test['bs_low']}-{blocksize * test['bs_high']}"
853 if 'mdsize_adjustment' in test:
854 test['fio_opts']['md_per_io_size'] += test['mdsize_adjustment']
856 # Set fio pi_chk parameter at runtime. If the device is formatted
857 # with Type 3 protection information, this means that the reference
858 # tag is not checked and I/O commands may throw an error if they
859 # are submitted with the REFTAG bit set in pi_chk. Make sure fio
860 # does not set pi_chk's REFTAG bit if the device is formatted with
863 if pitype == 3 and 'REFTAG' in test['pi_chk']:
864 test['fio_opts']['pi_chk'] = test['pi_chk'].replace('REFTAG','')
865 logging.debug("Type 3 PI: dropping REFTAG bit")
867 test['fio_opts']['pi_chk'] = test['pi_chk']
870 if pitype == 3 and 'type3' in test['skip']:
871 test['force_skip'] = True
872 logging.debug("Type 3 PI: skipping test case")
873 if elba and 'elba' in test['skip']:
874 test['force_skip'] = True
875 logging.debug("extended lba format: skipping test case")
877 logging.debug("Test %d: pi_act=%d, bsrange=%s, md_per_io_size=%d", test['test_id'],
878 test['fio_opts']['pi_act'], test['fio_opts']['bsrange'],
879 test['fio_opts']['md_per_io_size'])
881 return run_fio_tests(TEST_LIST, test_env, args)
886 Run tests using fio's io_uring_cmd ioengine to exercise end-to-end data
887 protection capabilities.
893 logging.basicConfig(level=logging.DEBUG)
895 logging.basicConfig(level=logging.INFO)
897 artifact_root = args.artifact_root if args.artifact_root else \
898 f"nvmept_pi-test-{time.strftime('%Y%m%d-%H%M%S')}"
899 os.mkdir(artifact_root)
900 print(f"Artifact directory is {artifact_root}")
903 fio_path = str(Path(args.fio).absolute())
906 print(f"fio path is {fio_path}")
908 lbaf_list = get_lbafs(args)
909 get_guard_pi(lbaf_list, args)
910 caps = get_capabilities(args)
911 print("Device capabilities:", caps)
913 for test in TEST_LIST:
914 test['fio_opts']['filename'] = args.dut
915 test['fio_opts']['ioengine'] = args.ioengine
918 'fio_path': fio_path,
919 'fio_root': str(Path(__file__).absolute().parent.parent),
920 'artifact_root': artifact_root,
921 'basename': 'nvmept_pi',
924 total = { 'passed': 0, 'failed': 0, 'skipped': 0 }
927 for lbaf, pil, pitype, elba in itertools.product(lbaf_list, caps['pil'], caps['pitype'],
929 print(f"\nlbaf: {lbaf}, pil: {pil}, pitype: {pitype}, elba: {elba}")
931 if not format_device(args, lbaf, pitype, pil, elba):
934 test_env['artifact_root'] = \
935 os.path.join(artifact_root, f"lbaf{lbaf['lbaf']}pil{pil}pitype{pitype}" \
937 os.mkdir(test_env['artifact_root'])
939 passed, failed, skipped = difdix_test(test_env, args, lbaf, pitype, elba)
941 total['passed'] += passed
942 total['failed'] += failed
943 total['skipped'] += skipped
944 except KeyboardInterrupt:
947 print(f"\n\n{total['passed']} test(s) passed, {total['failed']} failed, " \
948 f"{total['skipped']} skipped")
949 sys.exit(total['failed'])
952 if __name__ == '__main__':