Skip to content

Commit caf23ce

Browse files
committed
feat(virtio-pmem): add integration tests
Add functional and API tests for virtio-pmem device and its configuration fields Signed-off-by: Egor Lazarchuk <[email protected]>
1 parent 21baf9f commit caf23ce

File tree

5 files changed

+254
-1
lines changed

5 files changed

+254
-1
lines changed

tests/framework/http_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,5 @@ def __init__(self, api_usocket_full_name, *, on_error=None):
132132
self.snapshot_load = Resource(self, "/snapshot/load")
133133
self.cpu_config = Resource(self, "/cpu-config")
134134
self.entropy = Resource(self, "/entropy")
135+
self.pmem = Resource(self, "/pmem", "id")
135136
self.serial = Resource(self, "/serial")

tests/framework/microvm.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,24 @@ def add_net_iface(self, iface=None, api=True, **kwargs):
973973

974974
return iface
975975

976+
def add_pmem(
977+
self,
978+
pmem_id,
979+
path_on_host,
980+
root_device=False,
981+
read_only=False,
982+
):
983+
"""Add a pmem device."""
984+
985+
path_on_jail = self.create_jailed_resource(path_on_host)
986+
self.api.pmem.put(
987+
id=pmem_id,
988+
path_on_host=path_on_jail,
989+
root_device=root_device,
990+
read_only=read_only,
991+
)
992+
self.disks[pmem_id] = path_on_host
993+
976994
def start(self):
977995
"""Start the microvm.
978996

tests/framework/vm_config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@
3131
"logger": null,
3232
"metrics": null,
3333
"mmds-config": null,
34-
"entropy": null
34+
"entropy": null,
35+
"pmem": []
3536
}

tests/integration_tests/functional/test_api.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,67 @@ def test_api_balloon(uvm_nano):
10441044
test_microvm.api.balloon.patch(amount_mib=33554432)
10451045

10461046

1047+
def test_pmem_api(uvm_plain_any, rootfs):
1048+
"""
1049+
Test virtio-pmem API commands
1050+
"""
1051+
1052+
vm = uvm_plain_any
1053+
vm.spawn()
1054+
vm.basic_config(add_root_device=False)
1055+
1056+
invalid_pmem_path_on_host = os.path.join(vm.fsfiles, "invalid_scratch")
1057+
utils.check_output(f"touch {invalid_pmem_path_on_host}")
1058+
invalid_pmem_file_path = vm.create_jailed_resource(str(invalid_pmem_path_on_host))
1059+
1060+
pmem_size_mb = 2
1061+
pmem_path_on_host = drive_tools.FilesystemFile(
1062+
os.path.join(vm.fsfiles, "scratch"), size=pmem_size_mb
1063+
)
1064+
pmem_file_path = vm.create_jailed_resource(pmem_path_on_host.path)
1065+
1066+
# Try to add pmem without setting `path_on_host`
1067+
expected_msg = re.escape(
1068+
"An error occurred when deserializing the json body of a request: missing field `path_on_host`"
1069+
)
1070+
with pytest.raises(RuntimeError, match=expected_msg):
1071+
vm.api.pmem.put(id="pmem")
1072+
1073+
# Try to add pmem with 0 sized backing file
1074+
expected_msg = re.escape("Error backing file size is 0")
1075+
with pytest.raises(RuntimeError, match=expected_msg):
1076+
vm.api.pmem.put(id="pmem", path_on_host=invalid_pmem_file_path)
1077+
1078+
# Try to add pmem as root while block is set as root
1079+
vm.api.drive.put(drive_id="drive", path_on_host=pmem_file_path, is_root_device=True)
1080+
expected_msg = re.escape(
1081+
"Attempt to add pmem as a root device while the root device defined as a block device"
1082+
)
1083+
with pytest.raises(RuntimeError, match=expected_msg):
1084+
vm.api.pmem.put(id="pmem", path_on_host=pmem_file_path, root_device=True)
1085+
1086+
# Reset block from being root
1087+
vm.api.drive.put(
1088+
drive_id="drive", path_on_host=pmem_file_path, is_root_device=False
1089+
)
1090+
1091+
# Try to add pmem as root twice
1092+
vm.api.pmem.put(id="pmem", path_on_host=pmem_file_path, root_device=True)
1093+
expected_msg = re.escape("A root pmem device already exist")
1094+
with pytest.raises(RuntimeError, match=expected_msg):
1095+
vm.api.pmem.put(id="pmem2", path_on_host=pmem_file_path, root_device=True)
1096+
1097+
# Reset pmem from being root
1098+
vm.api.pmem.put(id="pmem", path_on_host=pmem_file_path, root_device=False)
1099+
1100+
# Add a rootfs to boot a vm
1101+
vm.add_pmem("rootfs", rootfs, True, True)
1102+
1103+
# No post boot API calls to pmem
1104+
with pytest.raises(RuntimeError):
1105+
vm.api.pmem.put(id="pmem")
1106+
1107+
10471108
def test_get_full_config_after_restoring_snapshot(microvm_factory, uvm_nano):
10481109
"""
10491110
Test the configuration of a microVM after restoring from a snapshot.
@@ -1085,6 +1146,21 @@ def test_get_full_config_after_restoring_snapshot(microvm_factory, uvm_nano):
10851146
}
10861147
]
10871148

1149+
uvm_nano.api.pmem.put(
1150+
id="pmem",
1151+
path_on_host="/" + uvm_nano.rootfs_file.name,
1152+
root_device=False,
1153+
read_only=False,
1154+
)
1155+
setup_cfg["pmem"] = [
1156+
{
1157+
"id": "pmem",
1158+
"path_on_host": "/" + uvm_nano.rootfs_file.name,
1159+
"root_device": False,
1160+
"read_only": False,
1161+
}
1162+
]
1163+
10881164
# Add a memory balloon device.
10891165
uvm_nano.api.balloon.put(amount_mib=1, deflate_on_oom=True)
10901166
setup_cfg["balloon"] = {
@@ -1196,6 +1272,21 @@ def test_get_full_config(uvm_plain):
11961272
}
11971273
]
11981274

1275+
test_microvm.api.pmem.put(
1276+
id="pmem",
1277+
path_on_host="/" + test_microvm.rootfs_file.name,
1278+
root_device=False,
1279+
read_only=False,
1280+
)
1281+
expected_cfg["pmem"] = [
1282+
{
1283+
"id": "pmem",
1284+
"path_on_host": "/" + test_microvm.rootfs_file.name,
1285+
"root_device": False,
1286+
"read_only": False,
1287+
}
1288+
]
1289+
11991290
# Add a memory balloon device.
12001291
test_microvm.api.balloon.put(amount_mib=1, deflate_on_oom=True)
12011292
expected_cfg["balloon"] = {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Tests for the virtio-pmem device."""
4+
5+
import json
6+
import os
7+
8+
import host_tools.drive as drive_tools
9+
10+
ALIGNMENT = 2 << 20
11+
12+
13+
def align(size: int) -> int:
14+
"""
15+
Align the value to ALIGNMENT
16+
"""
17+
return (size + ALIGNMENT - 1) & ~(ALIGNMENT - 1)
18+
19+
20+
def check_pmem_exist(vm, index, root, read_only, size, extension):
21+
"""
22+
Check the pmem exist with correct parameters
23+
"""
24+
vm.ssh.check_output(f"ls /dev/pmem{index}")
25+
26+
if root:
27+
_, stdout, _ = vm.ssh.check_output("mount")
28+
if read_only:
29+
assert f"/dev/pmem0 on / type {extension} (ro" in stdout
30+
else:
31+
assert f"/dev/pmem0 on / type {extension} (rw" in stdout
32+
33+
_, stdout, _ = vm.ssh.check_output("lsblk -J")
34+
35+
j = json.loads(stdout)
36+
blocks = j["blockdevices"]
37+
for block in blocks:
38+
if block["name"] == f"pmem{index}":
39+
assert block["size"][-1] == "M"
40+
block_size_mb = int(block["size"][:-1])
41+
assert int(block_size_mb << 20) == size
42+
if root:
43+
assert "/" in block["mountpoints"]
44+
return
45+
assert False
46+
47+
48+
def test_pmem_add(uvm_plain_any, microvm_factory):
49+
"""
50+
Test addition of pmem devices to the VM and
51+
writes persistance
52+
"""
53+
54+
vm = uvm_plain_any
55+
vm.spawn()
56+
vm.basic_config(add_root_device=True)
57+
vm.add_net_iface()
58+
59+
# Pmem should work with non 2MB aligned files as well
60+
pmem_size_mb_1 = 1
61+
fs_1 = drive_tools.FilesystemFile(
62+
os.path.join(vm.fsfiles, "scratch_1"), size=pmem_size_mb_1
63+
)
64+
pmem_size_mb_2 = 2
65+
fs_2 = drive_tools.FilesystemFile(
66+
os.path.join(vm.fsfiles, "scratch_2"), size=pmem_size_mb_2
67+
)
68+
vm.add_pmem("pmem_1", fs_1.path, False, False)
69+
vm.add_pmem("pmem_2", fs_2.path, False, True)
70+
vm.start()
71+
72+
# Both 1MB and 2MB block will show as 2MB because of
73+
# the aligment
74+
check_pmem_exist(vm, 0, False, False, align(pmem_size_mb_1 << 20), "ext4")
75+
check_pmem_exist(vm, 1, False, True, align(pmem_size_mb_2 << 20), "ext4")
76+
77+
# Write something to the pmem0 to see that it is indeed saved to
78+
# underlying file when VM shots down
79+
test_string = "testing pmem persistance"
80+
vm.ssh.check_output("mkdir /tmp/mnt")
81+
vm.ssh.check_output("mount /dev/pmem0 -o dax=always /tmp/mnt")
82+
vm.ssh.check_output(f'echo "{test_string}" > /tmp/mnt/test')
83+
84+
snapshot = vm.snapshot_full()
85+
# Killing or rebooting an old VM will make OS to flush writes to the underlying file
86+
vm.kill()
87+
88+
restored_vm = microvm_factory.build_from_snapshot(snapshot)
89+
check_pmem_exist(restored_vm, 0, False, False, align(pmem_size_mb_1 << 20), "ext4")
90+
check_pmem_exist(restored_vm, 1, False, True, align(pmem_size_mb_2 << 20), "ext4")
91+
92+
# The /tmp/mnt and the mount still persist after snapshot restore.
93+
# Since we used `dax=always` during mounting there is no data in guest page
94+
# cache, so the read happens directly from pmem device
95+
_, stdout, _ = restored_vm.ssh.check_output("cat /tmp/mnt/test")
96+
assert stdout.strip() == test_string
97+
98+
99+
def test_pmem_add_as_root_rw(uvm_plain_any, rootfs_rw, microvm_factory):
100+
"""
101+
Test addition of a single root pmem device in read-write mode
102+
"""
103+
104+
vm = uvm_plain_any
105+
vm.memory_monitor = None
106+
vm.monitors = []
107+
vm.spawn()
108+
vm.basic_config(add_root_device=False)
109+
vm.add_net_iface()
110+
111+
rootfs_size = os.path.getsize(rootfs_rw)
112+
vm.add_pmem("pmem", rootfs_rw, True, False)
113+
vm.start()
114+
115+
check_pmem_exist(vm, 0, True, False, align(rootfs_size), "ext4")
116+
117+
snapshot = vm.snapshot_full()
118+
restored_vm = microvm_factory.build_from_snapshot(snapshot)
119+
check_pmem_exist(restored_vm, 0, True, False, align(rootfs_size), "ext4")
120+
121+
122+
def test_pmem_add_as_root_ro(uvm_plain_any, rootfs, microvm_factory):
123+
"""
124+
Test addition of a single root pmem device in read-only mode
125+
"""
126+
127+
vm = uvm_plain_any
128+
vm.memory_monitor = None
129+
vm.monitors = []
130+
vm.spawn()
131+
vm.basic_config(add_root_device=False)
132+
vm.add_net_iface()
133+
134+
rootfs_size = os.path.getsize(rootfs)
135+
vm.add_pmem("pmem", rootfs, True, True)
136+
vm.start()
137+
138+
check_pmem_exist(vm, 0, True, True, align(rootfs_size), "squashfs")
139+
140+
snapshot = vm.snapshot_full()
141+
restored_vm = microvm_factory.build_from_snapshot(snapshot)
142+
check_pmem_exist(restored_vm, 0, True, True, align(rootfs_size), "squashfs")

0 commit comments

Comments
 (0)