Skip to content

Commit c2ebbb4

Browse files
committed
tests: refactor test_vulnerabilities
Some further simplifications: - Simplify the "_host" tests as they will provide the same result in both A/B situations. - Add a second MicrovmFactory fixture with the "A" firecracker. - Add a uvm_any fixture that includes all CPU templates and also a boot/restored dimension. This decreases the need for separate tests. For example the guest test test_check_vulnerability_files_ab can now test all variants: 2 (restored/booted) * 3 (kernels) * 9 (cpu templates) = 54 tests Signed-off-by: Pablo Barbáchano <[email protected]>
1 parent 110e330 commit c2ebbb4

File tree

8 files changed

+213
-467
lines changed

8 files changed

+213
-467
lines changed

tests/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,14 @@ that are pre-initialized with specific guest kernels and rootfs:
306306
24.04 squashfs as rootfs,
307307
- `uvm_plain` yields a Firecracker process pre-initialized with a 5.10 kernel
308308
and the same Ubuntu 24.04 squashfs.
309-
310-
Generally, tests should use the former if you are testing some interaction
311-
between the guest and Firecracker, while the latter should be used if
312-
Firecracker functionality unrelated to the guest is being tested.
309+
- `uvm_any` yields started microvms, parametrized by all supported kernels, all
310+
CPU templates (static, custom and none), and either booted or restored from a
311+
snapshot.
312+
- `uvm_any_booted` works the same as `uvm_any`, but only for booted VMs.
313+
314+
Generally, tests should use `uvm_plain_any` if you are testing some interaction
315+
between the guest and Firecracker, and `uvm_plain` should be used if Firecracker
316+
functionality unrelated to the guest is being tested.
313317
314318
### Markers
315319

tests/conftest.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,38 @@ def uvm_with_initrd(
459459
uvm = microvm_factory.build(guest_kernel_linux_5_10)
460460
uvm.initrd_file = fs
461461
yield uvm
462+
463+
464+
def uvm_booted(microvm_factory, guest_kernel, rootfs, cpu_template):
465+
"""Return a booted uvm"""
466+
uvm = microvm_factory.build(guest_kernel, rootfs)
467+
uvm.spawn()
468+
uvm.basic_config(vcpu_count=2, mem_size_mib=256)
469+
uvm.set_cpu_template(cpu_template)
470+
uvm.add_net_iface()
471+
uvm.start()
472+
return uvm
473+
474+
475+
def uvm_restored(microvm_factory, guest_kernel, rootfs, cpu_template):
476+
"""Return a restored uvm"""
477+
uvm = uvm_booted(microvm_factory, guest_kernel, rootfs, cpu_template)
478+
snapshot = uvm.snapshot_full()
479+
uvm.kill()
480+
uvm2 = microvm_factory.build()
481+
uvm2.spawn()
482+
uvm2.restore_from_snapshot(snapshot, resume=True)
483+
uvm2.cpu_template_name = uvm.cpu_template_name
484+
return uvm2
485+
486+
487+
@pytest.fixture(params=[uvm_booted, uvm_restored])
488+
def uvm_ctor(request):
489+
"""Fixture to return uvms with different constructors"""
490+
return request.param
491+
492+
493+
@pytest.fixture
494+
def uvm_any(microvm_factory, uvm_ctor, guest_kernel, rootfs, cpu_template_any):
495+
"""Return booted and restored uvms"""
496+
return uvm_ctor(microvm_factory, guest_kernel, rootfs, cpu_template_any)

tests/framework/ab_test.py

Lines changed: 4 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
of both invocations is the same, the test passes (with us being alerted to this situtation via a special pipeline that
2222
does not block PRs). If not, it fails, preventing PRs from introducing new vulnerable dependencies.
2323
"""
24-
import os
2524
import statistics
2625
from pathlib import Path
2726
from tempfile import TemporaryDirectory
@@ -31,14 +30,14 @@
3130

3231
from framework import utils
3332
from framework.defs import FC_WORKSPACE_DIR
34-
from framework.microvm import Microvm
33+
from framework.properties import global_props
3534
from framework.utils import CommandReturn
3635
from framework.with_filelock import with_filelock
37-
from host_tools.cargo_build import DEFAULT_TARGET_DIR, get_firecracker_binaries
36+
from host_tools.cargo_build import DEFAULT_TARGET_DIR
3837

3938
# Locally, this will always compare against main, even if we try to merge into, say, a feature branch.
4039
# We might want to do a more sophisticated way to determine a "parent" branch here.
41-
DEFAULT_A_REVISION = os.environ.get("BUILDKITE_PULL_REQUEST_BASE_BRANCH") or "main"
40+
DEFAULT_A_REVISION = global_props.buildkite_revision_a or "main"
4241

4342

4443
T = TypeVar("T")
@@ -120,11 +119,6 @@ def binary_ab_test(
120119
return result_a, result_b, comparator(result_a, result_b)
121120

122121

123-
def is_pr() -> bool:
124-
"""Returns `True` iff we are executing in the context of a build kite run on a pull request"""
125-
return os.environ.get("BUILDKITE_PULL_REQUEST", "false") != "false"
126-
127-
128122
def git_ab_test_host_command_if_pr(
129123
command: str,
130124
*,
@@ -134,7 +128,7 @@ def git_ab_test_host_command_if_pr(
134128
"""Runs the given bash command as an A/B-Test if we're in a pull request context (asserting that its stdout and
135129
stderr did not change across the PR). Otherwise runs the command, asserting it returns a zero exit code
136130
"""
137-
if is_pr():
131+
if global_props.buildkite_pr:
138132
git_ab_test_host_command(command, comparator=comparator)
139133
return None
140134

@@ -176,56 +170,6 @@ def set_did_not_grow_comparator(
176170
)
177171

178172

179-
def precompiled_ab_test_guest_command(
180-
microvm_factory: Callable[[Path, Path], Microvm],
181-
command: str,
182-
*,
183-
comparator: Callable[[CommandReturn, CommandReturn], bool] = default_comparator,
184-
a_revision: str = DEFAULT_A_REVISION,
185-
b_revision: Optional[str] = None,
186-
):
187-
"""The same as git_ab_test_command, but via SSH. The closure argument should setup a microvm using the passed
188-
paths to firecracker and jailer binaries."""
189-
b_directory = (
190-
DEFAULT_B_DIRECTORY
191-
if b_revision is None
192-
else FC_WORKSPACE_DIR / "build" / b_revision
193-
)
194-
195-
def test_runner(bin_dir, _is_a: bool):
196-
microvm = microvm_factory(bin_dir / "firecracker", bin_dir / "jailer")
197-
return microvm.ssh.run(command)
198-
199-
(_, old_out, old_err), (_, new_out, new_err), the_same = binary_ab_test(
200-
test_runner,
201-
comparator,
202-
a_directory=FC_WORKSPACE_DIR / "build" / a_revision,
203-
b_directory=b_directory,
204-
)
205-
206-
assert (
207-
the_same
208-
), f"The output of running command `{command}` changed:\nOld:\nstdout:\n{old_out}\nstderr\n{old_err}\n\nNew:\nstdout:\n{new_out}\nstderr:\n{new_err}"
209-
210-
211-
def precompiled_ab_test_guest_command_if_pr(
212-
microvm_factory: Callable[[Path, Path], Microvm],
213-
command: str,
214-
*,
215-
comparator=default_comparator,
216-
check_in_nonpr=True,
217-
):
218-
"""The same as git_ab_test_command_if_pr, but via SSH"""
219-
if is_pr():
220-
precompiled_ab_test_guest_command(
221-
microvm_factory, command, comparator=comparator
222-
)
223-
return None
224-
225-
microvm = microvm_factory(*get_firecracker_binaries())
226-
return microvm.ssh.run(command, check=check_in_nonpr)
227-
228-
229173
def check_regression(
230174
a_samples: List[float], b_samples: List[float], *, n_resamples: int = 9999
231175
):

tests/framework/microvm.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ def __init__(
244244
self.disks_vhost_user = {}
245245
self.vcpus_count = None
246246
self.mem_size_bytes = None
247+
self.cpu_template_name = None
247248

248249
self._pre_cmd = []
249250
if numa_node:
@@ -735,12 +736,14 @@ def basic_config(
735736
smt=smt,
736737
mem_size_mib=mem_size_mib,
737738
track_dirty_pages=track_dirty_pages,
738-
cpu_template=cpu_template,
739739
huge_pages=huge_pages,
740740
)
741741
self.vcpus_count = vcpu_count
742742
self.mem_size_bytes = mem_size_mib * 2**20
743743

744+
if cpu_template is not None:
745+
self.set_cpu_template(cpu_template)
746+
744747
if self.memory_monitor:
745748
self.memory_monitor.start()
746749

@@ -773,6 +776,19 @@ def basic_config(
773776
if enable_entropy_device:
774777
self.enable_entropy_device()
775778

779+
def set_cpu_template(self, cpu_template):
780+
"""Set guest CPU template."""
781+
if cpu_template is None:
782+
return
783+
# static CPU template
784+
if isinstance(cpu_template, str):
785+
self.api.machine_config.patch(cpu_template=cpu_template)
786+
self.cpu_template_name = cpu_template.lower()
787+
# custom CPU template
788+
elif isinstance(cpu_template, dict):
789+
self.api.cpu_config.put(**cpu_template["template"])
790+
self.cpu_template_name = cpu_template["name"].lower()
791+
776792
def add_drive(
777793
self,
778794
drive_id,

tests/framework/properties.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,14 @@ def __init__(self):
7272
# major.minor.patch
7373
self.host_linux_patch = get_kernel_version(2)
7474
self.os = get_os_version()
75-
self.host_os = get_host_os()
75+
self.host_os = get_host_os() or "NA"
7676
self.libc_ver = "-".join(platform.libc_ver())
7777
self.rust_version = run_cmd("rustc --version |awk '{print $2}'")
78+
# Buildkite/PR information
7879
self.buildkite_pipeline_slug = os.environ.get("BUILDKITE_PIPELINE_SLUG")
7980
self.buildkite_build_number = os.environ.get("BUILDKITE_BUILD_NUMBER")
81+
self.buildkite_pr = os.environ.get("BUILDKITE_PULL_REQUEST", "false") != "false"
82+
self.buildkite_revision_a = os.environ.get("BUILDKITE_PULL_REQUEST_BASE_BRANCH")
8083

8184
if self._in_git_repo():
8285
self.git_commit_id = run_cmd("git rev-parse HEAD")

tests/framework/utils_cpu_templates.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
"""Utilities for CPU template related functionality."""
55

6+
# pylint:disable=too-many-return-statements
7+
68
import json
79
from pathlib import Path
810

@@ -23,9 +25,7 @@ def get_supported_cpu_templates():
2325
"""
2426
Return the list of CPU templates supported by the platform.
2527
"""
26-
# pylint:disable=too-many-return-statements
2728
host_linux = global_props.host_linux_version_tpl
28-
2929
match get_cpu_vendor(), global_props.cpu_codename:
3030
# T2CL template is only supported on Cascade Lake and newer CPUs.
3131
case CpuVendor.INTEL, CpuModel.INTEL_SKYLAKE:

tests/integration_tests/functional/test_net.py

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,23 @@ def test_multi_queue_unsupported(uvm_plain):
8484
)
8585

8686

87-
def run_udp_offload_test(vm):
87+
@pytest.fixture
88+
def uvm_any(microvm_factory, uvm_ctor, guest_kernel, rootfs):
89+
"""Return booted and restored uvm with no CPU templates"""
90+
return uvm_ctor(microvm_factory, guest_kernel, rootfs, None)
91+
92+
93+
def test_tap_offload(uvm_any):
8894
"""
95+
Verify that tap offload features are configured for a booted/restored VM.
96+
8997
- Start a socat UDP server in the guest.
9098
- Try to send a UDP message with UDP offload enabled.
9199
92100
If tap offload features are not configured, an attempt to send a message will fail with EIO "Input/output error".
93101
More info (search for "TUN_F_CSUM is a must"): https://blog.cloudflare.com/fr-fr/virtual-networking-101-understanding-tap/
94102
"""
103+
vm = uvm_any
95104
port = "81"
96105
out_filename = "/tmp/out.txt"
97106
message = "x"
@@ -112,35 +121,3 @@ def run_udp_offload_test(vm):
112121
# Check that the server received the message
113122
ret = vm.ssh.run(f"cat {out_filename}")
114123
assert ret.stdout == message, f"{ret.stdout=} {ret.stderr=}"
115-
116-
117-
def test_tap_offload_booted(uvm_plain_any):
118-
"""
119-
Verify that tap offload features are configured for a booted VM.
120-
"""
121-
vm = uvm_plain_any
122-
vm.spawn()
123-
vm.basic_config()
124-
vm.add_net_iface()
125-
vm.start()
126-
127-
run_udp_offload_test(vm)
128-
129-
130-
def test_tap_offload_restored(microvm_factory, guest_kernel, rootfs):
131-
"""
132-
Verify that tap offload features are configured for a restored VM.
133-
"""
134-
src = microvm_factory.build(guest_kernel, rootfs, monitor_memory=False)
135-
src.spawn()
136-
src.basic_config()
137-
src.add_net_iface()
138-
src.start()
139-
snapshot = src.snapshot_full()
140-
src.kill()
141-
142-
dst = microvm_factory.build()
143-
dst.spawn()
144-
dst.restore_from_snapshot(snapshot, resume=True)
145-
146-
run_udp_offload_test(dst)

0 commit comments

Comments
 (0)