From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 4580B46B03; Fri, 4 Jul 2025 06:28:08 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id E6E004028B; Fri, 4 Jul 2025 06:28:07 +0200 (CEST) Received: from mail-pj1-f43.google.com (mail-pj1-f43.google.com [209.85.216.43]) by mails.dpdk.org (Postfix) with ESMTP id 4EF0E4027D for ; Fri, 4 Jul 2025 06:28:06 +0200 (CEST) Received: by mail-pj1-f43.google.com with SMTP id 98e67ed59e1d1-315cd33fa79so473687a91.3 for ; Thu, 03 Jul 2025 21:28:06 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1751603285; x=1752208085; darn=dpdk.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=ohBArDdjLOHeH2wjMuJeErrhBTydPKsdks1Ip5Hf5dE=; b=MRDhR3dUwwc6wG8IlllkhRptiP1eDMsrjZUTPzhRsJezTwTwxWkhx/8e9ERdh/MWfz bNATdmok7dcpf2eYM0R6ZlCpzlLZlPaAwFMbfLW1JWr+7TEXx7/jH3AJojEEXDSJLvQV 3JeEv5ZaY9C3Z2ygn/kt8JJDiaMc73XEwuBj4= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1751603285; x=1752208085; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=ohBArDdjLOHeH2wjMuJeErrhBTydPKsdks1Ip5Hf5dE=; b=R0XEvkaNgDpQ9Hk9xSsGUIAotiwuqQqjZpKFSK+vWJz/K6tJemxtKGAFQ5HE8GOC0Z 1cSwBTtvkg5+G/1PkkRcob/A8AuHfCc3UZraGwB8CjITQkWYpsuWXBcm+Kuest1juXOt tuL7g0CkV+5TcWRYL6rU4II7b9PJBgSGFRwqeoeFEIIq2Ll3OhGS5hSNjdFGMchPrpF+ jYZBukrJ9m0S8nD6Jie1+lVT9dBAniLE+0SRMClDdUbvFh4OwIk5qfFzZGmugyRr1Vj9 rTLaR5V4yCTHAX3yFwQSB49IVLeRKNig4JDoh6fhCmY5CeiGjCrjnKqiNx8Sfn12Dpio faiQ== X-Forwarded-Encrypted: i=1; AJvYcCXgeRaEdwldH4DaG0EQp5+ZBjPaVmqVKStEjNSf58v1oiLS7MPaybNLZ6fVBlg5LSyNJzo=@dpdk.org X-Gm-Message-State: AOJu0Yzwhh9W4qTDZEP/+J4GvyZrkUBlKHE4BK9oMsy1kVuQxgHctNjp 2gdBRWDeD7fFMmRzk4d2wDIW5rJi0dfRXlKIVlVFHqEFBZ3bdYvSeqonoFuXKVyaJiU5rbbt5cI G2a/OCgbCOIwhOWdy7eZ6uOR24N3IdydHJu1Wmyaidw== X-Gm-Gg: ASbGncvtbWlw6rvPRvbnz+5eTEn+ue/cfaJ0Dj/0rZxcFdy5jzVdty+N7R/HMTyoWY7 YhvzDkUDpr2cQJkfzoscyBBQlXpp5R0GzNuObuqdyuCOX0+YG2GkioLjnzrLLvRMxEHNtN8bOpo N5J4m/iLKJusDOU0s62Mq2Ak1EsRhUUdA8ACbSW7KJSeohn0v7UEG5nuO+7rk= X-Google-Smtp-Source: AGHT+IGxmiH12FVCk/0o4Xzmt1rejTv4MEBOJ9ivPRgqKmCIZIMRZS4xXMtHYy/Ewo8Q9GuWLV/H5woO8iTsrVQnJpI= X-Received: by 2002:a17:90b:5210:b0:311:d258:3473 with SMTP id 98e67ed59e1d1-31aac4620e5mr1829730a91.13.1751603285335; Thu, 03 Jul 2025 21:28:05 -0700 (PDT) MIME-Version: 1.0 References: <20250626152755.197560-3-dmarx@iol.unh.edu> <20250702162331.352313-1-dmarx@iol.unh.edu> In-Reply-To: <20250702162331.352313-1-dmarx@iol.unh.edu> From: Patrick Robb Date: Fri, 4 Jul 2025 00:22:28 -0400 X-Gm-Features: Ac12FXwIRWOAVfC6TPVtek2RxPkQfulqQPEPLJe8fspledefS1TCaDKAVI8XTHg Message-ID: Subject: Re: [PATCH v3 1/4] dts: add virtual functions to framework To: Dean Marx Cc: luca.vizzarro@arm.com, yoan.picchi@foss.arm.com, Honnappa.Nagarahalli@arm.com, paul.szczepanek@arm.com, dev@dpdk.org Content-Type: multipart/alternative; boundary="0000000000009c3b7f063912eb46" X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org --0000000000009c3b7f063912eb46 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable On Wed, Jul 2, 2025 at 12:23=E2=80=AFPM Dean Marx wrote= : > Add virtual functions to DTS framework, along with > a field for specifying VF test runs in the config file. > > Signed-off-by: Patrick Robb > Signed-off-by: Dean Marx > --- > dts/framework/config/test_run.py | 2 + > dts/framework/test_run.py | 7 +++ > dts/framework/testbed_model/linux_session.py | 53 +++++++++++++++++++- > dts/framework/testbed_model/os_session.py | 42 ++++++++++++++++ > dts/framework/testbed_model/topology.py | 53 +++++++++++++++++++- > 5 files changed, 154 insertions(+), 3 deletions(-) > > diff --git a/dts/framework/config/test_run.py > b/dts/framework/config/test_run.py > index b6e4099eeb..eefa32c3cb 100644 > --- a/dts/framework/config/test_run.py > +++ b/dts/framework/config/test_run.py > @@ -467,6 +467,8 @@ class TestRunConfiguration(FrozenModel): > perf: bool > #: Whether to run functional tests. > func: bool > + #: Whether to run the testing with virtual functions instead of > physical functions > + virtual_functions_testrun: bool > #: Whether to skip smoke tests. > skip_smoke_tests: bool =3D False > #: The names of test suites and/or test cases to execute. > diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py > index fd49a7dc74..ee919e30d9 100644 > --- a/dts/framework/test_run.py > +++ b/dts/framework/test_run.py > @@ -346,6 +346,10 @@ def next(self) -> State | None: > test_run.ctx.tg_node.setup() > test_run.ctx.dpdk.setup() > test_run.ctx.topology.setup() > + > + if test_run.config.virtual_functions_testrun: > + test_run.ctx.topology.instantiate_vf_ports() > + > test_run.ctx.topology.configure_ports("sut", "dpdk") > test_run.ctx.tg.setup(test_run.ctx.topology) > > @@ -432,6 +436,9 @@ def description(self) -> str: > > def next(self) -> State | None: > """Next state.""" > + if self.test_run.config.virtual_functions_testrun: > + self.test_run.ctx.topology.delete_vf_ports() > + > self.test_run.ctx.shell_pool.terminate_current_pool() > self.test_run.ctx.tg.teardown() > self.test_run.ctx.topology.teardown() > diff --git a/dts/framework/testbed_model/linux_session.py > b/dts/framework/testbed_model/linux_session.py > index e01c2dd712..604245d855 100644 > --- a/dts/framework/testbed_model/linux_session.py > +++ b/dts/framework/testbed_model/linux_session.py > @@ -17,7 +17,11 @@ > > from typing_extensions import NotRequired > > -from framework.exception import ConfigurationError, InternalError, > RemoteCommandExecutionError > +from framework.exception import ( > + ConfigurationError, > + InternalError, > + RemoteCommandExecutionError, > +) > from framework.testbed_model.os_session import PortInfo > from framework.utils import expand_range > > @@ -211,11 +215,58 @@ def devbind_script_path(self) -> PurePath: > """ > raise InternalError("Accessed devbind script path before setup."= ) > > + def create_vfs(self, pf_port: Port) -> None: > + """Overrides :meth:`~.os_session.OSSession.create_vfs`. > + > + Raises: > + InternalError: If there are existing VFs which have to be > deleted. > + """ > + sys_bus_path =3D f"/sys/bus/pci/devices/{pf_port.pci}".replace("= :", > "\\:") > + curr_num_vfs =3D int( > + self.send_command(f"cat {sys_bus_path}/sriov_numvfs", > privileged=3DTrue).stdout > + ) > + if 0 < curr_num_vfs: > + raise InternalError("There are existing VFs on the port whic= h > must be deleted.") > + if curr_num_vfs =3D=3D 0: > + self.send_command(f"echo 1 | sudo tee > {sys_bus_path}/sriov_numvfs", privileged=3DTrue) > + self.refresh_lshw() > + > + def delete_vfs(self, pf_port: Port) -> None: > + """Overrides :meth:`~.os_session.OSSession.delete_vfs`.""" > + sys_bus_path =3D f"/sys/bus/pci/devices/{pf_port.pci}".replace("= :", > "\\:") > + curr_num_vfs =3D int( > + self.send_command(f"cat {sys_bus_path}/sriov_numvfs", > privileged=3DTrue).stdout > + ) > + if curr_num_vfs =3D=3D 0: > + self._logger.debug(f"No VFs found on port {pf_port.pci}, > skipping deletion") > + else: > + self.send_command(f"echo 0 | sudo tee > {sys_bus_path}/sriov_numvfs", privileged=3DTrue) > + > + def get_pci_addr_of_vfs(self, pf_port: Port) -> list[str]: > + """Overrides > :meth:`~.os_session.OSSession.get_pci_addr_of_vfs`.""" > + sys_bus_path =3D f"/sys/bus/pci/devices/{pf_port.pci}".replace("= :", > "\\:") > + curr_num_vfs =3D int(self.send_command(f"cat > {sys_bus_path}/sriov_numvfs").stdout) > + if curr_num_vfs > 0: > + pci_addrs =3D self.send_command( > + 'awk -F "PCI_SLOT_NAME=3D" "/PCI_SLOT_NAME=3D/ {print \\= $2}" ' > + + f"{sys_bus_path}/virtfn*/uevent", > + privileged=3DTrue, > + ) > + return pci_addrs.stdout.splitlines() > + else: > + return [] > + > @cached_property > def _lshw_net_info(self) -> list[LshwOutput]: > output =3D self.send_command("lshw -quiet -json -C network", > verify=3DTrue) > return json.loads(output.stdout) > > + def refresh_lshw(self) -> None: > + """Force refresh of cached lshw network info.""" > + if "_lshw_net_info" in self.__dict__: > + del self.__dict__["_lshw_net_info"] > + _ =3D self._lshw_net_info > + > def _update_port_attr(self, port: Port, attr_value: str | None, > attr_name: str) -> None: > if attr_value: > setattr(port, attr_name, attr_value) > diff --git a/dts/framework/testbed_model/os_session.py > b/dts/framework/testbed_model/os_session.py > index d7a09a0d5d..b6e03aa83d 100644 > --- a/dts/framework/testbed_model/os_session.py > +++ b/dts/framework/testbed_model/os_session.py > @@ -603,3 +603,45 @@ def configure_port_mtu(self, mtu: int, port: Port) -= > > None: > mtu: Desired MTU value. > port: Port to set `mtu` on. > """ > + > + @abstractmethod > + def create_vfs(self, pf_port: Port) -> None: > + """Creates virtual functions for `pf_port`. > + > + Checks how many virtual functions (VFs) `pf_port` supports, and > creates that > + number of VFs on the port. > + > + Args: > + pf_port: The port to create virtual functions on. > + > + Raises: > + InternalError: If the number of VFs is greater than 0 but > less than the > + maximum for `pf_port`. > + """ > + > + @abstractmethod > + def delete_vfs(self, pf_port: Port) -> None: > + """Deletes virtual functions for `pf_port`. > + > + Checks how many virtual functions (VFs) `pf_port` supports, and > deletes that > + number of VFs on the port. > + > + Args: > + pf_port: The port to delete virtual functions on. > + > + Raises: > + InternalError: If the number of VFs is greater than 0 but > less than the > + maximum for `pf_port`. > + """ > + > + @abstractmethod > + def get_pci_addr_of_vfs(self, pf_port: Port) -> list[str]: > + """Find the PCI addresses of all virtual functions (VFs) on the > port `pf_port`. > + > + Args: > + pf_port: The port to find the VFs on. > + > + Returns: > + A list containing all of the PCI addresses of the VFs on the > port. If the port has no > + VFs then the list will be empty. > + """ > diff --git a/dts/framework/testbed_model/topology.py > b/dts/framework/testbed_model/topology.py > index fb45969136..2bc69d46a9 100644 > --- a/dts/framework/testbed_model/topology.py > +++ b/dts/framework/testbed_model/topology.py > @@ -19,7 +19,7 @@ > from framework.exception import ConfigurationError, InternalError > from framework.testbed_model.node import Node > > -from .port import DriverKind, Port > +from .port import DriverKind, Port, PortConfig > > > class TopologyType(int, Enum): > @@ -74,6 +74,8 @@ class Topology: > type: TopologyType > sut_ports: list[Port] > tg_ports: list[Port] > + pf_ports: list[Port] > + vf_ports: list[Port] > > @classmethod > def from_port_links(cls, port_links: Iterator[PortLink]) -> Self: > @@ -101,7 +103,7 @@ def from_port_links(cls, port_links: > Iterator[PortLink]) -> Self: > msg =3D "More than two links in a topology are not > supported." > raise ConfigurationError(msg) > > - return cls(type, sut_ports, tg_ports) > + return cls(type, sut_ports, tg_ports, [], []) > > def node_and_ports_from_id(self, node_identifier: NodeIdentifier) -> > tuple[Node, list[Port]]: > """Retrieve node and its ports for the current topology. > @@ -160,6 +162,53 @@ def _setup_ports(self, node_identifier: > NodeIdentifier) -> None: > f"for port {port.name} in node {node.name}." > ) > > + def instantiate_vf_ports(self) -> None: > + """Create, setup, and add virtual functions to the list of ports > on the SUT node. > + > + Raises: > + InternalError: If virtual function creation fails. > + """ > + from framework.context import get_ctx > + > + ctx =3D get_ctx() > + > + for port in self.sut_ports: > + self.pf_ports.append(port) > + > + for port in self.pf_ports: > + ctx.sut_node.main_session.create_vfs(port) > + addr_list =3D > ctx.sut_node.main_session.get_pci_addr_of_vfs(port) > + if addr_list =3D=3D []: > + raise InternalError(f"Failed to create virtual function > on port {port.pci}") > + for addr in addr_list: > + vf_config =3D PortConfig( > + name=3Df"{port.name}-vf-{addr}", > + pci=3Daddr, > + os_driver_for_dpdk=3Dport.config.os_driver_for_dpdk, > + os_driver=3Dport.config.os_driver, > + ) > + self.vf_ports.append(Port(node=3Dport.node, > config=3Dvf_config)) > + ctx.sut_node.main_session.send_command(f"ip link set > {port.logical_name} vf 0 trust on") > + > + self.sut_ports.clear() > + self.sut_ports.extend(self.vf_ports) > + > + for port in self.pf_ports: > + ctx.sut_node.main_session.send_command( > + f"ip link set dev {port.logical_name} up", privileged=3D= True > + ) > I think this can become: ctx.sut_node.main_session.bring_up_link(self.pf_ports) which should do the exact same but runs through the method we already have implemented. > + > + def delete_vf_ports(self) -> None: > + """Delete virtual functions from the SUT node during test run > teardown.""" > + from framework.context import get_ctx > + > + ctx =3D get_ctx() > + > + for port in self.pf_ports: > + ctx.sut_node.main_session.delete_vfs(port) > + self.sut_ports.clear() > + self.sut_ports.extend(self.pf_ports) > + > def configure_ports( > self, node_identifier: NodeIdentifier, drivers: DriverKind | > tuple[DriverKind, ...] > ) -> None: > -- > 2.49.0 > > Reviewed-by: Patrick Robb Tested-by: Patrick Robb --0000000000009c3b7f063912eb46 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


On Wed, Jul 2, 2025 = at 12:23=E2=80=AFPM Dean Marx <dmarx@iol.unh.edu> wrote:
Add virtual functions to DTS framework, al= ong with
a field for specifying VF test runs in the config file.

Signed-off-by: Patrick Robb <probb@iol.unh.edu>
Signed-off-by: Dean Marx <dmarx@iol.unh.edu>
---
=C2=A0dts/framework/config/test_run.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0|=C2=A0 2 +
=C2=A0dts/framework/test_run.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 |=C2=A0 7 +++
=C2=A0dts/framework/testbed_model/linux_session.py | 53 +++++++++++++++++++= -
=C2=A0dts/framework/testbed_model/os_session.py=C2=A0 =C2=A0 | 42 +++++++++= +++++++
=C2=A0dts/framework/testbed_model/topology.py=C2=A0 =C2=A0 =C2=A0 | 53 ++++= +++++++++++++++-
=C2=A05 files changed, 154 insertions(+), 3 deletions(-)

diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_r= un.py
index b6e4099eeb..eefa32c3cb 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -467,6 +467,8 @@ class TestRunConfiguration(FrozenModel):
=C2=A0 =C2=A0 =C2=A0perf: bool
=C2=A0 =C2=A0 =C2=A0#: Whether to run functional tests.
=C2=A0 =C2=A0 =C2=A0func: bool
+=C2=A0 =C2=A0 #: Whether to run the testing with virtual functions instead= of physical functions
+=C2=A0 =C2=A0 virtual_functions_testrun: bool
=C2=A0 =C2=A0 =C2=A0#: Whether to skip smoke tests.
=C2=A0 =C2=A0 =C2=A0skip_smoke_tests: bool =3D False
=C2=A0 =C2=A0 =C2=A0#: The names of test suites and/or test cases to execut= e.
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py
index fd49a7dc74..ee919e30d9 100644
--- a/dts/framework/test_run.py
+++ b/dts/framework/test_run.py
@@ -346,6 +346,10 @@ def next(self) -> State | None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_run.ctx.tg_node.setup()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_run.ctx.dpdk.setup()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_run.ctx.topology.setup()
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if test_run.config.virtual_functions_testrun:<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 test_run.ctx.topology.instantiat= e_vf_ports()
+
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_run.ctx.topology.configure_ports(&qu= ot;sut", "dpdk")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0test_run.ctx.tg.setup(test_run.ctx.topolo= gy)

@@ -432,6 +436,9 @@ def description(self) -> str:

=C2=A0 =C2=A0 =C2=A0def next(self) -> State | None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""Next state.""= "
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.test_run.config.virtual_functions_test= run:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.test_run.ctx.topology.delet= e_vf_ports()
+
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.test_run.ctx.shell_pool.terminate_cu= rrent_pool()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.test_run.ctx.tg.teardown()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.test_run.ctx.topology.teardown()
diff --git a/dts/framework/testbed_model/linux_session.py b/dts/framework/t= estbed_model/linux_session.py
index e01c2dd712..604245d855 100644
--- a/dts/framework/testbed_model/linux_session.py
+++ b/dts/framework/testbed_model/linux_session.py
@@ -17,7 +17,11 @@

=C2=A0from typing_extensions import NotRequired

-from framework.exception import ConfigurationError, InternalError, RemoteC= ommandExecutionError
+from framework.exception import (
+=C2=A0 =C2=A0 ConfigurationError,
+=C2=A0 =C2=A0 InternalError,
+=C2=A0 =C2=A0 RemoteCommandExecutionError,
+)
=C2=A0from framework.testbed_model.os_session import PortInfo
=C2=A0from framework.utils import expand_range

@@ -211,11 +215,58 @@ def devbind_script_path(self) -> PurePath:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0raise InternalError("Accessed devbin= d script path before setup.")

+=C2=A0 =C2=A0 def create_vfs(self, pf_port: Port) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Overrides :meth:`~.os_sessio= n.OSSession.create_vfs`.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 InternalError: If there are exis= ting VFs which have to be deleted.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 sys_bus_path =3D f"/sys/bus/pci/devices/{= pf_port.pci}".replace(":", "\\:")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 curr_num_vfs =3D int(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"cat {sy= s_bus_path}/sriov_numvfs", privileged=3DTrue).stdout
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if 0 < curr_num_vfs:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise InternalError("There = are existing VFs on the port which must be deleted.")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if curr_num_vfs =3D=3D 0:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"echo 1 = | sudo tee {sys_bus_path}/sriov_numvfs", privileged=3DTrue)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.refresh_lshw()
+
+=C2=A0 =C2=A0 def delete_vfs(self, pf_port: Port) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Overrides :meth:`~.os_sessio= n.OSSession.delete_vfs`."""
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 sys_bus_path =3D f"/sys/bus/pci/devices/{= pf_port.pci}".replace(":", "\\:")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 curr_num_vfs =3D int(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"cat {sy= s_bus_path}/sriov_numvfs", privileged=3DTrue).stdout
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if curr_num_vfs =3D=3D 0:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.debug(f"No VFs= found on port {pf_port.pci}, skipping deletion")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"echo 0 = | sudo tee {sys_bus_path}/sriov_numvfs", privileged=3DTrue)
+
+=C2=A0 =C2=A0 def get_pci_addr_of_vfs(self, pf_port: Port) -> list[str]= :
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Overrides :meth:`~.os_sessio= n.OSSession.get_pci_addr_of_vfs`."""
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 sys_bus_path =3D f"/sys/bus/pci/devices/{= pf_port.pci}".replace(":", "\\:")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 curr_num_vfs =3D int(self.send_command(f"= cat {sys_bus_path}/sriov_numvfs").stdout)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if curr_num_vfs > 0:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pci_addrs =3D self.send_command(=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 'awk -F "= PCI_SLOT_NAME=3D" "/PCI_SLOT_NAME=3D/ {print \\$2}" ' +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 + f"{sys_bus_= path}/virtfn*/uevent",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 privileged=3DTrue,=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return pci_addrs.stdout.splitlin= es()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return []
+
=C2=A0 =C2=A0 =C2=A0@cached_property
=C2=A0 =C2=A0 =C2=A0def _lshw_net_info(self) -> list[LshwOutput]:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0output =3D self.send_command("lshw -= quiet -json -C network", verify=3DTrue)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return json.loads(output.stdout)

+=C2=A0 =C2=A0 def refresh_lshw(self) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Force refresh of cached lshw= network info."""
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if "_lshw_net_info" in self.__dict__= :
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 del self.__dict__["_lshw_ne= t_info"]
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 _ =3D self._lshw_net_info
+
=C2=A0 =C2=A0 =C2=A0def _update_port_attr(self, port: Port, attr_value: str= | None, attr_name: str) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if attr_value:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0setattr(port, attr_name, at= tr_value)
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/test= bed_model/os_session.py
index d7a09a0d5d..b6e03aa83d 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -603,3 +603,45 @@ def configure_port_mtu(self, mtu: int, port: Port) -&g= t; None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0mtu: Desired MTU value.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0port: Port to set `mtu` on.=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
+
+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def create_vfs(self, pf_port: Port) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Creates virtual functions fo= r `pf_port`.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Checks how many virtual functions (VFs) `pf_po= rt` supports, and creates that
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 number of VFs on the port.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pf_port: The port to create virt= ual functions on.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 InternalError: If the number of = VFs is greater than 0 but less than the
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 maximum for `pf_port`.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+
+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def delete_vfs(self, pf_port: Port) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Deletes virtual functions fo= r `pf_port`.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Checks how many virtual functions (VFs) `pf_po= rt` supports, and deletes that
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 number of VFs on the port.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pf_port: The port to delete virt= ual functions on.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 InternalError: If the number of = VFs is greater than 0 but less than the
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 maximum for `pf_port`.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+
+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def get_pci_addr_of_vfs(self, pf_port: Port) -> list[str]= :
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Find the PCI addresses of al= l virtual functions (VFs) on the port `pf_port`.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pf_port: The port to find the VF= s on.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 A list containing all of the PCI= addresses of the VFs on the port. If the port has no
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 VFs then the list will be empty.=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
diff --git a/dts/framework/testbed_model/topology.py b/dts/framework/testbe= d_model/topology.py
index fb45969136..2bc69d46a9 100644
--- a/dts/framework/testbed_model/topology.py
+++ b/dts/framework/testbed_model/topology.py
@@ -19,7 +19,7 @@
=C2=A0from framework.exception import ConfigurationError, InternalError
=C2=A0from framework.testbed_model.node import Node

-from .port import DriverKind, Port
+from .port import DriverKind, Port, PortConfig


=C2=A0class TopologyType(int, Enum):
@@ -74,6 +74,8 @@ class Topology:
=C2=A0 =C2=A0 =C2=A0type: TopologyType
=C2=A0 =C2=A0 =C2=A0sut_ports: list[Port]
=C2=A0 =C2=A0 =C2=A0tg_ports: list[Port]
+=C2=A0 =C2=A0 pf_ports: list[Port]
+=C2=A0 =C2=A0 vf_ports: list[Port]

=C2=A0 =C2=A0 =C2=A0@classmethod
=C2=A0 =C2=A0 =C2=A0def from_port_links(cls, port_links: Iterator[PortLink]= ) -> Self:
@@ -101,7 +103,7 @@ def from_port_links(cls, port_links: Iterator[PortLink]= ) -> Self:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0msg =3D "More than two links in a topology are not supported."=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0raise ConfigurationError(msg)

-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return cls(type, sut_ports, tg_ports)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return cls(type, sut_ports, tg_ports, [], [])<= br>
=C2=A0 =C2=A0 =C2=A0def node_and_ports_from_id(self, node_identifier: NodeI= dentifier) -> tuple[Node, list[Port]]:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""Retrieve node and its p= orts for the current topology.
@@ -160,6 +162,53 @@ def _setup_ports(self, node_identifier: NodeIdentifier= ) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0f"for port {port.name} in node {node.name}."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)

+=C2=A0 =C2=A0 def instantiate_vf_ports(self) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Create, setup, and add virtu= al functions to the list of ports on the SUT node.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 InternalError: If virtual functi= on creation fails.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 from framework.context import get_ctx
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 ctx =3D get_ctx()
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for port in self.sut_ports:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.pf_ports.append(port)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for port in self.pf_ports:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ctx.sut_node.main_session.create= _vfs(port)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 addr_list =3D ctx.sut_node.main_= session.get_pci_addr_of_vfs(port)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if addr_list =3D=3D []:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise InternalErro= r(f"Failed to create virtual function on port {port.pci}")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for addr in addr_list:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 vf_config =3D Port= Config(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 name= =3Df"{port.name}-vf-{addr}",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pci= =3Daddr,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 os_d= river_for_dpdk=3Dport.config.os_driver_for_dpdk,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 os_d= river=3Dport.config.os_driver,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.vf_ports.appe= nd(Port(node=3Dport.node, config=3Dvf_config))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ctx.sut_node.main_session.send_c= ommand(f"ip link set {port.logical_name} vf 0 trust on")
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_ports.clear()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_ports.extend(self.vf_ports)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for port in self.pf_ports:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ctx.sut_node.main_session.send_c= ommand(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"ip link set= dev {port.logical_name} up", privileged=3DTrue
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )

I think this can become:=C2=A0

ctx.sut_node.= main_session.bring_up_link(self.pf_ports)

which sh= ould do the exact same but runs through the method we already have implemen= ted.
=C2=A0
probb@iol.unh.edu>
Tested-by: Patrick Ro= bb <probb@iol.unh.edu>=C2=A0=
--0000000000009c3b7f063912eb46--