From 9d55162f641baac0de1e8f515699822a83ac5e7b Mon Sep 17 00:00:00 2001 From: Nilay Shroff Date: Thu, 4 Sep 2025 23:26:48 +0530 Subject: [PATCH 1/4] nvme: support option in show-topology command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Although the help text for the nvme show-topology command indicates support for a option, this option has no effect in practice — specifying an NVMe device name does not filter the output. This commit adds proper support for the option, enabling users to filter the topology output based on the specified NVMe device. Reviewed-by: Hannes Reinecke Signed-off-by: Nilay Shroff Signed-off-by: Daniel Wagner --- nvme-print-stdout.c | 9 +++++++++ nvme.c | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/nvme-print-stdout.c b/nvme-print-stdout.c index c597b60872..eb56349aec 100644 --- a/nvme-print-stdout.c +++ b/nvme-print-stdout.c @@ -5683,6 +5683,15 @@ static void stdout_simple_topology(nvme_root_t r, nvme_for_each_host(r, h) { nvme_for_each_subsystem(h, s) { + bool no_ctrl = true; + nvme_ctrl_t c; + + nvme_subsystem_for_each_ctrl(s, c) + no_ctrl = false; + + if (no_ctrl) + continue; + if (!first) printf("\n"); first = false; diff --git a/nvme.c b/nvme.c index ea5a57f1dd..be8b4e7294 100644 --- a/nvme.c +++ b/nvme.c @@ -10207,6 +10207,8 @@ static int show_topology_cmd(int argc, char **argv, struct command *command, str const char *ranking = "Ranking order: namespace|ctrl"; nvme_print_flags_t flags; _cleanup_nvme_root_ nvme_root_t r = NULL; + char *devname = NULL; + nvme_scan_filter_t filter = NULL; enum nvme_cli_topo_ranking rank; int err; @@ -10249,7 +10251,20 @@ static int show_topology_cmd(int argc, char **argv, struct command *command, str return -errno; } - err = nvme_scan_topology(r, NULL, NULL); + if (optind < argc) + devname = basename(argv[optind++]); + + if (devname) { + int subsys_id, nsid; + + if (sscanf(devname, "nvme%dn%d", &subsys_id, &nsid) != 2) { + nvme_show_error("Invalid device name %s\n", devname); + return -EINVAL; + } + filter = nvme_match_device_filter; + } + + err = nvme_scan_topology(r, filter, (void *)devname); if (err < 0) { nvme_show_error("Failed to scan topology: %s", nvme_strerror(errno)); return err; From 15f7bcc01bb3020dd723bcbac663d691020e3d8a Mon Sep 17 00:00:00 2001 From: Nilay Shroff Date: Thu, 4 Sep 2025 23:26:49 +0530 Subject: [PATCH 2/4] nvme: extend show-topology command to add support for multipath This commit enhances the show-topology command by adding support for NVMe multipath. With this change, users can now list all paths to a namespace from its corresponding head node device. Each NVMe path entry then also includes additional details such as ANA state, NUMA node, and queue depth, improving visibility into multipath configs. This information can be particularly helpful for debugging and analyzing NVMe multipath setups. To support this functionality, the "--ranking" option of the nvme show-topology command has been extended with a new sub-option: "multipath". Since this enhancement is specific to NVMe multipath, the iopolicy configured under each subsystem is now always displayed. Previously, iopolicy was shown only with nvme show-topology verbose output, but it is now included by default to improve usability and provide better context when reviewing multipath configurations via show-topology. With this update, users can view the multipath topology of a multi controller/port NVMe disk. Examples: $ nvme show-topology -r multipath nvme-subsys2 - NQN=nvmet_subsystem hostnqn=nqn.2014-08.org.nvmexpress:uuid:12b49f6e-0276-4746-b10c-56815b7e6dc2 iopolicy=numa _ _ _ / _ _ _ / / _ _ _ / / / | / / +- nvme2n1 (ns 1) / / \ | | +- nvme2c2n1 optimized 1,2 nvme2 tcp traddr=127.0.0.2,trsvcid=4460,src_addr=127.0.0.1 live +- nvme2c3n1 optimized 3,4 nvme3 tcp traddr=127.0.0.3,trsvcid=4460,src_addr=127.0.0.1 live For iopolicy=numa, only NUMA node list is shown (queue depth is hidden). $ nvme show-topology -r multipath nvme-subsys2 - NQN=nvmet_subsystem hostnqn=nqn.2014-08.org.nvmexpress:uuid:12b49f6e-0276-4746-b10c-56815b7e6dc2 iopolicy=queue-depth _ _ _ / _ _ _ / / / / _ _ _ | / / +- nvme2n1 (ns 1) / / \ | | +- nvme2c2n1 optimized 0 nvme2 tcp traddr=127.0.0.2,trsvcid=4460,src_addr=127.0.0.1 live +- nvme2c3n1 optimized 0 nvme3 tcp traddr=127.0.0.3,trsvcid=4460,src_addr=127.0.0.1 live For iopolicy=queue-depth, queue depth is shown (NUMA node list is hidden). $ nvme show-topology -r multipath nvme-subsys2 - NQN=nvmet_subsystem hostnqn=nqn.2014-08.org.nvmexpress:uuid:12b49f6e-0276-4746-b10c-56815b7e6dc2 iopolicy=round-robin _ _ _ / _ _ _ / / / / | / +- nvme2n1 (ns 1) / \ | +- nvme2c2n1 optimized nvme2 tcp traddr=127.0.0.2,trsvcid=4460,src_addr=127.0.0.1 live +- nvme2c3n1 optimized nvme3 tcp traddr=127.0.0.3,trsvcid=4460,src_addr=127.0.0.1 live For iopolicy=round-robin, both NUMA node list and queue depth are hidden. Note: The annotations above (e.g., , , , ) are for illustration only and are not part of the actual command output. A more human-friendly tabular format will be introduced in a follow-up patches. Signed-off-by: Nilay Shroff Reviewed-by: Hannes Reinecke Signed-off-by: Daniel Wagner --- nvme-print-binary.c | 1 + nvme-print-json.c | 36 +++++++++++++++---- nvme-print-stdout.c | 85 ++++++++++++++++++++++++++++++++++++++++++--- nvme-print.c | 4 ++- nvme-print.h | 1 + nvme.c | 4 ++- nvme.h | 1 + 7 files changed, 120 insertions(+), 12 deletions(-) diff --git a/nvme-print-binary.c b/nvme-print-binary.c index 351513b90a..17def142b0 100644 --- a/nvme-print-binary.c +++ b/nvme-print-binary.c @@ -428,6 +428,7 @@ static struct print_ops binary_print_ops = { .print_nvme_subsystem_list = NULL, .topology_ctrl = NULL, .topology_namespace = NULL, + .topology_multipath = NULL, /* status and error messages */ .connect_msg = NULL, diff --git a/nvme-print-json.c b/nvme-print-json.c index 829ba718e8..cac9e25aad 100644 --- a/nvme-print-json.c +++ b/nvme-print-json.c @@ -4743,6 +4743,7 @@ static unsigned int json_subsystem_topology_multipath(nvme_subsystem_t s, nvme_ns_t n; nvme_path_t p; unsigned int i = 0; + const char *iopolicy = nvme_subsystem_get_iopolicy(s); nvme_subsystem_for_each_ns(s, n) { struct json_object *ns_attrs; @@ -4750,19 +4751,40 @@ static unsigned int json_subsystem_topology_multipath(nvme_subsystem_t s, ns_attrs = json_create_object(); obj_add_int(ns_attrs, "NSID", nvme_ns_get_nsid(n)); + obj_add_str(ns_attrs, "Name", nvme_ns_get_name(n)); paths = json_create_array(); nvme_namespace_for_each_path(n, p) { struct json_object *path_attrs; - - nvme_ctrl_t c = nvme_path_get_ctrl(p); + struct json_object *ctrls, *ctrl_attrs; + nvme_ctrl_t c; path_attrs = json_create_object(); - obj_add_str(path_attrs, "Name", nvme_ctrl_get_name(c)); - obj_add_str(path_attrs, "Transport", nvme_ctrl_get_transport(c)); - obj_add_str(path_attrs, "Address", nvme_ctrl_get_address(c)); - obj_add_str(path_attrs, "State", nvme_ctrl_get_state(c)); + obj_add_str(path_attrs, "Path", nvme_path_get_name(p)); obj_add_str(path_attrs, "ANAState", nvme_path_get_ana_state(p)); + + /* + * For iopolicy numa exclude "Qdepth", for iopolicy + * queue-depth exclude "NUMANodes" and for iopolicy + * round-robin exclude both "Qdepth" and "NUMANodes". + */ + if (!strcmp(iopolicy, "numa")) + obj_add_str(path_attrs, "NUMANodes", + nvme_path_get_numa_nodes(p)); + else if (!strcmp(iopolicy, "queue-depth")) + obj_add_int(path_attrs, "Qdepth", + nvme_path_get_queue_depth(p)); + + c = nvme_path_get_ctrl(p); + ctrls = json_create_array(); + ctrl_attrs = json_create_object(); + obj_add_str(ctrl_attrs, "Name", nvme_ctrl_get_name(c)); + obj_add_str(ctrl_attrs, "Transport", nvme_ctrl_get_transport(c)); + obj_add_str(ctrl_attrs, "Address", nvme_ctrl_get_address(c)); + obj_add_str(ctrl_attrs, "State", nvme_ctrl_get_state(c)); + array_add_obj(ctrls, ctrl_attrs); + obj_add_array(path_attrs, "Controller", ctrls); + array_add_obj(paths, path_attrs); } obj_add_array(ns_attrs, "Paths", paths); @@ -4787,6 +4809,7 @@ static void json_print_nvme_subsystem_topology(nvme_subsystem_t s, ns_attrs = json_create_object(); obj_add_int(ns_attrs, "NSID", nvme_ns_get_nsid(n)); + obj_add_str(ns_attrs, "Name", nvme_ns_get_name(n)); ctrl = json_create_array(); ctrl_attrs = json_create_object(); @@ -5546,6 +5569,7 @@ static struct print_ops json_print_ops = { .print_nvme_subsystem_list = json_print_nvme_subsystem_list, .topology_ctrl = json_simple_topology, .topology_namespace = json_simple_topology, + .topology_multipath = json_simple_topology, /* status and error messages */ .connect_msg = json_connect_msg, diff --git a/nvme-print-stdout.c b/nvme-print-stdout.c index eb56349aec..204c8d9d8e 100644 --- a/nvme-print-stdout.c +++ b/nvme-print-stdout.c @@ -1126,6 +1126,8 @@ static void stdout_subsys_config(nvme_subsystem_t s) nvme_subsystem_get_nqn(s)); printf("%*s hostnqn=%s\n", len, " ", nvme_host_get_hostnqn(nvme_subsystem_get_host(s))); + printf("%*s iopolicy=%s\n", len, " ", + nvme_subsystem_get_iopolicy(s)); if (stdout_print_ops.flags & VERBOSE) { printf("%*s model=%s\n", len, " ", @@ -1134,8 +1136,6 @@ static void stdout_subsys_config(nvme_subsystem_t s) nvme_subsystem_get_serial(s)); printf("%*s firmware=%s\n", len, " ", nvme_subsystem_get_fw_rev(s)); - printf("%*s iopolicy=%s\n", len, " ", - nvme_subsystem_get_iopolicy(s)); printf("%*s type=%s\n", len, " ", nvme_subsystem_get_type(s)); } @@ -5595,6 +5595,7 @@ static void stdout_subsystem_topology_multipath(nvme_subsystem_t s, nvme_ns_t n; nvme_path_t p; nvme_ctrl_t c; + const char *iopolicy = nvme_subsystem_get_iopolicy(s); if (ranking == NVME_CLI_TOPO_NAMESPACE) { nvme_subsystem_for_each_ns(s, n) { @@ -5615,7 +5616,7 @@ static void stdout_subsystem_topology_multipath(nvme_subsystem_t s, nvme_path_get_ana_state(p)); } } - } else { + } else if (ranking == NVME_CLI_TOPO_CTRL) { /* NVME_CLI_TOPO_CTRL */ nvme_subsystem_for_each_ctrl(s, c) { printf(" +- %s %s %s\n", @@ -5636,6 +5637,59 @@ static void stdout_subsystem_topology_multipath(nvme_subsystem_t s, } } } + } else { + /* NVME_CLI_TOPO_MULTIPATH */ + nvme_subsystem_for_each_ns(s, n) { + printf(" +- %s (ns %d)\n", + nvme_ns_get_name(n), + nvme_ns_get_nsid(n)); + printf(" \\\n"); + nvme_namespace_for_each_path(n, p) { + c = nvme_path_get_ctrl(p); + + if (!strcmp(iopolicy, "numa")) { + /* + * For iopolicy numa, exclude printing + * qdepth. + */ + printf(" +- %s %s %s %s %s %s %s\n", + nvme_path_get_name(p), + nvme_path_get_ana_state(p), + nvme_path_get_numa_nodes(p), + nvme_ctrl_get_name(c), + nvme_ctrl_get_transport(c), + nvme_ctrl_get_address(c), + nvme_ctrl_get_state(c)); + + } else if (!strcmp(iopolicy, "queue-depth")) { + /* + * For iopolicy queue-depth, exclude + * printing numa nodes. + */ + printf(" +- %s %s %d %s %s %s %s\n", + nvme_path_get_name(p), + nvme_path_get_ana_state(p), + nvme_path_get_queue_depth(p), + nvme_ctrl_get_name(c), + nvme_ctrl_get_transport(c), + nvme_ctrl_get_address(c), + nvme_ctrl_get_state(c)); + + } else { /* round-robin */ + /* + * For iopolicy round-robin, exclude + * printing numa nodes and qdepth. + */ + printf(" +- %s %s %s %s %s %s\n", + nvme_path_get_name(p), + nvme_path_get_ana_state(p), + nvme_ctrl_get_name(c), + nvme_ctrl_get_transport(c), + nvme_ctrl_get_address(c), + nvme_ctrl_get_state(c)); + } + } + } } } @@ -5657,7 +5711,7 @@ static void stdout_subsystem_topology(nvme_subsystem_t s, nvme_ctrl_get_state(c)); } } - } else { + } else if (ranking == NVME_CLI_TOPO_CTRL) { /* NVME_CLI_TOPO_CTRL */ nvme_subsystem_for_each_ctrl(s, c) { printf(" +- %s %s %s\n", @@ -5671,6 +5725,23 @@ static void stdout_subsystem_topology(nvme_subsystem_t s, nvme_ctrl_get_state(c)); } } + } else { + /* NVME_CLI_TOPO_MULTIPATH */ + nvme_subsystem_for_each_ctrl(s, c) { + nvme_ctrl_for_each_ns(c, n) { + c = nvme_ns_get_ctrl(n); + + printf(" +- %s (ns %d)\n", + nvme_ns_get_name(n), + nvme_ns_get_nsid(n)); + printf(" \\\n"); + printf(" +- %s %s %s %s\n", + nvme_ctrl_get_name(c), + nvme_ctrl_get_transport(c), + nvme_ctrl_get_address(c), + nvme_ctrl_get_state(c)); + } + } } } @@ -5717,6 +5788,11 @@ static void stdout_topology_ctrl(nvme_root_t r) stdout_simple_topology(r, NVME_CLI_TOPO_CTRL); } +static void stdout_topology_multipath(nvme_root_t r) +{ + stdout_simple_topology(r, NVME_CLI_TOPO_MULTIPATH); +} + static void stdout_message(bool error, const char *msg, va_list ap) { vfprintf(error ? stderr : stdout, msg, ap); @@ -6286,6 +6362,7 @@ static struct print_ops stdout_print_ops = { .print_nvme_subsystem_list = stdout_subsystem_list, .topology_ctrl = stdout_topology_ctrl, .topology_namespace = stdout_topology_namespace, + .topology_multipath = stdout_topology_multipath, /* status and error messages */ .connect_msg = stdout_connect_msg, diff --git a/nvme-print.c b/nvme-print.c index 3a71dffcbe..473a681485 100644 --- a/nvme-print.c +++ b/nvme-print.c @@ -1551,8 +1551,10 @@ void nvme_show_topology(nvme_root_t r, { if (ranking == NVME_CLI_TOPO_NAMESPACE) nvme_print(topology_namespace, flags, r); - else + else if (ranking == NVME_CLI_TOPO_CTRL) nvme_print(topology_ctrl, flags, r); + else + nvme_print(topology_multipath, flags, r); } void nvme_show_message(bool error, const char *msg, ...) diff --git a/nvme-print.h b/nvme-print.h index 4ccf5e6351..0f23b7115c 100644 --- a/nvme-print.h +++ b/nvme-print.h @@ -106,6 +106,7 @@ struct print_ops { void (*print_nvme_subsystem_list)(nvme_root_t r, bool show_ana); void (*topology_ctrl)(nvme_root_t r); void (*topology_namespace)(nvme_root_t r); + void (*topology_multipath)(nvme_root_t r); /* status and error messages */ void (*connect_msg)(nvme_ctrl_t c); diff --git a/nvme.c b/nvme.c index be8b4e7294..4f1854b1de 100644 --- a/nvme.c +++ b/nvme.c @@ -10204,7 +10204,7 @@ static int tls_key(int argc, char **argv, struct command *command, struct plugin static int show_topology_cmd(int argc, char **argv, struct command *command, struct plugin *plugin) { const char *desc = "Show the topology\n"; - const char *ranking = "Ranking order: namespace|ctrl"; + const char *ranking = "Ranking order: namespace|ctrl|multipath"; nvme_print_flags_t flags; _cleanup_nvme_root_ nvme_root_t r = NULL; char *devname = NULL; @@ -10240,6 +10240,8 @@ static int show_topology_cmd(int argc, char **argv, struct command *command, str rank = NVME_CLI_TOPO_NAMESPACE; } else if (!strcmp(cfg.ranking, "ctrl")) { rank = NVME_CLI_TOPO_CTRL; + } else if (!strcmp(cfg.ranking, "multipath")) { + rank = NVME_CLI_TOPO_MULTIPATH; } else { nvme_show_error("Invalid ranking argument: %s", cfg.ranking); return -EINVAL; diff --git a/nvme.h b/nvme.h index 248c5c7ca6..f02f39cadc 100644 --- a/nvme.h +++ b/nvme.h @@ -46,6 +46,7 @@ typedef uint32_t nvme_print_flags_t; enum nvme_cli_topo_ranking { NVME_CLI_TOPO_NAMESPACE, NVME_CLI_TOPO_CTRL, + NVME_CLI_TOPO_MULTIPATH, }; #define SYS_NVME "/sys/class/nvme" From d75226881c490df1c03267af7a9a46b76209788f Mon Sep 17 00:00:00 2001 From: Nilay Shroff Date: Thu, 4 Sep 2025 23:26:50 +0530 Subject: [PATCH 3/4] nvme: add common APIs for printing tabular format output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some nvme-cli commands, such as nvme list and nvme list -v, support output in tabular format. Currently, the table output is not well aligned because column widths are fixed at print time, regardless of the length of the data in each column. This often results in uneven and hard-to-read output.For any new CLI command that requires tabular output, developers must manually specify the column width and row value width, which is both error-prone and inconsistent. This patch introduces a set of common table APIs that: - Automatically calculate column widths based on the content - Maintain proper alignment regardless of value length - Simplify adding tabular output support to new and existing commands The new APIs are: 1. table_init() — Allocate a table instance. 2. table_add_columns() — Add column definitions (struct table_column), including name and alignment (LEFT, RIGHT, CENTERED). 3. table_add_columns_filter() - Same as table_add_columns() but also provide a filter function callback which could be then used by the caller for filtering any column. 3. table_get_row_id() — Reserve a row index for inserting values. 4. table_add_row() — Add a row to the table. 5. table_print() — Print the table with auto-calculated widths. 6. table_free() — Free resources allocated for the table. For adding values, the following setter APIs are provided, each supporting alignment types (LEFT, RIGHT, or CENTERED): - table_set_value_str() - table_set_value_int() - table_set_value_unsigned() - table_set_value_long() - table_set_value_unsigned_long() Usage steps: 1. Call table_init() to create a table handle. 2. Define an array of struct table_column specifying column names and alignment, then call table_add_columns() or if you want to filter column then table_add_columns_filter(). 3. Obtain a row ID using table_get_row_id() and set values using the appropriate setter table APIs : table_set_value_*() function. 4. Add the completed row using table_add_row(). 5. Repeat steps 3–4 for each additional row. 5. Call table_print() to display the table. 6. Call table_free() to release table resources. With these APIs, developers no longer need to pre-calculate column or row widths. The output is consistently aligned and easy to read. Suggested-by: Daniel Wagner Signed-off-by: Nilay Shroff Signed-off-by: Daniel Wagner --- util/meson.build | 3 +- util/table.c | 320 +++++++++++++++++++++++++++++++++++++++++++++++ util/table.h | 149 ++++++++++++++++++++++ 3 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 util/table.c create mode 100644 util/table.h diff --git a/util/meson.build b/util/meson.build index 75aed49c30..5b402b1a32 100644 --- a/util/meson.build +++ b/util/meson.build @@ -8,7 +8,8 @@ sources += [ 'util/sighdl.c', 'util/suffix.c', 'util/types.c', - 'util/utils.c' + 'util/utils.c', + 'util/table.c' ] if json_c_dep.found() diff --git a/util/table.c b/util/table.c new file mode 100644 index 0000000000..88062699df --- /dev/null +++ b/util/table.c @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * table.c : Common APIs for printing tabular format output. + * + * Copyright (c) 2025 Nilay Shroff, IBM + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include +#include +#include +#include + +#include "table.h" + +static int table_get_value_width(struct value *v) +{ + char buf[64]; + int len = 0; + + switch (v->type) { + case FMT_STRING: + len = strlen((const char *)v->s); + break; + case FMT_INT: + len = snprintf(buf, sizeof(buf), "%d", v->i); + break; + default: + printf("Invalid print format!\n"); + break; + } + return len; +} + +static void table_print_centered(struct value *val, int width, enum fmt_type type) +{ + int i, len, left_pad, right_pad; + char buf[64]; + + if (type == FMT_STRING) + len = strlen(val->s); + else if (type == FMT_INT) + len = snprintf(buf, sizeof(buf), "%d", val->i); + else if (type == FMT_UNSIGNED) + len = snprintf(buf, sizeof(buf), "%u", val->u); + else if (type == FMT_LONG) + len = snprintf(buf, sizeof(buf), "%ld", val->ld); + else if (type == FMT_UNSIGNED_LONG) + len = snprintf(buf, sizeof(buf), "%lu", val->lu); + else { + fprintf(stderr, "Invalid format!\n"); + return; + } + + left_pad = (width - len) / 2; + right_pad = width - len - left_pad; + + /* add left padding */ + for (i = 0; i < left_pad; i++) + putchar(' '); + + /* print value */ + if (type == FMT_STRING) + printf("%s ", val->s); + else if (type == FMT_INT) + printf("%d ", val->i); + else if (type == FMT_UNSIGNED) + printf("%u ", val->u); + else if (type == FMT_LONG) + printf("%ld ", val->ld); + else if (type == FMT_UNSIGNED_LONG) + printf("%lu", val->lu); + + /* add right padding */ + for (i = 0; i < right_pad; i++) + putchar(' '); +} + +static void table_print_columns(const struct table *t) +{ + int col, j, width; + struct table_column *c; + struct value v; + + for (col = 0; col < t->num_columns; col++) { + c = &t->columns[col]; + width = c->width; + if (c->align == LEFT) + width *= -1; + + if (c->align == CENTERED) { + v.s = c->name; + v.align = c->align; + table_print_centered(&v, width, FMT_STRING); + } else + printf("%*s ", width, c->name); + } + + printf("\n"); + + for (col = 0; col < t->num_columns; col++) { + for (j = 0; j < t->columns[col].width; j++) + putchar('-'); + printf(" "); + } + + printf("\n"); +} + +static void table_print_rows(const struct table *t) +{ + int row, col; + struct table_column *c; + struct table_row *r; + int width; + struct value *v; + + for (row = 0; row < t->num_rows; row++) { + for (col = 0; col < t->num_columns; col++) { + c = &t->columns[col]; + r = &t->rows[row]; + v = &r->val[col]; + + width = c->width; + if (v->align == LEFT) + width *= -1; + + if (v->align == CENTERED) + table_print_centered(v, width, v->type); + else { + switch (v->type) { + case FMT_STRING: + printf("%*s ", width, v->s); + break; + + case FMT_INT: + printf("%*d ", width, v->i); + break; + + case FMT_UNSIGNED: + printf("%*u ", width, v->u); + break; + + case FMT_LONG: + printf("%*ld ", width, v->ld); + break; + + case FMT_UNSIGNED_LONG: + printf("%*lu ", width, v->lu); + break; + + default: + fprintf(stderr, "Invalid format!\n"); + break; + } + } + } + printf("\n"); + } +} + +void table_print(struct table *t) +{ + /* first print columns */ + table_print_columns(t); + + /* next print rows */ + table_print_rows(t); +} + +int table_get_row_id(struct table *t) +{ + struct table_row *new_rows; + int row = t->num_rows; + + new_rows = reallocarray(t->rows, (row + 1), sizeof(struct table_row)); + if (!new_rows) + return -ENOMEM; + + t->rows = new_rows; + t->rows[row].val = calloc(t->num_columns, sizeof(struct value)); + if (!t->rows->val) + return -ENOMEM; + + t->num_rows++; + return row; +} + +void table_add_row(struct table *t, int row_id) +{ + int col, max_width, width; + struct table_row *row = &t->rows[row_id]; + + /* Adjust the column width based on the row value. */ + for (col = 0; col < t->num_columns; col++) { + max_width = t->columns[col].width; + width = table_get_value_width(&row->val[col]); + if (width > max_width) + t->columns[col].width = width; + } +} + +struct table *table_init(void) +{ + struct table *t; + + t = malloc(sizeof(struct table)); + if (!t) + return NULL; + + memset(t, 0, sizeof(struct table)); + return t; +} + +static int table_add_column(struct table *t, struct table_column *c) +{ + struct table_column *new_columns; + int col = t->num_columns; + + new_columns = reallocarray(t->columns, t->num_columns + 1, + sizeof(struct table_column)); + if (!new_columns) + return -ENOMEM; + + t->columns = new_columns; + t->columns[col].name = strdup(c->name); + if (!t->columns[col].name) + return -ENOMEM; + t->columns[col].align = c->align; + t->columns[col].width = strlen(c->name); + t->num_columns++; + + return 0; +} + +int table_add_columns_filter(struct table *t, struct table_column *c, + int num_columns, + bool (*filter)(const char *name, void *arg), + void *arg) +{ + int col; + + if (!filter) + return table_add_columns(t, c, num_columns); + + for (col = 0; col < num_columns; col++) { + if (!filter(c[col].name, arg)) + continue; /* skip this column */ + + if (table_add_column(t, &c[col])) + goto out; + } + return 0; +out: + return -ENOMEM; +} + +int table_add_columns(struct table *t, struct table_column *c, int num_columns) +{ + int col; + + t->columns = calloc(num_columns, sizeof(struct table_column)); + if (!t->columns) + return -ENOMEM; + + for (col = 0; col < num_columns; col++) { + t->columns[col].name = strdup(c[col].name); + if (!t->columns[col].name) + goto free_col; + + t->columns[col].align = c[col].align; + t->columns[col].width = strlen(t->columns[col].name); + } + t->num_columns = num_columns; + + return 0; +free_col: + while (--col >= 0) + free(t->columns[col].name); + free(t->columns); + t->columns = NULL; + return -ENOMEM; +} + +void table_free(struct table *t) +{ + int row, col; + struct table_row *r; + struct value *v; + + /* free rows */ + for (row = 0; row < t->num_rows; row++) { + r = &t->rows[row]; + for (col = 0; col < t->num_columns; col++) { + v = &r->val[col]; + + if (v->type == FMT_STRING) + free(v->s); + } + free(r->val); + } + free(t->rows); + + /* free columns */ + for (col = 0; col < t->num_columns; col++) + free(t->columns[col].name); + free(t->columns); + + /* free table */ + free(t); +} diff --git a/util/table.h b/util/table.h new file mode 100644 index 0000000000..1c98c990de --- /dev/null +++ b/util/table.h @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#ifndef _TABLE_H_ +#define _TABLE_H_ + +#include + +enum fmt_type { + FMT_STRING, + FMT_INT, + FMT_UNSIGNED, + FMT_LONG, + FMT_UNSIGNED_LONG, +}; + +enum alignment { + RIGHT, + LEFT, + CENTERED +}; + +struct value { + union { + char *s; + int i; + unsigned int u; + long ld; + unsigned long lu; + }; + enum alignment align; + enum fmt_type type; +}; + +struct table_row { + struct value *val; +}; + +struct table_column { + char *name; + enum alignment align; + int width; /* auto populated */ +}; + +struct table { + struct table_column *columns; + int num_columns; + struct table_row *rows; + int num_rows; +}; + +static inline int table_set_value_str(struct table *t, int col, int row, + const char *str, enum alignment align) +{ + struct table_row *r; + struct value *v; + char *s; + + if (col >= t->num_columns || row >= t->num_rows) + return -EINVAL; + + s = strdup(str); + if (!s) + return -ENOMEM; + + r = &t->rows[row]; + v = &r->val[col]; + v->s = s; + v->align = align; + v->type = FMT_STRING; + + return 0; +} + +static inline int table_set_value_int(struct table *t, int col, int row, + int i, enum alignment align) +{ + struct table_row *r; + struct value *v; + + if (col >= t->num_columns || row >= t->num_rows) + return -EINVAL; + + r = &t->rows[row]; + v = &r->val[col]; + v->i = i; + v->align = align; + v->type = FMT_INT; + + return 0; +} + +static inline int table_set_value_unsigned(struct table *t, int col, int row, + int u, enum alignment align) +{ + struct table_row *r; + struct value *v; + + if (col >= t->num_columns || row >= t->num_rows) + return -EINVAL; + + r = &t->rows[row]; + v = &r->val[col]; + v->u = u; + v->align = align; + v->type = FMT_UNSIGNED; + + return 0; +} + +static inline int table_set_value_long(struct table *t, int col, int row, + long ld, enum alignment align) +{ + struct table_row *r; + struct value *v; + + if (col >= t->num_columns || row >= t->num_rows) + return -EINVAL; + + r = &t->rows[row]; + v = &r->val[col]; + v->ld = ld; + v->align = align; + v->type = FMT_LONG; + + return 0; +} + +static inline void table_set_value_unsigned_long(struct table *t, int col, + int row, long lu, enum alignment align) +{ + struct table_row *r = &t->rows[row]; + struct value *v = &r->val[col]; + + v->lu = lu; + v->align = align; + v->type = FMT_UNSIGNED_LONG; +} + +struct table *table_init(void); +int table_add_columns(struct table *t, struct table_column *c, int num_columns); +int table_add_columns_filter(struct table *t, struct table_column *c, + int num_columns, + bool (*filter)(const char *name, void *arg), + void *arg); +int table_get_row_id(struct table *t); +void table_add_row(struct table *t, int row); +void table_print(struct table *t); +void table_free(struct table *t); + +#endif /* _TABLE_H_ */ From 7cde355e107ea2e6d07730f33be969905d89da1c Mon Sep 17 00:00:00 2001 From: Nilay Shroff Date: Thu, 4 Sep 2025 23:26:51 +0530 Subject: [PATCH 4/4] nvme: add support for printing show-topology in tabular form The nvme CLI command show-topology currently prints the topology output in a tree format. However, in some cases it is more convenient and easier to read or interpret the output when displayed in a tabular form. This patch adds support for printing the show-topology output in a tabular format. To achieve this, the --output-format option, which previously supported only normal and json formats, is extended to include a new tabular format. With this change, the user can now choose to print the topology in any of the following formats: normal, json, or tabular. The new tabular output leverages the recently introduced table APIs to produce well- aligned, easy-to-read output. Suggested-by: Daniel Wagner Signed-off-by: Nilay Shroff Signed-off-by: Daniel Wagner --- nvme-print-stdout.c | 218 ++++++++++++++++++++++++++++++++++++++++++++ nvme-print.c | 5 + nvme-print.h | 2 + nvme.c | 8 +- nvme.h | 1 + 5 files changed, 233 insertions(+), 1 deletion(-) diff --git a/nvme-print-stdout.c b/nvme-print-stdout.c index 204c8d9d8e..8b32eb7af8 100644 --- a/nvme-print-stdout.c +++ b/nvme-print-stdout.c @@ -20,6 +20,7 @@ #include "nvme-models.h" #include "util/suffix.h" #include "util/types.h" +#include "util/table.h" #include "logging.h" #include "common.h" @@ -5589,6 +5590,127 @@ static void stdout_list_items(nvme_root_t r) stdout_simple_list(r); } +static bool subsystem_iopolicy_filter(const char *name, void *arg) +{ + nvme_subsystem_t s = arg; + const char *iopolicy = nvme_subsystem_get_iopolicy(s); + + if (!strcmp(iopolicy, "queue-depth")) { + /* exclude "Nodes" for iopolicy queue-depth */ + if (!strcmp(name, "Nodes")) + return false; + } else if (!strcmp(iopolicy, "numa")) { + /* exclude "Qdepth" for iopolicy numa */ + if (!strcmp(name, "Qdepth")) + return false; + } else { /* round-robin */ + /* exclude "Nodes" and "Qdepth" for iopolicy round-robin */ + if (!strcmp(name, "Nodes") || !strcmp(name, "Qdepth")) + return false; + } + + return true; +} + +static void stdout_tabular_subsystem_topology_multipath(nvme_subsystem_t s) +{ + nvme_ns_t n; + nvme_path_t p; + nvme_ctrl_t c; + int row, col; + bool first; + struct table *t; + const char *iopolicy = nvme_subsystem_get_iopolicy(s); + struct table_column columns[] = { + {"NSHead", LEFT, 0}, + {"NSID", LEFT, 0}, + {"NSPath", LEFT, 0}, + {"ANAState", LEFT, 0}, + {"Nodes", LEFT, 0}, + {"Qdepth", LEFT, 0}, + {"Controller", LEFT, 0}, + {"TrType", LEFT, 0}, + {"Address", LEFT, 0}, + {"State", LEFT, 0}, + }; + + t = table_init(); + if (!t) { + printf("Failed to init table\n"); + return; + } + + if (table_add_columns_filter(t, columns, ARRAY_SIZE(columns), + subsystem_iopolicy_filter, (void *)s) < 0) { + printf("Failed to add columns\n"); + goto free_tbl; + } + + nvme_subsystem_for_each_ns(s, n) { + first = true; + nvme_namespace_for_each_path(n, p) { + c = nvme_path_get_ctrl(p); + + row = table_get_row_id(t); + if (row < 0) { + printf("Failed to add row\n"); + goto free_tbl; + } + /* For the first row we print actual NSHead name, + * however, for the subsequent rows we print "arrow" + * ("-->") symbol for NSHead. This "arrow" style makes + * it visually obvious that susequenet entries (if + * present) are a path under the first NSHead. + */ + col = -1; + /* col 0: NSHead */ + if (first) { + table_set_value_str(t, ++col, row, + nvme_ns_get_name(n), LEFT); + first = false; + } else + table_set_value_str(t, ++col, row, + "-->", CENTERED); + /* col 1: NSID */ + table_set_value_int(t, ++col, row, + nvme_ns_get_nsid(n), CENTERED); + /* col 2: NSPath */ + table_set_value_str(t, ++col, row, + nvme_path_get_name(p), LEFT); + /* col 3: ANAState */ + table_set_value_str(t, ++col, row, + nvme_path_get_ana_state(p), LEFT); + + if (!strcmp(iopolicy, "numa")) + /* col 4: Nodes */ + table_set_value_str(t, ++col, row, + nvme_path_get_numa_nodes(p), CENTERED); + else if (!strcmp(iopolicy, "queue-depth")) + /* col 4 : Qdepth */ + table_set_value_int(t, ++col, row, + nvme_path_get_queue_depth(p), CENTERED); + + /* col 5: Controller */ + table_set_value_str(t, ++col, row, + nvme_ctrl_get_name(c), LEFT); + /* col 6: TrType */ + table_set_value_str(t, ++col, row, + nvme_ctrl_get_transport(c), LEFT); + /* col 7: Address */ + table_set_value_str(t, ++col, row, + nvme_ctrl_get_address(c), LEFT); + /* col 8: State */ + table_set_value_str(t, ++col, row, + nvme_ctrl_get_state(c), LEFT); + + table_add_row(t, row); + } + } + table_print(t); +free_tbl: + table_free(t); +} + static void stdout_subsystem_topology_multipath(nvme_subsystem_t s, enum nvme_cli_topo_ranking ranking) { @@ -5693,6 +5815,69 @@ static void stdout_subsystem_topology_multipath(nvme_subsystem_t s, } } +static void stdout_tabular_subsystem_topology(nvme_subsystem_t s) +{ + nvme_ctrl_t c; + nvme_ns_t n; + int row; + struct table *t; + struct table_column columns[] = { + {"Namespace", LEFT, 0}, + {"NSID", LEFT, 0}, + {"Controller", LEFT, 0}, + {"Trtype", LEFT, 0}, + {"Address", LEFT, 0}, + {"State", LEFT, 0}, + }; + + t = table_init(); + if (!t) { + printf("Failed to init table\n"); + return; + } + + if (table_add_columns(t, columns, ARRAY_SIZE(columns)) < 0) { + printf("Failed to add columns\n"); + goto free_tbl; + } + + nvme_subsystem_for_each_ctrl(s, c) { + nvme_ctrl_for_each_ns(c, n) { + c = nvme_ns_get_ctrl(n); + + row = table_get_row_id(t); + if (row < 0) { + printf("Failed to add row\n"); + goto free_tbl; + } + + /* col 0: Namespace */ + table_set_value_str(t, 0, row, + nvme_ns_get_name(n), LEFT); + /* col 1: NSID */ + table_set_value_int(t, 1, row, + nvme_ns_get_nsid(n), CENTERED); + /* col 2: Controller */ + table_set_value_str(t, 2, row, + nvme_ctrl_get_name(c), LEFT); + /* col 3: Trtype */ + table_set_value_str(t, 3, row, + nvme_ctrl_get_transport(c), LEFT); + /* col 4: Address */ + table_set_value_str(t, 4, row, + nvme_ctrl_get_address(c), LEFT); + /* col 5: State */ + table_set_value_str(t, 5, row, + nvme_ctrl_get_state(c), LEFT); + + table_add_row(t, row); + } + } + table_print(t); +free_tbl: + table_free(t); +} + static void stdout_subsystem_topology(nvme_subsystem_t s, enum nvme_cli_topo_ranking ranking) { @@ -5745,6 +5930,38 @@ static void stdout_subsystem_topology(nvme_subsystem_t s, } } +static void stdout_topology_tabular(nvme_root_t r) +{ + nvme_host_t h; + nvme_subsystem_t s; + bool first = true; + + nvme_for_each_host(r, h) { + nvme_for_each_subsystem(h, s) { + bool no_ctrl = true; + nvme_ctrl_t c; + + nvme_subsystem_for_each_ctrl(s, c) + no_ctrl = false; + + if (no_ctrl) + continue; + + if (!first) + printf("\n"); + first = false; + + stdout_subsys_config(s); + printf("\n"); + + if (nvme_is_multipath(s)) + stdout_tabular_subsystem_topology_multipath(s); + else + stdout_tabular_subsystem_topology(s); + } + } +} + static void stdout_simple_topology(nvme_root_t r, enum nvme_cli_topo_ranking ranking) { @@ -6363,6 +6580,7 @@ static struct print_ops stdout_print_ops = { .topology_ctrl = stdout_topology_ctrl, .topology_namespace = stdout_topology_namespace, .topology_multipath = stdout_topology_multipath, + .topology_tabular = stdout_topology_tabular, /* status and error messages */ .connect_msg = stdout_connect_msg, diff --git a/nvme-print.c b/nvme-print.c index 473a681485..d1af8284e0 100644 --- a/nvme-print.c +++ b/nvme-print.c @@ -1557,6 +1557,11 @@ void nvme_show_topology(nvme_root_t r, nvme_print(topology_multipath, flags, r); } +void nvme_show_topology_tabular(nvme_root_t r, nvme_print_flags_t flags) +{ + nvme_print(topology_tabular, flags, r); +} + void nvme_show_message(bool error, const char *msg, ...) { struct print_ops *ops = nvme_print_ops(NORMAL); diff --git a/nvme-print.h b/nvme-print.h index 0f23b7115c..a798256604 100644 --- a/nvme-print.h +++ b/nvme-print.h @@ -107,6 +107,7 @@ struct print_ops { void (*topology_ctrl)(nvme_root_t r); void (*topology_namespace)(nvme_root_t r); void (*topology_multipath)(nvme_root_t r); + void (*topology_tabular)(nvme_root_t r); /* status and error messages */ void (*connect_msg)(nvme_ctrl_t c); @@ -251,6 +252,7 @@ void nvme_show_list_ns(struct nvme_ns_list *ns_list, void nvme_show_topology(nvme_root_t t, enum nvme_cli_topo_ranking ranking, nvme_print_flags_t flags); +void nvme_show_topology_tabular(nvme_root_t t, nvme_print_flags_t flags); void nvme_feature_show(enum nvme_features_id fid, int sel, unsigned int result); void nvme_feature_show_fields(enum nvme_features_id fid, unsigned int result, unsigned char *buf); diff --git a/nvme.c b/nvme.c index 4f1854b1de..3f1e286b51 100644 --- a/nvme.c +++ b/nvme.c @@ -545,6 +545,8 @@ int validate_output_format(const char *format, nvme_print_flags_t *flags) #endif /* CONFIG_JSONC */ else if (!strcmp(format, "binary")) f = BINARY; + else if (!strcmp(format, "tabular")) + f = TABULAR; else return -EINVAL; @@ -10204,6 +10206,7 @@ static int tls_key(int argc, char **argv, struct command *command, struct plugin static int show_topology_cmd(int argc, char **argv, struct command *command, struct plugin *plugin) { const char *desc = "Show the topology\n"; + const char *output_format = "Output format: normal|json|binary|tabular"; const char *ranking = "Ranking order: namespace|ctrl|multipath"; nvme_print_flags_t flags; _cleanup_nvme_root_ nvme_root_t r = NULL; @@ -10272,7 +10275,10 @@ static int show_topology_cmd(int argc, char **argv, struct command *command, str return err; } - nvme_show_topology(r, rank, flags); + if (flags & TABULAR) + nvme_show_topology_tabular(r, flags); + else + nvme_show_topology(r, rank, flags); return err; } diff --git a/nvme.h b/nvme.h index f02f39cadc..c4f0f0cf14 100644 --- a/nvme.h +++ b/nvme.h @@ -39,6 +39,7 @@ enum nvme_print_flags { JSON = 1 << 1, /* display in json format */ VS = 1 << 2, /* hex dump vendor specific data areas */ BINARY = 1 << 3, /* binary dump raw bytes */ + TABULAR = 1 << 4, /* prints aligned columns for easy reading */ }; typedef uint32_t nvme_print_flags_t;