Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/conftest.py
1955 views
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Imported by pytest at the start of every test session.
4
5
# Fixture Goals
6
7
Fixtures herein are made available to every test collected by pytest. They are
8
designed with the following goals in mind:
9
- Running a test on a microvm is as easy as importing a microvm fixture.
10
- Adding a new microvm image (kernel, rootfs) for tests to run on is as easy as
11
uploading that image to a key-value store, e.g., an s3 bucket.
12
13
# Solution
14
15
- Keep microvm test images in an S3 bucket, structured as follows:
16
17
``` tree
18
s3://<bucket-url>/img/
19
<microvm_test_image_folder_n>/
20
kernel/
21
<optional_kernel_name.>vmlinux.bin
22
fsfiles/
23
<rootfs_name>rootfs.ext4
24
<other_fsfile_n>
25
...
26
<other_resource_n>
27
...
28
...
29
```
30
31
- Tag `<microvm_test_image_folder_n>` with the capabilities of that image:
32
33
``` json
34
TagSet = [{"key": "capability:<cap_name>", "value": ""}, ...]
35
```
36
37
- Make available fixtures that expose microvms based on any given capability.
38
For example, a test function using the fixture `test_microvm_any` should run
39
on all microvm images in the S3 bucket, while a test using the fixture
40
`test_microvm_with_net` should only run on the microvm images tagged with
41
`capability:net`. Note that a test function that uses a parameterized fixture
42
will yield one test case for every possible parameter of that fixture. For
43
example, using `test_microvm_any` in a test will create as many test cases
44
as there are microvm images in the S3 bucket.
45
46
- Provide fixtures that simplify other common testing operations, like http
47
over local unix domain sockets.
48
49
# Example
50
51
```
52
def test_with_any_microvm(test_microvm_any):
53
54
response = test_microvm_any.machine_cfg.put(
55
vcpu_count=8
56
)
57
assert(test_microvm_any.api_session.is_good_response(response.status_code))
58
59
# [...]
60
61
response = test_microvm_any.actions.put(action_type='InstanceStart')
62
assert(test_microvm_any.api_session.is_good_response(response.status_code))
63
```
64
65
The test above makes use of the "any" test microvm fixture, so this test will
66
be run on every microvm image in the bucket, each as a separate test case.
67
68
# Notes
69
70
- Reading up on pytest fixtures is probably needed when editing this file.
71
72
# TODO
73
- A fixture that allows per-test-function dependency installation.
74
- Support generating fixtures with more than one capability. This is supported
75
by the MicrovmImageFetcher, but not by the fixture template.
76
"""
77
78
import os
79
import platform
80
import shutil
81
import sys
82
import tempfile
83
import uuid
84
85
import pytest
86
87
import host_tools.cargo_build as build_tools
88
import host_tools.network as net_tools
89
import host_tools.proc as proc
90
import framework.utils as utils
91
import framework.defs as defs
92
from framework.artifacts import ArtifactCollection
93
from framework.microvm import Microvm
94
from framework.s3fetcher import MicrovmImageS3Fetcher
95
from framework.scheduler import PytestScheduler
96
97
# Tests root directory.
98
SCRIPT_FOLDER = os.path.dirname(os.path.realpath(__file__))
99
100
# This codebase uses Python features available in Python 3.6 or above
101
if sys.version_info < (3, 6):
102
raise SystemError("This codebase requires Python 3.6 or above.")
103
104
105
# Some tests create system-level resources; ensure we run as root.
106
if os.geteuid() != 0:
107
raise PermissionError("Test session needs to be run as root.")
108
109
110
# Style related tests are run only on AMD.
111
if "AMD" not in proc.proc_type():
112
collect_ignore = [os.path.join(SCRIPT_FOLDER, "integration_tests/style")]
113
114
115
def _test_images_s3_bucket():
116
"""Auxiliary function for getting this session's bucket name."""
117
return os.environ.get(
118
defs.ENV_TEST_IMAGES_S3_BUCKET,
119
defs.DEFAULT_TEST_IMAGES_S3_BUCKET
120
)
121
122
123
ARTIFACTS_COLLECTION = ArtifactCollection(_test_images_s3_bucket())
124
MICROVM_S3_FETCHER = MicrovmImageS3Fetcher(_test_images_s3_bucket())
125
126
127
class ResultsFileDumper: # pylint: disable=too-few-public-methods
128
"""Class responsible with outputting test results to files."""
129
130
def __init__(self, test_name: str, append=True):
131
"""Initialize the instance."""
132
if not append:
133
flags = "w"
134
else:
135
flags = "a"
136
137
self._root_path = defs.TEST_RESULTS_DIR
138
139
# Create the root directory, if it doesn't exist.
140
self._root_path.mkdir(exist_ok=True)
141
142
self._file = open(self._root_path / test_name, flags)
143
144
def writeln(self, data: str):
145
"""Write the `data` string to the output file, appending a newline."""
146
self._file.write(data)
147
self._file.write("\n")
148
self._file.flush()
149
150
151
def init_microvm(root_path, bin_cloner_path,
152
fc_binary=None, jailer_binary=None):
153
"""Auxiliary function for instantiating a microvm and setting it up."""
154
# pylint: disable=redefined-outer-name
155
# The fixture pattern causes a pylint false positive for that rule.
156
microvm_id = str(uuid.uuid4())
157
158
# Update permissions for custom binaries.
159
if fc_binary is not None:
160
os.chmod(fc_binary, 0o555)
161
if jailer_binary is not None:
162
os.chmod(jailer_binary, 0o555)
163
164
if fc_binary is None or jailer_binary is None:
165
fc_binary, jailer_binary = build_tools.get_firecracker_binaries()
166
167
# Make sure we always have both binaries.
168
assert fc_binary
169
assert jailer_binary
170
171
vm = Microvm(
172
resource_path=root_path,
173
fc_binary_path=fc_binary,
174
jailer_binary_path=jailer_binary,
175
microvm_id=microvm_id,
176
bin_cloner_path=bin_cloner_path)
177
vm.setup()
178
return vm
179
180
181
def pytest_configure(config):
182
"""Pytest hook - initialization.
183
184
Initialize the test scheduler and IPC services.
185
"""
186
config.addinivalue_line("markers", "nonci: mark test as nonci.")
187
PytestScheduler.instance().register_mp_singleton(
188
net_tools.UniqueIPv4Generator.instance()
189
)
190
config.pluginmanager.register(PytestScheduler.instance())
191
192
193
def pytest_addoption(parser):
194
"""Pytest hook. Add command line options."""
195
parser.addoption(
196
"--dump-results-to-file",
197
action="store_true",
198
help="Flag to dump test results to the test_results folder.",
199
)
200
return PytestScheduler.instance().do_pytest_addoption(parser)
201
202
203
def test_session_root_path():
204
"""Create and return the testrun session root directory.
205
206
Testrun session root directory confines any other test temporary file.
207
If it exists, consider this as a noop.
208
"""
209
os.makedirs(defs.DEFAULT_TEST_SESSION_ROOT_PATH, exist_ok=True)
210
return defs.DEFAULT_TEST_SESSION_ROOT_PATH
211
212
213
@pytest.fixture(autouse=True, scope='session')
214
def test_fc_session_root_path():
215
"""Ensure and yield the fc session root directory.
216
217
Create a unique temporary session directory. This is important, since the
218
scheduler will run multiple pytest sessions concurrently.
219
"""
220
fc_session_root_path = tempfile.mkdtemp(
221
prefix="fctest-",
222
dir=f"{test_session_root_path()}"
223
)
224
yield fc_session_root_path
225
shutil.rmtree(fc_session_root_path)
226
227
228
@pytest.fixture
229
def test_session_tmp_path(test_fc_session_root_path):
230
"""Yield a random temporary directory. Destroyed on teardown."""
231
# pylint: disable=redefined-outer-name
232
# The fixture pattern causes a pylint false positive for that rule.
233
234
tmp_path = tempfile.mkdtemp(prefix=test_fc_session_root_path)
235
yield tmp_path
236
shutil.rmtree(tmp_path)
237
238
239
@pytest.fixture
240
def results_file_dumper(request):
241
"""Yield the custom --dump-results-to-file test flag."""
242
if request.config.getoption("--dump-results-to-file"):
243
return ResultsFileDumper(request.node.originalname)
244
245
return None
246
247
248
def _gcc_compile(src_file, output_file, extra_flags="-static -O3"):
249
"""Build a source file with gcc."""
250
compile_cmd = 'gcc {} -o {} {}'.format(
251
src_file,
252
output_file,
253
extra_flags
254
)
255
utils.run_cmd(compile_cmd)
256
257
258
@pytest.fixture(scope='session')
259
def bin_cloner_path(test_fc_session_root_path):
260
"""Build a binary that `clone`s into the jailer.
261
262
It's necessary because Python doesn't interface well with the `clone()`
263
syscall directly.
264
"""
265
# pylint: disable=redefined-outer-name
266
# The fixture pattern causes a pylint false positive for that rule.
267
cloner_bin_path = os.path.join(test_fc_session_root_path, 'newpid_cloner')
268
_gcc_compile(
269
'host_tools/newpid_cloner.c',
270
cloner_bin_path
271
)
272
yield cloner_bin_path
273
274
275
@pytest.fixture(scope='session')
276
def bin_vsock_path(test_fc_session_root_path):
277
"""Build a simple vsock client/server application."""
278
# pylint: disable=redefined-outer-name
279
# The fixture pattern causes a pylint false positive for that rule.
280
vsock_helper_bin_path = os.path.join(
281
test_fc_session_root_path,
282
'vsock_helper'
283
)
284
_gcc_compile(
285
'host_tools/vsock_helper.c',
286
vsock_helper_bin_path
287
)
288
yield vsock_helper_bin_path
289
290
291
@pytest.fixture(scope='session')
292
def change_net_config_space_bin(test_fc_session_root_path):
293
"""Build a binary that changes the MMIO config space."""
294
# pylint: disable=redefined-outer-name
295
change_net_config_space_bin = os.path.join(
296
test_fc_session_root_path,
297
'change_net_config_space'
298
)
299
_gcc_compile(
300
'host_tools/change_net_config_space.c',
301
change_net_config_space_bin,
302
extra_flags=""
303
)
304
yield change_net_config_space_bin
305
306
307
@pytest.fixture(scope='session')
308
def bin_seccomp_paths(test_fc_session_root_path):
309
"""Build jailers and jailed binaries to test seccomp.
310
311
They currently consist of:
312
313
* a jailer that receives filter generated using seccompiler-bin;
314
* a jailed binary that follows the seccomp rules;
315
* a jailed binary that breaks the seccomp rules.
316
"""
317
# pylint: disable=redefined-outer-name
318
# The fixture pattern causes a pylint false positive for that rule.
319
seccomp_build_path = os.path.join(
320
test_fc_session_root_path,
321
build_tools.CARGO_RELEASE_REL_PATH
322
)
323
324
extra_args = '--release --target {}-unknown-linux-musl'
325
extra_args = extra_args.format(platform.machine())
326
build_tools.cargo_build(seccomp_build_path,
327
extra_args=extra_args,
328
src_dir='integration_tests/security/demo_seccomp')
329
330
release_binaries_path = os.path.join(
331
test_fc_session_root_path,
332
build_tools.CARGO_RELEASE_REL_PATH,
333
build_tools.RELEASE_BINARIES_REL_PATH
334
)
335
336
demo_jailer = os.path.normpath(
337
os.path.join(
338
release_binaries_path,
339
'demo_jailer'
340
)
341
)
342
demo_harmless = os.path.normpath(
343
os.path.join(
344
release_binaries_path,
345
'demo_harmless'
346
)
347
)
348
demo_malicious = os.path.normpath(
349
os.path.join(
350
release_binaries_path,
351
'demo_malicious'
352
)
353
)
354
355
yield {
356
'demo_jailer': demo_jailer,
357
'demo_harmless': demo_harmless,
358
'demo_malicious': demo_malicious
359
}
360
361
362
@pytest.fixture()
363
def microvm(test_fc_session_root_path, bin_cloner_path):
364
"""Instantiate a microvm."""
365
# pylint: disable=redefined-outer-name
366
# The fixture pattern causes a pylint false positive for that rule.
367
368
# Make sure the necessary binaries are there before instantiating the
369
# microvm.
370
vm = init_microvm(test_fc_session_root_path, bin_cloner_path)
371
yield vm
372
vm.kill()
373
shutil.rmtree(os.path.join(test_fc_session_root_path, vm.id))
374
375
376
@pytest.fixture
377
def network_config():
378
"""Yield a UniqueIPv4Generator."""
379
yield net_tools.UniqueIPv4Generator.instance()
380
381
382
@pytest.fixture(
383
params=MICROVM_S3_FETCHER.list_microvm_images(
384
capability_filter=['*']
385
)
386
)
387
def test_microvm_any(request, microvm):
388
"""Yield a microvm that can have any image in the spec bucket.
389
390
A test case using this fixture will run for every microvm image.
391
392
When using a pytest parameterized fixture, a test case is created for each
393
parameter in the list. We generate the list dynamically based on the
394
capability filter. This will result in
395
`len(MICROVM_S3_FETCHER.list_microvm_images(capability_filter=['*']))`
396
test cases for each test that depends on this fixture, each receiving a
397
microvm instance with a different microvm image.
398
"""
399
# pylint: disable=redefined-outer-name
400
# The fixture pattern causes a pylint false positive for that rule.
401
402
MICROVM_S3_FETCHER.init_vm_resources(request.param, microvm)
403
yield microvm
404
405
406
@pytest.fixture
407
def test_multiple_microvms(
408
test_fc_session_root_path,
409
context,
410
bin_cloner_path
411
):
412
"""Yield one or more microvms based on the context provided.
413
414
`context` is a dynamically parameterized fixture created inside the special
415
function `pytest_generate_tests` and it holds a tuple containing the name
416
of the guest image used to spawn a microvm and the number of microvms
417
to spawn.
418
"""
419
# pylint: disable=redefined-outer-name
420
# The fixture pattern causes a pylint false positive for that rule.
421
microvms = []
422
(microvm_resources, how_many) = context
423
424
# When the context specifies multiple microvms, we use the first vm to
425
# populate the other ones by hardlinking its resources.
426
first_vm = init_microvm(test_fc_session_root_path, bin_cloner_path)
427
MICROVM_S3_FETCHER.init_vm_resources(
428
microvm_resources,
429
first_vm
430
)
431
microvms.append(first_vm)
432
433
# It is safe to do this as the dynamically generated fixture `context`
434
# asserts that the `how_many` parameter is always positive
435
# (i.e strictly greater than 0).
436
for _ in range(how_many - 1):
437
vm = init_microvm(test_fc_session_root_path, bin_cloner_path)
438
MICROVM_S3_FETCHER.hardlink_vm_resources(
439
microvm_resources,
440
first_vm,
441
vm
442
)
443
microvms.append(vm)
444
445
yield microvms
446
447
for i in range(how_many):
448
microvms[i].kill()
449
shutil.rmtree(os.path.join(test_fc_session_root_path, microvms[i].id))
450
451
452
def pytest_generate_tests(metafunc):
453
"""Implement customized parametrization scheme.
454
455
This is a special hook which is called by the pytest infrastructure when
456
collecting a test function. The `metafunc` contains the requesting test
457
context. Amongst other things, the `metafunc` provides the list of fixture
458
names that the calling test function is using. If we find a fixture that
459
is called `context`, we check the calling function through the
460
`metafunc.function` field for the `_pool_size` attribute which we
461
previously set with a decorator. Then we create the list of parameters
462
for this fixture.
463
The parameter will be a list of tuples of the form (cap, pool_size).
464
For each parameter from the list (i.e. tuple) a different test case
465
scenario will be created.
466
"""
467
if 'context' in metafunc.fixturenames:
468
# In order to create the params for the current fixture, we need the
469
# capability and the number of vms we want to spawn.
470
471
# 1. Look if the test function set the pool size through the decorator.
472
# If it did not, we set it to 1.
473
how_many = int(getattr(metafunc.function, '_pool_size', None))
474
assert how_many > 0
475
476
# 2. Check if the test function set the capability field through
477
# the decorator. If it did not, we set it to any.
478
cap = getattr(metafunc.function, '_capability', '*')
479
480
# 3. Before parametrization, get the list of images that have the
481
# desired capability. By parametrize-ing the fixture with it, we
482
# trigger tests cases for each of them.
483
image_list = MICROVM_S3_FETCHER.list_microvm_images(
484
capability_filter=[cap]
485
)
486
metafunc.parametrize(
487
'context',
488
[(item, how_many) for item in image_list],
489
ids=['{}, {} instance(s)'.format(
490
item, how_many
491
) for item in image_list]
492
)
493
494
495
TEST_MICROVM_CAP_FIXTURE_TEMPLATE = (
496
"@pytest.fixture("
497
" params=MICROVM_S3_FETCHER.list_microvm_images(\n"
498
" capability_filter=['CAP']\n"
499
" )\n"
500
")\n"
501
"def test_microvm_with_CAP(request, microvm):\n"
502
" MICROVM_S3_FETCHER.init_vm_resources(\n"
503
" request.param, microvm\n"
504
" )\n"
505
" yield microvm"
506
)
507
508
# To make test writing easy, we want to dynamically create fixtures with all
509
# capabilities present in the test microvm images bucket. `pytest` doesn't
510
# provide a way to do that outright, but luckily all of python is just lists of
511
# of lists and a cursor, so exec() works fine here.
512
for capability in MICROVM_S3_FETCHER.enum_capabilities():
513
TEST_MICROVM_CAP_FIXTURE = (
514
TEST_MICROVM_CAP_FIXTURE_TEMPLATE.replace('CAP', capability)
515
)
516
# pylint: disable=exec-used
517
# This is the most straightforward way to achieve this result.
518
exec(TEST_MICROVM_CAP_FIXTURE)
519
520