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 (<listen>) will be filled in for each
+# namespace that is created so each individual dbus-daemon has its own socket
+# and address.
dbus_config = '''
<!DOCTYPE busconfig PUBLIC \
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" \
@@ -133,7 +136,6 @@ dbus_config = '''
busconfig.dtd\">
<busconfig>
<type>system</type>
-<listen>unix:path=/run/dbus/system_bus_socket</listen>
<limit name=\"reply_timeout\">2147483647</limit>
<auth>ANONYMOUS</auth>
<allow_anonymous/>
@@ -151,7 +153,6 @@ busconfig.dtd\">
<allow send_destination=\"*\" eavesdrop=\"true\"/>
<allow eavesdrop=\"true\"/>
</policy>
-</busconfig>
'''
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('<listen>%s</listen>\n' % self.dbus_address)
+ f.write('</busconfig>\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
-
- # Handle any glob matches
- for item in self.args.verbose:
- if process in glob(item):
- return True
-
- return False
+ def get_namespace(self, ns):
+ for n in self.namespaces:
+ if n.name == ns:
+ return n
- 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()
--
2.26.2