diff --git a/schedule/functional/extra_tests_network_bonding.yaml b/schedule/functional/extra_tests_network_bonding.yaml new file mode 100644 index 000000000000..3761955dab86 --- /dev/null +++ b/schedule/functional/extra_tests_network_bonding.yaml @@ -0,0 +1,15 @@ +name: extra_tests_network_bonding +description: > + Maintainer: vkatkalov. + Extra bonding tests +conditional_schedule: + bonding: + HOSTNAME: + 'client': + - network/network_bonding + 'server': +schedule: + - boot/boot_to_desktop + - installation/bootloader_start + - network/network_bonding_setup + - '{{bonding}}' \ No newline at end of file diff --git a/tests/network/network_bonding.pm b/tests/network/network_bonding.pm new file mode 100644 index 000000000000..3923df0b962a --- /dev/null +++ b/tests/network/network_bonding.pm @@ -0,0 +1,368 @@ +# SUSE's openQA tests +# +# Copyright 2016-2023 SUSE LLC +# SPDX-License-Identifier: FSFAP + +# Summary: Test network bonding capability and connectivity +# Maintainer: QE Core + +use base "consoletest"; +use strict; +use warnings; +use testapi; +use power_action_utils "power_action"; +use utils qw(validate_script_output_retry); +use serial_terminal qw(select_serial_terminal); +use lockapi; +use utils; +use console::ovs_utils; +use version_utils; + +my $BARRIER_BONDING_TESTS_DONE = 'BONDING_TESTS_DONE'; + +my $server_ip = "10.0.2.101"; +my $client_ip = "10.0.2.102"; +my $subnet = "/24"; + +my $use_nm; + +sub cidr_to_netmask { + my ($cidr_str) = @_; + + # Extract the numeric part from the string (e.g., "/32" -> 32) + $cidr_str =~ /(\d+)/; + my $cidr = $1; + + my $binmask = '1' x $cidr . '0' x (32 - $cidr); + my @octets = unpack("C4", pack("B32", $binmask)); + return join('.', @octets); +} + +sub set_nics_link_speed_duplex { + my ($nics_ref) = @_; + my @nics = @$nics_ref; + + for my $nic (@nics) { + record_info("SET $nic link speed and duplex", "ethtool -s $nic speed 1000 duplex full autoneg off"); + assert_script_run("ethtool -s $nic speed 1000 duplex full autoneg off"); + } +} + +sub is_nm_used { + return script_run("systemctl is-active NetworkManager") == 0; +} + +sub is_wicked_used { + return script_run("systemctl is-active wicked") == 0; +} + +sub check_connectivity { + my ($bond_name) = @_; + my $ping_host = $server_ip; + my $ping_command = "ping -c1 -I $bond_name $ping_host"; + + validate_script_output_retry( + $ping_command, + sub { m/1 packets transmitted, 1 received, 0% packet loss,/ } + ); +} + +sub get_nics_nm { + my ($bond_name) = @_; + my @devices; + + # Use nmcli to get the list of network interfaces and their states + my @nmcli_output = split(/\n/, script_output('nmcli -f DEVICE,STATE device')); + + foreach my $line (@nmcli_output) { + # Skip the header line + next if $line =~ /^DEVICE\s+STATE$/; + + # Split each line into device and state + my ($device, $state) = split(/\s+/, $line, 2); + + # Skip if the device is 'connected (externally)' or is the loopback or bond interface + next if ($state =~ /^connected \(externally\)/ || $device eq 'lo' || $device eq $bond_name); + + # Include the device if it's 'connected' or any 'connecting' state (e.g., 'connecting (getting IP configuration)') + push @devices, $device if ($state =~ /^connected\b/ || $state =~ /^connecting\b/); + } + + return @devices; +} + +sub get_nics_wicked { + my ($bond_name) = @_; + my @devices; + my $within_interface_block = 0; + + # Use wicked show-xml to get the list of network interfaces + my @xml_output = split(/\n/, script_output('wicked show-xml')); + + foreach my $line (@xml_output) { + # Check if we are entering an block + if ($line =~ //) { + $within_interface_block = 1; + } + + # Check if we are exiting an block + if ($line =~ /<\/interface>/) { + $within_interface_block = 0; + } + + # If inside an block and we find a tag, capture the interface name + if ($within_interface_block && $line =~ /([^<]+)<\/name>/) { + $within_interface_block = 0; + my $device = $1; + # Skip the loopback device 'lo' and any bond interface + next if $device eq 'lo'; + next if $device eq $bond_name; + # Add the device to the list + push @devices, $device; + # We found the within this block, no need to look further in this block + } + } + + return @devices; +} + +sub get_nics { + my ($bond_name) = @_; + + if ($use_nm) { + return get_nics_nm($bond_name); + } else { + return get_nics_wicked($bond_name); + } +} + +sub delete_existing_connections_nm { + my $output = script_output('nmcli -g DEVICE,UUID conn show'); + my %seen_uuids; + + foreach my $line (split "\n", $output) { + next if $line =~ /^\s*$/; + + my ($device, $uuid) = split /:/, $line; + next if defined $device && $device eq 'lo'; + next if exists $seen_uuids{$uuid}; + + $seen_uuids{$uuid} = 1; + script_run "nmcli con delete uuid '$uuid'"; + } +} + +sub delete_existing_connections_wicked { + script_run "wicked ifdown all"; + script_run "rm -f /etc/sysconfig/network/ifcfg-*"; +} + +sub delete_existing_connections { + if ($use_nm) { + delete_existing_connections_nm(); + } else { + delete_existing_connections_wicked(); + } +} + +sub create_bond_nm { + my ($bond_name, $bond_mode, $miimon) = @_; + assert_script_run "nmcli con add type bond ifname $bond_name con-name $bond_name bond.options \"mode=$bond_mode, miimon=$miimon\""; + assert_script_run "nmcli connection modify $bond_name connection.autoconnect-slaves 1"; +} + +sub create_and_configure_bond_wicked { + my ($bond_name, $bond_mode, $miimon, $ip_addr, $subnet, $gateway, @devices) = @_; + + # Remove the old configuration file if it exists + script_run "rm -f /etc/sysconfig/network/ifcfg-$bond_name"; + + # Assuming $subnet holds the CIDR notation like '/24' + my $ifcfg_subnet = cidr_to_netmask $subnet; + + # Create the new configuration file + script_run "echo 'BOOTPROTO=static' > /etc/sysconfig/network/ifcfg-$bond_name"; + script_run "echo 'STARTMODE=auto' >> /etc/sysconfig/network/ifcfg-$bond_name"; + script_run "echo 'BONDING_MASTER=yes' >> /etc/sysconfig/network/ifcfg-$bond_name"; + script_run "echo 'BONDING_SLAVE=no' >> /etc/sysconfig/network/ifcfg-$bond_name"; + script_run "echo 'BONDING_MODULE_OPTS=\"mode=$bond_mode miimon=$miimon\"' >> /etc/sysconfig/network/ifcfg-$bond_name"; + script_run "echo 'IPADDR=$ip_addr' >> /etc/sysconfig/network/ifcfg-$bond_name"; + script_run "echo 'NETMASK=$ifcfg_subnet' >> /etc/sysconfig/network/ifcfg-$bond_name"; + script_run "echo 'GATEWAY=$gateway' >> /etc/sysconfig/network/ifcfg-$bond_name"; + + # Append slave interfaces to the bond configuration + my $index = 1; + foreach my $device (@devices) { + script_run "echo 'BONDING_SLAVE_$index=$device' >> /etc/sysconfig/network/ifcfg-$bond_name"; + $index++; + } +} + +sub configure_device_route_wicked { + my ($bond_name, $gateway) = @_; + my $route_config_file = "/etc/sysconfig/network/ifroute-$bond_name"; + + # Delete existing route configuration if it exists + script_run "rm -f $route_config_file"; + + # Create a new route configuration file + script_run "echo 'default $gateway dev $bond_name' > $route_config_file"; +} + +sub configure_device_route { + my ($bond_name, $gateway) = @_; + if ($use_nm) { + } else { + configure_device_route_wicked($bond_name, $gateway); + } +} + +sub create_bond { + my ($bond_name, $bond_mode, $miimon, @nics) = @_; + if ($use_nm) { + create_bond_nm($bond_name, $bond_mode, $miimon); + } else { + create_and_configure_bond_wicked($bond_name, $bond_mode, $miimon, $client_ip, $subnet, $server_ip, @nics); + } +} + +sub add_devices_to_bond_nm { + my ($bond_name, @devices) = @_; + foreach my $device (@devices) { + assert_script_run "nmcli con add type ethernet ifname $device master $bond_name"; + } +} + +sub add_devices_to_bond_wicked { + my ($bond_name, @devices) = @_; + + foreach my $device (@devices) { + # Remove the old configuration file for the device if it exists + script_run "rm -f /etc/sysconfig/network/ifcfg-$device"; + + # Create the new configuration file for the device + script_run "echo 'BOOTPROTO=static' > /etc/sysconfig/network/ifcfg-$device"; + script_run "echo 'STARTMODE=auto' >> /etc/sysconfig/network/ifcfg-$device"; + script_run "echo 'BONDING_MASTER=no' >> /etc/sysconfig/network/ifcfg-$device"; + script_run "echo 'BONDING_SLAVE=yes' >> /etc/sysconfig/network/ifcfg-$device"; + script_run "echo 'BONDING_MASTER_IF=$bond_name' >> /etc/sysconfig/network/ifcfg-$device"; + } +} + +sub add_devices_to_bond { + my ($bond_name, @devices) = @_; + if ($use_nm) { + add_devices_to_bond_nm($bond_name, @devices); + } else { + add_devices_to_bond_wicked($bond_name, @devices); + } +} + +sub configure_client_ip { + my ($bond_name) = @_; + if ($use_nm) { + assert_script_run "nmcli con modify $bond_name ipv4.addresses ${client_ip}${subnet}"; + assert_script_run "nmcli con modify $bond_name ipv4.gateway $server_ip"; + assert_script_run "nmcli con modify $bond_name ipv4.method manual"; + assert_script_run "nmcli con up $bond_name"; + } else { + # No need to handle wicked configuration here, as it's done in create_and_configure_bond_wicked + systemctl 'restart apparmor'; + } +} + +sub test_failover { + my ($bond_mode, $bond_name, $device, $description, $nics_ref) = @_; + my @nics_status = map { [$_, $_ eq $device ? 0 : 1] } @$nics_ref; + + record_info("Testing Failover for Mode: $bond_mode", "NIC: $device"); + + assert_script_run "ip link set dev $device down"; + script_run 'ip a'; + + # Validate bond mode and NIC statuses (the downed NIC should be "down") + validate_bond_mode_and_slaves($bond_name, $description, \@nics_status); + + check_connectivity $bond_name; + + assert_script_run "ip link set dev $device up"; + systemctl 'restart wicked' unless $use_nm; +} + +sub validate_bond_mode_and_slaves { + my ($bond_name, $description, $devices_ref) = @_; + + assert_script_run "cat /proc/net/bonding/$bond_name | grep 'Mode:' | grep '$description'"; + + foreach my $device_info (@$devices_ref) { + my ($device, $status_up) = @$device_info; + my $expected_status = $status_up ? 'up' : 'down'; + + validate_script_output_retry( + "grep -A 1 'Slave Interface: $device' /proc/net/bonding/$bond_name", + sub { m/MII Status: $expected_status/ } + ); + } +} + +sub test_bonding_mode { + my ($self, $nics_ref, $miimon, $bond_name, $bond_mode, $description) = @_; + my @nics = @$nics_ref; + + select_serial_terminal; + + delete_existing_connections; + + create_bond($bond_name, $bond_mode, $miimon, @nics); + add_devices_to_bond($bond_name, @nics); + configure_device_route($bond_name, $server_ip); + + configure_client_ip($bond_name); + + power_action('reboot', textmode => 1); + $self->wait_boot; + select_serial_terminal; + set_nics_link_speed_duplex(\@nics); + check_connectivity $bond_name; + + # Validate that all NICs are "up" + validate_bond_mode_and_slaves($bond_name, $description, [map { [$_, 1] } @nics]); + + # Testing failover for each NIC + test_failover($bond_mode, $bond_name, $_, $description, \@nics) for @nics; + + delete_existing_connections; +} + +sub run { + my ($self) = @_; + select_serial_terminal; + + $use_nm = is_nm_used(); + + my $bond_name = "bond0"; + my $miimon = 200; + my @nics = get_nics($bond_name); + + record_info(scalar(@nics) . " NICs Detected", join(', ', @nics)); + + my @bond_modes = ( + ['balance-rr', 'load balancing (round-robin)'], + ['active-backup', 'fault-tolerance (active-backup)'], + ['balance-xor', 'load balancing (xor)'], + ['broadcast', 'fault-tolerance (broadcast)'], + ['802.3ad', 'IEEE 802.3ad Dynamic link aggregation'], + ['balance-tlb', 'transmit load balancing'], + ['balance-alb', 'adaptive load balancing'] + ); + + foreach my $mode_info (@bond_modes) { + my ($bond_mode, $description) = @$mode_info; + record_info("Testing Bonding Mode: $bond_mode", $description); + test_bonding_mode($self, \@nics, $miimon, $bond_name, $bond_mode, $description); + } + + barrier_wait $BARRIER_BONDING_TESTS_DONE; +} + +1; diff --git a/tests/network/network_bonding_setup.pm b/tests/network/network_bonding_setup.pm new file mode 100644 index 000000000000..43f0c82332ee --- /dev/null +++ b/tests/network/network_bonding_setup.pm @@ -0,0 +1,116 @@ +# SUSE's openQA tests +# +# Copyright 2024 SUSE LLC +# SPDX-License-Identifier: FSFAP + +# Summary: Setup simple network topology for bonding tests +# +# Maintainer: Volodymyr Katkalov +# +use base 'consoletest'; +use strict; +use warnings; +use testapi; +use serial_terminal 'select_serial_terminal'; +use Utils::Systemd qw(disable_and_stop_service systemctl check_unit_file); +use lockapi; +use utils; + +my $BARRIER_MM_SETUP_DONE = 'MM_SETUP_DONE'; +my $BARRIER_BONDING_TESTS_DONE = 'BONDING_TESTS_DONE'; + +my $server_ip = "10.0.2.101"; +my $client_ip = "10.0.2.102"; +my $subnet = "/24"; + +sub get_nics { + # Detect non-loopback network interfaces + return split(/\n/, script_output("ip -o link show | grep -v 'lo' | awk -F: '{print \$2}' | awk '{print \$1}'")); +} + +sub set_nics_link_speed_duplex { + my ($nics_ref) = @_; + my @nics = @$nics_ref; + + for my $nic (@nics) { + record_info("SET $nic link speed and duplex", "ethtool -s $nic speed 1000 duplex full autoneg off"); + assert_script_run("ethtool -s $nic speed 1000 duplex full autoneg off"); + } +} + +sub setup_server_network { + my ($nics_ref) = @_; + my @nics = @$nics_ref; + + record_info("SETUP_SERVER_NETWORK"); + die "No active NICs found" unless @nics; + record_info(scalar(@nics) . " NICs Detected", join(', ', @nics)); + + my $nic0 = $nics[0]; + + # Bring down all non-loopback interfaces except the first one + foreach my $nic (@nics[1 .. $#nics]) { + record_info("Non-loopback interface $nic detected, bringing it down..."); + assert_script_run("ip link set $nic down"); + } + + # Determine whether Wicked or NetworkManager is being used + my $network_manager_status = script_run("systemctl is-active NetworkManager"); + my $wicked_status = script_run("systemctl is-active wicked"); + + if ($network_manager_status == 0) { + # NetworkManager is active + record_info("NetworkManager detected, configuring with nmcli", $nic0); + assert_script_run("nmcli device disconnect $nic0"); + assert_script_run("nmcli connection delete $nic0 || true"); + assert_script_run("nmcli connection add type ethernet ifname $nic0 ip4 ${server_ip}${subnet} gw4 $server_ip con-name $nic0"); + assert_script_run("nmcli connection up $nic0"); + } elsif ($wicked_status == 0) { + # Wicked is active + record_info("Wicked detected, configuring with wicked", $nic0); + assert_script_run("wicked ifdown $nic0"); + assert_script_run("ip addr flush dev $nic0"); + assert_script_run("ip addr add ${server_ip}${subnet} dev $nic0"); + assert_script_run("ip link set $nic0 up"); + assert_script_run("ip route add default via $server_ip"); + } else { + die "Neither NetworkManager nor Wicked is active. Cannot configure network."; + } + + # Validate IP has been set for the first interface + my $configured_ip = script_output("ip -4 addr show dev $nic0 | grep inet | awk '{print \$2}' | cut -d/ -f1"); + die "Failed to configure IP address on $nic0" unless $configured_ip eq $server_ip; + + barrier_wait $BARRIER_MM_SETUP_DONE; + barrier_wait $BARRIER_BONDING_TESTS_DONE; +} + +sub run { + my ($self) = @_; + my $hostname = get_var('HOSTNAME'); + my $is_server = ($hostname =~ /server|master/); + + if ($is_server) { + barrier_create $BARRIER_MM_SETUP_DONE, 2; + barrier_create $BARRIER_BONDING_TESTS_DONE, 2; + mutex_create 'barrier_setup_mm_done'; + } + mutex_wait 'barrier_setup_mm_done'; + + select_serial_terminal; + + my @nics = get_nics(); + + set_nics_link_speed_duplex(\@nics); + + assert_script_run("echo \"$server_ip server master\" >> /etc/hosts"); + assert_script_run("echo \"$client_ip client minion\" >> /etc/hosts"); + + disable_and_stop_service($self->firewall) if check_unit_file($self->firewall); + disable_and_stop_service('apparmor', ignore_failure => 1); + + setup_server_network(\@nics) if $is_server; + barrier_wait $BARRIER_MM_SETUP_DONE unless $is_server; +} + +1;