22# Licensed under the MIT license.
33import re
44from functools import partial
5- from typing import Any , Dict , Tuple , cast
5+ from typing import Any , Callable , Dict , List , Tuple , cast
6+
7+ from microsoft .testsuites .performance .common import (
8+ perf_iperf ,
9+ perf_ntttcp ,
10+ perf_tcp_pps ,
11+ )
612
713from lisa import (
814 Logger ,
2026from lisa .sut_orchestrator import CLOUD_HYPERVISOR
2127from lisa .sut_orchestrator .libvirt .context import get_node_context
2228from lisa .testsuite import TestResult
23- from lisa .tools import Dhclient , Lspci , Sysctl
29+ from lisa .tools import Dhclient , Kill , Lspci , Sysctl
2430from lisa .tools .iperf3 import (
2531 IPERF_TCP_BUFFER_LENGTHS ,
2632 IPERF_TCP_CONCURRENCY ,
2733 IPERF_UDP_BUFFER_LENGTHS ,
2834 IPERF_UDP_CONCURRENCY ,
2935)
3036from lisa .util import (
37+ LisaException ,
3138 SkippedException ,
3239 constants ,
3340 find_group_in_lines ,
3441 find_groups_in_lines ,
3542)
3643from lisa .util .logger import get_logger
3744from lisa .util .parallel import run_in_parallel
38- from microsoft .testsuites .performance .common import (
39- cleanup_process ,
40- perf_iperf ,
41- perf_ntttcp ,
42- perf_tcp_pps ,
43- )
4445
4546
4647@TestSuiteMetadata (
5051 This test suite is to validate linux network performance
5152 for various NIC passthrough scenarios.
5253 """ ,
54+ requirement = simple_requirement (
55+ supported_platform_type = [CLOUD_HYPERVISOR ],
56+ unsupported_os = [Windows ],
57+ ),
5358)
54- class NetworkPerformace (TestSuite ):
59+ class NetworkPerformance (TestSuite ):
60+ # Timeout values:
61+ # TIMEOUT: 12000s (3.3 hrs) - accounts for test execution + network setup overhead
62+ # PPS_TIMEOUT: 3000s (50 min) - shorter for PPS tests which are less intensive
5563 TIMEOUT = 12000
5664 PPS_TIMEOUT = 3000
5765
66+ # Track baremetal host nodes for cleanup
67+ _baremetal_hosts : list [RemoteNode ] = []
68+
5869 # Network device passthrough tests between host and guest
5970 @TestCaseMetadata (
6071 description = """
@@ -239,7 +250,7 @@ def perf_tcp_ntttcp_passthrough_host_guest(
239250
240251 @TestCaseMetadata (
241252 description = """
242- This test case uses ntttcp to test sriov tcp network throughput.
253+ This test case uses ntttcp to test passthrough udp network throughput.
243254 """ ,
244255 priority = 3 ,
245256 timeout = TIMEOUT ,
@@ -423,7 +434,8 @@ def perf_tcp_max_pps_passthrough_two_guest(self, result: TestResult) -> None:
423434
424435 @TestCaseMetadata (
425436 description = """
426- This test case uses ntttcp to test sriov tcp network throughput.
437+ This test case uses ntttcp to test passthrough tcp network throughput
438+ between two guest VMs.
427439 """ ,
428440 priority = 3 ,
429441 timeout = TIMEOUT ,
@@ -458,7 +470,8 @@ def perf_tcp_ntttcp_passthrough_two_guest(self, result: TestResult) -> None:
458470
459471 @TestCaseMetadata (
460472 description = """
461- This test case uses ntttcp to test sriov tcp network throughput.
473+ This test case uses ntttcp to test passthrough udp network throughput
474+ between two guest VMs.
462475 """ ,
463476 priority = 3 ,
464477 timeout = TIMEOUT ,
@@ -500,14 +513,6 @@ def _configure_passthrough_nic_for_node(
500513 if not ctx .passthrough_devices :
501514 raise SkippedException ("No passthrough devices found for node" )
502515
503- # Configure the nw interface on guest
504- node .tools [Dhclient ].run (
505- force_run = True ,
506- sudo = True ,
507- expected_exit_code = 0 ,
508- expected_exit_code_failure_message = "dhclient run failed" ,
509- )
510-
511516 lspci = node .tools [Lspci ]
512517 pci_devices = lspci .get_devices_by_type (
513518 constants .DEVICE_TYPE_SRIOV , force_run = True
@@ -521,8 +526,14 @@ def _configure_passthrough_nic_for_node(
521526 device_addr = device .slot
522527 break
523528
529+ if device_addr is None :
530+ raise LisaException (
531+ f"No non-virtio passthrough device found. "
532+ f"Available devices: { [d .slot for d in pci_devices ]} "
533+ )
534+
524535 # Get the interface name
525- err_msg : str = "Can't find interface from PCI address"
536+ err_msg : str = f "Can't find interface from PCI address: { device_addr } "
526537 device_path = node .execute (
527538 cmd = (
528539 "find /sys/class/net/*/device/subsystem/devices"
@@ -542,6 +553,38 @@ def _configure_passthrough_nic_for_node(
542553 interface_name = interface_name_raw .get ("INTERFACE_NAME" , "" )
543554 assert interface_name , "Can not find interface name"
544555
556+ # Bring the interface up before configuring it
557+ node .execute (
558+ cmd = f"ip link set { interface_name } up" ,
559+ sudo = True ,
560+ expected_exit_code = 0 ,
561+ expected_exit_code_failure_message = (
562+ f"Failed to bring up interface { interface_name } "
563+ ),
564+ )
565+
566+ # Wait for carrier (link up) - some NICs take time to negotiate
567+ # Exit code 124 = timeout (no carrier), which is acceptable if DHCP works anyway
568+ carrier_result = node .execute (
569+ cmd = f"timeout 30 sh -c 'until cat /sys/class/net/{ interface_name } /carrier"
570+ f" 2>/dev/null | grep -q 1; do sleep 1; done'" ,
571+ sudo = True ,
572+ shell = True ,
573+ )
574+ if carrier_result .exit_code == 124 :
575+ node .log .warning (
576+ f"Interface { interface_name } carrier not detected after 30s. "
577+ f"Proceeding with DHCP anyway - may fail if no physical link."
578+ )
579+ elif carrier_result .exit_code != 0 :
580+ raise LisaException (
581+ f"Failed to check carrier on { interface_name } : "
582+ f"exit code { carrier_result .exit_code } "
583+ )
584+
585+ # Configure the nw interface on guest with dhclient for the specific interface
586+ node .tools [Dhclient ].renew (interface_name )
587+
545588 # Get the interface ip
546589 err_msg = f"Failed to get interface details for: { interface_name } "
547590 interface_details = node .execute (
@@ -557,7 +600,11 @@ def _configure_passthrough_nic_for_node(
557600 single_line = False ,
558601 )
559602 passthrough_nic_ip = interface_ip .get ("INTERFACE_IP" , "" )
560- assert passthrough_nic_ip , "Can not find interface IP"
603+ if not passthrough_nic_ip :
604+ raise LisaException (
605+ f"Failed to get IP for passthrough interface '{ interface_name } '. "
606+ f"Interface details: { interface_details [:200 ]} "
607+ )
561608
562609 test_node = cast (RemoteNode , node )
563610 test_node .internal_address = passthrough_nic_ip
@@ -568,9 +615,14 @@ def _get_host_as_server(self, variables: Dict[str, Any]) -> RemoteNode:
568615 ip = variables .get ("baremetal_host_ip" , "" )
569616 username = variables .get ("baremetal_host_username" , "" )
570617 passwd = variables .get ("baremetal_host_password" , "" )
618+ private_key = variables .get ("baremetal_host_private_key_file" , "" )
571619
572- if not (ip and username and passwd ):
573- raise SkippedException ("Server-Node details are not provided" )
620+ if not (ip and username and (passwd or private_key )):
621+ raise SkippedException (
622+ "Server-Node details are not provided. Required: "
623+ "baremetal_host_ip, baremetal_host_username, and either "
624+ "baremetal_host_password or baremetal_host_private_key_file"
625+ )
574626
575627 server = RemoteNode (
576628 runbook = schema .Node (name = "baremetal-host" ),
@@ -584,7 +636,11 @@ def _get_host_as_server(self, variables: Dict[str, Any]) -> RemoteNode:
584636 public_port = 22 ,
585637 username = username ,
586638 password = passwd ,
639+ private_key_file = private_key ,
587640 )
641+ # Track baremetal host for cleanup
642+ if server not in self ._baremetal_hosts :
643+ self ._baremetal_hosts .append (server )
588644 return server
589645
590646 def _get_host_nic_name (self , node : RemoteNode ) -> str :
@@ -620,26 +676,28 @@ def _get_host_nic_name(self, node: RemoteNode) -> str:
620676 def after_case (self , log : Logger , ** kwargs : Any ) -> None :
621677 environment : Environment = kwargs .pop ("environment" )
622678
679+ # Combine environment nodes and baremetal host nodes for cleanup
680+ all_nodes = list (environment .nodes .list ())
681+ if self ._baremetal_hosts :
682+ all_nodes .extend (self ._baremetal_hosts )
683+
623684 # use these cleanup functions
624- def do_process_cleanup (process : str ) -> None :
625- cleanup_process (environment , process )
685+ def do_process_cleanup (process : str , node : Node ) -> None :
686+ # Kill the process on the specific node
687+ kill = node .tools [Kill ]
688+ kill .by_name (process , ignore_not_exist = True )
626689
627690 def do_sysctl_cleanup (node : Node ) -> None :
628691 node .tools [Sysctl ].reset ()
629692
630- # to run parallel cleanup of processes and sysctl settings
631- run_in_parallel (
632- [
633- partial (do_process_cleanup , x )
634- for x in [
635- "lagscope" ,
636- "netperf" ,
637- "netserver" ,
638- "ntttcp" ,
639- "iperf3" ,
640- ]
641- ]
642- )
643- run_in_parallel (
644- [partial (do_sysctl_cleanup , x ) for x in environment .nodes .list ()]
645- )
693+ # to run parallel cleanup of processes on all nodes
694+ cleanup_tasks : List [Callable [[], None ]] = []
695+ for process in ["lagscope" , "netperf" , "netserver" , "ntttcp" , "iperf3" ]:
696+ for node in all_nodes :
697+ cleanup_tasks .append (partial (do_process_cleanup , process , node ))
698+
699+ run_in_parallel (cleanup_tasks )
700+ run_in_parallel ([partial (do_sysctl_cleanup , x ) for x in all_nodes ])
701+
702+ # Clear the baremetal hosts list after cleanup
703+ self ._baremetal_hosts .clear ()
0 commit comments