<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>