Skip to content

Commit 03d3565

Browse files
committed
tests: re-use network namespaces
Re-using namespaces saves a whole minute of the test run: before: 302 passed, 25 skipped in 303.29s (0:05:03) after: 301 passed, 25 skipped in 241.77s (0:04:01) Signed-off-by: Pablo Barbáchano <[email protected]>
1 parent bda67df commit 03d3565

File tree

5 files changed

+92
-44
lines changed

5 files changed

+92
-44
lines changed

tests/conftest.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
static_cpu_templates_params,
4545
)
4646
from host_tools.metrics import get_metrics_logger
47+
from host_tools.network import NetNs
4748

4849
# This codebase uses Python features available in Python 3.10 or above
4950
if sys.version_info < (3, 10):
@@ -265,15 +266,46 @@ def uffd_handler_paths():
265266
yield handlers
266267

267268

268-
@pytest.fixture()
269-
def microvm_factory(request, record_property, results_dir):
270-
"""Fixture to create microvms simply.
269+
@pytest.fixture(scope="session")
270+
def netns_factory(worker_id):
271+
"""A network namespace factory
271272
272-
In order to avoid running out of space when instantiating many microvms,
273-
we remove the directory manually when the fixture is destroyed
274-
(that is after every test).
275-
One can comment the removal line, if it helps with debugging.
273+
Network namespaces are created once per test session and re-used in subsequent tests.
276274
"""
275+
# pylint:disable=protected-access
276+
277+
class NetNsFactory:
278+
"""A Network namespace factory that reuses namespaces."""
279+
280+
def __init__(self, prefix: str):
281+
self._all = []
282+
self._returned = []
283+
self.prefix = prefix
284+
285+
def get(self, _netns_id):
286+
"""Get a free network namespace"""
287+
if len(self._returned) > 0:
288+
ns = self._returned.pop(0)
289+
while ns.is_used():
290+
pass
291+
return ns
292+
ns = NetNs(self.prefix + str(len(self._all)))
293+
# change the cleanup function so it is returned to the pool
294+
ns._cleanup_orig = ns.cleanup
295+
ns.cleanup = lambda: self._returned.append(ns)
296+
self._all.append(ns)
297+
return ns
298+
299+
netns_fcty = NetNsFactory(f"netns-{worker_id}-")
300+
yield netns_fcty.get
301+
302+
for netns in netns_fcty._all:
303+
netns._cleanup_orig()
304+
305+
306+
@pytest.fixture()
307+
def microvm_factory(request, record_property, results_dir, netns_factory):
308+
"""Fixture to create microvms simply."""
277309

278310
if binary_dir := request.config.getoption("--binary-dir"):
279311
fc_binary_path = Path(binary_dir) / "firecracker"
@@ -298,7 +330,10 @@ def microvm_factory(request, record_property, results_dir):
298330
# We could override the chroot base like so
299331
# jailer_kwargs={"chroot_base": "/srv/jailo"}
300332
uvm_factory = MicroVMFactory(
301-
fc_binary_path, jailer_binary_path, custom_cpu_template=custom_cpu_template
333+
fc_binary_path,
334+
jailer_binary_path,
335+
netns_factory=netns_factory,
336+
custom_cpu_template=custom_cpu_template,
302337
)
303338
yield uvm_factory
304339

tests/framework/microvm.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,7 @@ def __init__(self, fc_binary_path: Path, jailer_binary_path: Path, **kwargs):
10741074
self.vms = []
10751075
self.fc_binary_path = Path(fc_binary_path)
10761076
self.jailer_binary_path = Path(jailer_binary_path)
1077+
self.netns_factory = kwargs.pop("netns_factory", net_tools.NetNs)
10771078
self.kwargs = kwargs
10781079

10791080
def build(self, kernel=None, rootfs=None, **kwargs):
@@ -1086,7 +1087,7 @@ def build(self, kernel=None, rootfs=None, **kwargs):
10861087
jailer_binary_path=kwargs.pop(
10871088
"jailer_binary_path", self.jailer_binary_path
10881089
),
1089-
netns=kwargs.pop("netns", net_tools.NetNs(microvm_id)),
1090+
netns=kwargs.pop("netns", self.netns_factory(microvm_id)),
10901091
**kwargs,
10911092
)
10921093
vm.netns.setup()

tests/host_tools/network.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,13 @@ def __init__(self, name, netns, ip=None):
251251
It also creates a new tap device, brings it up and moves the interface
252252
to the specified namespace.
253253
"""
254-
# Avoid a conflict if two tests want to create the same tap device tap0
255-
# in the host before moving it into its own netns
256-
temp_name = "tap" + random_str(k=8)
257-
utils.check_output(f"ip tuntap add mode tap name {temp_name}")
258-
utils.check_output(f"ip link set {temp_name} name {name} netns {netns}")
259-
if ip:
260-
utils.check_output(f"ip netns exec {netns} ifconfig {name} {ip} up")
261254
self._name = name
262255
self._netns = netns
256+
# Create the tap device tap0 directly in the network namespace to avoid
257+
# conflicts
258+
self.netns.check_output(f"ip tuntap add mode tap name {name}")
259+
if ip:
260+
self.netns.check_output(f"ifconfig {name} {ip} up")
263261

264262
@property
265263
def name(self):
@@ -273,14 +271,10 @@ def netns(self):
273271

274272
def set_tx_queue_len(self, tx_queue_len):
275273
"""Set the length of the tap's TX queue."""
276-
utils.check_output(
277-
"ip netns exec {} ip link set {} txqueuelen {}".format(
278-
self.netns, self.name, tx_queue_len
279-
)
280-
)
274+
self.netns.check_output(f"ip link set {self.name} txqueuelen {tx_queue_len}")
281275

282276
def __repr__(self):
283-
return f"<Tap name={self.name} netns={self.netns}>"
277+
return f"<Tap name={self.name} netns={self.netns.id}>"
284278

285279

286280
@dataclass(frozen=True, repr=True)
@@ -315,7 +309,7 @@ def with_id(i, netmask_len=30):
315309
)
316310

317311

318-
@dataclass(frozen=True, repr=True)
312+
@dataclass(repr=True)
319313
class NetNs:
320314
"""Defines a network namespace."""
321315

@@ -334,6 +328,10 @@ def cmd_prefix(self):
334328
"""Return the jailer context netns file prefix."""
335329
return f"ip netns exec {self.id}"
336330

331+
def check_output(self, cmd: str):
332+
"""Run a command inside the netns."""
333+
return utils.check_output(f"{self.cmd_prefix()} {cmd}")
334+
337335
def setup(self):
338336
"""Set up this network namespace."""
339337
if not self.path.exists():
@@ -350,6 +348,19 @@ def add_tap(self, name, ip):
350348
We assume that a Tap is always configured with the same IP.
351349
"""
352350
if name not in self.taps:
353-
tap = Tap(name, self.id, ip)
351+
tap = Tap(name, self, ip)
354352
self.taps[name] = tap
355353
return self.taps[name]
354+
355+
def is_used(self):
356+
"""Are any of the TAPs still in use
357+
358+
Waits until there's no carrier signal.
359+
Otherwise trying to reuse the TAP may return
360+
`Resource busy (os error 16)`
361+
"""
362+
for tap in self.taps:
363+
_, stdout, _ = self.check_output(f"cat /sys/class/net/{tap}/carrier")
364+
if stdout.strip() != "0":
365+
return True
366+
return False

tests/integration_tests/functional/test_api.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,15 @@ def test_net_api_put_update_pre_boot(uvm_plain):
191191
test_microvm = uvm_plain
192192
test_microvm.spawn()
193193

194-
first_if_name = "first_tap"
195-
tap1 = net_tools.Tap(first_if_name, test_microvm.netns.id)
194+
tap1name = test_microvm.id[:8] + "tap1"
195+
tap1 = net_tools.Tap(tap1name, test_microvm.netns)
196196
test_microvm.api.network.put(
197197
iface_id="1", guest_mac="06:00:00:00:00:01", host_dev_name=tap1.name
198198
)
199199

200200
# Adding new network interfaces is allowed.
201-
second_if_name = "second_tap"
202-
tap2 = net_tools.Tap(second_if_name, test_microvm.netns.id)
201+
tap2name = test_microvm.id[:8] + "tap2"
202+
tap2 = net_tools.Tap(tap2name, test_microvm.netns)
203203
test_microvm.api.network.put(
204204
iface_id="2", guest_mac="07:00:00:00:00:01", host_dev_name=tap2.name
205205
)
@@ -209,28 +209,26 @@ def test_net_api_put_update_pre_boot(uvm_plain):
209209
expected_msg = f"The MAC address is already in use: {guest_mac}"
210210
with pytest.raises(RuntimeError, match=expected_msg):
211211
test_microvm.api.network.put(
212-
iface_id="2", host_dev_name=second_if_name, guest_mac=guest_mac
212+
iface_id="2", host_dev_name=tap2name, guest_mac=guest_mac
213213
)
214214

215215
# Updates to a network interface with an available MAC are allowed.
216216
test_microvm.api.network.put(
217-
iface_id="2", host_dev_name=second_if_name, guest_mac="08:00:00:00:00:01"
217+
iface_id="2", host_dev_name=tap2name, guest_mac="08:00:00:00:00:01"
218218
)
219219

220220
# Updates to a network interface with an unavailable name are not allowed.
221221
expected_msg = "Could not create the network device"
222222
with pytest.raises(RuntimeError, match=expected_msg):
223223
test_microvm.api.network.put(
224-
iface_id="1", host_dev_name=second_if_name, guest_mac="06:00:00:00:00:01"
224+
iface_id="1", host_dev_name=tap2name, guest_mac="06:00:00:00:00:01"
225225
)
226226

227227
# Updates to a network interface with an available name are allowed.
228-
iface_id = "1"
229-
tapname = test_microvm.id[:8] + "tap" + iface_id
230-
231-
tap3 = net_tools.Tap(tapname, test_microvm.netns.id)
228+
tap3name = test_microvm.id[:8] + "tap3"
229+
tap3 = net_tools.Tap(tap3name, test_microvm.netns)
232230
test_microvm.api.network.put(
233-
iface_id=iface_id, host_dev_name=tap3.name, guest_mac="06:00:00:00:00:01"
231+
iface_id="3", host_dev_name=tap3.name, guest_mac="06:00:00:00:00:01"
234232
)
235233

236234

@@ -266,7 +264,7 @@ def test_api_mmds_config(uvm_plain):
266264
test_microvm.api.mmds_config.put(network_interfaces=["foo"])
267265

268266
# Attach network interface.
269-
tap = net_tools.Tap("tap1", test_microvm.netns.id)
267+
tap = net_tools.Tap(f"tap1-{test_microvm.id[:6]}", test_microvm.netns)
270268
test_microvm.api.network.put(
271269
iface_id="1", guest_mac="06:00:00:00:00:01", host_dev_name=tap.name
272270
)
@@ -487,7 +485,7 @@ def test_api_put_update_post_boot(uvm_plain, io_engine):
487485

488486
iface_id = "1"
489487
tapname = test_microvm.id[:8] + "tap" + iface_id
490-
tap1 = net_tools.Tap(tapname, test_microvm.netns.id)
488+
tap1 = net_tools.Tap(tapname, test_microvm.netns)
491489

492490
test_microvm.api.network.put(
493491
iface_id=iface_id, host_dev_name=tap1.name, guest_mac="06:00:00:00:00:01"
@@ -595,7 +593,7 @@ def test_rate_limiters_api_config(uvm_plain, io_engine):
595593
# Test network with tx bw rate-limiting.
596594
iface_id = "1"
597595
tapname = test_microvm.id[:8] + "tap" + iface_id
598-
tap1 = net_tools.Tap(tapname, test_microvm.netns.id)
596+
tap1 = net_tools.Tap(tapname, test_microvm.netns)
599597

600598
test_microvm.api.network.put(
601599
iface_id=iface_id,
@@ -607,7 +605,7 @@ def test_rate_limiters_api_config(uvm_plain, io_engine):
607605
# Test network with rx bw rate-limiting.
608606
iface_id = "2"
609607
tapname = test_microvm.id[:8] + "tap" + iface_id
610-
tap2 = net_tools.Tap(tapname, test_microvm.netns.id)
608+
tap2 = net_tools.Tap(tapname, test_microvm.netns)
611609
test_microvm.api.network.put(
612610
iface_id=iface_id,
613611
guest_mac="06:00:00:00:00:02",
@@ -618,7 +616,7 @@ def test_rate_limiters_api_config(uvm_plain, io_engine):
618616
# Test network with tx and rx bw and ops rate-limiting.
619617
iface_id = "3"
620618
tapname = test_microvm.id[:8] + "tap" + iface_id
621-
tap3 = net_tools.Tap(tapname, test_microvm.netns.id)
619+
tap3 = net_tools.Tap(tapname, test_microvm.netns)
622620
test_microvm.api.network.put(
623621
iface_id=iface_id,
624622
guest_mac="06:00:00:00:00:03",
@@ -665,7 +663,7 @@ def test_api_patch_pre_boot(uvm_plain, io_engine):
665663

666664
iface_id = "1"
667665
tapname = test_microvm.id[:8] + "tap" + iface_id
668-
tap1 = net_tools.Tap(tapname, test_microvm.netns.id)
666+
tap1 = net_tools.Tap(tapname, test_microvm.netns)
669667
test_microvm.api.network.put(
670668
iface_id=iface_id, host_dev_name=tap1.name, guest_mac="06:00:00:00:00:01"
671669
)
@@ -714,7 +712,7 @@ def test_negative_api_patch_post_boot(uvm_plain, io_engine):
714712

715713
iface_id = "1"
716714
tapname = test_microvm.id[:8] + "tap" + iface_id
717-
tap1 = net_tools.Tap(tapname, test_microvm.netns.id)
715+
tap1 = net_tools.Tap(tapname, test_microvm.netns)
718716
test_microvm.api.network.put(
719717
iface_id=iface_id, host_dev_name=tap1.name, guest_mac="06:00:00:00:00:01"
720718
)
@@ -1245,7 +1243,7 @@ def test_get_full_config(uvm_plain):
12451243
# Add a net device.
12461244
iface_id = "1"
12471245
tapname = test_microvm.id[:8] + "tap" + iface_id
1248-
tap1 = net_tools.Tap(tapname, test_microvm.netns.id)
1246+
tap1 = net_tools.Tap(tapname, test_microvm.netns)
12491247
guest_mac = "06:00:00:00:00:01"
12501248
tx_rl = {
12511249
"bandwidth": {"size": 1000000, "refill_time": 100, "one_time_burst": None},

tests/integration_tests/functional/test_net.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ def test_multi_queue_unsupported(uvm_plain):
8383
guest_mac="AA:FC:00:00:00:01",
8484
)
8585

86+
# clean TAP device
87+
utils.run_cmd(f"{microvm.netns.cmd_prefix()} ip link del name {tapname}")
88+
8689

8790
@pytest.fixture
8891
def uvm_any(microvm_factory, uvm_ctor, guest_kernel, rootfs):

0 commit comments

Comments
 (0)