diff --git a/test/.env b/test/.env index f8f69c250..b4b2969d0 100644 --- a/test/.env +++ b/test/.env @@ -2,7 +2,7 @@ # shellcheck disable=SC2034,SC2154 # Current container image -INFIX_TEST=ghcr.io/kernelkit/infix-test:2.2 +INFIX_TEST=ghcr.io/kernelkit/infix-test:2.3 ixdir=$(readlink -f "$testdir/..") logdir=$(readlink -f "$testdir/.log") diff --git a/test/case/ietf_interfaces/Readme.adoc b/test/case/ietf_interfaces/Readme.adoc index de861eb0e..b9b01428f 100644 --- a/test/case/ietf_interfaces/Readme.adoc +++ b/test/case/ietf_interfaces/Readme.adoc @@ -29,6 +29,8 @@ include::bridge_veth/Readme.adoc[] include::bridge_vlan/Readme.adoc[] +include::speed_duplex_copper/Readme.adoc[] + include::bridge_vlan_separation/Readme.adoc[] include::dual_bridge/Readme.adoc[] diff --git a/test/case/ietf_interfaces/ietf_interfaces.yaml b/test/case/ietf_interfaces/ietf_interfaces.yaml index de0523fe6..1ff61d906 100644 --- a/test/case/ietf_interfaces/ietf_interfaces.yaml +++ b/test/case/ietf_interfaces/ietf_interfaces.yaml @@ -20,6 +20,9 @@ - name: vlan_qos case: vlan_qos/test.py +- name: speed_duplex_copper + case: speed_duplex_copper/test.py + - name: verify_all_interface_types case: verify_all_interface_types/test.py diff --git a/test/case/ietf_interfaces/speed_duplex_copper/Readme.adoc b/test/case/ietf_interfaces/speed_duplex_copper/Readme.adoc new file mode 120000 index 000000000..22d678f6b --- /dev/null +++ b/test/case/ietf_interfaces/speed_duplex_copper/Readme.adoc @@ -0,0 +1 @@ +speed_duplex_copper.adoc \ No newline at end of file diff --git a/test/case/ietf_interfaces/speed_duplex_copper/speed_duplex_copper.adoc b/test/case/ietf_interfaces/speed_duplex_copper/speed_duplex_copper.adoc new file mode 100644 index 000000000..0be4f0cf8 --- /dev/null +++ b/test/case/ietf_interfaces/speed_duplex_copper/speed_duplex_copper.adoc @@ -0,0 +1,34 @@ +=== Interface Speed Duplex (Copper) +==== Description +Verify that auto-negotiation results in expected speed/duplex mode. + +==== Topology +ifdef::topdoc[] +image::{topdoc}../../test/case/ietf_interfaces/speed_duplex_copper/topology.svg[Interface Speed Duplex (Copper) topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::speed_duplex_copper/topology.svg[Interface Speed Duplex (Copper) topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.svg[Interface Speed Duplex (Copper) topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUT +. Enable target interface +. Set fixed 10/full +. Set fixed 10/half +. Set fixed 100/full +. Set fixed 100/half +. Switch to auto-negotiation mode for target and host +. Configure host to advertise 10/Full only +. Configure host to advertise 10/Half only +. Configure host to advertise 100/Full only +. Configure host to advertise 100/Half only +. Configure host to advertise 10/half + 10/full + 100/half +. Configure host to advertise 10/half + 10/full + 100/half + 100/full + 1000/full + + +<<< + diff --git a/test/case/ietf_interfaces/speed_duplex_copper/test.py b/test/case/ietf_interfaces/speed_duplex_copper/test.py new file mode 100755 index 000000000..121a2b63e --- /dev/null +++ b/test/case/ietf_interfaces/speed_duplex_copper/test.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +""" +Interface Speed Duplex (Copper) + +Verify that auto-negotiation results in expected speed/duplex mode. +""" + +import infamy +import subprocess +from infamy.util import until + +ADVERTISE_MODES = { + # Values from ethtool's ETHTOOL_LINK_MODE bit positions + # See: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/ethtool.h + "10half": 0x0001, + "10full": 0x0002, + "100half": 0x0004, + "100full": 0x0008, + "1000full": 0x0020 +} + +def advertise_host_modes(interface, modes): + mask = 0 + for mode in modes: + mask |= ADVERTISE_MODES[mode] + try: + subprocess.run([ + "ethtool", "-s", interface, + "advertise", hex(mask) + ], check=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to advertise modes via ethtool: {e}") + +def get_target_speed_duplex(target, interface): + data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{interface}']") \ + ["interfaces"]["interface"][interface] + eth = data.get("ethernet", {}) + + return eth.get("speed"), eth.get("duplex") + +def set_target_speed_duplex(target, interface, speed, duplex): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": interface, + "ethernet": { + "auto-negotiation": { + "enable": False + }, + "speed": speed / 1000, + "duplex": duplex + } + }] + } + } + }) + +def set_host_speed_duplex(interface, speed, duplex): + try: + subprocess.run([ + "ethtool", "-s", interface, + "speed", str(speed), + "duplex", duplex, + "autoneg", "off" + ], check=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to set speed/duplex via ethtool: {e}") + +def verify_speed_duplex(target, ns, interface, exp_speed, exp_duplex): + until(lambda: speed_duplex_present(target, interface)) + act_speed, act_duplex = get_target_speed_duplex(target, interface) + if act_speed is None or act_duplex is None: + print(f"Could not fetch speed or duplex from target for interface {interface}") + test.fail() + + exp_speed_gbps = exp_speed / 1000 + if float(act_speed) != exp_speed_gbps: + print(f"act_speed: {act_speed}, exp_speed: {exp_speed_gbps}") + test.fail() + + if act_duplex.lower() != exp_duplex.lower(): + print(f"act_duplex: {act_duplex}, exp_duplex: {exp_duplex}") + test.fail() + + ns.must_reach("10.0.0.2") + + print(f"Verified: {interface} is operating at {act_speed} Gbps, {act_duplex} duplex") + +def speed_duplex_present(target, interface): + speed, duplex = get_target_speed_duplex(target, interface) + return speed is not None and duplex is not None + +def enable_target_interface(target, interface): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": interface, + "enabled": True, + "ipv4": { + "address": [ + { + "ip": "10.0.0.2", + "prefix-length": 24 + } + ] + } + }] + } + } + }) + +def enable_target_autoneg(target, interface): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": interface, + "ethernet": { + "auto-negotiation": { + "enable": True + } + } + }] + } + } + }) + +def enable_host_autoneg(interface): + subprocess.run(["ethtool", "-s", interface, "autoneg", "on"], check=True) + +def cleanup(target, hdata, tdata): + """ + Restore both host and target interfaces to autonegotiation mode + to ensure clean state for future tests. + """ + + print("Restoring interfaces to default (autoneg on)") + try: + enable_host_autoneg(hdata) + except Exception as e: + print(f"Host autoneg restore failed: {e}") + try: + enable_target_interface(target, tdata) + enable_target_autoneg(target, tdata) + except Exception as e: + print(f"Target autoneg restore failed: {e}") + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + _, hdata = env.ltop.xlate("host", "data") + _, tdata = env.ltop.xlate("target", "data") + + # Append a test cleanup function + test.push_test_cleanup(lambda: cleanup(target, hdata, tdata)) + + with test.step("Enable target interface"): + enable_target_interface(target, tdata) + + with infamy.IsolatedMacVlan(hdata) as ns: + ns.addip("10.0.0.1") + + # Fixed mode tests + with test.step("Set fixed 10/full"): + set_host_speed_duplex(hdata, 10, "full") + set_target_speed_duplex(target, tdata, 10, "full") + verify_speed_duplex(target, ns, tdata, 10, "full") + + with test.step("Set fixed 10/half"): + set_host_speed_duplex(hdata, 10, "half") + set_target_speed_duplex(target, tdata, 10, "half") + verify_speed_duplex(target, ns, tdata, 10, "half") + + with test.step("Set fixed 100/full"): + set_host_speed_duplex(hdata, 100, "full") + set_target_speed_duplex(target, tdata, 100, "full") + verify_speed_duplex(target, ns, tdata, 100, "full") + + with test.step("Set fixed 100/half"): + set_host_speed_duplex(hdata, 100, "half") + set_target_speed_duplex(target, tdata, 100, "half") + verify_speed_duplex(target, ns, tdata, 100, "half") + + # Auto-negotiation tests: host advertises, Infix negotiates + with test.step("Switch to auto-negotiation mode for target and host"): + enable_host_autoneg(hdata) + enable_target_autoneg(target, tdata) + + with test.step("Configure host to advertise 10/Full only"): + advertise_host_modes(hdata, ["10full"]) + verify_speed_duplex(target, ns, tdata, 10, "full") + + with test.step("Configure host to advertise 10/Half only"): + advertise_host_modes(hdata, ["10half"]) + verify_speed_duplex(target, ns, tdata, 10, "half") + + with test.step("Configure host to advertise 100/Full only"): + advertise_host_modes(hdata, ["100full"]) + verify_speed_duplex(target, ns, tdata, 100, "full") + + with test.step("Configure host to advertise 100/Half only"): + advertise_host_modes(hdata, ["100half"]) + verify_speed_duplex(target, ns, tdata, 100, "half") + + with test.step("Configure host to advertise 10/half + 10/full + 100/half"): + advertise_host_modes(hdata, ["10half", "10full", "100half"]) + verify_speed_duplex(target, ns, tdata, 100, "half") + + with test.step("Configure host to advertise 10/half + 10/full + 100/half + 100/full + 1000/full"): + advertise_host_modes(hdata, ["10half", "10full", "100half", "100full", "1000full"]) + verify_speed_duplex(target, ns, tdata, 1000, "full") + + test.succeed() \ No newline at end of file diff --git a/test/case/ietf_interfaces/speed_duplex_copper/topology.dot b/test/case/ietf_interfaces/speed_duplex_copper/topology.dot new file mode 100644 index 000000000..fe4821e8a --- /dev/null +++ b/test/case/ietf_interfaces/speed_duplex_copper/topology.dot @@ -0,0 +1,24 @@ +graph "1x2" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data } | target", + pos="10,12!", + + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey] + host:data -- target:data [color=black, requires="link-ctrl copper"] +} diff --git a/test/case/ietf_interfaces/speed_duplex_copper/topology.svg b/test/case/ietf_interfaces/speed_duplex_copper/topology.svg new file mode 100644 index 000000000..ff3d246be --- /dev/null +++ b/test/case/ietf_interfaces/speed_duplex_copper/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index b87f25689..c9682dd0b 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -26,7 +26,8 @@ RUN apk add --no-cache \ openssl \ curl \ e2tools \ - make + make \ + ethtool ARG MTOOL_VERSION="3.0" RUN wget https://github.com/troglobit/mtools/releases/download/v3.0/mtools-$MTOOL_VERSION.tar.gz -O /tmp/mtools-$MTOOL_VERSION.tar.gz