Skip to content

Commit 3cf3a58

Browse files
committed
tests: functional: Add hot-unplug tests
Add a delete() method to the test framework HTTP API client and extend the hotplug tests to verify device hot-unplug. Also extend the test_hotplug_max_devices() test to unplugs all devices, verifies slots are freed, and plug them again. Signed-off-by: Ilias Stamatis <ilstam@amazon.com>
1 parent 3f57453 commit 3cf3a58

2 files changed

Lines changed: 149 additions & 5 deletions

File tree

tests/framework/http_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ def get(self):
8484

8585
return res
8686

87+
def delete(self, resource_id):
88+
"""Make a DELETE request"""
89+
path = self.resource + "/" + resource_id
90+
url = self._api.endpoint + path
91+
try:
92+
res = self._api.session.delete(url)
93+
except Exception as e:
94+
if self._api.error_callback:
95+
self._api.error_callback("DELETE", path, str(e))
96+
raise
97+
if res.status_code != HTTPStatus.NO_CONTENT:
98+
json = res.json()
99+
msg = json.get("fault_message", json.get("error", res.content))
100+
raise RuntimeError(msg, json, res)
101+
return res
102+
87103
def request(self, method, path, **kwargs):
88104
"""Make an HTTP request"""
89105
kwargs = {key: val for key, val in kwargs.items() if val is not None}

tests/integration_tests/functional/test_hotplug.py

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def test_hotplug_block(uvm_any_with_pci):
2020
Test hotplugging a block device after VM start.
2121
Test that the device appears in lspci and is usable.
2222
Test that invalid hotplug request are rejected.
23+
Test hot-unplugging the device.
2324
"""
2425
vm = uvm_any_with_pci
2526

@@ -94,12 +95,37 @@ def test_hotplug_block(uvm_any_with_pci):
9495
_, lspci_final, _ = vm.ssh.check_output("lspci -n")
9596
assert lspci_final == lspci_after
9697

98+
# Unplugging a non-existent device must be rejected
99+
with pytest.raises(RuntimeError, match="Device not found"):
100+
vm.api.drive.delete("nonexistent")
101+
102+
# Unplugging the root block device must be rejected
103+
with pytest.raises(RuntimeError, match="Cannot unplug root device"):
104+
vm.api.drive.delete("rootfs")
105+
106+
# No unplug notification mechanism exists yet, so the guest needs to
107+
# gracefully prepare for the detach before the host issues the unplug.
108+
vm.ssh.check_output("umount /tmp/block0_mnt")
109+
vm.ssh.check_output(f"echo 1 > /sys/bus/pci/devices/0000:{bdf}/remove")
110+
111+
# Unplug the block device
112+
vm.api.drive.delete("block0")
113+
114+
# Verify the device is gone
115+
_, lspci_after_unplug, _ = vm.ssh.check_output("lspci -n")
116+
assert lspci_after_unplug == lspci_before
117+
118+
# Unplugging the same device again must be rejected
119+
with pytest.raises(RuntimeError, match="Device not found"):
120+
vm.api.drive.delete("block0")
121+
97122

98123
def test_hotplug_pmem(uvm_any_with_pci):
99124
"""
100125
Test hotplugging a pmem device after VM start.
101126
Test that the device appears in lspci and is usable.
102127
Test that invalid hotplug request are rejected.
128+
Test hot-unplugging the device.
103129
"""
104130
vm = uvm_any_with_pci
105131

@@ -174,12 +200,33 @@ def test_hotplug_pmem(uvm_any_with_pci):
174200
_, lspci_final, _ = vm.ssh.check_output("lspci -n")
175201
assert lspci_final == lspci_after
176202

203+
# Unplugging a non-existent device must be rejected
204+
with pytest.raises(RuntimeError, match="Device not found"):
205+
vm.api.pmem.delete("nonexistent")
206+
207+
# No unplug notification mechanism exists yet, so the guest needs to
208+
# gracefully prepare for the detach before the host issues the unplug.
209+
vm.ssh.check_output("umount /tmp/pmem0_mnt")
210+
vm.ssh.check_output(f"echo 1 > /sys/bus/pci/devices/0000:{bdf}/remove")
211+
212+
# Unplug the pmem device
213+
vm.api.pmem.delete("pmem0")
214+
215+
# Verify the device is gone
216+
_, lspci_after_unplug, _ = vm.ssh.check_output("lspci -n")
217+
assert lspci_after_unplug == lspci_before
218+
219+
# Unplugging the same device again must be rejected
220+
with pytest.raises(RuntimeError, match="Device not found"):
221+
vm.api.pmem.delete("pmem0")
222+
177223

178224
def test_hotplug_net(uvm_any_with_pci):
179225
"""
180226
Test hotplugging a net device after VM start.
181227
Test that the device appears in lspci and is usable.
182228
Test that invalid hotplug request are rejected.
229+
Test hot-unplugging the device.
183230
"""
184231
vm = uvm_any_with_pci
185232

@@ -260,10 +307,48 @@ def test_hotplug_net(uvm_any_with_pci):
260307
_, lspci_final, _ = vm.ssh.check_output("lspci -n")
261308
assert lspci_final == lspci_after
262309

310+
# Unplugging a non-existent device must be rejected
311+
with pytest.raises(RuntimeError, match="Device not found"):
312+
vm.api.network.delete("nonexistent")
313+
314+
# No unplug notification mechanism exists yet, so the guest needs to
315+
# gracefully prepare for the detach before the host issues the unplug.
316+
vm.ssh.check_output(f"ip link set {iface_name} down")
317+
vm.ssh.check_output(f"echo 1 > /sys/bus/pci/devices/0000:{bdf}/remove")
318+
319+
# Unplug the net device
320+
vm.api.network.delete(iface1.dev_name)
321+
322+
# Verify the device is gone
323+
_, lspci_after_unplug, _ = vm.ssh.check_output("lspci -n")
324+
assert lspci_after_unplug == lspci_before
325+
326+
# Unplugging the same device again must be rejected
327+
with pytest.raises(RuntimeError, match="Device not found"):
328+
vm.api.network.delete(iface1.dev_name)
329+
330+
331+
def test_unplug_root_pmem(microvm_factory, guest_kernel_acpi, rootfs):
332+
"""
333+
Unplugging the root pmem device must be rejected.
334+
"""
335+
vm = microvm_factory.build(guest_kernel_acpi, rootfs, pci=True)
336+
vm.memory_monitor = None
337+
vm.monitors = []
338+
vm.spawn()
339+
vm.basic_config(add_root_device=False)
340+
vm.add_pmem("pmem_root", rootfs, root_device=True)
341+
vm.add_net_iface()
342+
vm.start()
343+
344+
with pytest.raises(RuntimeError, match="Cannot unplug root device"):
345+
vm.api.pmem.delete("pmem_root")
346+
263347

264348
def test_hotplug_no_pci(uvm_any_without_pci):
265349
"""
266-
Hotplugging any device type must be rejected when PCI is not enabled.
350+
Hotplugging and unplugging any device type must be rejected when PCI is not
351+
enabled.
267352
"""
268353
vm = uvm_any_without_pci
269354

@@ -294,6 +379,15 @@ def test_hotplug_no_pci(uvm_any_without_pci):
294379
guest_mac=iface1.guest_mac,
295380
)
296381

382+
with pytest.raises(RuntimeError, match="PCI is not enabled"):
383+
vm.api.drive.delete("block0")
384+
385+
with pytest.raises(RuntimeError, match="PCI is not enabled"):
386+
vm.api.pmem.delete("pmem0")
387+
388+
with pytest.raises(RuntimeError, match="PCI is not enabled"):
389+
vm.api.network.delete("eth0")
390+
297391

298392
def test_hotplug_preserved_after_snapshot(uvm_any_with_pci, microvm_factory):
299393
"""
@@ -352,8 +446,8 @@ def test_hotplug_max_devices(uvm_any_with_pci):
352446
vm = uvm_any_with_pci
353447

354448
# Count how many PCI slots are already in use
355-
_, lspci, _ = vm.ssh.check_output("lspci -n")
356-
used_slots = len(lspci.strip().splitlines())
449+
_, lspci_initial, _ = vm.ssh.check_output("lspci -n")
450+
used_slots = len(lspci_initial.strip().splitlines())
357451
free_slots = pci_max_slots - used_slots
358452

359453
for i in range(free_slots):
@@ -369,8 +463,8 @@ def test_hotplug_max_devices(uvm_any_with_pci):
369463

370464
# Verify all PCI slots are occupied
371465
vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan")
372-
_, lspci, _ = vm.ssh.check_output("lspci -n")
373-
assert len(lspci.strip().splitlines()) == pci_max_slots
466+
_, lspci_full, _ = vm.ssh.check_output("lspci -n")
467+
assert len(lspci_full.strip().splitlines()) == pci_max_slots
374468

375469
# The next hotplug must fail — no PCI slots left
376470
host_file = drive_tools.FilesystemFile(
@@ -385,3 +479,37 @@ def test_hotplug_max_devices(uvm_any_with_pci):
385479
is_root_device=False,
386480
is_read_only=False,
387481
)
482+
483+
# Unplug all hotplugged devices
484+
for i in range(free_slots):
485+
vm.api.drive.delete(f"block{i}")
486+
487+
# Remove the stale devices from the guest
488+
new_bdfs = [
489+
l.split()[0]
490+
for l in set(lspci_full.strip().splitlines())
491+
- set(lspci_initial.strip().splitlines())
492+
]
493+
for bdf in new_bdfs:
494+
vm.ssh.check_output(f"echo 1 > /sys/bus/pci/devices/0000:{bdf}/remove")
495+
496+
# Verify we're back to the initial number of devices
497+
_, lspci, _ = vm.ssh.check_output("lspci -n")
498+
assert len(lspci.strip().splitlines()) == used_slots
499+
500+
# Re-plug all devices to verify the slots were truly freed
501+
for i in range(free_slots):
502+
host_file = drive_tools.FilesystemFile(
503+
os.path.join(vm.fsfiles, f"block_re{i}"), size=1
504+
)
505+
vm.api.drive.put(
506+
drive_id=f"block_re{i}",
507+
path_on_host=vm.create_jailed_resource(host_file.path),
508+
is_root_device=False,
509+
is_read_only=False,
510+
)
511+
512+
# Verify all PCI slots are occupied again
513+
vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan")
514+
_, lspci, _ = vm.ssh.check_output("lspci -n")
515+
assert len(lspci.strip().splitlines()) == pci_max_slots

0 commit comments

Comments
 (0)