Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/integration_tests/functional/test_api.py
1958 views
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Tests that ensure the correctness of the Firecracker API."""
4
5
# Disable pylint C0302: Too many lines in module
6
# pylint: disable=C0302
7
import os
8
import platform
9
import resource
10
import time
11
12
import pytest
13
14
from framework.builder import MicrovmBuilder
15
import framework.utils_cpuid as utils
16
import host_tools.drive as drive_tools
17
import host_tools.logging as log_tools
18
import host_tools.network as net_tools
19
20
MEM_LIMIT = 1000000000
21
22
23
def test_api_happy_start(test_microvm_with_api):
24
"""Test a regular microvm API start sequence."""
25
test_microvm = test_microvm_with_api
26
test_microvm.spawn()
27
28
# Set up the microVM with 2 vCPUs, 256 MiB of RAM and
29
# a root file system with the rw permission.
30
test_microvm.basic_config()
31
32
test_microvm.start()
33
34
35
def test_api_put_update_pre_boot(test_microvm_with_api):
36
"""Test that PUT updates are allowed before the microvm boots."""
37
test_microvm = test_microvm_with_api
38
test_microvm.spawn()
39
40
# Set up the microVM with 2 vCPUs, 256 MiB of RAM and
41
# a root file system with the rw permission.
42
test_microvm.basic_config()
43
44
fs1 = drive_tools.FilesystemFile(
45
os.path.join(test_microvm.fsfiles, 'scratch')
46
)
47
response = test_microvm.drive.put(
48
drive_id='scratch',
49
path_on_host=test_microvm.create_jailed_resource(fs1.path),
50
is_root_device=False,
51
is_read_only=False
52
)
53
assert test_microvm.api_session.is_status_no_content(response.status_code)
54
55
# Updates to `kernel_image_path` with an invalid path are not allowed.
56
response = test_microvm.boot.put(
57
kernel_image_path='foo.bar'
58
)
59
assert test_microvm.api_session.is_status_bad_request(response.status_code)
60
assert "The kernel file cannot be opened: No such file or directory " \
61
"(os error 2)" in response.text
62
63
# Updates to `kernel_image_path` with a valid path are allowed.
64
response = test_microvm.boot.put(
65
kernel_image_path=test_microvm.get_jailed_resource(
66
test_microvm.kernel_file
67
)
68
)
69
assert test_microvm.api_session.is_status_no_content(response.status_code)
70
71
# Updates to `path_on_host` with an invalid path are not allowed.
72
response = test_microvm.drive.put(
73
drive_id='rootfs',
74
path_on_host='foo.bar',
75
is_read_only=True,
76
is_root_device=True
77
)
78
assert test_microvm.api_session.is_status_bad_request(response.status_code)
79
assert "Invalid block device path" in response.text
80
81
# Updates to `is_root_device` that result in two root block devices are not
82
# allowed.
83
response = test_microvm.drive.put(
84
drive_id='scratch',
85
path_on_host=test_microvm.get_jailed_resource(fs1.path),
86
is_read_only=False,
87
is_root_device=True
88
)
89
assert test_microvm.api_session.is_status_bad_request(response.status_code)
90
assert "A root block device already exists" in response.text
91
92
# Valid updates to `path_on_host` and `is_read_only` are allowed.
93
fs2 = drive_tools.FilesystemFile(
94
os.path.join(test_microvm.fsfiles, 'otherscratch')
95
)
96
response = test_microvm.drive.put(
97
drive_id='scratch',
98
path_on_host=test_microvm.create_jailed_resource(fs2.path),
99
is_read_only=True,
100
is_root_device=False
101
)
102
assert test_microvm.api_session.is_status_no_content(response.status_code)
103
104
# Valid updates to all fields in the machine configuration are allowed.
105
# The machine configuration has a default value, so all PUTs are updates.
106
microvm_config_json = {
107
'vcpu_count': 4,
108
'ht_enabled': True,
109
'mem_size_mib': 256,
110
'track_dirty_pages': True
111
}
112
if platform.machine() == 'x86_64':
113
microvm_config_json['cpu_template'] = 'C3'
114
115
if platform.machine() == 'aarch64':
116
response = test_microvm.machine_cfg.put(
117
vcpu_count=microvm_config_json['vcpu_count'],
118
ht_enabled=microvm_config_json['ht_enabled'],
119
mem_size_mib=microvm_config_json['mem_size_mib'],
120
track_dirty_pages=microvm_config_json['track_dirty_pages']
121
)
122
else:
123
response = test_microvm.machine_cfg.put(
124
vcpu_count=microvm_config_json['vcpu_count'],
125
ht_enabled=microvm_config_json['ht_enabled'],
126
mem_size_mib=microvm_config_json['mem_size_mib'],
127
cpu_template=microvm_config_json['cpu_template'],
128
track_dirty_pages=microvm_config_json['track_dirty_pages']
129
)
130
131
assert test_microvm.api_session.is_status_no_content(response.status_code)
132
133
response = test_microvm.machine_cfg.get()
134
assert test_microvm.api_session.is_status_ok(response.status_code)
135
response_json = response.json()
136
137
vcpu_count = microvm_config_json['vcpu_count']
138
assert response_json['vcpu_count'] == vcpu_count
139
140
ht_enabled = microvm_config_json['ht_enabled']
141
assert response_json['ht_enabled'] == ht_enabled
142
143
mem_size_mib = microvm_config_json['mem_size_mib']
144
assert response_json['mem_size_mib'] == mem_size_mib
145
146
if platform.machine() == 'x86_64':
147
cpu_template = str(microvm_config_json['cpu_template'])
148
assert response_json['cpu_template'] == cpu_template
149
150
track_dirty_pages = microvm_config_json['track_dirty_pages']
151
assert response_json['track_dirty_pages'] == track_dirty_pages
152
153
154
def test_net_api_put_update_pre_boot(test_microvm_with_api):
155
"""Test PUT updates on network configurations before the microvm boots."""
156
test_microvm = test_microvm_with_api
157
test_microvm.spawn()
158
159
first_if_name = 'first_tap'
160
tap1 = net_tools.Tap(first_if_name, test_microvm.jailer.netns)
161
response = test_microvm.network.put(
162
iface_id='1',
163
guest_mac='06:00:00:00:00:01',
164
host_dev_name=tap1.name
165
)
166
assert test_microvm.api_session.is_status_no_content(response.status_code)
167
168
# Adding new network interfaces is allowed.
169
second_if_name = 'second_tap'
170
tap2 = net_tools.Tap(second_if_name, test_microvm.jailer.netns)
171
response = test_microvm.network.put(
172
iface_id='2',
173
guest_mac='07:00:00:00:00:01',
174
host_dev_name=tap2.name
175
)
176
assert test_microvm.api_session.is_status_no_content(response.status_code)
177
178
# Updates to a network interface with an unavailable MAC are not allowed.
179
guest_mac = '06:00:00:00:00:01'
180
response = test_microvm.network.put(
181
iface_id='2',
182
host_dev_name=second_if_name,
183
guest_mac=guest_mac
184
)
185
assert test_microvm.api_session.is_status_bad_request(response.status_code)
186
assert \
187
"The guest MAC address {} is already in use.".format(guest_mac) \
188
in response.text
189
190
# Updates to a network interface with an available MAC are allowed.
191
response = test_microvm.network.put(
192
iface_id='2',
193
host_dev_name=second_if_name,
194
guest_mac='08:00:00:00:00:01'
195
)
196
assert test_microvm.api_session.is_status_no_content(response.status_code)
197
198
# Updates to a network interface with an unavailable name are not allowed.
199
response = test_microvm.network.put(
200
iface_id='1',
201
host_dev_name=second_if_name,
202
guest_mac='06:00:00:00:00:01'
203
)
204
assert test_microvm.api_session.is_status_bad_request(response.status_code)
205
assert "Could not create Network Device" \
206
in response.text
207
208
# Updates to a network interface with an available name are allowed.
209
iface_id = '1'
210
tapname = test_microvm.id[:8] + 'tap' + iface_id
211
212
tap3 = net_tools.Tap(tapname, test_microvm.jailer.netns)
213
response = test_microvm.network.put(
214
iface_id=iface_id,
215
host_dev_name=tap3.name,
216
guest_mac='06:00:00:00:00:01'
217
)
218
assert test_microvm.api_session.is_status_no_content(response.status_code)
219
220
221
def test_api_put_machine_config(test_microvm_with_api):
222
"""Test /machine_config PUT scenarios that unit tests can't cover."""
223
test_microvm = test_microvm_with_api
224
test_microvm.spawn()
225
226
# Test invalid vcpu count < 0.
227
response = test_microvm.machine_cfg.put(
228
vcpu_count='-2'
229
)
230
assert test_microvm.api_session.is_status_bad_request(response.status_code)
231
232
# Test invalid type for ht_enabled flag.
233
response = test_microvm.machine_cfg.put(
234
ht_enabled='random_string'
235
)
236
assert test_microvm.api_session.is_status_bad_request(response.status_code)
237
238
# Test invalid CPU template.
239
response = test_microvm.machine_cfg.put(
240
cpu_template='random_string'
241
)
242
assert test_microvm.api_session.is_status_bad_request(response.status_code)
243
244
response = test_microvm.machine_cfg.patch(
245
track_dirty_pages=True
246
)
247
assert test_microvm.api_session.is_status_bad_request(response.status_code)
248
249
response = test_microvm.machine_cfg.patch(
250
cpu_template='C3'
251
)
252
if platform.machine() == "x86_64":
253
assert test_microvm.api_session.is_status_no_content(
254
response.status_code
255
)
256
else:
257
assert test_microvm.api_session.is_status_bad_request(
258
response.status_code
259
)
260
assert "CPU templates are not supported on aarch64" in response.text
261
262
# Test invalid mem_size_mib < 0.
263
response = test_microvm.machine_cfg.put(
264
mem_size_mib='-2'
265
)
266
assert test_microvm.api_session.is_status_bad_request(response.status_code)
267
268
# Test invalid mem_size_mib > usize::MAX.
269
bad_size = 1 << 64
270
response = test_microvm.machine_cfg.put(
271
mem_size_mib=bad_size
272
)
273
fail_msg = "error occurred when deserializing the json body of a " \
274
"request: invalid type"
275
assert test_microvm.api_session.is_status_bad_request(response.status_code)
276
assert fail_msg in response.text
277
278
# Test mem_size_mib of valid type, but too large.
279
test_microvm.basic_config()
280
firecracker_pid = int(test_microvm.jailer_clone_pid)
281
resource.prlimit(
282
firecracker_pid,
283
resource.RLIMIT_AS,
284
(MEM_LIMIT, resource.RLIM_INFINITY)
285
)
286
287
bad_size = (1 << 64) - 1
288
response = test_microvm.machine_cfg.patch(
289
mem_size_mib=bad_size
290
)
291
assert test_microvm.api_session.is_status_no_content(response.status_code)
292
293
response = test_microvm.actions.put(action_type='InstanceStart')
294
fail_msg = "Invalid Memory Configuration: MmapRegion(Mmap(Os { code: " \
295
"12, kind: Other, message: Out of memory }))"
296
assert test_microvm.api_session.is_status_bad_request(response.status_code)
297
assert fail_msg in response.text
298
299
# Test invalid mem_size_mib = 0.
300
response = test_microvm.machine_cfg.patch(
301
mem_size_mib=0
302
)
303
assert test_microvm.api_session.is_status_bad_request(response.status_code)
304
assert "The memory size (MiB) is invalid." in response.text
305
306
# Test valid mem_size_mib.
307
response = test_microvm.machine_cfg.patch(
308
mem_size_mib=256
309
)
310
assert test_microvm.api_session.is_status_no_content(response.status_code)
311
312
response = test_microvm.actions.put(action_type='InstanceStart')
313
if utils.get_cpu_vendor() != utils.CpuVendor.INTEL:
314
# We shouldn't be able to apply Intel templates on AMD hosts
315
fail_msg = "Internal error while starting microVM: Error configuring" \
316
" the vcpu for boot: Cpuid error: InvalidVendor"
317
assert test_microvm.api_session.is_status_bad_request(
318
response.status_code)
319
assert fail_msg in response.text
320
else:
321
assert test_microvm.api_session.is_status_no_content(
322
response.status_code)
323
324
# Validate full vm configuration after patching machine config.
325
response = test_microvm.full_cfg.get()
326
assert test_microvm.api_session.is_status_ok(response.status_code)
327
assert response.json()['machine-config']['vcpu_count'] == 2
328
assert response.json()['machine-config']['mem_size_mib'] == 256
329
330
331
def test_api_put_update_post_boot(test_microvm_with_api):
332
"""Test that PUT updates are rejected after the microvm boots."""
333
test_microvm = test_microvm_with_api
334
test_microvm.spawn()
335
336
# Set up the microVM with 2 vCPUs, 256 MiB of RAM and
337
# a root file system with the rw permission.
338
test_microvm.basic_config()
339
340
iface_id = '1'
341
tapname = test_microvm.id[:8] + 'tap' + iface_id
342
tap1 = net_tools.Tap(tapname, test_microvm.jailer.netns)
343
response = test_microvm.network.put(
344
iface_id=iface_id,
345
host_dev_name=tap1.name,
346
guest_mac='06:00:00:00:00:01'
347
)
348
assert test_microvm.api_session.is_status_no_content(response.status_code)
349
350
test_microvm.start()
351
352
expected_err = "The requested operation is not supported " \
353
"after starting the microVM"
354
355
# Valid updates to `kernel_image_path` are not allowed after boot.
356
response = test_microvm.boot.put(
357
kernel_image_path=test_microvm.get_jailed_resource(
358
test_microvm.kernel_file
359
)
360
)
361
assert test_microvm.api_session.is_status_bad_request(response.status_code)
362
assert expected_err in response.text
363
364
# Valid updates to the machine configuration are not allowed after boot.
365
response = test_microvm.machine_cfg.patch(
366
vcpu_count=4
367
)
368
assert test_microvm.api_session.is_status_bad_request(response.status_code)
369
assert expected_err in response.text
370
371
response = test_microvm.machine_cfg.put(
372
vcpu_count=4,
373
ht_enabled=False,
374
mem_size_mib=128
375
)
376
assert test_microvm.api_session.is_status_bad_request(response.status_code)
377
assert expected_err in response.text
378
379
# Network interface update is not allowed after boot.
380
response = test_microvm.network.put(
381
iface_id='1',
382
host_dev_name=tap1.name,
383
guest_mac='06:00:00:00:00:02'
384
)
385
assert test_microvm.api_session.is_status_bad_request(response.status_code)
386
assert expected_err in response.text
387
388
# Block device update is not allowed after boot.
389
response = test_microvm.drive.put(
390
drive_id='rootfs',
391
path_on_host=test_microvm.jailer.jailed_path(test_microvm.rootfs_file),
392
is_read_only=False,
393
is_root_device=True
394
)
395
assert test_microvm.api_session.is_status_bad_request(response.status_code)
396
assert expected_err in response.text
397
398
399
def test_rate_limiters_api_config(test_microvm_with_api):
400
"""Test the Firecracker IO rate limiter API."""
401
test_microvm = test_microvm_with_api
402
test_microvm.spawn()
403
404
# Test the DRIVE rate limiting API.
405
406
# Test drive with bw rate-limiting.
407
fs1 = drive_tools.FilesystemFile(os.path.join(test_microvm.fsfiles, 'bw'))
408
response = test_microvm.drive.put(
409
drive_id='bw',
410
path_on_host=test_microvm.create_jailed_resource(fs1.path),
411
is_read_only=False,
412
is_root_device=False,
413
rate_limiter={
414
'bandwidth': {
415
'size': 1000000,
416
'refill_time': 100
417
}
418
}
419
)
420
assert test_microvm.api_session.is_status_no_content(response.status_code)
421
422
# Test drive with ops rate-limiting.
423
fs2 = drive_tools.FilesystemFile(os.path.join(test_microvm.fsfiles, 'ops'))
424
response = test_microvm.drive.put(
425
drive_id='ops',
426
path_on_host=test_microvm.create_jailed_resource(fs2.path),
427
is_read_only=False,
428
is_root_device=False,
429
rate_limiter={
430
'ops': {
431
'size': 1,
432
'refill_time': 100
433
}
434
}
435
)
436
assert test_microvm.api_session.is_status_no_content(response.status_code)
437
438
# Test drive with bw and ops rate-limiting.
439
fs3 = drive_tools.FilesystemFile(
440
os.path.join(test_microvm.fsfiles, 'bwops')
441
)
442
response = test_microvm.drive.put(
443
drive_id='bwops',
444
path_on_host=test_microvm.create_jailed_resource(fs3.path),
445
is_read_only=False,
446
is_root_device=False,
447
rate_limiter={
448
'bandwidth': {
449
'size': 1000000,
450
'refill_time': 100
451
},
452
'ops': {
453
'size': 1,
454
'refill_time': 100
455
}
456
}
457
)
458
assert test_microvm.api_session.is_status_no_content(response.status_code)
459
460
# Test drive with 'empty' rate-limiting (same as not specifying the field)
461
fs4 = drive_tools.FilesystemFile(os.path.join(
462
test_microvm.fsfiles, 'nada'
463
))
464
response = test_microvm.drive.put(
465
drive_id='nada',
466
path_on_host=test_microvm.create_jailed_resource(fs4.path),
467
is_read_only=False,
468
is_root_device=False,
469
rate_limiter={}
470
)
471
assert test_microvm.api_session.is_status_no_content(response.status_code)
472
473
# Test the NET rate limiting API.
474
475
# Test network with tx bw rate-limiting.
476
iface_id = '1'
477
tapname = test_microvm.id[:8] + 'tap' + iface_id
478
tap1 = net_tools.Tap(tapname, test_microvm.jailer.netns)
479
480
response = test_microvm.network.put(
481
iface_id=iface_id,
482
guest_mac='06:00:00:00:00:01',
483
host_dev_name=tap1.name,
484
tx_rate_limiter={
485
'bandwidth': {
486
'size': 1000000,
487
'refill_time': 100
488
}
489
}
490
)
491
assert test_microvm.api_session.is_status_no_content(response.status_code)
492
493
# Test network with rx bw rate-limiting.
494
iface_id = '2'
495
tapname = test_microvm.id[:8] + 'tap' + iface_id
496
tap2 = net_tools.Tap(tapname, test_microvm.jailer.netns)
497
response = test_microvm.network.put(
498
iface_id=iface_id,
499
guest_mac='06:00:00:00:00:02',
500
host_dev_name=tap2.name,
501
rx_rate_limiter={
502
'bandwidth': {
503
'size': 1000000,
504
'refill_time': 100
505
}
506
}
507
)
508
assert test_microvm.api_session.is_status_no_content(response.status_code)
509
510
# Test network with tx and rx bw and ops rate-limiting.
511
iface_id = '3'
512
tapname = test_microvm.id[:8] + 'tap' + iface_id
513
tap3 = net_tools.Tap(tapname, test_microvm.jailer.netns)
514
response = test_microvm.network.put(
515
iface_id=iface_id,
516
guest_mac='06:00:00:00:00:03',
517
host_dev_name=tap3.name,
518
rx_rate_limiter={
519
'bandwidth': {
520
'size': 1000000,
521
'refill_time': 100
522
},
523
'ops': {
524
'size': 1,
525
'refill_time': 100
526
}
527
},
528
tx_rate_limiter={
529
'bandwidth': {
530
'size': 1000000,
531
'refill_time': 100
532
},
533
'ops': {
534
'size': 1,
535
'refill_time': 100
536
}
537
}
538
)
539
assert test_microvm.api_session.is_status_no_content(response.status_code)
540
541
542
def test_api_patch_pre_boot(test_microvm_with_api):
543
"""Tests PATCH updates before the microvm boots."""
544
test_microvm = test_microvm_with_api
545
test_microvm.spawn()
546
547
# Sets up the microVM with 2 vCPUs, 256 MiB of RAM, 1 network interface
548
# and a root file system with the rw permission.
549
test_microvm.basic_config()
550
551
fs1 = drive_tools.FilesystemFile(
552
os.path.join(test_microvm.fsfiles, 'scratch')
553
)
554
drive_id = 'scratch'
555
response = test_microvm.drive.put(
556
drive_id=drive_id,
557
path_on_host=test_microvm.create_jailed_resource(fs1.path),
558
is_root_device=False,
559
is_read_only=False
560
)
561
assert test_microvm.api_session.is_status_no_content(response.status_code)
562
563
# Configure metrics.
564
metrics_fifo_path = os.path.join(test_microvm.path, 'metrics_fifo')
565
metrics_fifo = log_tools.Fifo(metrics_fifo_path)
566
567
response = test_microvm.metrics.put(
568
metrics_path=test_microvm.create_jailed_resource(metrics_fifo.path)
569
)
570
assert test_microvm.api_session.is_status_no_content(response.status_code)
571
572
iface_id = '1'
573
tapname = test_microvm.id[:8] + 'tap' + iface_id
574
tap1 = net_tools.Tap(tapname, test_microvm.jailer.netns)
575
response = test_microvm.network.put(
576
iface_id=iface_id,
577
host_dev_name=tap1.name,
578
guest_mac='06:00:00:00:00:01'
579
)
580
assert test_microvm.api_session.is_status_no_content(response.status_code)
581
582
# Partial updates to the boot source are not allowed.
583
response = test_microvm.boot.patch(
584
kernel_image_path='otherfile'
585
)
586
assert test_microvm.api_session.is_status_bad_request(response.status_code)
587
assert "Invalid request method" in response.text
588
589
# Partial updates to the machine configuration are allowed before boot.
590
response = test_microvm.machine_cfg.patch(vcpu_count=4)
591
assert test_microvm.api_session.is_status_no_content(response.status_code)
592
response_json = test_microvm.machine_cfg.get().json()
593
assert response_json['vcpu_count'] == 4
594
595
# Partial updates to the logger configuration are not allowed.
596
response = test_microvm.logger.patch(level='Error')
597
assert test_microvm.api_session.is_status_bad_request(response.status_code)
598
assert "Invalid request method" in response.text
599
600
# Patching drive before boot is not allowed.
601
response = test_microvm.drive.patch(
602
drive_id=drive_id,
603
path_on_host='foo.bar'
604
)
605
assert test_microvm.api_session.is_status_bad_request(response.status_code)
606
assert "The requested operation is not supported before starting the " \
607
"microVM." in response.text
608
609
# Patching net before boot is not allowed.
610
response = test_microvm.network.patch(
611
iface_id=iface_id
612
)
613
assert test_microvm.api_session.is_status_bad_request(response.status_code)
614
assert "The requested operation is not supported before starting the " \
615
"microVM." in response.text
616
617
618
def test_api_patch_post_boot(test_microvm_with_api):
619
"""Test PATCH updates after the microvm boots."""
620
test_microvm = test_microvm_with_api
621
test_microvm.spawn()
622
623
# Sets up the microVM with 2 vCPUs, 256 MiB of RAM, 1 network iface and
624
# a root file system with the rw permission.
625
test_microvm.basic_config()
626
627
fs1 = drive_tools.FilesystemFile(
628
os.path.join(test_microvm.fsfiles, 'scratch')
629
)
630
response = test_microvm.drive.put(
631
drive_id='scratch',
632
path_on_host=test_microvm.create_jailed_resource(fs1.path),
633
is_root_device=False,
634
is_read_only=False
635
)
636
assert test_microvm.api_session.is_status_no_content(response.status_code)
637
638
# Configure metrics.
639
metrics_fifo_path = os.path.join(test_microvm.path, 'metrics_fifo')
640
metrics_fifo = log_tools.Fifo(metrics_fifo_path)
641
642
response = test_microvm.metrics.put(
643
metrics_path=test_microvm.create_jailed_resource(metrics_fifo.path)
644
)
645
assert test_microvm.api_session.is_status_no_content(response.status_code)
646
647
iface_id = '1'
648
tapname = test_microvm.id[:8] + 'tap' + iface_id
649
tap1 = net_tools.Tap(tapname, test_microvm.jailer.netns)
650
response = test_microvm.network.put(
651
iface_id=iface_id,
652
host_dev_name=tap1.name,
653
guest_mac='06:00:00:00:00:01'
654
)
655
assert test_microvm.api_session.is_status_no_content(response.status_code)
656
657
test_microvm.start()
658
659
# Partial updates to the boot source are not allowed.
660
response = test_microvm.boot.patch(
661
kernel_image_path='otherfile'
662
)
663
assert test_microvm.api_session.is_status_bad_request(response.status_code)
664
assert "Invalid request method" in response.text
665
666
# Partial updates to the machine configuration are not allowed after boot.
667
expected_err = "The requested operation is not supported " \
668
"after starting the microVM"
669
response = test_microvm.machine_cfg.patch(vcpu_count=4)
670
assert test_microvm.api_session.is_status_bad_request(response.status_code)
671
assert expected_err in response.text
672
673
# Partial updates to the logger configuration are not allowed.
674
response = test_microvm.logger.patch(level='Error')
675
assert test_microvm.api_session.is_status_bad_request(response.status_code)
676
assert "Invalid request method" in response.text
677
678
679
def test_drive_patch(test_microvm_with_api):
680
"""Test drive PATCH before and after boot."""
681
test_microvm = test_microvm_with_api
682
test_microvm.spawn()
683
684
# Sets up the microVM with 2 vCPUs, 256 MiB of RAM and
685
# a root file system with the rw permission.
686
test_microvm.basic_config()
687
688
# The drive to be patched.
689
fs = drive_tools.FilesystemFile(
690
os.path.join(test_microvm.fsfiles, 'scratch')
691
)
692
response = test_microvm.drive.put(
693
drive_id='scratch',
694
path_on_host=test_microvm.create_jailed_resource(fs.path),
695
is_root_device=False,
696
is_read_only=False
697
)
698
assert test_microvm.api_session.is_status_no_content(response.status_code)
699
700
# Patching drive before boot is not allowed.
701
response = test_microvm.drive.patch(
702
drive_id='scratch',
703
path_on_host='foo.bar'
704
)
705
assert test_microvm.api_session.is_status_bad_request(response.status_code)
706
assert "The requested operation is not supported before starting the " \
707
"microVM." in response.text
708
709
test_microvm.start()
710
711
_drive_patch(test_microvm)
712
713
714
@pytest.mark.skipif(
715
platform.machine() != "x86_64",
716
reason="not yet implemented on aarch64"
717
)
718
def test_send_ctrl_alt_del(test_microvm_with_api):
719
"""Test shutting down the microVM gracefully, by sending CTRL+ALT+DEL.
720
721
This relies on i8042 and AT Keyboard support being present in the guest
722
kernel.
723
"""
724
test_microvm = test_microvm_with_api
725
test_microvm.spawn()
726
727
test_microvm.basic_config()
728
test_microvm.start()
729
730
# Wait around for the guest to boot up and initialize the user space
731
time.sleep(2)
732
733
response = test_microvm.actions.put(
734
action_type='SendCtrlAltDel'
735
)
736
assert test_microvm.api_session.is_status_no_content(response.status_code)
737
738
firecracker_pid = test_microvm.jailer_clone_pid
739
740
# If everyting goes as expected, the guest OS will issue a reboot,
741
# causing Firecracker to exit.
742
# We'll keep poking Firecracker for at most 30 seconds, waiting for it
743
# to die.
744
start_time = time.time()
745
shutdown_ok = False
746
while time.time() - start_time < 30:
747
try:
748
os.kill(firecracker_pid, 0)
749
time.sleep(0.01)
750
except OSError:
751
shutdown_ok = True
752
break
753
754
assert shutdown_ok
755
756
757
def _drive_patch(test_microvm):
758
"""Exercise drive patch test scenarios."""
759
# Patches without mandatory fields are not allowed.
760
response = test_microvm.drive.patch(
761
drive_id='scratch'
762
)
763
assert test_microvm.api_session.is_status_bad_request(response.status_code)
764
assert "at least one property to patch: path_on_host, rate_limiter" \
765
in response.text
766
767
# Cannot patch drive permissions post boot.
768
response = test_microvm.drive.patch(
769
drive_id='scratch',
770
path_on_host='foo.bar',
771
is_read_only=True
772
)
773
assert test_microvm.api_session.is_status_bad_request(response.status_code)
774
assert "unknown field `is_read_only`" in response.text
775
776
# Updates to `is_root_device` with a valid value are not allowed.
777
response = test_microvm.drive.patch(
778
drive_id='scratch',
779
path_on_host='foo.bar',
780
is_root_device=False
781
)
782
assert test_microvm.api_session.is_status_bad_request(response.status_code)
783
assert "unknown field `is_root_device`" in response.text
784
785
# Updates to `path_on_host` with an invalid path are not allowed.
786
response = test_microvm.drive.patch(
787
drive_id='scratch',
788
path_on_host='foo.bar'
789
)
790
assert test_microvm.api_session.is_status_bad_request(response.status_code)
791
assert "drive update (patch): device error: No such file or directory" \
792
in response.text
793
794
fs = drive_tools.FilesystemFile(
795
os.path.join(test_microvm.fsfiles, 'scratch_new')
796
)
797
# Updates to `path_on_host` with a valid path are allowed.
798
response = test_microvm.drive.patch(
799
drive_id='scratch',
800
path_on_host=test_microvm.create_jailed_resource(fs.path)
801
)
802
assert test_microvm.api_session.is_status_no_content(response.status_code)
803
804
# Updates to valid `path_on_host` and `rate_limiter` are allowed.
805
response = test_microvm.drive.patch(
806
drive_id='scratch',
807
path_on_host=test_microvm.create_jailed_resource(fs.path),
808
rate_limiter={
809
'bandwidth': {
810
'size': 1000000,
811
'refill_time': 100
812
},
813
'ops': {
814
'size': 1,
815
'refill_time': 100
816
}
817
}
818
)
819
assert test_microvm.api_session.is_status_no_content(response.status_code)
820
821
# Updates to `rate_limiter` only are allowed.
822
response = test_microvm.drive.patch(
823
drive_id='scratch',
824
rate_limiter={
825
'bandwidth': {
826
'size': 5000,
827
'refill_time': 100
828
},
829
'ops': {
830
'size': 500,
831
'refill_time': 100
832
}
833
}
834
)
835
assert test_microvm.api_session.is_status_no_content(response.status_code)
836
837
# Updates to `rate_limiter` and invalid path fail.
838
response = test_microvm.drive.patch(
839
drive_id='scratch',
840
path_on_host='foo.bar',
841
rate_limiter={
842
'bandwidth': {
843
'size': 5000,
844
'refill_time': 100
845
},
846
'ops': {
847
'size': 500,
848
'refill_time': 100
849
}
850
}
851
)
852
assert test_microvm.api_session.is_status_bad_request(response.status_code)
853
assert "No such file or directory" in response.text
854
855
# Validate full vm configuration after patching drives.
856
response = test_microvm.full_cfg.get()
857
assert test_microvm.api_session.is_status_ok(response.status_code)
858
assert response.json()['drives'] == [{
859
'drive_id': 'rootfs',
860
'path_on_host': '/xenial.rootfs.ext4',
861
'is_root_device': True,
862
'partuuid': None,
863
'is_read_only': False,
864
'cache_type': 'Unsafe',
865
'rate_limiter': None
866
}, {
867
'drive_id': 'scratch',
868
'path_on_host': '/scratch_new.ext4',
869
'is_root_device': False,
870
'partuuid': None,
871
'is_read_only': False,
872
'cache_type': 'Unsafe',
873
'rate_limiter': {
874
'bandwidth': {
875
'size': 5000,
876
'one_time_burst': None,
877
'refill_time': 100
878
},
879
'ops': {
880
'size': 500,
881
'one_time_burst': None,
882
'refill_time': 100
883
}
884
}
885
}]
886
887
888
def test_api_vsock(test_microvm_with_api):
889
"""Test vsock related API commands."""
890
test_microvm = test_microvm_with_api
891
test_microvm.spawn()
892
test_microvm.basic_config()
893
894
response = test_microvm.vsock.put(
895
vsock_id='vsock1',
896
guest_cid=15,
897
uds_path='vsock.sock'
898
)
899
assert test_microvm.api_session.is_status_no_content(response.status_code)
900
901
# Updating an existing vsock is currently fine.
902
response = test_microvm.vsock.put(
903
vsock_id='vsock1',
904
guest_cid=166,
905
uds_path='vsock.sock'
906
)
907
assert test_microvm.api_session.is_status_no_content(response.status_code)
908
909
# No other vsock action is allowed after booting the VM.
910
test_microvm.start()
911
912
# Updating an existing vsock should not be fine at this point.
913
response = test_microvm.vsock.put(
914
vsock_id='vsock1',
915
guest_cid=17,
916
uds_path='vsock.sock'
917
)
918
assert test_microvm.api_session.is_status_bad_request(response.status_code)
919
920
# Attaching a new vsock device should not be fine at this point.
921
response = test_microvm.vsock.put(
922
vsock_id='vsock3',
923
guest_cid=18,
924
uds_path='vsock.sock'
925
)
926
assert test_microvm.api_session.is_status_bad_request(response.status_code)
927
928
response = test_microvm.vm.patch(state='Paused')
929
assert test_microvm.api_session.is_status_no_content(response.status_code)
930
931
932
def test_api_balloon(test_microvm_with_ssh_and_balloon):
933
"""Test balloon related API commands."""
934
test_microvm = test_microvm_with_ssh_and_balloon
935
test_microvm.spawn()
936
test_microvm.basic_config()
937
938
# Updating an inexistent balloon device should give an error.
939
response = test_microvm.balloon.patch(amount_mib=0)
940
assert test_microvm.api_session.is_status_bad_request(response.status_code)
941
942
# Adding a memory balloon should be OK.
943
response = test_microvm.balloon.put(
944
amount_mib=1,
945
deflate_on_oom=True
946
)
947
assert test_microvm.api_session.is_status_no_content(response.status_code)
948
949
# As is overwriting one.
950
response = test_microvm.balloon.put(
951
amount_mib=0,
952
deflate_on_oom=False,
953
stats_polling_interval_s=5
954
)
955
assert test_microvm.api_session.is_status_no_content(response.status_code)
956
957
# Getting the device configuration should be available pre-boot.
958
response = test_microvm.balloon.get()
959
assert test_microvm.api_session.is_status_ok(response.status_code)
960
assert response.json()['amount_mib'] == 0
961
assert response.json()['deflate_on_oom'] is False
962
assert response.json()['stats_polling_interval_s'] == 5
963
964
# Updating an existing balloon device is forbidden before boot.
965
response = test_microvm.balloon.patch(amount_mib=2)
966
assert test_microvm.api_session.is_status_bad_request(response.status_code)
967
968
# We can't have a balloon device with a target size greater than
969
# the available amount of memory.
970
response = test_microvm.balloon.put(
971
amount_mib=1024,
972
deflate_on_oom=False,
973
stats_polling_interval_s=5
974
)
975
assert test_microvm.api_session.is_status_bad_request(response.status_code)
976
977
# Start the microvm.
978
test_microvm.start()
979
980
# Updating should fail as driver didn't have time to initialize.
981
response = test_microvm.balloon.patch(amount_mib=4)
982
assert test_microvm.api_session.is_status_bad_request(response.status_code)
983
984
# Overwriting the existing device should give an error now.
985
response = test_microvm.balloon.put(
986
amount_mib=3,
987
deflate_on_oom=False,
988
stats_polling_interval_s=3
989
)
990
assert test_microvm.api_session.is_status_bad_request(response.status_code)
991
992
# Give the balloon driver time to initialize.
993
# 500 ms is the maximum acceptable boot time.
994
time.sleep(0.5)
995
996
# But updating should be OK.
997
response = test_microvm.balloon.patch(amount_mib=4)
998
assert test_microvm.api_session.is_status_no_content(response.status_code)
999
1000
# Check we can't request more than the total amount of VM memory.
1001
response = test_microvm.balloon.patch(amount_mib=300)
1002
assert test_microvm.api_session.is_status_bad_request(response.status_code)
1003
1004
# Check we can't disable statistics as they were enabled at boot.
1005
# We can, however, change the interval to a non-zero value.
1006
response = test_microvm.balloon.patch_stats(stats_polling_interval_s=5)
1007
assert test_microvm.api_session.is_status_no_content(response.status_code)
1008
1009
# Getting the device configuration should be available post-boot.
1010
response = test_microvm.balloon.get()
1011
assert test_microvm.api_session.is_status_ok(response.status_code)
1012
assert response.json()['amount_mib'] == 4
1013
assert response.json()['deflate_on_oom'] is False
1014
assert response.json()['stats_polling_interval_s'] == 5
1015
1016
# Check we can't overflow the `num_pages` field in the config space by
1017
# requesting too many MB. There are 256 4K pages in a MB. Here, we are
1018
# requesting u32::MAX / 128.
1019
response = test_microvm.balloon.patch(amount_mib=33554432)
1020
assert test_microvm.api_session.is_status_bad_request(response.status_code)
1021
1022
1023
def test_get_full_config(test_microvm_with_ssh_and_balloon):
1024
"""Configure microVM with all resources and get configuration."""
1025
test_microvm = test_microvm_with_ssh_and_balloon
1026
1027
expected_cfg = {}
1028
1029
test_microvm.spawn()
1030
# Basic config also implies a root block device.
1031
test_microvm.basic_config()
1032
expected_cfg['machine-config'] = {
1033
'vcpu_count': 2,
1034
'mem_size_mib': 256,
1035
'ht_enabled': False,
1036
'track_dirty_pages': False
1037
}
1038
expected_cfg['boot-source'] = {
1039
'kernel_image_path': '/vmlinux.bin',
1040
'initrd_path': None
1041
}
1042
expected_cfg['drives'] = [{
1043
'drive_id': 'rootfs',
1044
'path_on_host': '/debian.rootfs.ext4',
1045
'is_root_device': True,
1046
'partuuid': None,
1047
'is_read_only': False,
1048
'cache_type': 'Unsafe',
1049
'rate_limiter': None
1050
}]
1051
1052
# Add a memory balloon device.
1053
response = test_microvm.balloon.put(amount_mib=1, deflate_on_oom=True)
1054
assert test_microvm.api_session.is_status_no_content(response.status_code)
1055
expected_cfg['balloon'] = {
1056
'amount_mib': 1,
1057
'deflate_on_oom': True,
1058
'stats_polling_interval_s': 0
1059
}
1060
1061
# Add a vsock device.
1062
response = test_microvm.vsock.put(
1063
vsock_id='vsock',
1064
guest_cid=15,
1065
uds_path='vsock.sock'
1066
)
1067
assert test_microvm.api_session.is_status_no_content(response.status_code)
1068
expected_cfg['vsock'] = {
1069
'vsock_id': 'vsock',
1070
'guest_cid': 15,
1071
'uds_path': 'vsock.sock'
1072
}
1073
1074
# Add a net device.
1075
iface_id = '1'
1076
tapname = test_microvm.id[:8] + 'tap' + iface_id
1077
tap1 = net_tools.Tap(tapname, test_microvm.jailer.netns)
1078
guest_mac = '06:00:00:00:00:01'
1079
tx_rl = {
1080
'bandwidth': {
1081
'size': 1000000,
1082
'refill_time': 100,
1083
'one_time_burst': None
1084
},
1085
'ops': None
1086
}
1087
response = test_microvm.network.put(
1088
iface_id=iface_id,
1089
guest_mac=guest_mac,
1090
host_dev_name=tap1.name,
1091
tx_rate_limiter=tx_rl
1092
)
1093
assert test_microvm.api_session.is_status_no_content(response.status_code)
1094
expected_cfg['network-interfaces'] = [{
1095
'iface_id': iface_id,
1096
'host_dev_name': tap1.name,
1097
'guest_mac': '06:00:00:00:00:01',
1098
'rx_rate_limiter': None,
1099
'tx_rate_limiter': tx_rl,
1100
'allow_mmds_requests': False
1101
}]
1102
1103
expected_cfg['logger'] = None
1104
expected_cfg['metrics'] = None
1105
expected_cfg['mmds-config'] = None
1106
1107
# Getting full vm configuration should be available pre-boot.
1108
response = test_microvm.full_cfg.get()
1109
assert test_microvm.api_session.is_status_ok(response.status_code)
1110
assert response.json() == expected_cfg
1111
1112
# Start the microvm.
1113
test_microvm.start()
1114
1115
# Validate full vm configuration post-boot as well.
1116
response = test_microvm.full_cfg.get()
1117
assert test_microvm.api_session.is_status_ok(response.status_code)
1118
assert response.json() == expected_cfg
1119
1120
1121
def test_negative_api_lifecycle(bin_cloner_path):
1122
"""Test some vm lifecycle error scenarios."""
1123
builder = MicrovmBuilder(bin_cloner_path)
1124
vm_instance = builder.build_vm_nano()
1125
basevm = vm_instance.vm
1126
1127
# Try to pause microvm when not running, it must fail.
1128
response = basevm.vm.patch(state='Paused')
1129
assert "not supported before starting the microVM" \
1130
in response.text
1131
1132
# Try to resume microvm when not running, it must fail.
1133
response = basevm.vm.patch(state='Resumed')
1134
assert "not supported before starting the microVM" \
1135
in response.text
1136
1137