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.
This commit is contained in:
James Prestwood 2020-11-17 12:53:01 -08:00 committed by Denis Kenzior
parent dcaf0150b9
commit e1e1c4edd1
1 changed files with 196 additions and 102 deletions

View File

@ -126,6 +126,9 @@ dev_table = [
DevInfo('/proc/self/fd/2', '/dev/stderr') 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 = ''' dbus_config = '''
<!DOCTYPE busconfig PUBLIC \ <!DOCTYPE busconfig PUBLIC \
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" \ "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" \
@ -133,7 +136,6 @@ dbus_config = '''
busconfig.dtd\"> busconfig.dtd\">
<busconfig> <busconfig>
<type>system</type> <type>system</type>
<listen>unix:path=/run/dbus/system_bus_socket</listen>
<limit name=\"reply_timeout\">2147483647</limit> <limit name=\"reply_timeout\">2147483647</limit>
<auth>ANONYMOUS</auth> <auth>ANONYMOUS</auth>
<allow_anonymous/> <allow_anonymous/>
@ -151,7 +153,6 @@ busconfig.dtd\">
<allow send_destination=\"*\" eavesdrop=\"true\"/> <allow send_destination=\"*\" eavesdrop=\"true\"/>
<allow eavesdrop=\"true\"/> <allow eavesdrop=\"true\"/>
</policy> </policy>
</busconfig>
''' '''
class Process: class Process:
''' '''
@ -162,7 +163,7 @@ class Process:
test exits. test exits.
''' '''
def __init__(self, args, wait=False, multi_test=False, env=None, ctx=None, check=False, 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.args = args
self.wait = wait self.wait = wait
self.name = args[0] self.name = args[0]
@ -172,6 +173,10 @@ class Process:
self.ret = None self.ret = None
self.ctx = ctx self.ctx = ctx
if namespace:
self.args = ['ip', 'netns', 'exec', namespace]
self.args.extend(args)
if ctx: if ctx:
set_stdout = False set_stdout = False
@ -209,7 +214,6 @@ class Process:
self.stdout = sys.__stdout__ self.stdout = sys.__stdout__
self.stderr = sys.__stderr__ self.stderr = sys.__stderr__
self.pid = subprocess.Popen(self.args, stdout=self.stdout, \ self.pid = subprocess.Popen(self.args, stdout=self.stdout, \
stderr=self.stderr, env=env, \ stderr=self.stderr, env=env, \
cwd=os.getcwd()) cwd=os.getcwd())
@ -233,9 +237,8 @@ class Process:
def __del__(self): def __del__(self):
print("Del process %s" % self.args) print("Del process %s" % self.args)
if self.ctx and self.ctx.args.log: self.stdout = None
self.stdout.close() self.stderr = None
self.stderr.close()
def kill(self, force=False): def kill(self, force=False):
print("Killing process %s" % self.args) print("Killing process %s" % self.args)
@ -469,12 +472,172 @@ class Hostapd:
self.instances = None self.instances = None
self.process.kill() 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 Contains all information for a given set of tests being run
such as processes, radios, interfaces and test results. such as processes, radios, interfaces and test results.
''' '''
def __init__(self, args): def __init__(self, args):
self.name = "root"
self.processes = [] self.processes = []
self.args = args self.args = args
self.hw_config = None self.hw_config = None
@ -484,31 +647,15 @@ class TestContext:
self.cur_iface_id = 0 self.cur_iface_id = 0
self.radios = [] self.radios = []
self.loopback_started = False self.loopback_started = False
self.iwd_extra_options = None
self.results = {} self.results = {}
self.mainloop = GLib.MainLoop() self.mainloop = GLib.MainLoop()
self.namespaces = []
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)
def start_dbus_monitor(self): def start_dbus_monitor(self):
if not self.is_verbose('dbus-monitor'): if not self.is_verbose('dbus-monitor'):
return return
self.start_process(['dbus-monitor', '--system']) self.start_process(['dbus-monitor', '--address', self.dbus_address])
def start_haveged(self): def start_haveged(self):
self.start_process(['haveged'], multi_test=True) self.start_process(['haveged'], multi_test=True)
@ -561,38 +708,6 @@ class TestContext:
else: else:
self.create_radios() 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): def start_hostapd(self):
if not 'HOSTAPD' in self.hw_config: if not 'HOSTAPD' in self.hw_config:
return return
@ -677,66 +792,39 @@ class TestContext:
print("Ofono started") print("Ofono started")
def is_verbose(self, process): def create_namespaces(self):
process = os.path.basename(process) if not self.hw_config.has_section('NameSpaces'):
return
if self.args is None: for key, value in self.hw_config.items('NameSpaces'):
return False 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 # Remove radios from 'root' namespace
if self.args.log: self.radios = list(set(self.radios) - set(radios))
return True
if process in self.args.verbose: self.namespaces.append(Namespace(self.args, key, radios))
return True
# Special case here to enable verbose output with valgrind running def get_namespace(self, ns):
if process == 'valgrind' and 'iwd' in self.args.verbose: for n in self.namespaces:
return True if n.name == ns:
return n
# Handle any glob matches return None
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)
def stop_test_processes(self): def stop_test_processes(self):
self.radios = [] self.namespaces = []
self.hostapd = None self.hostapd = None
self.wpas_interfaces = 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]: self.reset()
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
def __str__(self): def __str__(self):
ret = 'Arguments:\n' ret = 'Arguments:\n'
for arg in vars(self.args): for arg in vars(self.args):
ret += '\t --%s %s\n' % (arg, str(getattr(self.args, arg))) 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' ret += 'Hostapd:\n'
if self.hostapd: if self.hostapd:
for h in self.hostapd.instances: for h in self.hostapd.instances:
@ -744,6 +832,11 @@ class TestContext:
else: else:
ret += '\tNo Hostapd instances\n' ret += '\tNo Hostapd instances\n'
ret += super().__str__()
for n in self.namespaces:
ret += n.__str__()
return ret return ret
def prepare_sandbox(): def prepare_sandbox():
@ -921,6 +1014,7 @@ def pre_test(ctx, test):
ctx.start_dbus_monitor() ctx.start_dbus_monitor()
ctx.start_radios() ctx.start_radios()
ctx.create_namespaces()
ctx.start_hostapd() ctx.start_hostapd()
ctx.start_wpas_interfaces() ctx.start_wpas_interfaces()
ctx.start_ofono() ctx.start_ofono()