Reviewed-by: Nicholas Pratte <npra...@iol.unh.edu>
On Fri, Mar 14, 2025 at 9:19 AM Luca Vizzarro <luca.vizza...@arm.com> wrote: > > Multi inheritance has proven to be flaky in Python, therefore revert > back to a simpler approach where classes will only inherit one base > class. As part of this change, instead of making the > ScapyTrafficGenerator class inehrit a PythonShell, use simple class > composition, by making the shell a class attribute. > > Signed-off-by: Luca Vizzarro <luca.vizza...@arm.com> > Reviewed-by: Paul Szczepanek <paul.szczepa...@arm.com> > --- > .../remote_session/interactive_shell.py | 9 +---- > .../testbed_model/traffic_generator/scapy.py | 38 +++++++++---------- > .../traffic_generator/traffic_generator.py | 6 +-- > dts/framework/utils.py | 14 ------- > 4 files changed, 22 insertions(+), 45 deletions(-) > > diff --git a/dts/framework/remote_session/interactive_shell.py > b/dts/framework/remote_session/interactive_shell.py > index 6b7ca0b6af..d7e566e5c4 100644 > --- a/dts/framework/remote_session/interactive_shell.py > +++ b/dts/framework/remote_session/interactive_shell.py > @@ -37,10 +37,9 @@ > from framework.params import Params > from framework.settings import SETTINGS > from framework.testbed_model.node import Node > -from framework.utils import MultiInheritanceBaseClass > > > -class InteractiveShell(MultiInheritanceBaseClass, ABC): > +class InteractiveShell(ABC): > """The base class for managing interactive shells. > > This class shouldn't be instantiated directly, but instead be extended. > It contains > @@ -90,20 +89,15 @@ def __init__( > name: str | None = None, > privileged: bool = False, > app_params: Params = Params(), > - **kwargs, > ) -> None: > """Create an SSH channel during initialization. > > - Additional keyword arguments can be passed through `kwargs` if > needed for fulfilling other > - constructors in the case of multiple inheritance. > - > Args: > node: The node on which to run start the interactive shell. > name: Name for the interactive shell to use for logging. This > name will be appended to > the name of the underlying node which it is running on. > privileged: Enables the shell to run as superuser. > app_params: The command line parameters to be passed to the > application on startup. > - **kwargs: Any additional arguments if any. > """ > self._node = node > if name is None: > @@ -112,7 +106,6 @@ def __init__( > self._app_params = app_params > self._privileged = privileged > self._timeout = SETTINGS.timeout > - super().__init__(**kwargs) > > def _setup_ssh_channel(self): > self._ssh_channel = > self._node.main_session.interactive_session.session.invoke_shell() > diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py > b/dts/framework/testbed_model/traffic_generator/scapy.py > index 520561b2eb..78a6ded74c 100644 > --- a/dts/framework/testbed_model/traffic_generator/scapy.py > +++ b/dts/framework/testbed_model/traffic_generator/scapy.py > @@ -34,7 +34,7 @@ > from .capturing_traffic_generator import CapturingTrafficGenerator > > > -class ScapyTrafficGenerator(PythonShell, CapturingTrafficGenerator): > +class ScapyTrafficGenerator(CapturingTrafficGenerator): > """Provides access to scapy functions on a traffic generator node. > > This class extends the base with remote execution of scapy functions. > All methods for > @@ -56,6 +56,8 @@ class also extends > :class:`.capturing_traffic_generator.CapturingTrafficGenerato > first. > """ > > + _shell: PythonShell > + > _config: ScapyTrafficGeneratorConfig > > #: Name of sniffer to ensure the same is used in all places > @@ -82,25 +84,21 @@ def __init__(self, tg_node: Node, config: > ScapyTrafficGeneratorConfig, **kwargs) > tg_node.config.os == OS.linux > ), "Linux is the only supported OS for scapy traffic generation" > > - super().__init__(node=tg_node, config=config, tg_node=tg_node, > **kwargs) > - self.start_application() > + super().__init__(tg_node=tg_node, config=config, **kwargs) > + self._shell = PythonShell(tg_node, "scapy", privileged=True) > > def setup(self, ports: Iterable[Port]): > """Extends :meth:`.traffic_generator.TrafficGenerator.setup`. > > - Brings up the port links. > + Starts up the traffic generator and brings up the port links. > """ > - super().setup(ports) > self._tg_node.main_session.bring_up_link(ports) > + self._shell.start_application() > + self._shell.send_command("from scapy.all import *") > > - def start_application(self) -> None: > - """Extends > :meth:`framework.remote_session.interactive_shell.start_application`. > - > - Adds a command that imports everything from the scapy library > immediately after starting > - the shell for usage in later calls to the methods of this class. > - """ > - super().start_application() > - self.send_command("from scapy.all import *") > + def close(self): > + """Close traffic generator.""" > + self._shell.close() > > def _send_packets(self, packets: list[Packet], port: Port) -> None: > """Implementation for sending packets without capturing any received > traffic. > @@ -116,7 +114,7 @@ def _send_packets(self, packets: list[Packet], port: > Port) -> None: > "verbose=True", > ")", > ] > - self.send_command(f"\n{self._python_indentation}".join(send_command)) > + > self._shell.send_command(f"\n{self._python_indentation}".join(send_command)) > > def _send_packets_and_capture( > self, > @@ -155,7 +153,7 @@ def _shell_set_packet_list(self, packets: list[Packet]) > -> None: > packets: The list of packets to recreate in the shell. > """ > self._logger.info("Building a list of packets to send.") > - self.send_command( > + self._shell.send_command( > f"{self._send_packet_list_name} = [{', > '.join(map(Packet.command, packets))}]" > ) > > @@ -202,7 +200,7 @@ def _shell_create_sniffer( > """ > self._shell_set_packet_list(packets_to_send) > > - self.send_command("import time") > + self._shell.send_command("import time") > sniffer_commands = [ > f"{self._sniffer_name} = AsyncSniffer(", > f"iface='{recv_port.logical_name}',", > @@ -220,7 +218,7 @@ def _shell_create_sniffer( > if filter_config: > sniffer_commands.insert(-1, f"filter='{filter_config}'") > > - > self.send_command(f"\n{self._python_indentation}".join(sniffer_commands)) > + > self._shell.send_command(f"\n{self._python_indentation}".join(sniffer_commands)) > > def _shell_start_and_stop_sniffing(self, duration: float) -> > list[Packet]: > """Start asynchronous sniffer, run for a set `duration`, then > collect received packets. > @@ -237,12 +235,12 @@ def _shell_start_and_stop_sniffing(self, duration: > float) -> list[Packet]: > A list of all packets that were received while the sniffer was > running. > """ > sniffed_packets_name = "gathered_packets" > - self.send_command(f"{self._sniffer_name}.start()") > + self._shell.send_command(f"{self._sniffer_name}.start()") > # Insert a one second delay to prevent timeout errors from occurring > time.sleep(duration + 1) > - self.send_command(f"{sniffed_packets_name} = > {self._sniffer_name}.stop(join=True)") > + self._shell.send_command(f"{sniffed_packets_name} = > {self._sniffer_name}.stop(join=True)") > # An extra newline is required here due to the nature of interactive > Python shells > - packet_strs = self.send_command( > + packet_strs = self._shell.send_command( > f"for pakt in {sniffed_packets_name}: > print(bytes_base64(pakt.build()))\n" > ) > # In the string of bytes "b'XXXX'", we only want the contents > ("XXXX") > diff --git > a/dts/framework/testbed_model/traffic_generator/traffic_generator.py > b/dts/framework/testbed_model/traffic_generator/traffic_generator.py > index 804662e114..fbd9771eba 100644 > --- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py > +++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py > @@ -17,10 +17,10 @@ > from framework.logger import DTSLogger, get_dts_logger > from framework.testbed_model.node import Node > from framework.testbed_model.port import Port > -from framework.utils import MultiInheritanceBaseClass, get_packet_summaries > +from framework.utils import get_packet_summaries > > > -class TrafficGenerator(MultiInheritanceBaseClass, ABC): > +class TrafficGenerator(ABC): > """The base traffic generator. > > Exposes the common public methods of all traffic generators and defines > private methods > @@ -48,13 +48,13 @@ def __init__(self, tg_node: Node, config: > TrafficGeneratorConfig, **kwargs): > self._config = config > self._tg_node = tg_node > self._logger = get_dts_logger(f"{self._tg_node.name} > {self._config.type}") > - super().__init__(**kwargs) > > def setup(self, ports: Iterable[Port]): > """Setup the traffic generator.""" > > def teardown(self, ports: Iterable[Port]): > """Teardown the traffic generator.""" > + self.close() > > def send_packet(self, packet: Packet, port: Port) -> None: > """Send `packet` and block until it is fully sent. > diff --git a/dts/framework/utils.py b/dts/framework/utils.py > index d6f4c11d58..24277633c0 100644 > --- a/dts/framework/utils.py > +++ b/dts/framework/utils.py > @@ -299,20 +299,6 @@ def _make_packet() -> Packet: > return [_make_packet() for _ in range(number_of)] > > > -class MultiInheritanceBaseClass: > - """A base class for classes utilizing multiple inheritance. > - > - This class enables it's subclasses to support both single and multiple > inheritance by acting as > - a stopping point in the tree of calls to the constructors of > superclasses. This class is able > - to exist at the end of the Method Resolution Order (MRO) so that > subclasses can call > - :meth:`super.__init__` without repercussion. > - """ > - > - def __init__(self) -> None: > - """Call the init method of :class:`object`.""" > - super().__init__() > - > - > def to_pascal_case(text: str) -> str: > """Convert `text` from snake_case to PascalCase.""" > return "".join([seg.capitalize() for seg in text.split("_")]) > -- > 2.43.0 >