Skip to content

Commit 5c449be

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 70ac154 commit 5c449be

File tree

5 files changed

+91
-44
lines changed

5 files changed

+91
-44
lines changed

tests/conftest.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
static_cpu_templates_params,
4444
)
4545
from host_tools.metrics import get_metrics_logger
46+
from host_tools.network import NetNs
4647

4748
# This codebase uses Python features available in Python 3.10 or above
4849
if sys.version_info < (3, 10):
@@ -251,15 +252,46 @@ def uffd_handler_paths():
251252
yield handlers
252253

253254

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

264296
if binary_dir := request.config.getoption("--binary-dir"):
265297
fc_binary_path = Path(binary_dir) / "firecracker"
@@ -272,7 +304,9 @@ def microvm_factory(request, record_property, results_dir):
272304

273305
# We could override the chroot base like so
274306
# jailer_kwargs={"chroot_base": "/srv/jailo"}
275-
uvm_factory = MicroVMFactory(fc_binary_path, jailer_binary_path)
307+
uvm_factory = MicroVMFactory(
308+
fc_binary_path, jailer_binary_path, netns_factory=netns_factory
309+
)
276310
yield uvm_factory
277311

278312
# if the test failed, save important files from the root of the uVM into `test_results` for troubleshooting

tests/framework/microvm.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,7 @@ def __init__(self, fc_binary_path: Path, jailer_binary_path: Path, **kwargs):
10671067
self.vms = []
10681068
self.fc_binary_path = Path(fc_binary_path)
10691069
self.jailer_binary_path = Path(jailer_binary_path)
1070+
self.netns_factory = kwargs.pop("netns_factory", net_tools.NetNs)
10701071
self.kwargs = kwargs
10711072

10721073
def build(self, kernel=None, rootfs=None, **kwargs):
@@ -1079,7 +1080,7 @@ def build(self, kernel=None, rootfs=None, **kwargs):
10791080
jailer_binary_path=kwargs.pop(
10801081
"jailer_binary_path", self.jailer_binary_path
10811082
),
1082-
netns=kwargs.pop("netns", net_tools.NetNs(microvm_id)),
1083+
netns=kwargs.pop("netns", self.netns_factory(microvm_id)),
10831084
**kwargs,
10841085
)
10851086
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)