3
3
4
4
"""Tests for verifying the virtio-mem is working correctly"""
5
5
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"""
6
22
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
12
23
uvm .spawn ()
13
24
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 )
17
40
uvm .add_net_iface ()
18
- uvm .api .memory_hotplug .put (total_size_mib = 1024 )
19
41
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"
20
113
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 ()
21
120
_ , stdout , _ = uvm .ssh .check_output ("dmesg | grep 'virtio_mem'" )
22
121
for line in stdout .splitlines ():
23
122
_ , key , value = line .strip ().split (":" )
@@ -27,12 +126,162 @@ def test_virtio_mem_detected(uvm_plain_6_1):
27
126
case "start address" :
28
127
assert value == (512 << 30 ), "start address doesn't match"
29
128
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"
31
132
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"
33
136
case "plugged size" :
34
137
assert value == 0 , "plugged size doesn't match"
35
138
case "requested size" :
36
139
assert value == 0 , "requested size doesn't match"
37
140
case _:
38
141
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