<div dir="ltr"><div dir="ltr"><br></div><br><div class="gmail_quote gmail_quote_container"><div dir="ltr" class="gmail_attr">On Wed, Jun 24, 2026 at 10:06 PM Patrick Robb <<a href="mailto:patrickrobb1997@gmail.com">patrickrobb1997@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div dir="ltr"><br></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Wed, Jun 24, 2026 at 5:33 PM Koushik Bhargav Nimoji <<a href="mailto:knimoji@iol.unh.edu" target="_blank">knimoji@iol.unh.edu</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">This patch gathers NIC info during a DTS run and writes it to an output<br>
json file. This allows the json file to be used when reporting results<br>
on the DTS results dashboard.<br>
<br>
Signed-off-by: Koushik Bhargav Nimoji <<a href="mailto:knimoji@iol.unh.edu" target="_blank">knimoji@iol.unh.edu</a>><br>
---<br>
v2:<br>
    *Resolved merge conflicts<br>
v3:<br>
    *Fixed an issue with retrieving<br>
     the NIC's hardware version   <br>
v4:<br>
    *Moved nic info gathering step before the nics get<br>
     binded to their respective drivers <br>
    *Condensed some areas of code in order to make them<br>
     more readable<br>
    *Removed redundant None checks and added some where<br>
     required<br>
    *Fixed LshwOutput class to better reflect the lshw<br>
     command output<br>
---<br>
 dts/framework/test_run.py                    |  8 +++<br>
 dts/framework/testbed_model/linux_session.py | 68 ++++++++++++++++++++<br>
 dts/framework/testbed_model/os_session.py    | 11 ++++<br>
 3 files changed, 87 insertions(+)<br>
<br>
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py<br>
index 94dc6023a7..c92fe90f2e 100644<br>
--- a/dts/framework/test_run.py<br>
+++ b/dts/framework/test_run.py<br>
@@ -98,6 +98,7 @@<br>
         "InternalError" -> "exit":ew<br>
 """<br>
<br>
+import json<br>
 import random<br>
 from collections import deque<br>
 from collections.abc import Iterable<br>
@@ -347,6 +348,12 @@ def next(self) -> State | None:<br>
         test_run.ctx.dpdk.setup()<br>
         test_run.ctx.topology.setup()<br>
<br>
+        used_nic_info: list[dict[str, str]] = self.test_run.ctx.sut_node.main_session.get_nic_info()<br></blockquote><div><br></div><div>drop "used" for nic_info or change to testrun_nic_info?</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
+        with open(f"{SETTINGS.output_dir}/dut_info.json", "w") as file:<br>
+            json.dump(used_nic_info, file, indent=3)<br>
+<br>
+        <a href="http://self.logger.info" rel="noreferrer" target="_blank">self.logger.info</a>(f"DUT NIC info written to: {SETTINGS.output_dir}/dut_info.json")<br>
+<br>
         if test_run.config.use_virtual_functions:<br>
             test_run.ctx.topology.instantiate_vf_ports()<br>
         if test_run.ctx.sut_node.cryptodevs and test_run.config.crypto:<br>
@@ -370,6 +377,7 @@ def next(self) -> State | None:<br>
         test_run.supported_capabilities = get_supported_capabilities(<br>
             test_run.ctx.sut_node, test_run.ctx.topology, test_run.required_capabilities<br>
         )<br>
+<br>
         return TestRunExecution(test_run, self.result)<br>
<br>
     def on_error(self, ex: BaseException) -> State | None:<br>
diff --git a/dts/framework/testbed_model/linux_session.py b/dts/framework/testbed_model/linux_session.py<br>
index 3a6e97974b..9e9146c372 100644<br>
--- a/dts/framework/testbed_model/linux_session.py<br>
+++ b/dts/framework/testbed_model/linux_session.py<br>
@@ -38,6 +38,8 @@ class LshwConfigurationOutput(TypedDict):<br>
     driver: str<br>
     #:<br>
     link: str<br>
+    #:<br>
+    firmware: str<br>
<br>
<br>
 class LshwOutput(TypedDict):<br>
@@ -61,6 +63,12 @@ class LshwOutput(TypedDict):<br>
             ...<br>
     """<br>
<br>
+    #:<br>
+    vendor: NotRequired[str]<br>
+    #:<br>
+    product: NotRequired[str]<br>
+    #:<br>
+    version: NotRequired[str]<br>
     #:<br>
     businfo: str<br>
     #:<br>
@@ -197,6 +205,66 @@ def unbind_ports(self, ports: list[Port]):<br>
         if self._lshw_net_info:<br>
             del self._lshw_net_info<br>
<br>
+    def get_nic_info(self) -> list[dict[str, str]]:<br>
+        """Overrides :meth`~.os_session.OSSession.get_nic_info`.<br>
+<br>
+        Raises:<br>
+            ConfigurationError: If the NIC info could not be found.<br>
+        """<br>
+        port_data = {<br>
+            port.get("businfo"): port for port in self._lshw_net_info if port.get("businfo")<br>
+        }<br>
+<br>
+        all_nic_info: list[dict[str, str]] = []<br>
+        for port in self._config.ports:<br>
+            pci_addr = port.pci<br>
+<br>
+            command_result = self.send_command(<br></blockquote><div><br></div><div>rename to lshw_result please.</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
+                f"sudo lshw -c network -businfo | grep '{pci_addr}' | cut -d'@' -f1"<br>
+            )<br>
+            if command_result.return_code != 0 and command_result.stdout == "":<br>
+                raise ConfigurationError(f"Unable to get bus type for port {pci_addr}.")<br>
+            bus_type = command_result.stdout<br>
+<br>
+            bus_info = f"{bus_type}@{pci_addr}"<br>
+            nic_port: LshwOutput | None = port_data[bus_info]<br>
+            if nic_port is None:<br>
+                raise ConfigurationError(f"Port {pci_addr} could not be found on the node.")<br>
+<br>
+            config: LshwConfigurationOutput | None = nic_port["configuration"]<br>
+            if config is None:<br>
+                raise ConfigurationError(<br>
+                    f"Configuration info for port {pci_addr} could not be found on the node."<br>
+                )<br>
+<br>
+            if "logicalname" not in nic_port:<br>
+                raise ConfigurationError(<br>
+                    f"Logical name for port {pci_addr} could not be found on the node."<br>
+                )<br>
+<br>
+            command_result = self.send_command(<br></blockquote><div><br></div><div>ethtool_result</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
+                f"ethtool {nic_port['logicalname']} | grep 'Speed:' | awk '{{print $2}}'"<br>
+            )<span style="background-color:transparent"> </span></blockquote><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
+            if command_result.return_code == 0 and command_result.stdout:<br>
+                nic_speed = command_result.stdout<br>
+            else:<br>
+                self._logger.error(f"Unable to get speed for NIC: {pci_addr}")<br>
+                nic_speed = None<br>
+<br>
+            dut_json = {<br>
+                "make": nic_port["vendor"] if "vendor" in nic_port else "Unknown",<br>
+                "model": nic_port["product"] if "product" in nic_port else "Unknown",<br>
+                "hardware version": nic_port["version"] if "version" in nic_port else "Unknown",<br>
+                "firmware version": config["firmware"] if "firmware" in config else "Unknown",<br>
+                "deviceBusType": bus_type,<br>
+                "deviceId": nic_port["serial"] if "serial" in nic_port else "Unknown",<br>
+                "pmd": config["driver"] if "driver" in config else "Unknown",<br>
+                "speed": nic_speed or "Unknown",<br>
+            }<br>
+            all_nic_info.append(dut_json)<br>
+<br>
+        return all_nic_info<br>
+<br></blockquote><div><br></div><div>What is the intended behavior for cryptodev tests? I realize the ports list will be empty and we will not enter the initial loop, but is this intended? Do we want to gether cryptodev info too?</div><div> </div></div></div></blockquote><div>The intended behavior here is to skip cryptodev devices. Not entering the initial loop, and therefore returning an empty list is the expected behavior when running cryptodev tests.</div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div class="gmail_quote"></div></div></blockquote><div><span style="background-color:transparent"> </span></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div class="gmail_quote"><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
     def bind_ports_to_driver(self, ports: list[Port], driver_name: str) -> None:<br><br></blockquote><div><br></div><div>Reviewed-by: Patrick Robb <<a href="mailto:patrickrobb1997@gmail.com" target="_blank">patrickrobb1997@gmail.com</a>> </div></div></div>
</blockquote></div></div>