<div dir="ltr"><div dir="ltr"><div dir="ltr"><br></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Wed, Jul 2, 2025 at 12:23 PM Dean Marx <<a href="mailto:dmarx@iol.unh.edu" target="_blank">dmarx@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">Add virtual functions to DTS framework, along with<br>
a field for specifying VF test runs in the config file.<br>
<br>
Signed-off-by: Patrick Robb <<a href="mailto:probb@iol.unh.edu" target="_blank">probb@iol.unh.edu</a>><br>
Signed-off-by: Dean Marx <<a href="mailto:dmarx@iol.unh.edu" target="_blank">dmarx@iol.unh.edu</a>><br>
---<br>
 dts/framework/config/test_run.py             |  2 +<br>
 dts/framework/test_run.py                    |  7 +++<br>
 dts/framework/testbed_model/linux_session.py | 53 +++++++++++++++++++-<br>
 dts/framework/testbed_model/os_session.py    | 42 ++++++++++++++++<br>
 dts/framework/testbed_model/topology.py      | 53 +++++++++++++++++++-<br>
 5 files changed, 154 insertions(+), 3 deletions(-)<br>
<br>
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py<br>
index b6e4099eeb..eefa32c3cb 100644<br>
--- a/dts/framework/config/test_run.py<br>
+++ b/dts/framework/config/test_run.py<br>
@@ -467,6 +467,8 @@ class TestRunConfiguration(FrozenModel):<br>
     perf: bool<br>
     #: Whether to run functional tests.<br>
     func: bool<br>
+    #: Whether to run the testing with virtual functions instead of physical functions<br>
+    virtual_functions_testrun: bool<br>
     #: Whether to skip smoke tests.<br>
     skip_smoke_tests: bool = False<br>
     #: The names of test suites and/or test cases to execute.<br>
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py<br>
index fd49a7dc74..ee919e30d9 100644<br>
--- a/dts/framework/test_run.py<br>
+++ b/dts/framework/test_run.py<br>
@@ -346,6 +346,10 @@ def next(self) -> State | None:<br>
         test_run.ctx.tg_node.setup()<br>
         test_run.ctx.dpdk.setup()<br>
         test_run.ctx.topology.setup()<br>
+<br>
+        if test_run.config.virtual_functions_testrun:<br>
+            test_run.ctx.topology.instantiate_vf_ports()<br>
+<br>
         test_run.ctx.topology.configure_ports("sut", "dpdk")<br>
         test_run.ctx.tg.setup(test_run.ctx.topology)<br>
<br>
@@ -432,6 +436,9 @@ def description(self) -> str:<br>
<br>
     def next(self) -> State | None:<br>
         """Next state."""<br>
+        if self.test_run.config.virtual_functions_testrun:<br>
+            self.test_run.ctx.topology.delete_vf_ports()<br>
+<br>
         self.test_run.ctx.shell_pool.terminate_current_pool()<br>
         self.test_run.ctx.tg.teardown()<br>
         self.test_run.ctx.topology.teardown()<br>
diff --git a/dts/framework/testbed_model/linux_session.py b/dts/framework/testbed_model/linux_session.py<br>
index e01c2dd712..604245d855 100644<br>
--- a/dts/framework/testbed_model/linux_session.py<br>
+++ b/dts/framework/testbed_model/linux_session.py<br>
@@ -17,7 +17,11 @@<br>
<br>
 from typing_extensions import NotRequired<br>
<br>
-from framework.exception import ConfigurationError, InternalError, RemoteCommandExecutionError<br>
+from framework.exception import (<br>
+    ConfigurationError,<br>
+    InternalError,<br>
+    RemoteCommandExecutionError,<br>
+)<br>
 from framework.testbed_model.os_session import PortInfo<br>
 from framework.utils import expand_range<br>
<br>
@@ -211,11 +215,58 @@ def devbind_script_path(self) -> PurePath:<br>
         """<br>
         raise InternalError("Accessed devbind script path before setup.")<br>
<br>
+    def create_vfs(self, pf_port: Port) -> None:<br>
+        """Overrides :meth:`~.os_session.OSSession.create_vfs`.<br>
+<br>
+        Raises:<br>
+            InternalError: If there are existing VFs which have to be deleted.<br>
+        """<br>
+        sys_bus_path = f"/sys/bus/pci/devices/{pf_port.pci}".replace(":", "\\:")<br>
+        curr_num_vfs = int(<br>
+            self.send_command(f"cat {sys_bus_path}/sriov_numvfs", privileged=True).stdout<br>
+        )<br>
+        if 0 < curr_num_vfs:<br>
+            raise InternalError("There are existing VFs on the port which must be deleted.")<br>
+        if curr_num_vfs == 0:<br>
+            self.send_command(f"echo 1 | sudo tee {sys_bus_path}/sriov_numvfs", privileged=True)<br>
+            self.refresh_lshw()<br>
+<br>
+    def delete_vfs(self, pf_port: Port) -> None:<br>
+        """Overrides :meth:`~.os_session.OSSession.delete_vfs`."""<br>
+        sys_bus_path = f"/sys/bus/pci/devices/{pf_port.pci}".replace(":", "\\:")<br>
+        curr_num_vfs = int(<br>
+            self.send_command(f"cat {sys_bus_path}/sriov_numvfs", privileged=True).stdout<br>
+        )<br>
+        if curr_num_vfs == 0:<br>
+            self._logger.debug(f"No VFs found on port {pf_port.pci}, skipping deletion")<br>
+        else:<br>
+            self.send_command(f"echo 0 | sudo tee {sys_bus_path}/sriov_numvfs", privileged=True)<br>
+<br>
+    def get_pci_addr_of_vfs(self, pf_port: Port) -> list[str]:<br>
+        """Overrides :meth:`~.os_session.OSSession.get_pci_addr_of_vfs`."""<br>
+        sys_bus_path = f"/sys/bus/pci/devices/{pf_port.pci}".replace(":", "\\:")<br>
+        curr_num_vfs = int(self.send_command(f"cat {sys_bus_path}/sriov_numvfs").stdout)<br>
+        if curr_num_vfs > 0:<br>
+            pci_addrs = self.send_command(<br>
+                'awk -F "PCI_SLOT_NAME=" "/PCI_SLOT_NAME=/ {print \\$2}" '<br>
+                + f"{sys_bus_path}/virtfn*/uevent",<br>
+                privileged=True,<br>
+            )<br>
+            return pci_addrs.stdout.splitlines()<br>
+        else:<br>
+            return []<br>
+<br>
     @cached_property<br>
     def _lshw_net_info(self) -> list[LshwOutput]:<br>
         output = self.send_command("lshw -quiet -json -C network", verify=True)<br>
         return json.loads(output.stdout)<br>
<br>
+    def refresh_lshw(self) -> None:<br>
+        """Force refresh of cached lshw network info."""<br>
+        if "_lshw_net_info" in self.__dict__:<br>
+            del self.__dict__["_lshw_net_info"]<br>
+        _ = self._lshw_net_info<br>
+<br>
     def _update_port_attr(self, port: Port, attr_value: str | None, attr_name: str) -> None:<br>
         if attr_value:<br>
             setattr(port, attr_name, attr_value)<br>
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py<br>
index d7a09a0d5d..b6e03aa83d 100644<br>
--- a/dts/framework/testbed_model/os_session.py<br>
+++ b/dts/framework/testbed_model/os_session.py<br>
@@ -603,3 +603,45 @@ def configure_port_mtu(self, mtu: int, port: Port) -> None:<br>
             mtu: Desired MTU value.<br>
             port: Port to set `mtu` on.<br>
         """<br>
+<br>
+    @abstractmethod<br>
+    def create_vfs(self, pf_port: Port) -> None:<br>
+        """Creates virtual functions for `pf_port`.<br>
+<br>
+        Checks how many virtual functions (VFs) `pf_port` supports, and creates that<br>
+        number of VFs on the port.<br>
+<br>
+        Args:<br>
+            pf_port: The port to create virtual functions on.<br>
+<br>
+        Raises:<br>
+            InternalError: If the number of VFs is greater than 0 but less than the<br>
+            maximum for `pf_port`.<br>
+        """<br>
+<br>
+    @abstractmethod<br>
+    def delete_vfs(self, pf_port: Port) -> None:<br>
+        """Deletes virtual functions for `pf_port`.<br>
+<br>
+        Checks how many virtual functions (VFs) `pf_port` supports, and deletes that<br>
+        number of VFs on the port.<br>
+<br>
+        Args:<br>
+            pf_port: The port to delete virtual functions on.<br>
+<br>
+        Raises:<br>
+            InternalError: If the number of VFs is greater than 0 but less than the<br>
+            maximum for `pf_port`.<br>
+        """<br>
+<br>
+    @abstractmethod<br>
+    def get_pci_addr_of_vfs(self, pf_port: Port) -> list[str]:<br>
+        """Find the PCI addresses of all virtual functions (VFs) on the port `pf_port`.<br>
+<br>
+        Args:<br>
+            pf_port: The port to find the VFs on.<br>
+<br>
+        Returns:<br>
+            A list containing all of the PCI addresses of the VFs on the port. If the port has no<br>
+            VFs then the list will be empty.<br>
+        """<br>
diff --git a/dts/framework/testbed_model/topology.py b/dts/framework/testbed_model/topology.py<br>
index fb45969136..2bc69d46a9 100644<br>
--- a/dts/framework/testbed_model/topology.py<br>
+++ b/dts/framework/testbed_model/topology.py<br>
@@ -19,7 +19,7 @@<br>
 from framework.exception import ConfigurationError, InternalError<br>
 from framework.testbed_model.node import Node<br>
<br>
-from .port import DriverKind, Port<br>
+from .port import DriverKind, Port, PortConfig<br>
<br>
<br>
 class TopologyType(int, Enum):<br>
@@ -74,6 +74,8 @@ class Topology:<br>
     type: TopologyType<br>
     sut_ports: list[Port]<br>
     tg_ports: list[Port]<br>
+    pf_ports: list[Port]<br>
+    vf_ports: list[Port]<br>
<br>
     @classmethod<br>
     def from_port_links(cls, port_links: Iterator[PortLink]) -> Self:<br>
@@ -101,7 +103,7 @@ def from_port_links(cls, port_links: Iterator[PortLink]) -> Self:<br>
                     msg = "More than two links in a topology are not supported."<br>
                     raise ConfigurationError(msg)<br>
<br>
-        return cls(type, sut_ports, tg_ports)<br>
+        return cls(type, sut_ports, tg_ports, [], [])<br>
<br>
     def node_and_ports_from_id(self, node_identifier: NodeIdentifier) -> tuple[Node, list[Port]]:<br>
         """Retrieve node and its ports for the current topology.<br>
@@ -160,6 +162,53 @@ def _setup_ports(self, node_identifier: NodeIdentifier) -> None:<br>
                     f"for port {<a href="http://port.name" rel="noreferrer" target="_blank">port.name</a>} in node {<a href="http://node.name" rel="noreferrer" target="_blank">node.name</a>}."<br>
                 )<br>
<br>
+    def instantiate_vf_ports(self) -> None:<br>
+        """Create, setup, and add virtual functions to the list of ports on the SUT node.<br>
+<br>
+        Raises:<br>
+            InternalError: If virtual function creation fails.<br>
+        """<br>
+        from framework.context import get_ctx<br>
+<br>
+        ctx = get_ctx()<br>
+<br>
+        for port in self.sut_ports:<br>
+            self.pf_ports.append(port)<br>
+<br>
+        for port in self.pf_ports:<br>
+            ctx.sut_node.main_session.create_vfs(port)<br>
+            addr_list = ctx.sut_node.main_session.get_pci_addr_of_vfs(port)<br>
+            if addr_list == []:<br>
+                raise InternalError(f"Failed to create virtual function on port {port.pci}")<br>
+            for addr in addr_list:<br>
+                vf_config = PortConfig(<br>
+                    name=f"{<a href="http://port.name" rel="noreferrer" target="_blank">port.name</a>}-vf-{addr}",<br>
+                    pci=addr,<br>
+                    os_driver_for_dpdk=port.config.os_driver_for_dpdk,<br>
+                    os_driver=port.config.os_driver,<br>
+                )<br>
+                self.vf_ports.append(Port(node=port.node, config=vf_config))<br>
+            ctx.sut_node.main_session.send_command(f"ip link set {port.logical_name} vf 0 trust on")<br>
+<br>
+        self.sut_ports.clear()<br>
+        self.sut_ports.extend(self.vf_ports)<br>
+<br>
+        for port in self.pf_ports:<br>
+            ctx.sut_node.main_session.send_command(<br>
+                f"ip link set dev {port.logical_name} up", privileged=True<br>
+            )<br></blockquote><div><br></div><div>I think this can become: </div><div><br></div><div>ctx.sut_node.main_session.bring_up_link(self.pf_ports)</div><div><br></div><div>which should do the exact same but runs through the method we already have implemented.</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">
+<br>
+    def delete_vf_ports(self) -> None:<br>
+        """Delete virtual functions from the SUT node during test run teardown."""<br>
+        from framework.context import get_ctx<br>
+<br>
+        ctx = get_ctx()<br>
+<br>
+        for port in self.pf_ports:<br>
+            ctx.sut_node.main_session.delete_vfs(port)<br>
+        self.sut_ports.clear()<br>
+        self.sut_ports.extend(self.pf_ports)<br>
+<br>
     def configure_ports(<br>
         self, node_identifier: NodeIdentifier, drivers: DriverKind | tuple[DriverKind, ...]<br>
     ) -> None:<br>
-- <br>
2.49.0<br>
<br></blockquote><div><br></div>Reviewed-by: Patrick Robb <<a href="mailto:probb@iol.unh.edu">probb@iol.unh.edu</a>><div>Tested-by: Patrick Robb <<a href="mailto:probb@iol.unh.edu">probb@iol.unh.edu</a>> </div></div></div>
</div>