From e1e1c4edd12b5b1da8a16a2b9273708b901f9e9f Mon Sep 17 00:00:00 2001 From: James Prestwood Date: Tue, 17 Nov 2020 12:53:01 -0800 Subject: [PATCH] test-runner: introduce network namespaces Our simulated environment was really only meant to test air-to-air communication by using mac80211_hwsim. Protocols like DHCP use IP communication which starts to fall apart when using hwsim radios. Mainly unicast sockets do not work since there is no underlying network infrastructure. In order to simulate a more realistic environment network namespaces are introduced in this patch. This allows wireless phy's to be added to a network namespace and unique IWD instances manage those phys. This is done automatically when 'NameSpaces' entries are configured in hw.conf: [SETUP] num_radios=2 [NameSpaces] ns0=rad1,... This will create a namespace named ns0, and add rad1 to that namespace. rad1 will not appear as a phy in what's being called the 'root' namespace (the default namespace). As far as a test is concerned you can create a new IWD() class and pass the namespace in. This will start a new IWD instance in that namespace: ns0 = ctx.get_namespace('ns0') wd_ns0 = IWD(start_iwd=True, namespace=ns0) 'wd_ns0' can now be used to interact with IWD in that namespace, just like any other IWD class object. --- tools/test-runner | 298 ++++++++++++++++++++++++++++++---------------- 1 file changed, 196 insertions(+), 102 deletions(-) diff --git a/tools/test-runner b/tools/test-runner index 803d4da5..3a9cfce2 100755 --- a/tools/test-runner +++ b/tools/test-runner @@ -126,6 +126,9 @@ dev_table = [ DevInfo('/proc/self/fd/2', '/dev/stderr') ] +# Partial DBus config. The remainder () will be filled in for each +# namespace that is created so each individual dbus-daemon has its own socket +# and address. dbus_config = ''' system -unix:path=/run/dbus/system_bus_socket 2147483647 ANONYMOUS @@ -151,7 +153,6 @@ busconfig.dtd\"> - ''' class Process: ''' @@ -162,7 +163,7 @@ class Process: test exits. ''' def __init__(self, args, wait=False, multi_test=False, env=None, ctx=None, check=False, - outfile=None): + outfile=None, namespace=None): self.args = args self.wait = wait self.name = args[0] @@ -172,6 +173,10 @@ class Process: self.ret = None self.ctx = ctx + if namespace: + self.args = ['ip', 'netns', 'exec', namespace] + self.args.extend(args) + if ctx: set_stdout = False @@ -209,7 +214,6 @@ class Process: self.stdout = sys.__stdout__ self.stderr = sys.__stderr__ - self.pid = subprocess.Popen(self.args, stdout=self.stdout, \ stderr=self.stderr, env=env, \ cwd=os.getcwd()) @@ -233,9 +237,8 @@ class Process: def __del__(self): print("Del process %s" % self.args) - if self.ctx and self.ctx.args.log: - self.stdout.close() - self.stderr.close() + self.stdout = None + self.stderr = None def kill(self, force=False): print("Killing process %s" % self.args) @@ -469,12 +472,172 @@ class Hostapd: self.instances = None self.process.kill() -class TestContext: +dbus_count = 0 + +class Namespace: + dbus_address = None + processes = [] + radios = [] + + def __init__(self, args, name, radios): + self.name = name + self.radios = radios + self.args = args + + Process(['ip', 'netns', 'add', name], wait=True) + for r in radios: + Process(['iw', 'phy', r.name, 'set', 'netns', 'name', name], wait=True) + + self.start_dbus(multi_test=False) + + def reset(self): + self.radios = [] + self._bus = None + + if self.name == "root": + self._bus = dbus.bus.BusConnection(address_or_type=self.dbus_address) + + for p in [p for p in self.processes if p.multi_test is False]: + print("Killing process %s" % p.name) + self.stop_process(p) + + def __del__(self): + print("Removing namespace %s" % self.name) + self.reset() + + Process(['ip', 'netns', 'del', self.name], wait=True) + + def get_bus(self): + return self._bus + + def start_process(self, args, env=None, **kwargs): + # Special case for 'root' namespace (aka TestContext) + if self.name == "root": + ns = None + else: + ns = self.name + + if not env: + env = os.environ.copy() + + # In case this process needs DBus... + env['DBUS_SYSTEM_BUS_ADDRESS'] = self.dbus_address + + p = Process(args, ctx=self, namespace=ns, env=env, **kwargs) + + if not kwargs.get('wait', False): + self.processes.append(p) + + return p + + def stop_process(self, p, force=False): + p.kill(force) + self.processes.remove(p) + + def is_process_running(self, process): + for p in self.processes: + if p.name == process: + return True + return False + + def start_dbus(self, multi_test=True): + global dbus_count + + self.dbus_address = 'unix:path=/tmp/dbus%d' % dbus_count + dbus_cfg = '/tmp/dbus%d.conf' % dbus_count + dbus_count += 1 + + with open(dbus_cfg, 'w+') as f: + f.write(dbus_config) + f.write('%s\n' % self.dbus_address) + f.write('\n') + + p = self.start_process(['dbus-daemon', '--config-file=%s' % dbus_cfg], + wait=False, multi_test=multi_test) + + p.wait_for_socket(self.dbus_address.split('=')[1], wait=5) + + self._bus = dbus.bus.BusConnection(address_or_type=self.dbus_address) + + def start_iwd(self, config_dir = '/tmp'): + args = [] + iwd_radios = ','.join([r.name for r in self.radios if r.use == 'iwd']) + + if self.args.valgrind: + args.extend(['valgrind', '--leak-check=full', '--track-origins=yes', + '--log-file=/tmp/valgrind.log']) + + args.extend(['iwd', '-p', iwd_radios]) + + if self.is_verbose(args[0]): + args.append('-d') + + env = os.environ.copy() + + env['CONFIGURATION_DIRECTORY'] = config_dir + env['STATE_DIRECTORY'] = '/tmp/iwd' + + if self.is_verbose('iwd-dhcp'): + env['IWD_DHCP_DEBUG'] = '1' + + if self.is_verbose('iwd-tls'): + env['IWD_TLS_DEBUG'] = '1' + + if self.is_verbose('iwd-genl'): + env['IWD_GENL_DEBUG'] = '1' + + pid = self.start_process(args, env=env) + return pid + + def is_verbose(self, process): + process = os.path.basename(process) + + if self.args is None: + return False + + # every process is verbose when logging is enabled + if self.args.log: + return True + + if process in self.args.verbose: + return True + + # Special case here to enable verbose output with valgrind running + if process == 'valgrind' and 'iwd' in self.args.verbose: + return True + + # Handle any glob matches + for item in self.args.verbose: + if process in glob(item): + return True + + return False + + def __str__(self): + ret = 'Namespace: %s\n' % self.name + ret += 'Processes:\n' + for p in self.processes: + ret += '\t%s\n' % str(p.args) + + ret += 'Radios:\n' + if len(self.radios) > 0: + for r in self.radios: + ret += '\t%s\n' % str(r) + else: + ret += '\tNo Radios\n' + + ret += 'DBus Address: %s\n' % self.dbus_address + ret += '===================================================\n\n' + + return ret + +class TestContext(Namespace): ''' Contains all information for a given set of tests being run such as processes, radios, interfaces and test results. ''' def __init__(self, args): + self.name = "root" self.processes = [] self.args = args self.hw_config = None @@ -484,31 +647,15 @@ class TestContext: self.cur_iface_id = 0 self.radios = [] self.loopback_started = False - self.iwd_extra_options = None self.results = {} self.mainloop = GLib.MainLoop() - - def start_process(self, args, **kwargs): - p = Process(args, ctx=self, **kwargs) - - if not kwargs.get('wait', False): - self.processes.append(p) - - return p - - def start_dbus(self): - with open('/usr/share/dbus-1/system.conf', 'w+') as f: - f.write(dbus_config) - - os.mkdir('/run/dbus', 755) - - self.start_process(['dbus-daemon', '--system', '--nosyslog'], multi_test=True) + self.namespaces = [] def start_dbus_monitor(self): if not self.is_verbose('dbus-monitor'): return - self.start_process(['dbus-monitor', '--system']) + self.start_process(['dbus-monitor', '--address', self.dbus_address]) def start_haveged(self): self.start_process(['haveged'], multi_test=True) @@ -561,38 +708,6 @@ class TestContext: else: self.create_radios() - def start_iwd(self, config_dir = '/tmp'): - args = [] - iwd_radios = ','.join([r.name for r in self.radios if r.use == 'iwd']) - - if self.args.valgrind: - args.extend(['valgrind', '--leak-check=full', '--track-origins=yes', - '--log-file=/tmp/valgrind.log']) - - args.extend(['iwd', '-p', iwd_radios]) - - if self.is_verbose(args[0]): - args.append('-d') - - if self.iwd_extra_options: - args.append(self.iwd_extra_options) - - env = os.environ.copy() - env['CONFIGURATION_DIRECTORY'] = config_dir - env['STATE_DIRECTORY'] = '/tmp/iwd' - - if self.is_verbose('iwd-dhcp'): - env['IWD_DHCP_DEBUG'] = '1' - - if self.is_verbose('iwd-tls'): - env['IWD_TLS_DEBUG'] = '1' - - if self.is_verbose('iwd-genl'): - env['IWD_GENL_DEBUG'] = '1' - - pid = self.start_process(args, env=env) - return pid - def start_hostapd(self): if not 'HOSTAPD' in self.hw_config: return @@ -677,66 +792,39 @@ class TestContext: print("Ofono started") - def is_verbose(self, process): - process = os.path.basename(process) + def create_namespaces(self): + if not self.hw_config.has_section('NameSpaces'): + return - if self.args is None: - return False + for key, value in self.hw_config.items('NameSpaces'): + radio_names = value.split(',') + # Gather up radio objects for this namespace + radios = [rad for rad in self.radios if rad.name in radio_names] - # every process is verbose when logging is enabled - if self.args.log: - return True + # Remove radios from 'root' namespace + self.radios = list(set(self.radios) - set(radios)) - if process in self.args.verbose: - return True + self.namespaces.append(Namespace(self.args, key, radios)) - # Special case here to enable verbose output with valgrind running - if process == 'valgrind' and 'iwd' in self.args.verbose: - return True + def get_namespace(self, ns): + for n in self.namespaces: + if n.name == ns: + return n - # Handle any glob matches - for item in self.args.verbose: - if process in glob(item): - return True - - return False - - def stop_process(self, p, force=False): - p.kill(force) - self.processes.remove(p) + return None def stop_test_processes(self): - self.radios = [] + self.namespaces = [] self.hostapd = None self.wpas_interfaces = None - self.iwd_extra_options = None - for p in [p for p in self.processes if p.multi_test is False]: - print("Killing process %s" % p.name) - self.stop_process(p) - - def is_process_running(self, process): - for p in self.processes: - if p.name == process: - return True - return False + self.reset() def __str__(self): ret = 'Arguments:\n' for arg in vars(self.args): ret += '\t --%s %s\n' % (arg, str(getattr(self.args, arg))) - ret += 'Processes:\n' - for p in self.processes: - ret += '\t%s\n' % str(p.args) - - ret += 'Radios:\n' - if len(self.radios) > 0: - for r in self.radios: - ret += '\t%s\n' % str(r) - else: - ret += '\tNo Radios\n' - ret += 'Hostapd:\n' if self.hostapd: for h in self.hostapd.instances: @@ -744,6 +832,11 @@ class TestContext: else: ret += '\tNo Hostapd instances\n' + ret += super().__str__() + + for n in self.namespaces: + ret += n.__str__() + return ret def prepare_sandbox(): @@ -921,6 +1014,7 @@ def pre_test(ctx, test): ctx.start_dbus_monitor() ctx.start_radios() + ctx.create_namespaces() ctx.start_hostapd() ctx.start_wpas_interfaces() ctx.start_ofono()