diff --git a/helpers/labgrid-raw-interface b/helpers/labgrid-raw-interface index 8aa4c9eac..f640fc5b0 100755 --- a/helpers/labgrid-raw-interface +++ b/helpers/labgrid-raw-interface @@ -9,6 +9,7 @@ import argparse import os +import string import sys import yaml @@ -45,7 +46,7 @@ def main(program, options): if options.ifname in denylist: raise ValueError(f"Interface name '{options.ifname}' is denied in denylist.") - programs = ["tcpreplay", "tcpdump"] + programs = ["tcpreplay", "tcpdump", "ip", "ethtool"] if program not in programs: raise ValueError(f"Invalid program {program} called with wrapper, valid programs are: {programs}") @@ -57,7 +58,7 @@ def main(program, options): args.append(f"--intf1={options.ifname}") args.append("-") - if program == "tcpdump": + elif program == "tcpdump": args.append("-n") args.append(f"--interface={options.ifname}") # Write out each packet as it is received @@ -79,6 +80,43 @@ def main(program, options): args.append("-W") args.append("1") + elif program == "ip": + args.append("link") + args.append("set") + args.append("dev") + args.append(options.ifname) + args.append(options.action) + + elif program == "ethtool": + allowed_chars = set(string.ascii_letters + string.digits + "-/:") + + if options.subcommand == "change": + for arg in options.ethtool_change_args: + if arg.startswith("-") or not allowed_chars.issuperset(arg): + raise ValueError(f"ethtool --change arg '{arg}' contains invalid characters") + + args.append("--change") + args.append(options.ifname) + args.extend(options.ethtool_change_args) + + elif options.subcommand == "set-eee": + for arg in options.ethtool_set_eee_args: + if arg.startswith("-") or not allowed_chars.issuperset(arg): + raise ValueError(f"ethtool --set-eee arg '{arg}' contains invalid characters") + + args.append("--set-eee") + args.append(options.ifname) + args.extend(options.ethtool_set_eee_args) + + elif options.subcommand == "pause": + for arg in options.ethtool_pause_args: + if arg.startswith("-") or not allowed_chars.issuperset(arg): + raise ValueError(f"ethtool --pause arg '{arg}' contains invalid characters") + + args.append("--pause") + args.append(options.ifname) + args.extend(options.ethtool_pause_args) + try: os.execvp(args[0], args) except FileNotFoundError as e: @@ -102,6 +140,36 @@ if __name__ == "__main__": tcpreplay_parser = subparsers.add_parser("tcpreplay") tcpreplay_parser.add_argument("ifname", type=str, help="interface name") + # ip + ip_parser = subparsers.add_parser("ip") + ip_parser.add_argument("ifname", type=str, help="interface name") + ip_parser.add_argument("action", type=str, choices=["up", "down"], help="action, one of {%(choices)s}") + + # ethtool + ethtool_parser = subparsers.add_parser("ethtool") + ethtool_subparsers = ethtool_parser.add_subparsers(dest="subcommand") + + # ethtool: change + ethtool_change_parser = ethtool_subparsers.add_parser("change") + ethtool_change_parser.add_argument("ifname", type=str, help="interface name") + ethtool_change_parser.add_argument( + "ethtool_change_args", metavar="ARG", nargs=argparse.REMAINDER, help="ethtool --change args" + ) + + # ethtool: set-eee + ethtool_change_parser = ethtool_subparsers.add_parser("set-eee") + ethtool_change_parser.add_argument("ifname", type=str, help="interface name") + ethtool_change_parser.add_argument( + "ethtool_set_eee_args", metavar="ARG", nargs=argparse.REMAINDER, help="ethtool --set-eee args" + ) + + # ethtool: pause + ethtool_change_parser = ethtool_subparsers.add_parser("pause") + ethtool_change_parser.add_argument("ifname", type=str, help="interface name") + ethtool_change_parser.add_argument( + "ethtool_pause_args", metavar="ARG", nargs=argparse.REMAINDER, help="ethtool --pause args" + ) + args = parser.parse_args() try: main(args.program, args) diff --git a/labgrid/driver/rawnetworkinterfacedriver.py b/labgrid/driver/rawnetworkinterfacedriver.py index 7fafa6e08..861628b15 100644 --- a/labgrid/driver/rawnetworkinterfacedriver.py +++ b/labgrid/driver/rawnetworkinterfacedriver.py @@ -2,6 +2,7 @@ import contextlib import json import subprocess +import time import attr @@ -10,6 +11,7 @@ from ..step import step from ..util.helper import processwrapper from ..util.managedfile import ManagedFile +from ..util.timeout import Timeout from ..resource.common import NetworkResource @@ -25,6 +27,14 @@ def __attrs_post_init__(self): self._record_handle = None self._replay_handle = None + def on_activate(self): + self._set_interface("up") + self._wait_state("up") + + def on_deactivate(self): + self._set_interface("down") + self._wait_state("down") + def _wrap_command(self, args): wrapper = ["sudo", "labgrid-raw-interface"] @@ -35,6 +45,126 @@ def _wrap_command(self, args): # keep wrapper and args as-is return wrapper + args + @step(args=["state"]) + def _set_interface(self, state): + """Set interface to given state.""" + cmd = ["ip", self.iface.ifname, state] + cmd = self._wrap_command(cmd) + subprocess.check_call(cmd) + + @Driver.check_active + def set_interface_up(self): + """Set bound interface up.""" + self._set_interface("up") + + @Driver.check_active + def set_interface_down(self): + """Set bound interface down.""" + self._set_interface("down") + + def _get_state(self): + """Returns the bound interface's operstate.""" + if_state = self.iface.extra.get("state") + if if_state: + return if_state + + cmd = self.iface.command_prefix + ["cat", f"/sys/class/net/{self.iface.ifname}/operstate"] + output = processwrapper.check_output(cmd).decode("ascii") + if_state = output.strip() + return if_state + + @Driver.check_active + def get_state(self): + """Returns the bound interface's operstate.""" + return self._get_state() + + @step(title="wait_state", args=["expected_state", "timeout"]) + def _wait_state(self, expected_state, timeout=60): + """Wait until the expected state is reached or the timeout expires.""" + timeout = Timeout(float(timeout)) + + while True: + if self._get_state() == expected_state: + return + if timeout.expired: + raise TimeoutError( + f"exported interface {self.iface.ifname} did not go {expected_state} within {timeout.timeout} seconds" + ) + time.sleep(1) + + @Driver.check_active + def wait_state(self, expected_state, timeout=60): + """Wait until the expected state is reached or the timeout expires.""" + self._wait_state(expected_state, timeout=timeout) + + @Driver.check_active + def get_ethtool_settings(self): + """ + Returns settings via ethtool of the bound network interface resource. + """ + cmd = self.iface.command_prefix + ["ethtool", "--json", self.iface.ifname] + output = subprocess.check_output(cmd, encoding="utf-8") + return json.loads(output)[0] + + @Driver.check_active + @step(args=["settings"]) + def ethtool_configure(self, **settings): + """ + Change settings on interface. + + Supported settings are described in ethtool(8) --change (use "_" instead of "-"). + """ + cmd = ["ethtool", "change", self.iface.ifname] + cmd += [item.replace("_", "-") for pair in settings.items() for item in pair] + cmd = self._wrap_command(cmd) + subprocess.check_call(cmd) + + @Driver.check_active + def get_ethtool_eee_settings(self): + """ + Returns Energy-Efficient Ethernet settings via ethtool of the bound network interface + resource. + """ + cmd = self.iface.command_prefix + ["ethtool", "--show-eee", "--json", self.iface.ifname] + output = subprocess.check_output(cmd, encoding="utf-8") + return json.loads(output)[0] + + @Driver.check_active + @step(args=["settings"]) + def ethtool_configure_eee(self, **settings): + """ + Change Energy-Efficient Ethernet settings via ethtool of the bound network interface + resource. + + Supported settings are described in ethtool(8) --set-eee (use "_" instead of "-"). + """ + cmd = ["ethtool", "set-eee", self.iface.ifname] + cmd += [item.replace("_", "-") for pair in settings.items() for item in pair] + cmd = self._wrap_command(cmd) + subprocess.check_call(cmd) + + @Driver.check_active + def get_ethtool_pause_settings(self): + """ + Returns pause parameters via ethtool of the bound network interface resource. + """ + cmd = self.iface.command_prefix + ["ethtool", "--json", "--show-pause", self.iface.ifname] + output = subprocess.check_output(cmd, encoding="utf-8") + return json.loads(output)[0] + + @Driver.check_active + @step(args=["settings"]) + def ethtool_configure_pause(self, **settings): + """ + Change pause parameters via ethtool of the bound network interface resource. + + Supported settings are described in ethtool(8) --pause + """ + cmd = ["ethtool", "pause", self.iface.ifname] + cmd += [item for pair in settings.items() for item in pair] + cmd = self._wrap_command(cmd) + subprocess.check_call(cmd) + def _stop(self, proc, *, timeout=None): assert proc is not None