From 51014c50254db5e90b56ad4580185f00899f2b5a Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Tue, 28 Apr 2026 11:31:26 +0000 Subject: [PATCH 01/10] nvme: add O_TRUNC when opening output files for read commands The file might already exist, thus in the case make sure it's truncated to zero before writing to it. Signed-off-by: Daniel Wagner --- nvme.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nvme.c b/nvme.c index 4e44aba838..5dce93cd4c 100644 --- a/nvme.c +++ b/nvme.c @@ -2256,7 +2256,7 @@ static int io_mgmt_recv(int argc, char **argv, struct command *acmd, struct plug cfg.nsid); if (cfg.file) { - dfd = open(cfg.file, O_WRONLY | O_CREAT, 0644); + dfd = open(cfg.file, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (dfd < 0) { nvme_show_perror(cfg.file); return -errno; @@ -8485,7 +8485,7 @@ static int submit_io(int opcode, char *command, const char *desc, int argc, char flags = O_RDONLY; } else { dfd = mfd = STDOUT_FILENO; - flags = O_WRONLY | O_CREAT; + flags = O_WRONLY | O_CREAT | O_TRUNC; } if (strlen(cfg.data)) { @@ -9363,7 +9363,7 @@ static int passthru(int argc, char **argv, bool admin, if (cfg.opcode & 0x02) { cfg.read = true; - flags = O_WRONLY | O_CREAT; + flags = O_WRONLY | O_CREAT | O_TRUNC; dfd = mfd = STDOUT_FILENO; } @@ -10483,7 +10483,7 @@ static int libnvme_mi(int argc, char **argv, __u8 admin_opcode, const char *desc fd = STDIN_FILENO; send = true; } else { - flags = O_WRONLY | O_CREAT; + flags = O_WRONLY | O_CREAT | O_TRUNC; fd = STDOUT_FILENO; send = false; } From 91be90e52cfe1ab7d16d7e77318aaba9fa144a0a Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Mon, 27 Apr 2026 22:28:21 +0200 Subject: [PATCH 02/10] feat: add support for Host Behavior Support Implement Host Behavior Support in the feat plugin to enable its use in the nvme_copy_test test case, which currently relies on parsing raw log pages. Signed-off-by: Daniel Wagner --- plugins/feat/feat-nvme.c | 98 ++++++++++++++++++++++++++++++++++++++++ plugins/feat/feat-nvme.h | 2 + 2 files changed, 100 insertions(+) diff --git a/plugins/feat/feat-nvme.c b/plugins/feat/feat-nvme.c index 61b23adfa4..a0d934d95e 100644 --- a/plugins/feat/feat-nvme.c +++ b/plugins/feat/feat-nvme.c @@ -47,6 +47,15 @@ struct err_recovery_config { __u8 sel; }; +struct host_behavior_config { + __u8 acre; + __u8 etdas; + __u8 lbafee; + __u8 hdisns; + __u16 cdfe; + __u8 sel; +}; + static const char *power_mgmt_feat = "power management feature"; static const char *sel = "[0-3]: current/default/saved/supported"; static const char *save = "Specifies that the controller shall save the attribute"; @@ -61,6 +70,7 @@ static const char *power_thresh_feat = "power threshold feature"; static const char *power_meas_feat = "power measurement feature"; static const char *err_recovery_feat = "error recovery feature"; static const char *num_queues_feat = "number of queues feature"; +static const char *host_behavior_feat = "host behavior support feature"; static int feat_get_nsid(struct libnvme_transport_handle *hdl, __u32 nsid, const __u8 fid, __u32 cdw11, __u8 sel, __u8 uidx, @@ -975,3 +985,91 @@ static int feat_num_queues(int argc, char **argv, struct command *acmd, return err; } + +static int host_behavior_set(struct libnvme_transport_handle *hdl, __u8 fid, + struct argconfig_commandline_options *opts, + struct host_behavior_config *cfg, bool sv) +{ + enum nvme_get_features_sel gsel = NVME_GET_FEATURES_SEL_CURRENT; + struct libnvme_passthru_cmd cmd; + struct nvme_feat_host_behavior data = { 0 }; + int err; + + if (sv) + gsel = NVME_GET_FEATURES_SEL_SAVED; + + nvme_init_get_features_host_behavior(&cmd, gsel, &data); + err = libnvme_submit_admin_passthru(hdl, &cmd); + if (err) { + nvme_show_err(err, "Get %s", host_behavior_feat); + return err; + } + + if (argconfig_parse_seen(opts, "acre")) + data.acre = cfg->acre; + if (argconfig_parse_seen(opts, "etdas")) + data.etdas = cfg->etdas; + if (argconfig_parse_seen(opts, "lbafee")) + data.lbafee = cfg->lbafee; + if (argconfig_parse_seen(opts, "hdisns")) + data.hdisns = cfg->hdisns; + if (argconfig_parse_seen(opts, "cdfe")) + data.cdfe = cpu_to_le16(cfg->cdfe); + + nvme_init_set_features_host_behavior(&cmd, sv, &data); + err = libnvme_submit_admin_passthru(hdl, &cmd); + if (err) { + nvme_show_err(err, "Set %s", host_behavior_feat); + return err; + } + + nvme_show_init(); + + nvme_show_result("Set %s: (%s)", host_behavior_feat, + sv ? "Save" : "Not save"); + nvme_feature_show_fields(fid, 0, (unsigned char *)&data); + + nvme_show_finish(); + + return err; +} + +static int feat_host_behavior_support(int argc, char **argv, struct command *acmd, + struct plugin *plugin) +{ + const char *acre_desc = "Advanced Command Retry Enable (0 or 1)"; + const char *etdas_desc = "Extended Telemetry Data Area 4 Supported (0 or 1)"; + const char *lbafee_desc = "LBA Format Extension Enable (0 or 1)"; + const char *hdisns_desc = "Host Dispersed Namespace Support (0 or 1)"; + const char *cdfe_desc = "Copy Descriptor Formats Enable bitmask"; + const __u8 fid = NVME_FEAT_FID_HOST_BEHAVIOR; + + __cleanup_nvme_global_ctx struct libnvme_global_ctx *ctx = NULL; + __cleanup_nvme_transport_handle struct libnvme_transport_handle *hdl = NULL; + int err; + + struct host_behavior_config cfg = { 0 }; + + FEAT_ARGS(opts, + OPT_BYTE("acre", 'a', &cfg.acre, acre_desc), + OPT_BYTE("etdas", 'e', &cfg.etdas, etdas_desc), + OPT_BYTE("lbafee", 'l', &cfg.lbafee, lbafee_desc), + OPT_BYTE("hdisns", 'H', &cfg.hdisns, hdisns_desc), + OPT_SHRT("cdfe", 'c', &cfg.cdfe, cdfe_desc)); + + err = parse_and_open(&ctx, &hdl, argc, argv, HOST_BEHAVIOR_DESC, opts); + if (err) + return err; + + if (argconfig_parse_seen(opts, "acre") || + argconfig_parse_seen(opts, "etdas") || + argconfig_parse_seen(opts, "lbafee") || + argconfig_parse_seen(opts, "hdisns") || + argconfig_parse_seen(opts, "cdfe")) + err = host_behavior_set(hdl, fid, opts, &cfg, + argconfig_parse_seen(opts, "save")); + else + err = feat_get(hdl, fid, 0, cfg.sel, 0, host_behavior_feat); + + return err; +} diff --git a/plugins/feat/feat-nvme.h b/plugins/feat/feat-nvme.h index 259d1c5621..50c1352523 100644 --- a/plugins/feat/feat-nvme.h +++ b/plugins/feat/feat-nvme.h @@ -22,6 +22,7 @@ #define POWER_MEAS_DESC "Get and set power measurement feature" #define ERR_RECOVERY_DESC "Get and set error recovery feature" #define NUM_QUEUES_DESC "Get and set number of queues feature" +#define HOST_BEHAVIOR_DESC "Get and set host behavior support feature" #define FEAT_ARGS(n, ...) \ NVME_ARGS(n, ##__VA_ARGS__, OPT_FLAG("save", 's', NULL, save), \ @@ -41,6 +42,7 @@ PLUGIN(NAME("feat", "NVMe feature extensions", FEAT_PLUGIN_VERSION), ENTRY("power-meas", POWER_MEAS_DESC, feat_power_meas) ENTRY("err-recovery", ERR_RECOVERY_DESC, feat_err_recovery) ENTRY("num-queues", NUM_QUEUES_DESC, feat_num_queues) + ENTRY("host-behavior-support", HOST_BEHAVIOR_DESC, feat_host_behavior_support) ) ); #endif /* !FEAT_NVME || CMD_HEADER_MULTI_READ */ From fe767b85b7c667b41b82b8b7f47d9a4cdfd1cbf4 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Mon, 27 Apr 2026 20:02:44 +0000 Subject: [PATCH 03/10] tests: use feat command to handle host behavior Replace the the raw output decoding of the Host Behavior Support feature with the new 'nvme feat host-behavior-support' command. Signed-off-by: Daniel Wagner --- tests/nvme_copy_test.py | 49 ++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/tests/nvme_copy_test.py b/tests/nvme_copy_test.py index f7c8661bc2..247c20bf35 100644 --- a/tests/nvme_copy_test.py +++ b/tests/nvme_copy_test.py @@ -16,7 +16,7 @@ """ -import base64 +import json from nvme_test import TestNVMe @@ -27,7 +27,7 @@ class TestNVMeCopy(TestNVMe): Represents NVMe Copy testcase. - Attributes: - ocfs : optional copy formats supported - - host_behavior_data : host behavior support data to restore during teardown + - original_cdfe : saved cdfe value to restore during teardown, or None - test_log_dir : directory for logs, temp files. """ @@ -35,27 +35,31 @@ def setUp(self): """ Pre Section for TestNVMeCopy """ super().setUp() self.ocfs = self.get_ocfs() - self.host_behavior_data = None + self.original_cdfe = None cross_namespace_copy = self.ocfs & 0xc if cross_namespace_copy: - # get host behavior support data - get_features_cmd = f"{self.nvme_bin} get-feature {self.ctrl} " + \ - "--feature-id=0x16 --data-len=512 --raw-binary" + get_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ + f"{self.ctrl} --output-format=json" result = self.run_cmd(get_features_cmd) - err = result.returncode - self.assertEqual(err, 0, "ERROR : nvme get-feature failed") - self.host_behavior_data = result.stdout - # enable cross-namespace copy formats - if int.from_bytes(base64.b64decode(self.host_behavior_data[4])) & cross_namespace_copy: - # skip if already enabled + self.assertEqual(result.returncode, 0, + "ERROR : nvme feat host-behavior-support failed") + data = json.loads(result.stdout) + fields = data.get("Feature: 0x16", [{}])[0] + current_cdfe = ( + (0x4 if fields.get("Copy Descriptor Format 2h Enable (CDF2E)") == "True" else 0) | + (0x8 if fields.get("Copy Descriptor Format 3h Enable (CDF3E)") == "True" else 0) | + (0x10 if fields.get("Copy Descriptor Format 4h Enable (CDF4E)") == "True" else 0) + ) + if current_cdfe & cross_namespace_copy: print("Cross-namespace copy already enabled, skipping set-features") - self.host_behavior_data = None else: - data = self.host_behavior_data[:4] + str(cross_namespace_copy.to_bytes(2, 'little')) + self.host_behavior_data[6:] - set_features_cmd = f"{self.nvme_bin} set-feature " + \ - f"{self.ctrl} --feature-id=0x16 --data-len=512" - result = self.run_cmd(set_features_cmd, stdin_data=data) - self.assertEqual(result.returncode, 0, "Failed to enable cross-namespace copy formats") + self.original_cdfe = current_cdfe + new_cdfe = current_cdfe | cross_namespace_copy + set_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ + f"{self.ctrl} --cdfe={new_cdfe}" + result = self.run_cmd(set_features_cmd) + self.assertEqual(result.returncode, 0, + "Failed to enable cross-namespace copy formats") get_ns_id_cmd = f"{self.nvme_bin} get-ns-id {self.ns1}" result = self.run_cmd(get_ns_id_cmd) err = result.returncode @@ -66,11 +70,10 @@ def setUp(self): def tearDown(self): """ Post Section for TestNVMeCopy """ - if self.host_behavior_data: - # restore saved host behavior support data - set_features_cmd = f"{self.nvme_bin} set-feature {self.ctrl} " + \ - "--feature-id=0x16 --data-len=512" - self.run_cmd(set_features_cmd, stdin_data=self.host_behavior_data) + if self.original_cdfe is not None: + set_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ + f"{self.ctrl} --cdfe={self.original_cdfe}" + self.run_cmd(set_features_cmd) super().tearDown() def copy(self, sdlba, blocks, slbs, **kwargs): From ff0dffa033af12addea20c147680bf38b25694ef Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Tue, 28 Apr 2026 07:47:56 +0000 Subject: [PATCH 04/10] tests: fix ms byte decoding get_lba_format_size incorrectly treats the 'ms' field as a power-of-2 exponent. Signed-off-by: Daniel Wagner --- tests/nvme_test.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index fabfb47cc0..d071547876 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -315,14 +315,9 @@ def get_lba_format_size(self): self.assertTrue(len(json_output['lbafs']) > self.flbas, "Error : could not match the given flbas to an existing lbaf") lbaf_json = json_output['lbafs'][int(self.flbas)] - ms_expo = int(lbaf_json['ms']) + ms = int(lbaf_json['ms']) ds_expo = int(lbaf_json['ds']) - ds = 0 - ms = 0 - if ds_expo > 0: - ds = (1 << ds_expo) - if ms_expo > 0: - ms = (1 << ms_expo) + ds = (1 << ds_expo) if ds_expo > 0 else 0 return (ds, ms) def get_ncap(self): From 61290acd06c986c482a8ca07b88c37697a394e75 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Mon, 27 Apr 2026 19:35:55 +0000 Subject: [PATCH 05/10] tests: split copy test into subtests Report the result of each individual test instead of combining them. Signed-off-by: Daniel Wagner --- tests/nvme_copy_test.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/nvme_copy_test.py b/tests/nvme_copy_test.py index 247c20bf35..025219e999 100644 --- a/tests/nvme_copy_test.py +++ b/tests/nvme_copy_test.py @@ -76,6 +76,11 @@ def tearDown(self): self.run_cmd(set_features_cmd) super().tearDown() + def _check_format_supported(self, desc_format): + """ Skip test if the given copy descriptor format is not supported """ + if not self.ocfs & (1 << desc_format): + self.skipTest(f"descriptor format {desc_format} is not supported") + def copy(self, sdlba, blocks, slbs, **kwargs): """ Wrapper for nvme copy - Args: @@ -88,11 +93,7 @@ def copy(self, sdlba, blocks, slbs, **kwargs): - Returns: - None """ - # skip if descriptor format not supported (default format is 0) desc_format = kwargs.get("descriptor_format", 0) - if not self.ocfs & (1 << desc_format): - print(f"Skip copy because descriptor format {desc_format} is not supported") - return # build copy command copy_cmd = f"{self.nvme_bin} copy {self.ns1} " + \ f"--format={desc_format} --sdlba={sdlba} --blocks={blocks} " + \ @@ -104,11 +105,32 @@ def copy(self, sdlba, blocks, slbs, **kwargs): # run and assert success self.assertEqual(self.exec_cmd(copy_cmd), 0) - def test_copy(self): - """ Testcase main """ + def test_copy_format_0(self): + """ Test copy with descriptor format 0 """ + self._check_format_supported(0) self.copy(0, 1, 2, descriptor_format=0) + + def test_copy_format_1(self): + """ Test copy with descriptor format 1 """ + self._check_format_supported(1) self.copy(0, 1, 2, descriptor_format=1) + + def test_copy_format_2(self): + """ Test copy with descriptor format 2 """ + self._check_format_supported(2) self.copy(0, 1, 2, descriptor_format=2, snsids=self.ns1_nsid) + + def test_copy_format_2_sopts(self): + """ Test copy with descriptor format 2 and source options """ + self._check_format_supported(2) self.copy(0, 1, 2, descriptor_format=2, snsids=self.ns1_nsid, sopts=0) + + def test_copy_format_3(self): + """ Test copy with descriptor format 3 """ + self._check_format_supported(3) self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid) + + def test_copy_format_3_sopts(self): + """ Test copy with descriptor format 3 and source options """ + self._check_format_supported(3) self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid, sopts=0) From 4755d0c016641b50ebead6c3084f53a7e1e71eb1 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Mon, 27 Apr 2026 19:45:14 +0000 Subject: [PATCH 06/10] tests: add mcl/mssrl/msrc limit checks to nvme_copy_test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The copy test can only run with valid mcl, mssrl, and msrc values. Therefore, check that they are non-zero. QEMU’s default namespace settings set these values to 0. The user must set them explicitly, e.g -device nvme-ns,drive=ns0,nsid=1,mcl=1024,mssrl=1024,msrc=63 Signed-off-by: Daniel Wagner --- tests/nvme_copy_test.py | 19 ++++++++++++++++++- tests/nvme_test.py | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/nvme_copy_test.py b/tests/nvme_copy_test.py index 025219e999..dc8d979e05 100644 --- a/tests/nvme_copy_test.py +++ b/tests/nvme_copy_test.py @@ -18,7 +18,7 @@ import json -from nvme_test import TestNVMe +from nvme_test import TestNVMe, to_decimal class TestNVMeCopy(TestNVMe): @@ -36,6 +36,9 @@ def setUp(self): super().setUp() self.ocfs = self.get_ocfs() self.original_cdfe = None + self.mcl = to_decimal(self.get_id_ns_field_value("mcl")) + self.mssrl = to_decimal(self.get_id_ns_field_value("mssrl")) + self.msrc = to_decimal(self.get_id_ns_field_value("msrc")) cross_namespace_copy = self.ocfs & 0xc if cross_namespace_copy: get_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ @@ -81,6 +84,14 @@ def _check_format_supported(self, desc_format): if not self.ocfs & (1 << desc_format): self.skipTest(f"descriptor format {desc_format} is not supported") + def _check_ns_copy_limits(self): + """ Skip test if namespace copy limits (mcl, mssrl, msrc) are not set """ + missing = [name for name, val in + [("mcl", self.mcl), ("mssrl", self.mssrl), ("msrc", self.msrc)] + if val == 0] + if missing: + self.skipTest(f"{', '.join(missing)} are 0, copy not supported on this namespace") + def copy(self, sdlba, blocks, slbs, **kwargs): """ Wrapper for nvme copy - Args: @@ -108,29 +119,35 @@ def copy(self, sdlba, blocks, slbs, **kwargs): def test_copy_format_0(self): """ Test copy with descriptor format 0 """ self._check_format_supported(0) + self._check_ns_copy_limits() self.copy(0, 1, 2, descriptor_format=0) def test_copy_format_1(self): """ Test copy with descriptor format 1 """ self._check_format_supported(1) + self._check_ns_copy_limits() self.copy(0, 1, 2, descriptor_format=1) def test_copy_format_2(self): """ Test copy with descriptor format 2 """ self._check_format_supported(2) + self._check_ns_copy_limits() self.copy(0, 1, 2, descriptor_format=2, snsids=self.ns1_nsid) def test_copy_format_2_sopts(self): """ Test copy with descriptor format 2 and source options """ self._check_format_supported(2) + self._check_ns_copy_limits() self.copy(0, 1, 2, descriptor_format=2, snsids=self.ns1_nsid, sopts=0) def test_copy_format_3(self): """ Test copy with descriptor format 3 """ self._check_format_supported(3) + self._check_ns_copy_limits() self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid) def test_copy_format_3_sopts(self): """ Test copy with descriptor format 3 and source options """ self._check_format_supported(3) + self._check_ns_copy_limits() self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid, sopts=0) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index d071547876..c50ad9ff47 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -345,6 +345,22 @@ def get_id_ctrl_field_value(self, field): f"ERROR : reading field '{field}' failed") return str(json_output[field]) + def get_id_ns_field_value(self, field): + """ Wrapper for extracting id-ns field values + - Args: + - field : field name to extract + - Returns: + - Field value of the given field as a string + """ + id_ns_cmd = f"{self.nvme_bin} id-ns {self.ns1} " + \ + "--output-format=json" + result = self.run_cmd(id_ns_cmd) + self.assertEqual(result.returncode, 0, "ERROR : reading id-ns failed") + json_output = json.loads(result.stdout) + self.assertTrue(field in json_output, + f"ERROR : reading field '{field}' failed") + return str(json_output[field]) + def get_ocfs(self): """ Wrapper for extracting optional copy formats supported - Args: From 0900b81e2b79150f27ed88605676c7764f11fe82 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Mon, 27 Apr 2026 20:59:11 +0000 Subject: [PATCH 07/10] tests: only setup cdfe for requested descriptor version The setUp method was enabling all cross-namespace cdfe bits (CDF2E | CDF3E = 12) for every test, including format 0 and 1. Only enable support necessary descriptor version used in the test. Signed-off-by: Daniel Wagner --- tests/nvme_copy_test.py | 57 ++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/tests/nvme_copy_test.py b/tests/nvme_copy_test.py index dc8d979e05..dca31d7647 100644 --- a/tests/nvme_copy_test.py +++ b/tests/nvme_copy_test.py @@ -39,30 +39,6 @@ def setUp(self): self.mcl = to_decimal(self.get_id_ns_field_value("mcl")) self.mssrl = to_decimal(self.get_id_ns_field_value("mssrl")) self.msrc = to_decimal(self.get_id_ns_field_value("msrc")) - cross_namespace_copy = self.ocfs & 0xc - if cross_namespace_copy: - get_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ - f"{self.ctrl} --output-format=json" - result = self.run_cmd(get_features_cmd) - self.assertEqual(result.returncode, 0, - "ERROR : nvme feat host-behavior-support failed") - data = json.loads(result.stdout) - fields = data.get("Feature: 0x16", [{}])[0] - current_cdfe = ( - (0x4 if fields.get("Copy Descriptor Format 2h Enable (CDF2E)") == "True" else 0) | - (0x8 if fields.get("Copy Descriptor Format 3h Enable (CDF3E)") == "True" else 0) | - (0x10 if fields.get("Copy Descriptor Format 4h Enable (CDF4E)") == "True" else 0) - ) - if current_cdfe & cross_namespace_copy: - print("Cross-namespace copy already enabled, skipping set-features") - else: - self.original_cdfe = current_cdfe - new_cdfe = current_cdfe | cross_namespace_copy - set_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ - f"{self.ctrl} --cdfe={new_cdfe}" - result = self.run_cmd(set_features_cmd) - self.assertEqual(result.returncode, 0, - "Failed to enable cross-namespace copy formats") get_ns_id_cmd = f"{self.nvme_bin} get-ns-id {self.ns1}" result = self.run_cmd(get_ns_id_cmd) err = result.returncode @@ -92,6 +68,35 @@ def _check_ns_copy_limits(self): if missing: self.skipTest(f"{', '.join(missing)} are 0, copy not supported on this namespace") + def _enable_cdfe_for_format(self, desc_format): + """ Enable the host-behavior-support cdfe bit for the given cross-namespace format. + Only the bit corresponding to desc_format is enabled; other bits are left unchanged. + The original value is saved in self.original_cdfe for tearDown to restore. + """ + cdfe_bit = 1 << desc_format + get_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ + f"{self.ctrl} --output-format=json" + result = self.run_cmd(get_features_cmd) + self.assertEqual(result.returncode, 0, + "ERROR : nvme feat host-behavior-support failed") + data = json.loads(result.stdout) + fields = data.get("Feature: 0x16", [{}])[0] + current_cdfe = ( + (0x4 if fields.get("Copy Descriptor Format 2h Enable (CDF2E)") == "True" else 0) | + (0x8 if fields.get("Copy Descriptor Format 3h Enable (CDF3E)") == "True" else 0) | + (0x10 if fields.get("Copy Descriptor Format 4h Enable (CDF4E)") == "True" else 0) + ) + if current_cdfe & cdfe_bit: + return + if self.original_cdfe is None: + self.original_cdfe = current_cdfe + new_cdfe = current_cdfe | cdfe_bit + set_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ + f"{self.ctrl} --cdfe={new_cdfe}" + result = self.run_cmd(set_features_cmd) + self.assertEqual(result.returncode, 0, + f"Failed to enable cdfe bit {cdfe_bit:#x} for format {desc_format}") + def copy(self, sdlba, blocks, slbs, **kwargs): """ Wrapper for nvme copy - Args: @@ -132,22 +137,26 @@ def test_copy_format_2(self): """ Test copy with descriptor format 2 """ self._check_format_supported(2) self._check_ns_copy_limits() + self._enable_cdfe_for_format(2) self.copy(0, 1, 2, descriptor_format=2, snsids=self.ns1_nsid) def test_copy_format_2_sopts(self): """ Test copy with descriptor format 2 and source options """ self._check_format_supported(2) self._check_ns_copy_limits() + self._enable_cdfe_for_format(2) self.copy(0, 1, 2, descriptor_format=2, snsids=self.ns1_nsid, sopts=0) def test_copy_format_3(self): """ Test copy with descriptor format 3 """ self._check_format_supported(3) self._check_ns_copy_limits() + self._enable_cdfe_for_format(3) self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid) def test_copy_format_3_sopts(self): """ Test copy with descriptor format 3 and source options """ self._check_format_supported(3) self._check_ns_copy_limits() + self._enable_cdfe_for_format(3) self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid, sopts=0) From 60a8a69aebd05bdb08492d7bd1802afd1d61b1d8 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Mon, 27 Apr 2026 21:18:11 +0000 Subject: [PATCH 08/10] tests: check and format namespace for copy test Copy Descriptor Formats 1h and 3h require the source namespace to be formatted with 64-bit guard protection information (pif=2 in the extended LBA format entry per nvm-id-ns). When the namespace uses a standard 16-bit guard LBA format the controller returns "Invalid Format" (0x410a). Signed-off-by: Daniel Wagner --- tests/nvme_copy_test.py | 185 ++++++++++++++++++++++++++++++++++------ 1 file changed, 160 insertions(+), 25 deletions(-) diff --git a/tests/nvme_copy_test.py b/tests/nvme_copy_test.py index dca31d7647..616850e94d 100644 --- a/tests/nvme_copy_test.py +++ b/tests/nvme_copy_test.py @@ -10,9 +10,15 @@ """ NVMe Copy Testcase:- - 1. Issue copy command on set of block; shall pass. - 2. If cross-namespace copy formats are supported, enable and test - cross-namespace copy formats. + Test classes are split by descriptor format group: + + TestNVMeCopyFormat0 - Descriptor Format 0 (16-bit guard, in-namespace copy). + TestNVMeCopyFormat1 - Descriptor Format 1 (64-bit guard, in-namespace copy). + The namespace is reformatted to a 64-bit guard LBA + format before the test runs. + TestNVMeCopyFormat23 - Descriptor Formats 2 and 3 (cross-namespace copy). + Format 3 additionally requires a 64-bit guard namespace + and reformats before running. """ @@ -24,11 +30,17 @@ class TestNVMeCopy(TestNVMe): """ - Represents NVMe Copy testcase. + Base class for NVMe Copy tests. + + Provides shared setUp/tearDown and helper methods used by all copy test + subclasses. - Attributes: - - ocfs : optional copy formats supported - - original_cdfe : saved cdfe value to restore during teardown, or None - - test_log_dir : directory for logs, temp files. + - ocfs : optional copy formats supported (from id-ctrl) + - original_cdfe : saved cdfe value restored in tearDown, or None + - mcl : Maximum Copy Length (blocks) + - mssrl : Maximum Single Source Range Length (blocks) + - msrc : Maximum Source Range Count + - ns1_nsid : numeric namespace ID of self.ns1 """ def setUp(self): @@ -36,15 +48,11 @@ def setUp(self): super().setUp() self.ocfs = self.get_ocfs() self.original_cdfe = None - self.mcl = to_decimal(self.get_id_ns_field_value("mcl")) - self.mssrl = to_decimal(self.get_id_ns_field_value("mssrl")) - self.msrc = to_decimal(self.get_id_ns_field_value("msrc")) + self._refresh_ns_copy_limits() get_ns_id_cmd = f"{self.nvme_bin} get-ns-id {self.ns1}" result = self.run_cmd(get_ns_id_cmd) - err = result.returncode - self.assertEqual(err, 0, "ERROR : nvme get-ns-id failed") - output = result.stdout - self.ns1_nsid = int(output.strip().split(':')[-1]) + self.assertEqual(result.returncode, 0, "ERROR : nvme get-ns-id failed") + self.ns1_nsid = int(result.stdout.strip().split(':')[-1]) self.setup_log_dir(self.__class__.__name__) def tearDown(self): @@ -55,6 +63,16 @@ def tearDown(self): self.run_cmd(set_features_cmd) super().tearDown() + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _refresh_ns_copy_limits(self): + """ Read MCL, MSSRL, MSRC from the current namespace into instance attrs """ + self.mcl = to_decimal(self.get_id_ns_field_value("mcl")) + self.mssrl = to_decimal(self.get_id_ns_field_value("mssrl")) + self.msrc = to_decimal(self.get_id_ns_field_value("msrc")) + def _check_format_supported(self, desc_format): """ Skip test if the given copy descriptor format is not supported """ if not self.ocfs & (1 << desc_format): @@ -68,10 +86,81 @@ def _check_ns_copy_limits(self): if missing: self.skipTest(f"{', '.join(missing)} are 0, copy not supported on this namespace") + def _find_64b_guard_lbaf_index(self): + """ + Search the nvm-id-ns elbafs for a format with 64-bit guard PI (pif == 2). + + Returns the lbaf index (0-based position in the lbafs[] array), or None + if no such format exists or the nvm-id-ns command is not supported. + """ + nvm_id_ns_cmd = f"{self.nvme_bin} nvm-id-ns {self.ns1} --output-format=json" + result = self.run_cmd(nvm_id_ns_cmd) + if result.returncode != 0: + return None + elbafs = json.loads(result.stdout).get("elbafs", []) + for i, elbaf in enumerate(elbafs): + if elbaf.get("pif", 0) == 2: # NVME_NVM_PIF_64B_GUARD = 2 + return i + return None + + def _create_ns_with_lbaf(self, lbaf_index): + """ + Delete and recreate the default namespace using the given lbaf_index. + + The lbaf_index is encoded into the flbas byte per NVMe spec: + flbas[3:0] = lbaf_index[3:0], flbas[6:5] = lbaf_index[5:4] + + After recreating, self.mcl/mssrl/msrc are refreshed from the new + namespace. Calls skipTest if namespace management is not supported + or if the create/attach step fails. + """ + if not self.ns_mgmt_supported: + self.skipTest("namespace management not supported; cannot reformat namespace") + + # encode lbaf_index into the 8-bit flbas field + flbas = (lbaf_index & 0xF) | (((lbaf_index >> 4) & 0x3) << 5) + + # get_lba_format_size() in the parent class indexes the id-ns lbafs[] array + # using self.flbas, so set it to lbaf_index here for the size look-up. + # This is intentional: lbaf_index is the direct array position, while flbas + # (computed above) is the encoded byte passed to create-ns --flbas. + self.flbas = lbaf_index + (ds, ms) = self.get_lba_format_size() + if ds == 0: + self.skipTest(f"lbaf {lbaf_index} reports zero data size; cannot create namespace") + ncap = int(self.get_ncap() / (ds + ms)) + + ctrl_id = self.get_ctrl_id() + self.delete_all_ns() + err = self.create_and_validate_ns(self.default_nsid, ncap, ncap, flbas, 0) + self.assertEqual(err, 0, + f"ERROR: failed to create namespace with lbaf {lbaf_index} (flbas={flbas:#x})") + self.assertEqual(self.attach_ns(ctrl_id, self.default_nsid), 0, + "ERROR: failed to attach reformatted namespace") + + # refresh copy limits for the new namespace + self._refresh_ns_copy_limits() + + def _setup_64b_guard_ns(self): + """ + Reformat the default namespace to a 64-bit guard PI LBA format (pif == 2). + + Skips the test if: + - namespace management is not supported, or + - no LBA format with 64-bit guard PI exists on this controller. + """ + lbaf_index = self._find_64b_guard_lbaf_index() + if lbaf_index is None: + self.skipTest("no LBA format with 64-bit guard PI (pif=2) found; " + "cannot run copy descriptor format 1/3 test") + self._create_ns_with_lbaf(lbaf_index) + def _enable_cdfe_for_format(self, desc_format): - """ Enable the host-behavior-support cdfe bit for the given cross-namespace format. - Only the bit corresponding to desc_format is enabled; other bits are left unchanged. - The original value is saved in self.original_cdfe for tearDown to restore. + """ + Enable the host-behavior-support cdfe bit for the given cross-namespace + copy descriptor format. Only the single required bit is enabled; other + bits are left unchanged. The original value is saved in self.original_cdfe + for tearDown to restore. """ cdfe_bit = 1 << desc_format get_features_cmd = f"{self.nvme_bin} feat host-behavior-support " + \ @@ -110,7 +199,6 @@ def copy(self, sdlba, blocks, slbs, **kwargs): - None """ desc_format = kwargs.get("descriptor_format", 0) - # build copy command copy_cmd = f"{self.nvme_bin} copy {self.ns1} " + \ f"--format={desc_format} --sdlba={sdlba} --blocks={blocks} " + \ f"--slbs={slbs}" @@ -118,21 +206,72 @@ def copy(self, sdlba, blocks, slbs, **kwargs): copy_cmd += f" --snsids={kwargs['snsids']}" if "sopts" in kwargs: copy_cmd += f" --sopts={kwargs['sopts']}" - # run and assert success self.assertEqual(self.exec_cmd(copy_cmd), 0) + +class TestNVMeCopyFormat0(TestNVMeCopy): + + """ + NVMe Copy tests using Descriptor Format 0. + + Format 0 uses 16-bit guard PI and copies within a single namespace. + No special namespace formatting is required. + """ + def test_copy_format_0(self): """ Test copy with descriptor format 0 """ self._check_format_supported(0) self._check_ns_copy_limits() self.copy(0, 1, 2, descriptor_format=0) + +class TestNVMeCopyFormat1(TestNVMeCopy): + + """ + NVMe Copy tests using Descriptor Format 1. + + Format 1 uses 64-bit guard PI and copies within a single namespace. + setUp reformats the namespace to a 64-bit guard LBA format; the test is + skipped if no such format is available or namespace management is not + supported. + """ + + def setUp(self): + """ Pre Section for TestNVMeCopyFormat1 """ + super().setUp() + self._setup_64b_guard_ns() + def test_copy_format_1(self): """ Test copy with descriptor format 1 """ self._check_format_supported(1) self._check_ns_copy_limits() self.copy(0, 1, 2, descriptor_format=1) + +class TestNVMeCopyFormat23(TestNVMeCopy): + + """ + NVMe Copy tests using Descriptor Formats 2 and 3. + + Formats 2 and 3 perform cross-namespace copy operations. + Format 2 uses 16-bit guard PI and works with the default namespace. + Format 3 uses 64-bit guard PI; those tests reformat the namespace inline + (rather than in setUp) so that format 2 tests can still use the standard + namespace. + """ + + def _run_format_3_copy(self, **kwargs): + """ + Reformat the namespace to 64-bit guard PI, check copy limits, enable + cdfe for format 3, then execute the copy command. + + Additional keyword arguments are forwarded to self.copy() (e.g. sopts). + """ + self._setup_64b_guard_ns() + self._check_ns_copy_limits() + self._enable_cdfe_for_format(3) + self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid, **kwargs) + def test_copy_format_2(self): """ Test copy with descriptor format 2 """ self._check_format_supported(2) @@ -150,13 +289,9 @@ def test_copy_format_2_sopts(self): def test_copy_format_3(self): """ Test copy with descriptor format 3 """ self._check_format_supported(3) - self._check_ns_copy_limits() - self._enable_cdfe_for_format(3) - self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid) + self._run_format_3_copy() def test_copy_format_3_sopts(self): """ Test copy with descriptor format 3 and source options """ self._check_format_supported(3) - self._check_ns_copy_limits() - self._enable_cdfe_for_format(3) - self.copy(0, 1, 2, descriptor_format=3, snsids=self.ns1_nsid, sopts=0) + self._run_format_3_copy(sopts=0) From f63d25892b35d15c20fa9eaa5b2a00af7328c054 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Tue, 28 Apr 2026 08:05:31 +0000 Subject: [PATCH 09/10] tests: skip 16-bit guard copy tests when namespace is in 64-bit guard PI mode When ns_mgmt_supported=False, TestNVMe.setUp/tearDown never touch the namespace. If the device is configured with 64-bit guard PI (e.g. QEMU started with pif=2, or namespace left reformatted by a previous run), copy descriptor format 0 and format 2 commands would fail with NVMe status "Invalid Format" instead of being skipped cleanly. Signed-off-by: Daniel Wagner --- tests/nvme_copy_test.py | 58 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/nvme_copy_test.py b/tests/nvme_copy_test.py index 616850e94d..f63df24647 100644 --- a/tests/nvme_copy_test.py +++ b/tests/nvme_copy_test.py @@ -86,6 +86,53 @@ def _check_ns_copy_limits(self): if missing: self.skipTest(f"{', '.join(missing)} are 0, copy not supported on this namespace") + def _get_current_ns_pif(self): + """ + Return the Protection Information Format (pif) of the currently active + LBA format on self.ns1. + + Reads the raw ``flbas`` byte from ``id-ns`` to determine the active + lbaf index (NVMe spec: bits[3:0] are lbaf_index[3:0], bits[6:5] are + lbaf_index[5:4]), then looks up that entry in the ``nvm-id-ns`` elbafs + array. Returns 0 if either command fails or the pif field is absent + (0 = 16-bit guard / no PI, the safe default for format 0/2 copy). + """ + id_ns_cmd = f"{self.nvme_bin} id-ns {self.ns1} --output-format=json" + result = self.run_cmd(id_ns_cmd) + if result.returncode != 0: + return 0 + flbas = int(json.loads(result.stdout).get("flbas", 0)) + lbaf_idx = (flbas & 0xF) | (((flbas >> 5) & 0x3) << 4) + + nvm_id_ns_cmd = f"{self.nvme_bin} nvm-id-ns {self.ns1} --output-format=json" + result = self.run_cmd(nvm_id_ns_cmd) + if result.returncode != 0: + return 0 + elbafs = json.loads(result.stdout).get("elbafs", []) + if lbaf_idx < len(elbafs): + return elbafs[lbaf_idx].get("pif", 0) + return 0 + + def _check_16b_guard_ns(self): + """ + Skip the test if the current namespace uses a non-16-bit-guard PI + format and namespace management is not available to restore it. + + Copy descriptor formats 0 and 2 require the namespace to use 16-bit + guard PI (pif=0) or no PI. When namespace management is supported, + TestNVMe.setUp() already recreates the namespace with flbas=0 (no + metadata, no PI), so this is a no-op in that case. When namespace + management is not available and the namespace is already in a 64-bit + guard PI format (e.g. QEMU started with pif=2, or left over from a + previous test run), the copy command would fail with "Invalid Format" + rather than being skipped cleanly. + """ + if not self.ns_mgmt_supported and self._get_current_ns_pif() != 0: + self.skipTest( + "current namespace uses non-16-bit-guard PI and namespace " + "management is not supported; cannot run 16-bit guard copy test" + ) + def _find_64b_guard_lbaf_index(self): """ Search the nvm-id-ns elbafs for a format with 64-bit guard PI (pif == 2). @@ -215,9 +262,16 @@ class TestNVMeCopyFormat0(TestNVMeCopy): NVMe Copy tests using Descriptor Format 0. Format 0 uses 16-bit guard PI and copies within a single namespace. - No special namespace formatting is required. + No special namespace formatting is required; the test is skipped if the + current namespace is already using a non-16-bit-guard PI format and + namespace management is not available to restore it. """ + def setUp(self): + """ Pre Section for TestNVMeCopyFormat0 """ + super().setUp() + self._check_16b_guard_ns() + def test_copy_format_0(self): """ Test copy with descriptor format 0 """ self._check_format_supported(0) @@ -275,6 +329,7 @@ def _run_format_3_copy(self, **kwargs): def test_copy_format_2(self): """ Test copy with descriptor format 2 """ self._check_format_supported(2) + self._check_16b_guard_ns() self._check_ns_copy_limits() self._enable_cdfe_for_format(2) self.copy(0, 1, 2, descriptor_format=2, snsids=self.ns1_nsid) @@ -282,6 +337,7 @@ def test_copy_format_2(self): def test_copy_format_2_sopts(self): """ Test copy with descriptor format 2 and source options """ self._check_format_supported(2) + self._check_16b_guard_ns() self._check_ns_copy_limits() self._enable_cdfe_for_format(2) self.copy(0, 1, 2, descriptor_format=2, snsids=self.ns1_nsid, sopts=0) From b720a4846fbaea3cb5cf7d06e5436f17360cc301 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Tue, 28 Apr 2026 09:13:50 +0000 Subject: [PATCH 10/10] tests: handle PI and MS settings Teach the test framework about PI and MS. Signed-off-by: Daniel Wagner --- tests/nvme_compare_test.py | 13 +++++- tests/nvme_read_write_test.py | 4 ++ tests/nvme_test.py | 79 +++++++++++++++++++++++++++++++++++ tests/nvme_test_io.py | 67 ++++++++++++++++++++++++++++- tests/nvme_writezeros_test.py | 4 ++ 5 files changed, 165 insertions(+), 2 deletions(-) diff --git a/tests/nvme_compare_test.py b/tests/nvme_compare_test.py index f704d6c2a7..4c323e4a96 100644 --- a/tests/nvme_compare_test.py +++ b/tests/nvme_compare_test.py @@ -60,13 +60,19 @@ def setUp(self): super().setUp() if not self.compare_cmd_supported(): self.skipTest("because: Optional NVM Command 'Compare' (NVMCMPS) not supported") - self.data_size = 1024 self.start_block = 1023 self.setup_log_dir(self.__class__.__name__) self.compare_file = self.test_log_dir + "/" + "compare_file.txt" self.write_file = self.test_log_dir + "/" + self.write_file self.create_data_file(self.write_file, self.data_size, "15") self.create_data_file(self.compare_file, self.data_size, "25") + self.compare_meta_file = None + if self.ms > 0 and not self.ns_meta_ext: + self.write_meta_file = self.test_log_dir + "/" + self.write_meta_file + self.compare_meta_file = \ + self.test_log_dir + "/" + "compare_meta_file.bin" + self.create_meta_file(self.write_meta_file, self.ms) + self.create_meta_file(self.compare_meta_file, self.ms) def tearDown(self): """ Post Section for TestNVMeCompareCmd """ @@ -83,6 +89,11 @@ def nvme_compare(self, cmp_file): f"--start-block={str(self.start_block)} " + \ f"--block-count={str(self.block_count)} " + \ f"--data-size={str(self.data_size)} --data={cmp_file}" + if self.prinfo: + compare_cmd += f" --prinfo={self.prinfo}" + if self.compare_meta_file: + compare_cmd += \ + f" --metadata-size={self.ms} --metadata={self.compare_meta_file}" return self.exec_cmd(compare_cmd) def test_nvme_compare(self): diff --git a/tests/nvme_read_write_test.py b/tests/nvme_read_write_test.py index 8cae140aac..f082d09231 100644 --- a/tests/nvme_read_write_test.py +++ b/tests/nvme_read_write_test.py @@ -54,6 +54,10 @@ def setUp(self): self.read_file = self.test_log_dir + "/" + self.read_file self.create_data_file(self.write_file, self.data_size, "15") open(self.read_file, 'a').close() + if self.ms > 0 and not self.ns_meta_ext: + self.write_meta_file = self.test_log_dir + "/" + self.write_meta_file + self.read_meta_file = self.test_log_dir + "/" + self.read_meta_file + self.create_meta_file(self.write_meta_file, self.ms) def tearDown(self): """ Post Section for TestNVMeReadWriteTest """ diff --git a/tests/nvme_test.py b/tests/nvme_test.py index c50ad9ff47..98f6b2daea 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -80,6 +80,9 @@ def setUp(self): self.do_validate_pci_device = True self.default_nsid = 0x1 self.flbas = 0 + self.ns_dps = 0 + self.ns_meta_ext = False + self.pif = 0 self.config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json') self.load_config() @@ -88,6 +91,11 @@ def setUp(self): self.ns_mgmt_supported = self.get_ns_mgmt_support() if self.ns_mgmt_supported: self.create_and_attach_default_ns() + else: + self.flbas = self._get_active_lbaf_index() + self.ns_dps = self._get_ns_dps() + self.ns_meta_ext = self._is_metadata_ext() + self.pif = self._get_pif() logger.debug("setup: ctrl: %s, ns1: %s, default_nsid: %s, flbas: %s", self.ctrl, self.ns1, self.default_nsid, self.flbas) @@ -300,6 +308,77 @@ def get_lba_status_supported(self): """ return to_decimal(self.get_id_ctrl_field_value("oacs")) & (1 << 9) + def _get_active_lbaf_index(self): + """ Return the index of the currently active LBA format for ns1. + - Args: + - None + - Returns: + - lbaf index (int) of the format whose in_use flag is set, + or 0 if no in_use entry is found. + """ + nvme_id_ns_cmd = f"{self.nvme_bin} id-ns {self.ns1} " + \ + "--output-format=json" + result = self.run_cmd(nvme_id_ns_cmd) + self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") + json_output = json.loads(result.stdout) + for lbaf in json_output.get('lbafs', []): + if lbaf.get('in_use') == 1: + return int(lbaf['lbaf']) + return 0 + + def _get_ns_dps(self): + """ Return the Data Protection Settings (DPS) field for ns1. + - Args: + - None + - Returns: + - dps value (int); bits 2:0 are the PI type (non-zero means + end-to-end PI is enabled), bits 5:3 are the Protection + Information Format (PIF) on NVMe 2.0+ devices. + """ + nvme_id_ns_cmd = f"{self.nvme_bin} id-ns {self.ns1} " + \ + "--output-format=json" + result = self.run_cmd(nvme_id_ns_cmd) + self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") + json_output = json.loads(result.stdout) + return int(json_output.get('dps', 0)) + + def _get_pif(self): + """ Return the Protection Information Format (PIF) for ns1. + + The PIF is stored in bits 5:3 of the DPS field (NVMe 2.0+): + PIF 0 - 8-byte PI, 16-bit CRC guard (Type 1/2/3, all NVMe 1.x) + PIF 1 - 16-byte PI, 64-bit CRC guard + PIF 2 - 8-byte PI, 32-bit CRC guard + + NVMe 1.x devices always return 0 for these bits. + + - Args: + - None + - Returns: + - pif value (int, 0-7). + """ + nvme_id_ns_cmd = f"{self.nvme_bin} id-ns {self.ns1} " + \ + "--output-format=json" + result = self.run_cmd(nvme_id_ns_cmd) + self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") + json_output = json.loads(result.stdout) + dps = int(json_output.get('dps', 0)) + return (dps >> 3) & 0x7 + + def _is_metadata_ext(self): + """ Return True if the active LBA format uses extended LBA (bit 4 of + the flbas field is set, meaning metadata is appended at the end of + the data buffer). Return False if bit 4 is clear, meaning metadata + is transferred as a separate, contiguous buffer. + """ + nvme_id_ns_cmd = f"{self.nvme_bin} id-ns {self.ns1} " + \ + "--output-format=json" + result = self.run_cmd(nvme_id_ns_cmd) + self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") + json_output = json.loads(result.stdout) + flbas = int(json_output.get('flbas', 0)) + return bool(flbas & (1 << 4)) + def get_lba_format_size(self): """ Wrapper for extracting lba format size of the given flbas - Args: diff --git a/tests/nvme_test_io.py b/tests/nvme_test_io.py index 4cd5571981..631183ec20 100644 --- a/tests/nvme_test_io.py +++ b/tests/nvme_test_io.py @@ -43,11 +43,54 @@ def setUp(self): """ Pre Section for TestNVMeIO """ super().setUp() # common code used in various testcases. - (self.data_size, _) = self.get_lba_format_size() + (ds, ms) = self.get_lba_format_size() + self.ms = ms + # PI type occupies bits 2:0 of the DPS field; bits 5:3 are PIF. + pi_type = self.ns_dps & 0x7 + if pi_type != 0 and ms != 0 and self.ns_meta_ext: + # PI active + extended LBA (metadata appended to data buffer). + # Use PRACT=1 (--prinfo=8) so the controller inserts and strips PI + # automatically. With PRACT=1 the PI bytes are not transferred + # over the host interface, so data_size equals the logical block + # data size only (ds), not ds+ms. This works for all PI sizes + # (8 bytes for PIF 0/2, 16 bytes for PIF 1) and all guard widths + # (16-bit, 32-bit, 64-bit CRC) because the controller handles + # the PI entirely. + self.prinfo = 8 + self.data_size = ds + elif pi_type != 0 and ms != 0 and not self.ns_meta_ext: + # PI active + separate metadata (flbas bit 4 clear). PRACT=1 + # (--prinfo=8) is invalid for the Compare command on this format + # (NVMe spec: PRACT=1 for Compare requires PI in the host data + # buffer, which only applies to the extended-LBA layout). Use + # prinfo=0 (PRACT=0, PRCHK=0) for all operations and supply an + # explicit zero-filled metadata buffer of ms bytes so that the + # stored metadata and the compared metadata are both known zeros. + # PRCHK=0 skips PI validation, so the zero PI bytes are accepted + # by the controller on write and matched exactly on compare. This + # is PI-format and guard-width agnostic: the entire ms-byte + # metadata slot (whether holding an 8-byte PI with 16-bit or + # 32-bit guard, or a 16-byte PI with 64-bit guard) is zeroed. + self.prinfo = 0 + self.data_size = ds + else: + # No PI. For extended LBA format (metadata appended to the data + # buffer) include the metadata bytes so that the controller sees + # a consistent data+metadata unit. For separate metadata format + # (flbas bit 4 clear) the metadata is transferred via a different + # pointer and must NOT be folded into the data buffer; use ds only + # so that the data transfer length matches exactly one LBA. + self.prinfo = 0 + self.data_size = ds + ms if self.ns_meta_ext else ds self.start_block = 0 self.block_count = 0 self.write_file = "write_file.txt" self.read_file = "read_file.txt" + # Basename only; subclasses must prepend the test_log_dir path before + # use (same convention as write_file and read_file above). + if self.ms > 0 and not self.ns_meta_ext: + self.write_meta_file = "write_meta_file.bin" + self.read_meta_file = "read_meta_file.bin" def tearDown(self): """ Post Section for TestNVMeIO """ @@ -70,6 +113,18 @@ def create_data_file(self, pathname, data_size, pattern): os.fsync(data_file.fileno()) data_file.close() + def create_meta_file(self, pathname, meta_size): + """ Creates a binary file of meta_size zero bytes for use as a + separate-metadata buffer in nvme write/read/compare commands. + - Args: + - pathname : metadata file path name. + - meta_size : total size of the metadata in bytes. + - Returns: + None + """ + with open(pathname, "wb") as meta_file: + meta_file.write(bytes(meta_size)) + def nvme_write(self): """ Wrapper for nvme write operation - Args: @@ -81,6 +136,11 @@ def nvme_write(self): f"--start-block={str(self.start_block)} " + \ f"--block-count={str(self.block_count)} " + \ f"--data-size={str(self.data_size)} --data={self.write_file}" + if self.prinfo: + write_cmd += f" --prinfo={self.prinfo}" + if self.ms > 0 and not self.ns_meta_ext: + write_cmd += \ + f" --metadata-size={self.ms} --metadata={self.write_meta_file}" return self.exec_cmd(write_cmd) def nvme_read(self): @@ -94,4 +154,9 @@ def nvme_read(self): f"--start-block={str(self.start_block)} " + \ f"--block-count={str(self.block_count)} " + \ f"--data-size={str(self.data_size)} --data={self.read_file}" + if self.prinfo: + read_cmd += f" --prinfo={self.prinfo}" + if self.ms > 0 and not self.ns_meta_ext: + read_cmd += \ + f" --metadata-size={self.ms} --metadata={self.read_meta_file}" return self.exec_cmd(read_cmd) diff --git a/tests/nvme_writezeros_test.py b/tests/nvme_writezeros_test.py index 75d5687427..381672034a 100644 --- a/tests/nvme_writezeros_test.py +++ b/tests/nvme_writezeros_test.py @@ -59,6 +59,10 @@ def setUp(self): self.create_data_file(self.write_file, self.data_size, "15") self.create_data_file(self.zero_file, self.data_size, '\0') open(self.read_file, 'a').close() + if self.ms > 0 and not self.ns_meta_ext: + self.write_meta_file = self.test_log_dir + "/" + self.write_meta_file + self.read_meta_file = self.test_log_dir + "/" + self.read_meta_file + self.create_meta_file(self.write_meta_file, self.ms) def tearDown(self): """ Post Section for TestNVMeWriteZeros """