Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/integration_tests/functional/test_balloon.py
1958 views
1
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Tests for guest-side operations on /balloon resources."""
4
5
import logging
6
import os
7
import platform
8
import subprocess
9
import time
10
11
from retry import retry
12
13
from conftest import _test_images_s3_bucket
14
from framework.artifacts import ArtifactCollection, ArtifactSet
15
from framework.builder import MicrovmBuilder, SnapshotBuilder, SnapshotType
16
from framework.matrix import TestMatrix, TestContext
17
from framework.utils import get_free_mem_ssh, run_cmd
18
19
import host_tools.network as net_tools # pylint: disable=import-error
20
21
22
MB_TO_PAGES = 256
23
24
25
@retry(delay=0.5, tries=10)
26
def get_stable_rss_mem_by_pid(pid, percentage_delta=0.5):
27
"""
28
Get the RSS memory that a guest uses, given the pid of the guest.
29
30
Wait till the fluctuations in RSS drop below percentage_delta. If timeout
31
is reached before the fluctuations drop, raise an exception.
32
"""
33
34
def get_rss_from_pmap():
35
_, output, _ = run_cmd("pmap -X {}".format(pid))
36
return int(output.split('\n')[-2].split()[1], 10)
37
38
first_rss = get_rss_from_pmap()
39
time.sleep(1)
40
second_rss = get_rss_from_pmap()
41
42
delta = (abs(first_rss - second_rss)/float(first_rss)) * 100
43
assert delta < percentage_delta
44
45
return second_rss
46
47
48
def make_guest_dirty_memory(ssh_connection, should_oom=False, amount=8192):
49
"""Tell the guest, over ssh, to dirty `amount` pages of memory."""
50
amount_in_mbytes = amount / MB_TO_PAGES
51
52
exit_code, _, _ = ssh_connection.execute_command(
53
"/sbin/fillmem {}".format(amount_in_mbytes)
54
)
55
56
cmd = "cat /tmp/fillmem_output.txt"
57
_, stdout, _ = ssh_connection.execute_command(cmd)
58
if should_oom:
59
assert exit_code == 0 and (
60
"OOM Killer stopped the program with "
61
"signal 9, exit code 0"
62
) in stdout.read()
63
else:
64
assert exit_code == 0 and (
65
"Memory filling was "
66
"successful"
67
) in stdout.read()
68
69
70
def build_test_matrix(network_config, bin_cloner_path, logger):
71
"""Build a test matrix using the kernel with the balloon driver."""
72
artifacts = ArtifactCollection(_test_images_s3_bucket())
73
# Testing matrix:
74
# - Guest kernel: Linux 4.14
75
# - Rootfs: Ubuntu 18.04
76
# - Microvm: 2vCPU with 256 MB RAM
77
# TODO: Multiple microvm sizes must be tested in the async pipeline.
78
microvm_artifacts = ArtifactSet(artifacts.microvms(keyword="2vcpu_256mb"))
79
kernel_artifacts = ArtifactSet(artifacts.kernels(
80
keyword="vmlinux-4.14"
81
))
82
disk_artifacts = ArtifactSet(artifacts.disks(keyword="ubuntu"))
83
84
# Create a test context and add builder, logger, network.
85
test_context = TestContext()
86
test_context.custom = {
87
'builder': MicrovmBuilder(bin_cloner_path),
88
'network_config': network_config,
89
'logger': logger,
90
'snapshot_type': SnapshotType.FULL,
91
'seq_len': 5
92
}
93
94
# Create the test matrix.
95
return TestMatrix(
96
context=test_context,
97
artifact_sets=[
98
microvm_artifacts,
99
kernel_artifacts,
100
disk_artifacts
101
]
102
)
103
104
105
def copy_util_to_rootfs(rootfs_path, util):
106
"""Build and copy the 'memfill' program to the rootfs."""
107
subprocess.check_call(
108
"gcc ./host_tools/{util}.c -o {util}".format(util=util),
109
shell=True
110
)
111
subprocess.check_call("mkdir tmpfs", shell=True)
112
subprocess.check_call("mount {} tmpfs".format(rootfs_path), shell=True)
113
subprocess.check_call(
114
"cp {util} tmpfs/sbin/{util}".format(util=util),
115
shell=True
116
)
117
subprocess.check_call("rm {}".format(util), shell=True)
118
subprocess.check_call("umount tmpfs", shell=True)
119
subprocess.check_call("rmdir tmpfs", shell=True)
120
121
122
def _test_rss_memory_lower(test_microvm):
123
"""Check inflating the balloon makes guest use less rss memory."""
124
# Get the firecracker pid, and open an ssh connection.
125
firecracker_pid = test_microvm.jailer_clone_pid
126
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
127
128
# Using deflate_on_oom, get the RSS as low as possible
129
response = test_microvm.balloon.patch(amount_mib=200)
130
assert test_microvm.api_session.is_status_no_content(
131
response.status_code
132
)
133
134
# Get initial rss consumption.
135
init_rss = get_stable_rss_mem_by_pid(firecracker_pid)
136
137
# Get the balloon back to 0.
138
response = test_microvm.balloon.patch(amount_mib=0)
139
assert test_microvm.api_session.is_status_no_content(
140
response.status_code
141
)
142
# This call will internally wait for rss to become stable.
143
_ = get_stable_rss_mem_by_pid(firecracker_pid)
144
145
# Dirty memory, then inflate balloon and get ballooned rss consumption.
146
make_guest_dirty_memory(ssh_connection)
147
148
response = test_microvm.balloon.patch(amount_mib=200)
149
assert test_microvm.api_session.is_status_no_content(
150
response.status_code
151
)
152
balloon_rss = get_stable_rss_mem_by_pid(firecracker_pid)
153
154
# Check that the ballooning reclaimed the memory.
155
assert balloon_rss - init_rss <= 15000
156
157
158
# pylint: disable=C0103
159
def test_rss_memory_lower(test_microvm_with_ssh_and_balloon, network_config):
160
"""Check inflating the balloon makes guest use less rss memory."""
161
test_microvm = test_microvm_with_ssh_and_balloon
162
test_microvm.spawn()
163
test_microvm.basic_config()
164
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
165
test_microvm.ssh_config['ssh_key_path'] = os.path.join(
166
test_microvm.fsfiles,
167
'debian.rootfs.id_rsa'
168
)
169
170
# Add a memory balloon.
171
response = test_microvm.balloon.put(
172
amount_mib=0,
173
deflate_on_oom=True,
174
stats_polling_interval_s=0
175
)
176
assert test_microvm.api_session.is_status_no_content(response.status_code)
177
178
# Start the microvm.
179
test_microvm.start()
180
181
_test_rss_memory_lower(test_microvm)
182
183
184
# pylint: disable=C0103
185
def test_inflate_reduces_free(test_microvm_with_ssh_and_balloon,
186
network_config):
187
"""Check that the output of free in guest changes with inflate."""
188
test_microvm = test_microvm_with_ssh_and_balloon
189
test_microvm.spawn()
190
test_microvm.basic_config()
191
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
192
test_microvm.ssh_config['ssh_key_path'] = os.path.join(
193
test_microvm.fsfiles,
194
'debian.rootfs.id_rsa'
195
)
196
197
# Install deflated balloon.
198
response = test_microvm.balloon.put(
199
amount_mib=0,
200
deflate_on_oom=False,
201
stats_polling_interval_s=1
202
)
203
assert test_microvm.api_session.is_status_no_content(response.status_code)
204
205
# Start the microvm
206
test_microvm.start()
207
208
# Get and open an ssh connection.
209
firecracker_pid = test_microvm.jailer_clone_pid
210
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
211
212
# Get the free memory before ballooning.
213
available_mem_deflated = get_free_mem_ssh(ssh_connection)
214
215
# Inflate 64 MB == 16384 page balloon.
216
response = test_microvm.balloon.patch(amount_mib=64)
217
assert test_microvm.api_session.is_status_no_content(response.status_code)
218
# This call will internally wait for rss to become stable.
219
_ = get_stable_rss_mem_by_pid(firecracker_pid)
220
221
# Get the free memory after ballooning.
222
available_mem_inflated = get_free_mem_ssh(ssh_connection)
223
224
# Assert that ballooning reclaimed about 64 MB of memory.
225
assert available_mem_inflated <= available_mem_deflated - 85 * 64000 / 100
226
227
228
# pylint: disable=C0103
229
def test_deflate_on_oom_true(test_microvm_with_ssh_and_balloon,
230
network_config):
231
"""Verify that setting the `deflate_on_oom` to True works correctly."""
232
test_microvm = test_microvm_with_ssh_and_balloon
233
test_microvm.spawn()
234
test_microvm.basic_config()
235
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
236
test_microvm.ssh_config['ssh_key_path'] = os.path.join(
237
test_microvm.fsfiles,
238
'debian.rootfs.id_rsa'
239
)
240
241
# Add a deflated memory balloon.
242
response = test_microvm.balloon.put(
243
amount_mib=0,
244
deflate_on_oom=True,
245
stats_polling_interval_s=0
246
)
247
assert test_microvm.api_session.is_status_no_content(response.status_code)
248
249
# Start the microvm.
250
test_microvm.start()
251
252
# Get an ssh connection to the microvm.
253
firecracker_pid = test_microvm.jailer_clone_pid
254
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
255
256
# Inflate the balloon
257
response = test_microvm.balloon.patch(amount_mib=180)
258
assert test_microvm.api_session.is_status_no_content(response.status_code)
259
# This call will internally wait for rss to become stable.
260
_ = get_stable_rss_mem_by_pid(firecracker_pid)
261
262
# Check that using memory doesn't lead to an out of memory error.
263
# Note that due to `test_deflate_on_oom_false`, we know that
264
# if `deflate_on_oom` were set to False, then such an error
265
# would have happened.
266
make_guest_dirty_memory(ssh_connection)
267
268
269
# pylint: disable=C0103
270
def test_deflate_on_oom_false(test_microvm_with_ssh_and_balloon,
271
network_config):
272
"""Verify that setting the `deflate_on_oom` to False works correctly."""
273
test_microvm = test_microvm_with_ssh_and_balloon
274
test_microvm.spawn()
275
test_microvm.basic_config()
276
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
277
test_microvm.ssh_config['ssh_key_path'] = os.path.join(
278
test_microvm.fsfiles,
279
'debian.rootfs.id_rsa'
280
)
281
282
# Add a memory balloon.
283
response = test_microvm.balloon.put(
284
amount_mib=0,
285
deflate_on_oom=False,
286
stats_polling_interval_s=0
287
)
288
assert test_microvm.api_session.is_status_no_content(response.status_code)
289
290
# Start the microvm.
291
test_microvm.start()
292
293
# Get an ssh connection to the microvm.
294
firecracker_pid = test_microvm.jailer_clone_pid
295
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
296
297
# Inflate the balloon.
298
response = test_microvm.balloon.patch(amount_mib=180)
299
assert test_microvm.api_session.is_status_no_content(response.status_code)
300
# This call will internally wait for rss to become stable.
301
_ = get_stable_rss_mem_by_pid(firecracker_pid)
302
303
# Check that using memory does lead to an out of memory error.
304
make_guest_dirty_memory(ssh_connection, should_oom=True)
305
306
307
# pylint: disable=C0103
308
def test_reinflate_balloon(test_microvm_with_ssh_and_balloon, network_config):
309
"""Verify that repeatedly inflating and deflating the balloon works."""
310
test_microvm = test_microvm_with_ssh_and_balloon
311
test_microvm.spawn()
312
test_microvm.basic_config()
313
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
314
test_microvm.ssh_config['ssh_key_path'] = os.path.join(
315
test_microvm.fsfiles,
316
'debian.rootfs.id_rsa'
317
)
318
319
# Add a deflated memory balloon.
320
response = test_microvm.balloon.put(
321
amount_mib=0,
322
deflate_on_oom=True,
323
stats_polling_interval_s=0
324
)
325
assert test_microvm.api_session.is_status_no_content(response.status_code)
326
327
# Start the microvm.
328
test_microvm.start()
329
330
# Get the firecracker pid, and open an ssh connection, get the RSS.
331
firecracker_pid = test_microvm.jailer_clone_pid
332
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
333
334
# First inflate the balloon to free up the uncertain amount of memory
335
# used by the kernel at boot and establish a baseline, then give back
336
# the memory.
337
response = test_microvm.balloon.patch(amount_mib=200)
338
assert test_microvm.api_session.is_status_no_content(response.status_code)
339
# This call will internally wait for rss to become stable.
340
_ = get_stable_rss_mem_by_pid(firecracker_pid)
341
342
response = test_microvm.balloon.patch(amount_mib=0)
343
assert test_microvm.api_session.is_status_no_content(response.status_code)
344
# This call will internally wait for rss to become stable.
345
_ = get_stable_rss_mem_by_pid(firecracker_pid)
346
347
# Get the guest to dirty memory.
348
make_guest_dirty_memory(ssh_connection)
349
first_reading = get_stable_rss_mem_by_pid(firecracker_pid)
350
351
# Now inflate the balloon.
352
response = test_microvm.balloon.patch(amount_mib=200)
353
assert test_microvm.api_session.is_status_no_content(response.status_code)
354
second_reading = get_stable_rss_mem_by_pid(firecracker_pid)
355
356
# Now deflate the balloon.
357
response = test_microvm.balloon.patch(amount_mib=0)
358
assert test_microvm.api_session.is_status_no_content(response.status_code)
359
# This call will internally wait for rss to become stable.
360
_ = get_stable_rss_mem_by_pid(firecracker_pid)
361
362
# Now have the guest dirty memory again.
363
make_guest_dirty_memory(ssh_connection)
364
third_reading = get_stable_rss_mem_by_pid(firecracker_pid)
365
366
# Now inflate the balloon again.
367
response = test_microvm.balloon.patch(amount_mib=200)
368
assert test_microvm.api_session.is_status_no_content(response.status_code)
369
fourth_reading = get_stable_rss_mem_by_pid(firecracker_pid)
370
371
# Check that the memory used is the same after regardless of the previous
372
# inflate history of the balloon (with the third reading being allowed
373
# to be smaller than the first, since memory allocated at booting up
374
# is probably freed after the first inflation.
375
assert (third_reading - first_reading) <= 20000
376
assert abs(second_reading - fourth_reading) <= 20000
377
378
379
# pylint: disable=C0103
380
def test_size_reduction(test_microvm_with_ssh_and_balloon, network_config):
381
"""Verify that ballooning reduces RSS usage on a newly booted guest."""
382
test_microvm = test_microvm_with_ssh_and_balloon
383
test_microvm.spawn()
384
test_microvm.basic_config()
385
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
386
test_microvm.ssh_config['ssh_key_path'] = os.path.join(
387
test_microvm.fsfiles,
388
'debian.rootfs.id_rsa'
389
)
390
391
# Add a memory balloon.
392
response = test_microvm.balloon.put(
393
amount_mib=0,
394
deflate_on_oom=True,
395
stats_polling_interval_s=0
396
)
397
assert test_microvm.api_session.is_status_no_content(response.status_code)
398
399
# Start the microvm.
400
test_microvm.start()
401
402
# Get the firecracker pid, and open an ssh connection.
403
firecracker_pid = test_microvm.jailer_clone_pid
404
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
405
406
# Check memory usage.
407
first_reading = get_stable_rss_mem_by_pid(firecracker_pid)
408
409
# Have the guest drop its caches.
410
ssh_connection.execute_command('sync; echo 3 > /proc/sys/vm/drop_caches')
411
time.sleep(5)
412
413
# Now inflate the balloon.
414
response = test_microvm.balloon.patch(amount_mib=40)
415
assert test_microvm.api_session.is_status_no_content(response.status_code)
416
417
# Check memory usage again.
418
second_reading = get_stable_rss_mem_by_pid(firecracker_pid)
419
420
# There should be a reduction of at least 10MB.
421
assert first_reading - second_reading >= 10000
422
423
424
# pylint: disable=C0103
425
def test_stats(test_microvm_with_ssh_and_balloon, network_config):
426
"""Verify that balloon stats work as expected."""
427
test_microvm = test_microvm_with_ssh_and_balloon
428
test_microvm.spawn()
429
test_microvm.basic_config()
430
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
431
test_microvm.ssh_config['ssh_key_path'] = os.path.join(
432
test_microvm.fsfiles,
433
'debian.rootfs.id_rsa'
434
)
435
436
# Add a memory balloon with stats enabled.
437
response = test_microvm.balloon.put(
438
amount_mib=0,
439
deflate_on_oom=True,
440
stats_polling_interval_s=1
441
)
442
assert test_microvm.api_session.is_status_no_content(response.status_code)
443
444
# Start the microvm.
445
test_microvm.start()
446
447
# Open an ssh connection to the microvm.
448
firecracker_pid = test_microvm.jailer_clone_pid
449
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
450
451
# Get an initial reading of the stats.
452
initial_stats = test_microvm.balloon.get_stats().json()
453
454
# Dirty 10MB of pages.
455
make_guest_dirty_memory(ssh_connection, amount=(10 * MB_TO_PAGES))
456
time.sleep(1)
457
# This call will internally wait for rss to become stable.
458
_ = get_stable_rss_mem_by_pid(firecracker_pid)
459
460
# Make sure that the stats catch the page faults.
461
after_workload_stats = test_microvm.balloon.get_stats().json()
462
assert initial_stats['minor_faults'] < after_workload_stats['minor_faults']
463
assert initial_stats['major_faults'] < after_workload_stats['major_faults']
464
465
# Now inflate the balloon with 10MB of pages.
466
response = test_microvm.balloon.patch(amount_mib=10)
467
assert test_microvm.api_session.is_status_no_content(response.status_code)
468
# This call will internally wait for rss to become stable.
469
_ = get_stable_rss_mem_by_pid(firecracker_pid)
470
471
# Get another reading of the stats after the polling interval has passed.
472
inflated_stats = test_microvm.balloon.get_stats().json()
473
474
# Ensure the stats reflect inflating the balloon.
475
assert (
476
after_workload_stats['free_memory'] >
477
inflated_stats['free_memory']
478
)
479
assert (
480
after_workload_stats['available_memory'] >
481
inflated_stats['available_memory']
482
)
483
484
# Deflate the balloon.check that the stats show the increase in
485
# available memory.
486
response = test_microvm.balloon.patch(amount_mib=0)
487
assert test_microvm.api_session.is_status_no_content(response.status_code)
488
# This call will internally wait for rss to become stable.
489
_ = get_stable_rss_mem_by_pid(firecracker_pid)
490
491
# Get another reading of the stats after the polling interval has passed.
492
deflated_stats = test_microvm.balloon.get_stats().json()
493
494
# Ensure the stats reflect deflating the balloon.
495
assert (
496
inflated_stats['free_memory'] <
497
deflated_stats['free_memory']
498
)
499
assert (
500
inflated_stats['available_memory'] <
501
deflated_stats['available_memory']
502
)
503
504
505
def test_stats_update(test_microvm_with_ssh_and_balloon, network_config):
506
"""Verify that balloon stats update correctly."""
507
test_microvm = test_microvm_with_ssh_and_balloon
508
test_microvm.spawn()
509
test_microvm.basic_config()
510
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
511
test_microvm.ssh_config['ssh_key_path'] = os.path.join(
512
test_microvm.fsfiles,
513
'debian.rootfs.id_rsa'
514
)
515
516
# Add a memory balloon with stats enabled.
517
response = test_microvm.balloon.put(
518
amount_mib=0,
519
deflate_on_oom=True,
520
stats_polling_interval_s=1
521
)
522
assert test_microvm.api_session.is_status_no_content(response.status_code)
523
524
# Start the microvm.
525
test_microvm.start()
526
527
# Open an ssh connection to the microvm.
528
firecracker_pid = test_microvm.jailer_clone_pid
529
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
530
531
# Dirty 30MB of pages.
532
make_guest_dirty_memory(ssh_connection, amount=(30 * MB_TO_PAGES))
533
534
# This call will internally wait for rss to become stable.
535
_ = get_stable_rss_mem_by_pid(firecracker_pid)
536
537
# Get an initial reading of the stats.
538
initial_stats = test_microvm.balloon.get_stats().json()
539
540
# Inflate the balloon to trigger a change in the stats.
541
response = test_microvm.balloon.patch(amount_mib=10)
542
assert test_microvm.api_session.is_status_no_content(response.status_code)
543
544
# Wait out the polling interval, then get the updated stats.
545
time.sleep(1)
546
next_stats = test_microvm.balloon.get_stats().json()
547
assert initial_stats['available_memory'] != next_stats['available_memory']
548
549
# Inflate the balloon more to trigger a change in the stats.
550
response = test_microvm.balloon.patch(amount_mib=30)
551
assert test_microvm.api_session.is_status_no_content(response.status_code)
552
553
# Change the polling interval.
554
response = test_microvm.balloon.patch_stats(stats_polling_interval_s=60)
555
assert test_microvm.api_session.is_status_no_content(response.status_code)
556
557
# The polling interval change should update the stats.
558
final_stats = test_microvm.balloon.get_stats().json()
559
assert next_stats['available_memory'] != final_stats['available_memory']
560
561
562
def test_balloon_snapshot(
563
network_config,
564
bin_cloner_path
565
):
566
"""Test that the balloon works after pause/resume."""
567
logger = logging.getLogger("snapshot_sequence")
568
569
# Create the test matrix.
570
test_matrix = build_test_matrix(network_config, bin_cloner_path, logger)
571
572
test_matrix.run_test(_test_balloon_snapshot)
573
574
575
def _test_balloon_snapshot(context):
576
logger = context.custom['logger']
577
vm_builder = context.custom['builder']
578
snapshot_type = context.custom['snapshot_type']
579
diff_snapshots = snapshot_type == SnapshotType.DIFF
580
581
logger.info("Testing {} with microvm: \"{}\", kernel {}, disk {} "
582
.format(snapshot_type,
583
context.microvm.name(),
584
context.kernel.name(),
585
context.disk.name()))
586
587
# Create a rw copy artifact.
588
root_disk = context.disk.copy()
589
590
# Get ssh key from read-only artifact.
591
ssh_key = context.disk.ssh_key()
592
# Create a fresh microvm from aftifacts.
593
vm_instance = vm_builder.build(kernel=context.kernel,
594
disks=[root_disk],
595
ssh_key=ssh_key,
596
config=context.microvm,
597
diff_snapshots=diff_snapshots)
598
basevm = vm_instance.vm
599
copy_util_to_rootfs(root_disk.local_path(), 'fillmem')
600
601
# Add a memory balloon with stats enabled.
602
response = basevm.balloon.put(
603
amount_mib=0,
604
deflate_on_oom=True,
605
stats_polling_interval_s=1
606
)
607
assert basevm.api_session.is_status_no_content(response.status_code)
608
609
basevm.start()
610
ssh_connection = net_tools.SSHConnection(basevm.ssh_config)
611
612
# Dirty 60MB of pages.
613
make_guest_dirty_memory(ssh_connection, amount=(60 * MB_TO_PAGES))
614
time.sleep(1)
615
616
# Get the firecracker pid, and open an ssh connection.
617
firecracker_pid = basevm.jailer_clone_pid
618
619
# Check memory usage.
620
first_reading = get_stable_rss_mem_by_pid(firecracker_pid)
621
622
# Now inflate the balloon with 20MB of pages.
623
response = basevm.balloon.patch(amount_mib=20)
624
assert basevm.api_session.is_status_no_content(response.status_code)
625
626
# Check memory usage again.
627
second_reading = get_stable_rss_mem_by_pid(firecracker_pid)
628
629
# There should be a reduction in RSS, but it's inconsistent.
630
# We only test that the reduction happens.
631
assert first_reading > second_reading
632
633
logger.info("Create {} #0.".format(snapshot_type))
634
# Create a snapshot builder from a microvm.
635
snapshot_builder = SnapshotBuilder(basevm)
636
637
# Create base snapshot.
638
snapshot = snapshot_builder.create([root_disk.local_path()],
639
ssh_key,
640
snapshot_type)
641
642
basevm.kill()
643
644
logger.info("Load snapshot #{}, mem {}".format(1, snapshot.mem))
645
microvm, _ = vm_builder.build_from_snapshot(snapshot,
646
True,
647
diff_snapshots)
648
# Attempt to connect to resumed microvm.
649
ssh_connection = net_tools.SSHConnection(microvm.ssh_config)
650
651
# Get the firecracker from snapshot pid, and open an ssh connection.
652
firecracker_pid = microvm.jailer_clone_pid
653
654
# Get the stats right after we take a snapshot.
655
stats_after_snap = microvm.balloon.get_stats().json()
656
657
# Check memory usage.
658
third_reading = get_stable_rss_mem_by_pid(firecracker_pid)
659
660
# Dirty 60MB of pages.
661
make_guest_dirty_memory(ssh_connection, amount=(60 * MB_TO_PAGES))
662
663
# Check memory usage.
664
fourth_reading = get_stable_rss_mem_by_pid(firecracker_pid)
665
666
assert fourth_reading > third_reading
667
668
# Inflate the balloon with another 20MB of pages.
669
response = microvm.balloon.patch(amount_mib=40)
670
assert microvm.api_session.is_status_no_content(response.status_code)
671
672
fifth_reading = get_stable_rss_mem_by_pid(firecracker_pid)
673
674
# There should be a reduction in RSS, but it's inconsistent.
675
# We only test that the reduction happens.
676
assert fourth_reading > fifth_reading
677
678
# Get the stats after we take a snapshot and dirty some memory,
679
# then reclaim it.
680
latest_stats = microvm.balloon.get_stats().json()
681
682
# Ensure the stats are still working after restore and show
683
# that the balloon inflated.
684
assert (
685
stats_after_snap['available_memory'] >
686
latest_stats['available_memory']
687
)
688
689
microvm.kill()
690
691
692
def test_snapshot_compatibility(
693
network_config,
694
bin_cloner_path
695
):
696
"""Test that the balloon serializes correctly."""
697
logger = logging.getLogger("snapshot_sequence")
698
699
# Create the test matrix.
700
test_matrix = build_test_matrix(network_config, bin_cloner_path, logger)
701
702
test_matrix.run_test(_test_snapshot_compatibility)
703
704
705
def _test_snapshot_compatibility(context):
706
logger = context.custom['logger']
707
vm_builder = context.custom['builder']
708
snapshot_type = context.custom['snapshot_type']
709
diff_snapshots = snapshot_type == SnapshotType.DIFF
710
711
logger.info("Testing {} with microvm: \"{}\", kernel {}, disk {} "
712
.format(snapshot_type,
713
context.microvm.name(),
714
context.kernel.name(),
715
context.disk.name()))
716
717
# Create a rw copy artifact.
718
root_disk = context.disk.copy()
719
# Get ssh key from read-only artifact.
720
ssh_key = context.disk.ssh_key()
721
# Create a fresh microvm from aftifacts.
722
vm_instance = vm_builder.build(
723
kernel=context.kernel,
724
disks=[root_disk],
725
ssh_key=ssh_key,
726
config=context.microvm,
727
diff_snapshots=diff_snapshots
728
)
729
microvm = vm_instance.vm
730
# Add a memory balloon with stats enabled.
731
response = microvm.balloon.put(
732
amount_mib=0,
733
deflate_on_oom=True,
734
stats_polling_interval_s=1
735
)
736
assert microvm.api_session.is_status_no_content(response.status_code)
737
738
microvm.start()
739
740
logger.info("Create {} #0.".format(snapshot_type))
741
742
# Pause the microVM in order to allow snapshots
743
response = microvm.vm.patch(state='Paused')
744
assert microvm.api_session.is_status_no_content(response.status_code)
745
746
# Try to create a snapshot with a balloon on version 0.23.0.
747
# This is skipped for aarch64, since the snapshotting feature
748
# was introduced in v0.24.0.
749
if platform.machine() == "x86_64":
750
response = microvm.snapshot.create(
751
mem_file_path='memfile',
752
snapshot_path='dummy',
753
diff=False,
754
version='0.23.0'
755
)
756
757
# This should fail as the balloon was introduced in 0.24.0.
758
assert microvm.api_session.is_status_bad_request(response.status_code)
759
assert (
760
'Target version does not implement the '
761
'virtio-balloon device'
762
) in response.json()['fault_message']
763
764
# Create a snapshot builder from a microvm.
765
snapshot_builder = SnapshotBuilder(microvm)
766
767
# Check we can create a snapshot with a balloon on current version.
768
snapshot_builder.create(
769
[root_disk.local_path()],
770
ssh_key,
771
snapshot_type
772
)
773
774
microvm.kill()
775
776
777
def test_memory_scrub(
778
network_config,
779
bin_cloner_path
780
):
781
"""Test that the memory is zeroed after deflate."""
782
logger = logging.getLogger()
783
784
# Create the test matrix.
785
test_matrix = build_test_matrix(network_config, bin_cloner_path, logger)
786
787
test_matrix.run_test(_test_memory_scrub)
788
789
790
def _test_memory_scrub(context):
791
vm_builder = context.custom['builder']
792
793
# Create a rw copy artifact.
794
root_disk = context.disk.copy()
795
# Get ssh key from read-only artifact.
796
ssh_key = context.disk.ssh_key()
797
# Create a fresh microvm from aftifacts.
798
vm_instance = vm_builder.build(
799
kernel=context.kernel,
800
disks=[root_disk],
801
ssh_key=ssh_key,
802
config=context.microvm
803
)
804
microvm = vm_instance.vm
805
806
copy_util_to_rootfs(root_disk.local_path(), 'fillmem')
807
copy_util_to_rootfs(root_disk.local_path(), 'readmem')
808
809
# Add a memory balloon with stats enabled.
810
response = microvm.balloon.put(
811
amount_mib=0,
812
deflate_on_oom=True,
813
stats_polling_interval_s=1
814
)
815
assert microvm.api_session.is_status_no_content(response.status_code)
816
817
microvm.start()
818
819
ssh_connection = net_tools.SSHConnection(microvm.ssh_config)
820
821
# Dirty 60MB of pages.
822
make_guest_dirty_memory(ssh_connection, amount=(60 * MB_TO_PAGES))
823
824
# Now inflate the balloon with 60MB of pages.
825
response = microvm.balloon.patch(amount_mib=60)
826
assert microvm.api_session.is_status_no_content(response.status_code)
827
828
# Get the firecracker pid, and open an ssh connection.
829
firecracker_pid = microvm.jailer_clone_pid
830
831
# Wait for the inflate to complete.
832
_ = get_stable_rss_mem_by_pid(firecracker_pid)
833
834
# Deflate the balloon completely.
835
response = microvm.balloon.patch(amount_mib=0)
836
assert microvm.api_session.is_status_no_content(response.status_code)
837
838
# Wait for the deflate to complete.
839
_ = get_stable_rss_mem_by_pid(firecracker_pid)
840
841
exit_code, _, _ = ssh_connection.execute_command(
842
"/sbin/readmem {} {}".format(60, 1)
843
)
844
assert exit_code == 0
845
846
microvm.kill()
847
848