Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/integration_tests/functional/test_drives.py
1958 views
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Tests for guest-side operations on /drives resources."""
4
5
import os
6
import platform
7
import pytest
8
9
import framework.utils as utils
10
11
import host_tools.drive as drive_tools
12
import host_tools.network as net_tools # pylint: disable=import-error
13
import host_tools.logging as log_tools
14
15
PARTUUID = {"x86_64": "0eaa91a0-01", "aarch64": "7bf14469-01"}
16
MB = 1024 * 1024
17
18
19
def test_rescan_file(test_microvm_with_ssh, network_config):
20
"""Verify that rescan works with a file-backed virtio device."""
21
test_microvm = test_microvm_with_ssh
22
test_microvm.spawn()
23
24
# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 0 network ifaces and
25
# a root file system with the rw permission. The network interface is
26
# added after we get a unique MAC and IP.
27
test_microvm.basic_config()
28
29
_tap, _, _ = test_microvm_with_ssh.ssh_network_config(network_config, '1')
30
31
block_size = 2
32
# Add a scratch block device.
33
fs = drive_tools.FilesystemFile(
34
os.path.join(test_microvm.fsfiles, 'scratch'),
35
size=block_size
36
)
37
test_microvm.add_drive(
38
'scratch',
39
fs.path,
40
)
41
42
test_microvm.start()
43
44
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
45
_check_block_size(ssh_connection, '/dev/vdb', fs.size())
46
47
# Check if reading from the entire disk results in a file of the same size
48
# or errors out, after a truncate on the host.
49
truncated_size = block_size//2
50
utils.run_cmd(f"truncate --size {truncated_size}M {fs.path}")
51
block_copy_name = "dev_vdb_copy"
52
_, _, stderr = ssh_connection.execute_command(
53
f"dd if=/dev/vdb of={block_copy_name} bs=1M count={block_size}")
54
assert "dd: error reading '/dev/vdb': Input/output error" in stderr.read()
55
_check_file_size(ssh_connection, f'{block_copy_name}',
56
truncated_size * MB)
57
58
response = test_microvm.drive.patch(
59
drive_id='scratch',
60
path_on_host=test_microvm.create_jailed_resource(fs.path),
61
)
62
assert test_microvm.api_session.is_status_no_content(response.status_code)
63
64
_check_block_size(
65
ssh_connection,
66
'/dev/vdb',
67
fs.size()
68
)
69
70
71
def test_device_ordering(test_microvm_with_ssh, network_config):
72
"""Verify device ordering.
73
74
The root device should correspond to /dev/vda in the guest and
75
the order of the other devices should match their configuration order.
76
"""
77
test_microvm = test_microvm_with_ssh
78
test_microvm.spawn()
79
80
# Add first scratch block device.
81
fs1 = drive_tools.FilesystemFile(
82
os.path.join(test_microvm.fsfiles, 'scratch1'),
83
size=128
84
)
85
test_microvm.add_drive(
86
'scratch1',
87
fs1.path
88
)
89
90
# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 0 network ifaces,
91
# a read-write root file system (this is the second block device added).
92
# The network interface is added after we get a unique MAC and IP.
93
test_microvm.basic_config()
94
95
# Add the third block device.
96
fs2 = drive_tools.FilesystemFile(
97
os.path.join(test_microvm.fsfiles, 'scratch2'),
98
size=512
99
)
100
test_microvm.add_drive(
101
'scratch2',
102
fs2.path
103
)
104
105
_tap, _, _ = test_microvm_with_ssh.ssh_network_config(network_config, '1')
106
107
test_microvm.start()
108
109
# Determine the size of the microVM rootfs in bytes.
110
try:
111
result = utils.run_cmd(
112
'du --apparent-size --block-size=1 {}'
113
.format(test_microvm.rootfs_file),
114
)
115
except ChildProcessError:
116
pytest.skip('Failed to get microVM rootfs size: {}'
117
.format(result.stderr))
118
119
assert len(result.stdout.split()) == 2
120
rootfs_size = result.stdout.split('\t')[0]
121
122
# The devices were added in this order: fs1, rootfs, fs2.
123
# However, the rootfs is the root device and goes first,
124
# so we expect to see this order: rootfs, fs1, fs2.
125
# The devices are identified by their size.
126
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
127
_check_block_size(ssh_connection, '/dev/vda', rootfs_size)
128
_check_block_size(ssh_connection, '/dev/vdb', fs1.size())
129
_check_block_size(ssh_connection, '/dev/vdc', fs2.size())
130
131
132
def test_rescan_dev(test_microvm_with_ssh, network_config):
133
"""Verify that rescan works with a device-backed virtio device."""
134
test_microvm = test_microvm_with_ssh
135
test_microvm.spawn()
136
session = test_microvm.api_session
137
138
# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 0 network ifaces and
139
# a root file system with the rw permission. The network interface is
140
# added after we get a unique MAC and IP.
141
test_microvm.basic_config()
142
143
_tap, _, _ = test_microvm_with_ssh.ssh_network_config(network_config, '1')
144
145
# Add a scratch block device.
146
fs1 = drive_tools.FilesystemFile(os.path.join(test_microvm.fsfiles, 'fs1'))
147
test_microvm.add_drive(
148
'scratch',
149
fs1.path
150
)
151
152
test_microvm.start()
153
154
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
155
156
_check_block_size(ssh_connection, '/dev/vdb', fs1.size())
157
158
fs2 = drive_tools.FilesystemFile(
159
os.path.join(test_microvm.fsfiles, 'fs2'),
160
size=512
161
)
162
163
losetup = ['losetup', '--find', '--show', fs2.path]
164
loopback_device = None
165
result = None
166
try:
167
result = utils.run_cmd(losetup)
168
loopback_device = result.stdout.rstrip()
169
except ChildProcessError:
170
pytest.skip('failed to create a lookback device: ' +
171
f'stdout={result.stdout}, stderr={result.stderr}')
172
173
try:
174
response = test_microvm.drive.patch(
175
drive_id='scratch',
176
path_on_host=test_microvm.create_jailed_resource(loopback_device),
177
)
178
assert session.is_status_no_content(response.status_code)
179
180
_check_block_size(ssh_connection, '/dev/vdb', fs2.size())
181
finally:
182
if loopback_device:
183
utils.run_cmd(['losetup', '--detach', loopback_device])
184
185
186
def test_non_partuuid_boot(test_microvm_with_ssh, network_config):
187
"""Test the output reported by blockdev when booting from /dev/vda."""
188
test_microvm = test_microvm_with_ssh
189
test_microvm.spawn()
190
191
# Sets up the microVM with 1 vCPUs, 256 MiB of RAM, no network ifaces and
192
# a root file system with the rw permission. The network interfaces is
193
# added after we get a unique MAC and IP.
194
test_microvm.basic_config(vcpu_count=1)
195
196
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
197
198
# Add another read-only block device.
199
fs = drive_tools.FilesystemFile(
200
os.path.join(test_microvm.fsfiles, 'readonly')
201
)
202
test_microvm.add_drive(
203
'scratch',
204
fs.path,
205
is_read_only=True
206
)
207
208
test_microvm.start()
209
210
# Prepare the input for doing the assertion
211
assert_dict = {}
212
# Keep an array of strings specifying the location where some string
213
# from the output is located.
214
# 1-0 means line 1, column 0.
215
keys_array = ['1-0', '1-8', '2-0']
216
# Keep a dictionary where the keys are the location and the values
217
# represent the input to assert against.
218
assert_dict[keys_array[0]] = 'rw'
219
assert_dict[keys_array[1]] = '/dev/vda'
220
assert_dict[keys_array[2]] = 'ro'
221
_check_drives(test_microvm, assert_dict, keys_array)
222
223
224
def test_partuuid_boot(test_microvm_with_partuuid, network_config):
225
"""Test the output reported by blockdev when booting with PARTUUID."""
226
test_microvm = test_microvm_with_partuuid
227
test_microvm.spawn()
228
229
# Set up the microVM with 1 vCPUs, 256 MiB of RAM, no network ifaces and
230
# a root file system with the rw permission. The network interfaces is
231
# added after we get a unique MAC and IP.
232
test_microvm.basic_config(
233
vcpu_count=1,
234
add_root_device=False
235
)
236
237
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
238
239
# Add the root block device specified through PARTUUID.
240
test_microvm.add_drive(
241
'rootfs',
242
test_microvm.rootfs_file,
243
root_device=True,
244
partuuid=PARTUUID[platform.machine()]
245
)
246
247
test_microvm.start()
248
249
assert_dict = {}
250
keys_array = ['1-0', '1-8', '2-0', '2-7']
251
assert_dict[keys_array[0]] = "rw"
252
assert_dict[keys_array[1]] = '/dev/vda'
253
assert_dict[keys_array[2]] = 'rw'
254
assert_dict[keys_array[3]] = '/dev/vda1'
255
_check_drives(test_microvm, assert_dict, keys_array)
256
257
258
def test_partuuid_update(test_microvm_with_ssh, network_config):
259
"""Test successful switching from PARTUUID boot to /dev/vda boot."""
260
test_microvm = test_microvm_with_ssh
261
test_microvm.spawn()
262
263
# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 0 network ifaces and
264
# a root file system with the rw permission. The network interfaces is
265
# added after we get a unique MAC and IP.
266
test_microvm.basic_config(
267
vcpu_count=1,
268
add_root_device=False
269
)
270
271
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
272
273
# Add the root block device specified through PARTUUID.
274
test_microvm.add_drive(
275
'rootfs',
276
test_microvm.rootfs_file,
277
root_device=True,
278
partuuid='0eaa91a0-01'
279
)
280
281
# Update the root block device to boot from /dev/vda.
282
test_microvm.add_drive(
283
'rootfs',
284
test_microvm.rootfs_file,
285
root_device=True,
286
)
287
288
test_microvm.start()
289
290
# Assert that the final booting method is from /dev/vda.
291
assert_dict = {}
292
keys_array = ['1-0', '1-8']
293
assert_dict[keys_array[0]] = 'rw'
294
assert_dict[keys_array[1]] = '/dev/vda'
295
_check_drives(test_microvm, assert_dict, keys_array)
296
297
298
def test_patch_drive(test_microvm_with_ssh, network_config):
299
"""Test replacing the backing filesystem after guest boot works."""
300
test_microvm = test_microvm_with_ssh
301
test_microvm.spawn()
302
303
# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 1 network iface, a root
304
# file system with the rw permission, and a scratch drive.
305
test_microvm.basic_config()
306
307
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
308
309
fs1 = drive_tools.FilesystemFile(
310
os.path.join(test_microvm.fsfiles, 'scratch')
311
)
312
test_microvm.add_drive(
313
'scratch',
314
fs1.path
315
)
316
317
test_microvm.start()
318
319
# Updates to `path_on_host` with a valid path are allowed.
320
fs2 = drive_tools.FilesystemFile(
321
os.path.join(test_microvm.fsfiles, 'otherscratch'), size=512
322
)
323
response = test_microvm.drive.patch(
324
drive_id='scratch',
325
path_on_host=test_microvm.create_jailed_resource(fs2.path)
326
)
327
assert test_microvm.api_session.is_status_no_content(response.status_code)
328
329
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
330
331
# The `lsblk` command should output 2 lines to STDOUT: "SIZE" and the size
332
# of the device, in bytes.
333
blksize_cmd = "lsblk -b /dev/vdb --output SIZE"
334
size_bytes_str = "536870912" # = 512 MiB
335
_, stdout, stderr = ssh_connection.execute_command(blksize_cmd)
336
assert stderr.read() == ''
337
stdout.readline() # skip "SIZE"
338
assert stdout.readline().strip() == size_bytes_str
339
340
341
def test_no_flush(test_microvm_with_ssh, network_config):
342
"""Verify default block ignores flush."""
343
test_microvm = test_microvm_with_ssh
344
test_microvm.spawn()
345
346
test_microvm.basic_config(
347
vcpu_count=1,
348
add_root_device=False
349
)
350
351
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
352
353
# Add the block device
354
test_microvm.add_drive(
355
'rootfs',
356
test_microvm.rootfs_file,
357
root_device=True,
358
)
359
360
# Configure the metrics.
361
metrics_fifo_path = os.path.join(test_microvm.path, 'metrics_fifo')
362
metrics_fifo = log_tools.Fifo(metrics_fifo_path)
363
response = test_microvm.metrics.put(
364
metrics_path=test_microvm.create_jailed_resource(metrics_fifo.path)
365
)
366
assert test_microvm.api_session.is_status_no_content(response.status_code)
367
368
test_microvm.start()
369
370
# Verify all flush commands were ignored during boot.
371
fc_metrics = test_microvm.flush_metrics(metrics_fifo)
372
assert fc_metrics['block']['flush_count'] == 0
373
374
# Have the guest drop the caches to generate flush requests.
375
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
376
cmd = "sync; echo 1 > /proc/sys/vm/drop_caches"
377
_, _, stderr = ssh_connection.execute_command(cmd)
378
assert stderr.read() == ''
379
380
# Verify all flush commands were ignored even after
381
# dropping the caches.
382
fc_metrics = test_microvm.flush_metrics(metrics_fifo)
383
assert fc_metrics['block']['flush_count'] == 0
384
385
386
def test_flush(test_microvm_with_ssh, network_config):
387
"""Verify block with flush actually flushes."""
388
test_microvm = test_microvm_with_ssh
389
test_microvm.spawn()
390
391
test_microvm.basic_config(
392
vcpu_count=1,
393
add_root_device=False
394
)
395
396
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
397
398
# Add the block device with explicitly enabling flush.
399
test_microvm.add_drive(
400
'rootfs',
401
test_microvm.rootfs_file,
402
root_device=True,
403
cache_type="Writeback",
404
)
405
406
# Configure metrics, to get later the `flush_count`.
407
metrics_fifo_path = os.path.join(test_microvm.path, 'metrics_fifo')
408
metrics_fifo = log_tools.Fifo(metrics_fifo_path)
409
response = test_microvm.metrics.put(
410
metrics_path=test_microvm.create_jailed_resource(metrics_fifo.path)
411
)
412
assert test_microvm.api_session.is_status_no_content(response.status_code)
413
414
test_microvm.start()
415
416
# Have the guest drop the caches to generate flush requests.
417
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
418
cmd = "sync; echo 1 > /proc/sys/vm/drop_caches"
419
_, _, stderr = ssh_connection.execute_command(cmd)
420
assert stderr.read() == ''
421
422
# On average, dropping the caches right after boot generates
423
# about 6 block flush requests.
424
fc_metrics = test_microvm.flush_metrics(metrics_fifo)
425
assert fc_metrics['block']['flush_count'] > 0
426
427
428
def test_block_default_cache_old_version(test_microvm_with_ssh):
429
"""Verify that saving a snapshot for old versions works correctly."""
430
test_microvm = test_microvm_with_ssh
431
test_microvm.spawn()
432
433
test_microvm.basic_config(
434
vcpu_count=1,
435
add_root_device=False
436
)
437
438
# Add the block device with explicitly enabling flush.
439
test_microvm.add_drive(
440
'rootfs',
441
test_microvm.rootfs_file,
442
root_device=True,
443
cache_type="Writeback",
444
)
445
446
test_microvm.start()
447
448
# Pause the VM to create the snapshot.
449
response = test_microvm.vm.patch(state='Paused')
450
assert test_microvm.api_session.is_status_no_content(response.status_code)
451
452
# Create the snapshot for a version without block cache type.
453
response = test_microvm.snapshot.create(
454
mem_file_path='memfile',
455
snapshot_path='snapsfile',
456
diff=False,
457
version='0.24.0'
458
)
459
assert test_microvm.api_session.is_status_no_content(response.status_code)
460
461
# We should find a warning in the logs for this case as this
462
# cache type was not supported in 0.24.0 and we should default
463
# to "Unsafe" mode.
464
log_data = test_microvm.log_data
465
assert "Target version does not implement the current cache type. "\
466
"Defaulting to \"unsafe\" mode." in log_data
467
468
469
def check_iops_limit(ssh_connection, block_size, count, min_time, max_time):
470
"""Verify if the rate limiter throttles block iops using dd."""
471
obs = block_size
472
byte_count = block_size * count
473
dd = "dd if=/dev/zero of=/dev/vdb ibs={} obs={} count={} oflag=direct"\
474
.format(block_size, obs, count)
475
print("Running cmd {}".format(dd))
476
# Check write iops (writing with oflag=direct is more reliable).
477
exit_code, _, stderr = ssh_connection.execute_command(dd)
478
assert exit_code == 0
479
480
# "dd" writes to stderr by design. We drop first lines
481
stderr.readline().strip()
482
stderr.readline().strip()
483
dd_result = stderr.readline().strip()
484
485
# Interesting output looks like this:
486
# 4194304 bytes (4.2 MB, 4.0 MiB) copied, 0.0528524 s, 79.4 MB/s
487
tokens = dd_result.split()
488
489
# Check total read bytes.
490
assert int(tokens[0]) == byte_count
491
# Check duration.
492
assert float(tokens[7]) > min_time
493
assert float(tokens[7]) < max_time
494
495
496
def test_patch_drive_limiter(test_microvm_with_ssh, network_config):
497
"""Test replacing the drive rate-limiter after guest boot works."""
498
test_microvm = test_microvm_with_ssh
499
test_microvm.jailer.daemonize = False
500
test_microvm.spawn()
501
# Set up the microVM with 2 vCPUs, 512 MiB of RAM, 1 network iface, a root
502
# file system with the rw permission, and a scratch drive.
503
test_microvm.basic_config(vcpu_count=2,
504
mem_size_mib=512,
505
boot_args='console=ttyS0 reboot=k panic=1')
506
507
_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')
508
509
fs1 = drive_tools.FilesystemFile(
510
os.path.join(test_microvm.fsfiles, 'scratch'),
511
size=512
512
)
513
response = test_microvm.drive.put(
514
drive_id='scratch',
515
path_on_host=test_microvm.create_jailed_resource(fs1.path),
516
is_root_device=False,
517
is_read_only=False,
518
rate_limiter={
519
'bandwidth': {
520
'size': 10 * MB,
521
'refill_time': 100
522
},
523
'ops': {
524
'size': 100,
525
'refill_time': 100
526
}
527
}
528
)
529
assert test_microvm.api_session.is_status_no_content(response.status_code)
530
test_microvm.start()
531
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
532
533
# Validate IOPS stays within above configured limits.
534
# For example, the below call will validate that reading 1000 blocks
535
# of 512b will complete in at 0.8-1.2 seconds ('dd' is not very accurate,
536
# so we target to stay within 20% error).
537
check_iops_limit(ssh_connection, 512, 1000, 0.8, 1.2)
538
check_iops_limit(ssh_connection, 4096, 1000, 0.8, 1.2)
539
540
# Patch ratelimiter
541
response = test_microvm.drive.patch(
542
drive_id='scratch',
543
rate_limiter={
544
'bandwidth': {
545
'size': 100 * MB,
546
'refill_time': 100
547
},
548
'ops': {
549
'size': 200,
550
'refill_time': 100
551
}
552
}
553
)
554
assert test_microvm.api_session.is_status_no_content(response.status_code)
555
556
check_iops_limit(ssh_connection, 512, 2000, 0.8, 1.2)
557
check_iops_limit(ssh_connection, 4096, 2000, 0.8, 1.2)
558
559
# Patch ratelimiter
560
response = test_microvm.drive.patch(
561
drive_id='scratch',
562
rate_limiter={
563
'ops': {
564
'size': 1000,
565
'refill_time': 100
566
}
567
}
568
)
569
assert test_microvm.api_session.is_status_no_content(response.status_code)
570
571
check_iops_limit(ssh_connection, 512, 10000, 0.8, 1.2)
572
check_iops_limit(ssh_connection, 4096, 10000, 0.8, 1.2)
573
574
575
def _check_block_size(ssh_connection, dev_path, size):
576
_, stdout, stderr = ssh_connection.execute_command(
577
'blockdev --getsize64 {}'.format(dev_path)
578
)
579
580
assert stderr.read() == ''
581
assert stdout.readline().strip() == str(size)
582
583
584
def _check_file_size(ssh_connection, dev_path, size):
585
_, stdout, stderr = ssh_connection.execute_command(
586
'stat --format=%s {}'.format(dev_path)
587
)
588
589
assert stderr.read() == ''
590
assert stdout.readline().strip() == str(size)
591
592
593
def _process_blockdev_output(blockdev_out, assert_dict, keys_array):
594
blockdev_out_lines = blockdev_out.splitlines()
595
596
for key in keys_array:
597
line = int(key.split('-')[0])
598
col = int(key.split('-')[1])
599
blockdev_out_line = blockdev_out_lines[line]
600
assert blockdev_out_line.split(" ")[col] == assert_dict[key]
601
602
603
def _check_drives(test_microvm, assert_dict, keys_array):
604
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
605
606
_, stdout, stderr = ssh_connection.execute_command('blockdev --report')
607
assert stderr.read() == ''
608
_process_blockdev_output(
609
stdout.read(),
610
assert_dict,
611
keys_array)
612
613