Skip to content

Commit cdcf610

Browse files
committed
tests: functional: Add hotplugging tests
Add integration tests for block, pmem and net hotplugging. The tests require a manual PCI bus rescan at the moment since no hotplug notification mechanism is implemented at the moment. Signed-off-by: Ilias Stamatis <ilstam@amazon.com>
1 parent 6fe5974 commit cdcf610

1 file changed

Lines changed: 283 additions & 0 deletions

File tree

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Tests for PCI device hotplug"""
4+
5+
import os
6+
7+
import pytest
8+
9+
import host_tools.drive as drive_tools
10+
import host_tools.network as net_tools
11+
12+
VIRTIO_PCI_VENDOR_ID = 0x1AF4
13+
VIRTIO_PCI_DEVICE_ID_NET = 0x1041
14+
VIRTIO_PCI_DEVICE_ID_BLOCK = 0x1042
15+
VIRTIO_PCI_DEVICE_ID_PMEM = 0x105B
16+
17+
18+
def test_hotplug_block(microvm_factory, guest_kernel_acpi, rootfs):
19+
"""
20+
Test hotplugging a block device after VM start.
21+
Test that the device appears in lspci and is usable.
22+
Test that invalid hotplug request are rejected.
23+
"""
24+
vm = microvm_factory.build(guest_kernel_acpi, rootfs, pci=True)
25+
vm.spawn()
26+
vm.basic_config()
27+
vm.add_net_iface()
28+
vm.start()
29+
30+
# Snapshot lspci output before hotplug
31+
_, lspci_before, _ = vm.ssh.check_output("lspci -n")
32+
33+
# Hotplug a block device
34+
host_file = drive_tools.FilesystemFile(os.path.join(vm.fsfiles, "block0"), size=4)
35+
vm.api.drive.put(
36+
drive_id="block0",
37+
path_on_host=vm.create_jailed_resource(host_file.path),
38+
is_root_device=False,
39+
is_read_only=False,
40+
)
41+
42+
# Rescan PCI bus since no hotplug notification mechanism exists yet
43+
vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan")
44+
45+
# Verify a new virtio-block device entry appeared in lspci
46+
_, lspci_after, _ = vm.ssh.check_output("lspci -n")
47+
new_entries = set(lspci_after.splitlines()) - set(lspci_before.splitlines())
48+
assert len(new_entries) == 1
49+
entry = new_entries.pop()
50+
assert f"{VIRTIO_PCI_VENDOR_ID:04x}:{VIRTIO_PCI_DEVICE_ID_BLOCK:04x}" in entry
51+
52+
# Discover the block device node from the PCI BDF via sysfs
53+
bdf = entry.split()[0]
54+
_, dev_name, _ = vm.ssh.check_output(
55+
f"ls /sys/bus/pci/devices/0000:{bdf}/virtio*/block/"
56+
)
57+
dev_path = f"/dev/{dev_name.strip()}"
58+
59+
# Ensure the device is usable by writing a file to it and reading it back
60+
vm.ssh.check_output("mkdir -p /tmp/block0_mnt")
61+
vm.ssh.check_output(f"mount {dev_path} /tmp/block0_mnt")
62+
vm.ssh.check_output("echo hotplug_test > /tmp/block0_mnt/test")
63+
_, stdout, _ = vm.ssh.check_output("cat /tmp/block0_mnt/test")
64+
assert stdout.strip() == "hotplug_test"
65+
66+
# Hotplugging a device with a duplicate ID must be rejected
67+
with pytest.raises(RuntimeError, match="Device ID in use"):
68+
vm.api.drive.put(
69+
drive_id="block0",
70+
path_on_host=vm.create_jailed_resource(host_file.path),
71+
is_root_device=False,
72+
is_read_only=False,
73+
)
74+
75+
# Hotplugging a root device must be rejected
76+
with pytest.raises(RuntimeError, match="A root block device already exists"):
77+
vm.api.drive.put(
78+
drive_id="block_root",
79+
path_on_host=vm.create_jailed_resource(host_file.path),
80+
is_root_device=True,
81+
is_read_only=False,
82+
)
83+
84+
# Verify no further devices appeared after the rejected requests
85+
vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan")
86+
_, lspci_final, _ = vm.ssh.check_output("lspci -n")
87+
assert lspci_final == lspci_after
88+
89+
90+
def test_hotplug_pmem(microvm_factory, guest_kernel_acpi, rootfs):
91+
"""
92+
Test hotplugging a pmem device after VM start.
93+
Test that the device appears in lspci and is usable.
94+
Test that invalid hotplug request are rejected.
95+
"""
96+
vm = microvm_factory.build(guest_kernel_acpi, rootfs, pci=True)
97+
vm.spawn()
98+
vm.basic_config()
99+
vm.add_net_iface()
100+
vm.start()
101+
102+
# Snapshot lspci output before hotplug
103+
_, lspci_before, _ = vm.ssh.check_output("lspci -n")
104+
105+
# Hotplug a pmem device
106+
host_file = drive_tools.FilesystemFile(os.path.join(vm.fsfiles, "pmem0"), size=4)
107+
vm.api.pmem.put(
108+
id="pmem0",
109+
path_on_host=vm.create_jailed_resource(host_file.path),
110+
root_device=False,
111+
read_only=False,
112+
)
113+
114+
# Rescan PCI bus since no hotplug notification mechanism exists yet
115+
vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan")
116+
117+
# Verify a new virtio-pmem device entry appeared in lspci
118+
_, lspci_after, _ = vm.ssh.check_output("lspci -n")
119+
new_entries = set(lspci_after.splitlines()) - set(lspci_before.splitlines())
120+
assert len(new_entries) == 1
121+
entry = new_entries.pop()
122+
assert f"{VIRTIO_PCI_VENDOR_ID:04x}:{VIRTIO_PCI_DEVICE_ID_PMEM:04x}" in entry
123+
124+
# Discover the pmem device node from the PCI BDF via sysfs.
125+
# The NVDIMM subsystem in the guest creates the ndbus/region/namespace/block
126+
# hierarchy asynchronously after driver probe, so we need to wait for it.
127+
vm.ssh.check_output("sleep 1")
128+
bdf = entry.split()[0]
129+
_, dev_name, _ = vm.ssh.check_output(
130+
f"ls /sys/bus/pci/devices/0000:{bdf}/virtio*/ndbus*/region*/namespace*/block/"
131+
)
132+
dev_path = f"/dev/{dev_name.strip()}"
133+
134+
# Ensure the device is usable by writing a file to it and reading it back
135+
vm.ssh.check_output("mkdir -p /tmp/pmem0_mnt")
136+
vm.ssh.check_output(f"mount {dev_path} /tmp/pmem0_mnt")
137+
vm.ssh.check_output("echo hotplug_test > /tmp/pmem0_mnt/test")
138+
_, stdout, _ = vm.ssh.check_output("cat /tmp/pmem0_mnt/test")
139+
assert stdout.strip() == "hotplug_test"
140+
141+
# Hotplugging a root pmem device must be rejected
142+
with pytest.raises(RuntimeError, match="Attempt to add pmem as a root device"):
143+
vm.api.pmem.put(
144+
id="pmem_root",
145+
path_on_host=vm.create_jailed_resource(host_file.path),
146+
root_device=True,
147+
read_only=False,
148+
)
149+
150+
# Hotplugging a device with a duplicate ID must be rejected
151+
with pytest.raises(RuntimeError, match="Device ID in use"):
152+
vm.api.pmem.put(
153+
id="pmem0",
154+
path_on_host=vm.create_jailed_resource(host_file.path),
155+
root_device=False,
156+
read_only=False,
157+
)
158+
159+
# Verify no further devices appeared after the rejected requests
160+
vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan")
161+
_, lspci_final, _ = vm.ssh.check_output("lspci -n")
162+
assert lspci_final == lspci_after
163+
164+
165+
def test_hotplug_net(microvm_factory, guest_kernel_acpi, rootfs):
166+
"""
167+
Test hotplugging a net device after VM start.
168+
Test that the device appears in lspci and is usable.
169+
Test that invalid hotplug request are rejected.
170+
"""
171+
vm = microvm_factory.build(guest_kernel_acpi, rootfs, pci=True)
172+
vm.spawn()
173+
vm.basic_config()
174+
vm.add_net_iface()
175+
vm.start()
176+
177+
# Snapshot lspci output before hotplug
178+
_, lspci_before, _ = vm.ssh.check_output("lspci -n")
179+
180+
# Hotplug a network device
181+
iface1 = net_tools.NetIfaceConfig.with_id(1)
182+
vm.netns.add_tap(iface1.tap_name, ip=f"{iface1.host_ip}/{iface1.netmask_len}")
183+
vm.api.network.put(
184+
iface_id=iface1.dev_name,
185+
host_dev_name=iface1.tap_name,
186+
guest_mac=iface1.guest_mac,
187+
)
188+
189+
# Rescan PCI bus since no hotplug notification mechanism exists yet
190+
vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan")
191+
192+
# Verify a new net device entry appeared in lspci
193+
_, lspci_after, _ = vm.ssh.check_output("lspci -n")
194+
new_entries = set(lspci_after.splitlines()) - set(lspci_before.splitlines())
195+
assert len(new_entries) == 1
196+
entry = new_entries.pop()
197+
assert f"{VIRTIO_PCI_VENDOR_ID:04x}:{VIRTIO_PCI_DEVICE_ID_NET:04x}" in entry
198+
199+
# Discover the net interface name from the PCI BDF via sysfs
200+
bdf = entry.split()[0]
201+
_, iface_name, _ = vm.ssh.check_output(
202+
f"ls /sys/bus/pci/devices/0000:{bdf}/virtio*/net/"
203+
)
204+
iface_name = iface_name.strip()
205+
206+
# Verify the hotplugged interface is usable
207+
vm.ssh.check_output(f"ip link show {iface_name}")
208+
vm.ssh.check_output(
209+
f"ip addr add {iface1.guest_ip}/{iface1.netmask_len} dev {iface_name}"
210+
)
211+
vm.ssh.check_output(f"ip link set {iface_name} up")
212+
213+
# Ping the host from the guest through the hotplugged interface
214+
_, stdout, _ = vm.ssh.check_output(f"ping -c 3 -W 3 {iface1.host_ip}")
215+
assert "3 packets transmitted, 3 received" in stdout
216+
217+
# Hotplugging a device with a duplicate ID must be rejected
218+
iface2 = net_tools.NetIfaceConfig.with_id(2)
219+
with pytest.raises(RuntimeError, match="Device ID in use"):
220+
vm.api.network.put(
221+
iface_id=iface1.dev_name,
222+
host_dev_name=iface2.tap_name,
223+
guest_mac=iface2.guest_mac,
224+
)
225+
226+
# Hotplugging a device with a duplicate MAC must be rejected
227+
with pytest.raises(RuntimeError, match="The MAC address is already in use"):
228+
vm.api.network.put(
229+
iface_id=iface2.dev_name,
230+
host_dev_name=iface2.tap_name,
231+
guest_mac=iface1.guest_mac,
232+
)
233+
234+
# Hotplugging a device that reuses the same TAP must be rejected
235+
with pytest.raises(RuntimeError, match="Resource busy"):
236+
vm.api.network.put(
237+
iface_id=iface2.dev_name,
238+
host_dev_name=iface1.tap_name,
239+
guest_mac=iface2.guest_mac,
240+
)
241+
242+
# Verify no further devices appeared after the rejected requests
243+
vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan")
244+
_, lspci_final, _ = vm.ssh.check_output("lspci -n")
245+
assert lspci_final == lspci_after
246+
247+
248+
def test_hotplug_no_pci(microvm_factory, guest_kernel_acpi, rootfs):
249+
"""
250+
Hotplugging any device type must be rejected when PCI is not enabled.
251+
"""
252+
vm = microvm_factory.build(guest_kernel_acpi, rootfs, pci=False)
253+
vm.spawn()
254+
vm.basic_config()
255+
vm.add_net_iface()
256+
vm.start()
257+
258+
host_file = drive_tools.FilesystemFile(os.path.join(vm.fsfiles, "disk"), size=4)
259+
260+
with pytest.raises(RuntimeError, match="PCI is not enabled"):
261+
vm.api.drive.put(
262+
drive_id="block0",
263+
path_on_host=vm.create_jailed_resource(host_file.path),
264+
is_root_device=False,
265+
is_read_only=False,
266+
)
267+
268+
with pytest.raises(RuntimeError, match="PCI is not enabled"):
269+
vm.api.pmem.put(
270+
id="pmem0",
271+
path_on_host=vm.create_jailed_resource(host_file.path),
272+
root_device=False,
273+
read_only=False,
274+
)
275+
276+
iface1 = net_tools.NetIfaceConfig.with_id(1)
277+
vm.netns.add_tap(iface1.tap_name, ip=f"{iface1.host_ip}/{iface1.netmask_len}")
278+
with pytest.raises(RuntimeError, match="PCI is not enabled"):
279+
vm.api.network.put(
280+
iface_id=iface1.dev_name,
281+
host_dev_name=iface1.tap_name,
282+
guest_mac=iface1.guest_mac,
283+
)

0 commit comments

Comments
 (0)