Skip to content

Commit 5c32222

Browse files
committed
test(virtio-mem): add functional integration tests for device
Add integration tests for the new device: - check that the device is detected - check that hotplugging and unplugging works - check that memory can be used after hotplugging - check that memory is freed on hotunplug - check different config combinations - check different uvm types - check that contents are preserved across snapshot-restore Signed-off-by: Riccardo Mancini <[email protected]>
1 parent f36c16a commit 5c32222

File tree

2 files changed

+288
-11
lines changed

2 files changed

+288
-11
lines changed

tests/framework/microvm.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,22 @@ def wait_for_ssh_up(self):
11861186
# run commands. The actual connection retry loop happens in SSHConnection._init_connection
11871187
_ = self.ssh_iface(0)
11881188

1189+
def hotplug_memory(
1190+
self, requested_size_mib: int, timeout: int = 60, poll: float = 0.1
1191+
):
1192+
"""Send a hot(un)plug request and wait up to timeout seconds for completion polling every poll seconds"""
1193+
self.api.memory_hotplug.patch(requested_size_mib=requested_size_mib)
1194+
# Wait for the hotplug to complete
1195+
deadline = time.time() + timeout
1196+
while time.time() < deadline:
1197+
if (
1198+
self.api.memory_hotplug.get().json()["plugged_size_mib"]
1199+
== requested_size_mib
1200+
):
1201+
return
1202+
time.sleep(poll)
1203+
raise TimeoutError(f"Hotplug did not complete within {timeout} seconds")
1204+
11891205

11901206
class MicroVMFactory:
11911207
"""MicroVM factory"""
@@ -1300,6 +1316,18 @@ def build_n_from_snapshot(
13001316
last_snapshot.delete()
13011317
current_snapshot.delete()
13021318

1319+
def clone_uvm(self, uvm, uffd_handler_name=None):
1320+
"""
1321+
Clone the given VM and start it.
1322+
"""
1323+
snapshot = uvm.snapshot_full()
1324+
restored_vm = self.build()
1325+
restored_vm.spawn()
1326+
restored_vm.restore_from_snapshot(
1327+
snapshot, resume=True, uffd_handler_name=uffd_handler_name
1328+
)
1329+
return restored_vm
1330+
13031331
def kill(self):
13041332
"""Clean up all built VMs"""
13051333
for vm in self.vms:

tests/integration_tests/functional/test_memory_hp.py

Lines changed: 260 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,120 @@
33

44
"""Tests for verifying the virtio-mem is working correctly"""
55

6+
import pytest
7+
from packaging import version
8+
from tenacity import Retrying, retry_if_exception_type, stop_after_delay, wait_fixed
9+
10+
from framework.guest_stats import MeminfoGuest
11+
from framework.microvm import HugePagesConfig
12+
from framework.utils import get_kernel_version, get_resident_memory
13+
14+
MEMHP_BOOTARGS = "console=ttyS0 reboot=k panic=1 memhp_default_state=online_movable"
15+
DEFAULT_CONFIG = {"total_size_mib": 1024, "slot_size_mib": 128, "block_size_mib": 2}
16+
17+
18+
def uvm_booted_memhp(
19+
uvm, rootfs, _microvm_factory, vhost_user, memhp_config, huge_pages, _uffd_handler
20+
):
21+
"""Boots a VM with the given memory hotplugging config"""
622

7-
def test_virtio_mem_detected(uvm_plain_6_1):
8-
"""
9-
Check that the guest kernel has enabled PV steal time.
10-
"""
11-
uvm = uvm_plain_6_1
1223
uvm.spawn()
1324
uvm.memory_monitor = None
14-
uvm.basic_config(
15-
boot_args="console=ttyS0 reboot=k panic=1 memhp_default_state=online_movable"
16-
)
25+
if vhost_user:
26+
# We need to setup ssh keys manually because we did not specify rootfs
27+
# in microvm_factory.build method
28+
ssh_key = rootfs.with_suffix(".id_rsa")
29+
uvm.ssh_key = ssh_key
30+
uvm.basic_config(
31+
boot_args=MEMHP_BOOTARGS, add_root_device=False, huge_pages=huge_pages
32+
)
33+
uvm.add_vhost_user_drive(
34+
"rootfs", rootfs, is_root_device=True, is_read_only=True
35+
)
36+
else:
37+
uvm.basic_config(boot_args=MEMHP_BOOTARGS, huge_pages=huge_pages)
38+
39+
uvm.api.memory_hotplug.put(**memhp_config)
1740
uvm.add_net_iface()
18-
uvm.api.memory_hotplug.put(total_size_mib=1024)
1941
uvm.start()
42+
return uvm
43+
44+
45+
def uvm_resumed_memhp(
46+
uvm_plain,
47+
rootfs,
48+
microvm_factory,
49+
vhost_user,
50+
memhp_config,
51+
huge_pages,
52+
uffd_handler,
53+
):
54+
"""Restores a VM with the given memory hotplugging config after booting and snapshotting"""
55+
if vhost_user:
56+
pytest.skip("vhost-user doesn't support snapshot/restore")
57+
if huge_pages and huge_pages != HugePagesConfig.NONE and not uffd_handler:
58+
pytest.skip("Hugepages requires a UFFD handler")
59+
uvm = uvm_booted_memhp(
60+
uvm_plain, rootfs, microvm_factory, vhost_user, memhp_config, huge_pages, None
61+
)
62+
return microvm_factory.clone_uvm(uvm, uffd_handler_name=uffd_handler)
63+
64+
65+
@pytest.fixture(
66+
params=[
67+
(uvm_booted_memhp, False, HugePagesConfig.NONE, None),
68+
(uvm_booted_memhp, False, HugePagesConfig.HUGETLBFS_2MB, None),
69+
(uvm_booted_memhp, True, HugePagesConfig.NONE, None),
70+
(uvm_resumed_memhp, False, HugePagesConfig.NONE, None),
71+
(uvm_resumed_memhp, False, HugePagesConfig.NONE, "on_demand"),
72+
(uvm_resumed_memhp, False, HugePagesConfig.HUGETLBFS_2MB, "on_demand"),
73+
],
74+
ids=[
75+
"booted",
76+
"booted-huge-pages",
77+
"booted-vhost-user",
78+
"resumed",
79+
"resumed-uffd",
80+
"resumed-uffd-huge-pages",
81+
],
82+
)
83+
def uvm_any_memhp(request, uvm_plain_6_1, rootfs, microvm_factory):
84+
"""Fixture that yields a booted or resumed VM with memory hotplugging"""
85+
ctor, vhost_user, huge_pages, uffd_handler = request.param
86+
yield ctor(
87+
uvm_plain_6_1,
88+
rootfs,
89+
microvm_factory,
90+
vhost_user,
91+
DEFAULT_CONFIG,
92+
huge_pages,
93+
uffd_handler,
94+
)
95+
96+
97+
def supports_hugetlbfs_discard():
98+
"""Returns True if the kernel supports hugetlbfs discard"""
99+
return version.parse(get_kernel_version()) >= version.parse("5.18.0")
100+
101+
102+
def validate_metrics(uvm):
103+
"""Validates that there are no fails in the metrics"""
104+
metrics_to_check = ["plug_fails", "unplug_fails", "unplug_all_fails", "state_fails"]
105+
if supports_hugetlbfs_discard():
106+
metrics_to_check.append("unplug_discard_fails")
107+
uvm.flush_metrics()
108+
for metrics in uvm.get_all_metrics():
109+
for k in metrics_to_check:
110+
assert (
111+
metrics["memory_hotplug"][k] == 0
112+
), f"{k}={metrics[k]} is greater than zero"
20113

114+
115+
def check_device_detected(uvm):
116+
"""
117+
Check that the guest kernel has enabled virtio-mem.
118+
"""
119+
hp_config = uvm.api.memory_hotplug.get().json()
21120
_, stdout, _ = uvm.ssh.check_output("dmesg | grep 'virtio_mem'")
22121
for line in stdout.splitlines():
23122
_, key, value = line.strip().split(":")
@@ -27,12 +126,162 @@ def test_virtio_mem_detected(uvm_plain_6_1):
27126
case "start address":
28127
assert value == (512 << 30), "start address doesn't match"
29128
case "region size":
30-
assert value == 1024 << 20, "region size doesn't match"
129+
assert (
130+
value == hp_config["total_size_mib"] << 20
131+
), "region size doesn't match"
31132
case "device block size":
32-
assert value == 2 << 20, "block size doesn't match"
133+
assert (
134+
value == hp_config["block_size_mib"] << 20
135+
), "block size doesn't match"
33136
case "plugged size":
34137
assert value == 0, "plugged size doesn't match"
35138
case "requested size":
36139
assert value == 0, "requested size doesn't match"
37140
case _:
38141
continue
142+
143+
144+
def check_memory_usable(uvm):
145+
"""Allocates memory to verify it's usable (5% margin to avoid OOM-kill)"""
146+
mem_available = MeminfoGuest(uvm).get().mem_available.bytes()
147+
# number of 64b ints to allocate as 95% of available memory
148+
count = mem_available * 95 // 100 // 8
149+
150+
uvm.ssh.check_output(
151+
f"python3 -c 'Q = 0x0123456789abcdef; a = [Q] * {count}; assert all(q == Q for q in a)'"
152+
)
153+
154+
155+
def check_hotplug(uvm, requested_size_mib):
156+
"""Verifies memory can be hot(un)plugged"""
157+
meminfo = MeminfoGuest(uvm)
158+
mem_total_fixed = (
159+
meminfo.get().mem_total.mib()
160+
- uvm.api.memory_hotplug.get().json()["plugged_size_mib"]
161+
)
162+
uvm.hotplug_memory(requested_size_mib)
163+
164+
# verify guest driver received the request
165+
_, stdout, _ = uvm.ssh.check_output(
166+
"dmesg | grep 'virtio_mem' | grep 'requested size' | tail -1"
167+
)
168+
assert (
169+
int(stdout.strip().split(":")[-1].strip(), base=0) == requested_size_mib << 20
170+
)
171+
172+
for attempt in Retrying(
173+
retry=retry_if_exception_type(AssertionError),
174+
stop=stop_after_delay(5),
175+
wait=wait_fixed(1),
176+
reraise=True,
177+
):
178+
with attempt:
179+
# verify guest driver executed the request
180+
mem_total_after = meminfo.get().mem_total.mib()
181+
assert mem_total_after == mem_total_fixed + requested_size_mib
182+
183+
184+
def check_hotunplug(uvm, requested_size_mib):
185+
"""Verifies memory can be hotunplugged and gets released"""
186+
187+
rss_before = get_resident_memory(uvm.ps)
188+
189+
check_hotplug(uvm, requested_size_mib)
190+
191+
rss_after = get_resident_memory(uvm.ps)
192+
193+
print(f"RSS before: {rss_before}, after: {rss_after}")
194+
195+
huge_pages = HugePagesConfig(uvm.api.machine_config.get().json()["huge_pages"])
196+
if huge_pages == HugePagesConfig.HUGETLBFS_2MB and supports_hugetlbfs_discard():
197+
assert rss_after < rss_before, "RSS didn't decrease"
198+
199+
200+
def test_virtio_mem_hotplug_hotunplug(uvm_any_memhp):
201+
"""
202+
Check that memory can be hotplugged into the VM.
203+
"""
204+
uvm = uvm_any_memhp
205+
check_device_detected(uvm)
206+
207+
check_hotplug(uvm, 1024)
208+
check_memory_usable(uvm)
209+
210+
check_hotunplug(uvm, 0)
211+
212+
# Check it works again
213+
check_hotplug(uvm, 1024)
214+
check_memory_usable(uvm)
215+
216+
validate_metrics(uvm)
217+
218+
219+
@pytest.mark.parametrize(
220+
"memhp_config",
221+
[
222+
{"total_size_mib": 256, "slot_size_mib": 128, "block_size_mib": 64},
223+
{"total_size_mib": 256, "slot_size_mib": 128, "block_size_mib": 128},
224+
{"total_size_mib": 256, "slot_size_mib": 256, "block_size_mib": 64},
225+
{"total_size_mib": 256, "slot_size_mib": 256, "block_size_mib": 256},
226+
],
227+
ids=["all_different", "slot_sized_block", "single_slot", "single_block"],
228+
)
229+
def test_virtio_mem_configs(uvm_plain_6_1, memhp_config):
230+
"""
231+
Check that the virtio mem device is working as expected for different configs
232+
"""
233+
uvm = uvm_booted_memhp(uvm_plain_6_1, None, None, False, memhp_config, None, None)
234+
if not uvm.pci_enabled:
235+
pytest.skip(
236+
"Skip tests on MMIO transport to save time as we don't expect any difference."
237+
)
238+
239+
check_device_detected(uvm)
240+
241+
for size in range(
242+
0, memhp_config["total_size_mib"] + 1, memhp_config["block_size_mib"]
243+
):
244+
check_hotplug(uvm, size)
245+
246+
check_memory_usable(uvm)
247+
248+
for size in range(
249+
memhp_config["total_size_mib"] - memhp_config["block_size_mib"],
250+
-1,
251+
-memhp_config["block_size_mib"],
252+
):
253+
check_hotunplug(uvm, size)
254+
255+
validate_metrics(uvm)
256+
257+
258+
def test_snapshot_restore_persistence(uvm_plain_6_1, microvm_factory):
259+
"""
260+
Check that hptplugged memory is persisted across snapshot/restore.
261+
"""
262+
if not uvm_plain_6_1.pci_enabled:
263+
pytest.skip(
264+
"Skip tests on MMIO transport to save time as we don't expect any difference."
265+
)
266+
uvm = uvm_booted_memhp(
267+
uvm_plain_6_1, None, microvm_factory, False, DEFAULT_CONFIG, None, None
268+
)
269+
270+
uvm.hotplug_memory(1024)
271+
272+
# Increase /dev/shm size as it defaults to half of the boot memory
273+
uvm.ssh.check_output("mount -o remount,size=1024M -t tmpfs tmpfs /dev/shm")
274+
275+
uvm.ssh.check_output("dd if=/dev/urandom of=/dev/shm/mem_hp_test bs=1M count=1024")
276+
277+
_, checksum_before, _ = uvm.ssh.check_output("sha256sum /dev/shm/mem_hp_test")
278+
279+
restored_vm = microvm_factory.clone_uvm(uvm)
280+
281+
_, checksum_after, _ = restored_vm.ssh.check_output(
282+
"sha256sum /dev/shm/mem_hp_test"
283+
)
284+
285+
assert checksum_before == checksum_after, "Checksums didn't match"
286+
287+
validate_metrics(restored_vm)

0 commit comments

Comments
 (0)