-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Expand file tree
/
Copy pathmicrovm.py
More file actions
1464 lines (1229 loc) · 51 KB
/
Copy pathmicrovm.py
File metadata and controls
1464 lines (1229 loc) · 51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Classes for working with microVMs.
This module defines `Microvm`, which can be used to create, test drive, and
destroy microvms.
- Use the Firecracker Open API spec to populate Microvm API resource URLs.
"""
# pylint:disable=too-many-lines
import json
import logging
import os
import re
import select
import shutil
import signal
import time
import uuid
from collections import namedtuple
from dataclasses import dataclass
from enum import Enum, auto
from functools import cached_property, lru_cache
from pathlib import Path
from typing import Optional
import psutil
from tenacity import Retrying, retry, stop_after_attempt, wait_fixed
import host_tools.cargo_build as build_tools
import host_tools.network as net_tools
from framework import utils
from framework.defs import DEFAULT_BINARY_DIR, MAX_API_CALL_DURATION_MS
from framework.guest import GuestDistro
from framework.http_api import Api
from framework.jailer import JailerContext
from framework.microvm_helpers import MicrovmHelpers
from framework.properties import global_props
from framework.utils_cpu_templates import get_cpu_template_name
from framework.utils_drive import VhostUserBlkBackend, VhostUserBlkBackendType
from framework.utils_uffd import spawn_pf_handler, uffd_handler
from host_tools.fcmetrics import FCMetricsMonitor
from host_tools.memory import MemoryMonitor
LOG = logging.getLogger("microvm")
class SnapshotType(Enum):
"""Supported snapshot types."""
FULL = auto()
DIFF = auto()
DIFF_MINCORE = auto()
def __repr__(self):
cls_name = self.__class__.__name__
return f"{cls_name}.{self.name}"
@property
def needs_rebase(self) -> bool:
"""Does this snapshot type need rebasing on top of a base snapshot before restoration?"""
return self in [SnapshotType.DIFF, SnapshotType.DIFF_MINCORE]
@property
def needs_dirty_page_tracking(self) -> bool:
"""Does taking this snapshot type require dirty page tracking to be enabled?"""
return self == SnapshotType.DIFF
@property
def api_type(self) -> str:
"""Converts this `SnapshotType` to the string value expected by the Firecracker API"""
match self:
case SnapshotType.FULL:
return "Full"
case SnapshotType.DIFF | SnapshotType.DIFF_MINCORE:
return "Diff"
def hardlink_or_copy(src, dst):
"""If src and dst are in the same device, hardlink. Otherwise, copy."""
dst.touch(exist_ok=False)
if dst.stat().st_dev == src.stat().st_dev:
dst.unlink()
dst.hardlink_to(src)
else:
shutil.copyfile(src, dst)
@dataclass(frozen=True, repr=True)
class Snapshot:
"""A Firecracker snapshot"""
vmstate: Path
mem: Path
net_ifaces: list
disks: dict
ssh_key: Path
snapshot_type: SnapshotType
meta: dict
def rebase_snapshot(
self, base, use_snapshot_editor=False, binary_dir=DEFAULT_BINARY_DIR
):
"""Rebases current incremental snapshot onto a specified base layer."""
if not self.snapshot_type.needs_rebase:
raise ValueError(f"Cannot rebase {self.snapshot_type}")
if use_snapshot_editor:
build_tools.run_snap_editor_rebase(
base.mem, self.mem, binary_dir=binary_dir
)
else:
build_tools.run_rebase_snap_bin(base.mem, self.mem)
new_args = self.__dict__ | {"mem": base.mem}
return Snapshot(**new_args)
def copy_to_chroot(self, chroot) -> "Snapshot":
"""
Move all the snapshot files into the microvm jail.
Use different names so a snapshot doesn't overwrite our original snapshot.
"""
mem_src = chroot / self.mem.with_suffix(".src").name
hardlink_or_copy(self.mem, mem_src)
vmstate_src = chroot / self.vmstate.with_suffix(".src").name
hardlink_or_copy(self.vmstate, vmstate_src)
return Snapshot(
vmstate=vmstate_src,
mem=mem_src,
net_ifaces=self.net_ifaces,
disks=self.disks,
ssh_key=self.ssh_key,
snapshot_type=self.snapshot_type,
meta=self.meta,
)
@classmethod
# TBD when Python 3.11: -> Self
def load_from(cls, src: Path) -> "Snapshot":
"""Load a snapshot saved with `save_to`"""
snap_json = src / "snapshot.json"
obj = json.loads(snap_json.read_text())
return cls(
vmstate=src / obj["vmstate"],
mem=src / obj["mem"],
net_ifaces=[net_tools.NetIfaceConfig(**d) for d in obj["net_ifaces"]],
disks={dsk: src / p for dsk, p in obj["disks"].items()},
ssh_key=src / obj["ssh_key"],
snapshot_type=SnapshotType(obj["snapshot_type"]),
meta=obj["meta"],
)
def save_to(self, dst: Path):
"""Serialize snapshot details to `dst`
Deserialize the snapshot with `load_from`
"""
for path in [self.vmstate, self.mem, self.ssh_key]:
new_path = dst / path.name
hardlink_or_copy(path, new_path)
new_disks = {}
for disk_id, path in self.disks.items():
new_path = dst / path.name
hardlink_or_copy(path, new_path)
new_disks[disk_id] = new_path.name
obj = {
"vmstate": self.vmstate.name,
"mem": self.mem.name,
"net_ifaces": [x.__dict__ for x in self.net_ifaces],
"disks": new_disks,
"ssh_key": self.ssh_key.name,
"snapshot_type": self.snapshot_type.value,
"meta": self.meta,
}
snap_json = dst / "snapshot.json"
snap_json.write_text(json.dumps(obj))
def delete(self):
"""Delete the backing files from disk."""
self.mem.unlink()
self.vmstate.unlink()
class HugePagesConfig(str, Enum):
"""Enum describing the huge pages configurations supported Firecracker"""
NONE = "None"
HUGETLBFS_2MB = "2M"
# pylint: disable=R0904
class Microvm:
"""Class to represent a Firecracker microvm.
A microvm is described by a unique identifier, a path to all the resources
it needs in order to be able to start and the binaries used to spawn it.
Besides keeping track of microvm resources and exposing microvm API
methods, `spawn()` and `kill()` can be used to start/end the microvm
process.
"""
def __init__(
self,
microvm_id: str,
fc_binary_path: Path,
jailer_binary_path: Path,
netns: net_tools.NetNs,
monitor_memory: bool = True,
jailer_kwargs: Optional[dict] = None,
numa_node=None,
custom_cpu_template: Path = None,
pci: bool = False,
):
"""Set up microVM attributes, paths, and data structures."""
# pylint: disable=too-many-statements
# Unique identifier for this machine.
assert microvm_id is not None
self._microvm_id = microvm_id
self.kernel_file = None
self.rootfs_file = None
self.distro = None
self.ssh_key = None
self.initrd_file = None
self.boot_args = None
self.uffd_handler = None
self.fc_binary_path = Path(fc_binary_path)
assert fc_binary_path.exists()
self.jailer_binary_path = Path(jailer_binary_path)
assert jailer_binary_path.exists()
jailer_kwargs = jailer_kwargs or {}
self.netns = netns
# Create the jailer context associated with this microvm.
self.jailer = JailerContext(
jailer_id=self._microvm_id,
exec_file=self.fc_binary_path,
netns=netns,
new_pid_ns=True,
**jailer_kwargs,
)
self.pci_enabled = pci
if pci:
self.jailer.extra_args["enable-pci"] = None
# Copy the /etc/localtime file in the jailer root
self.jailer.jailed_path("/etc/localtime", subdir="etc")
self._screen_pid = None
self.time_api_requests = global_props.host_linux_version != "6.1"
# disable the HTTP API timings as they cause a lot of false positives
if int(os.environ.get("PYTEST_XDIST_WORKER_COUNT", 1)) > 1:
self.time_api_requests = False
self.monitors = []
self.memory_monitor = None
if monitor_memory:
self.memory_monitor = MemoryMonitor(self)
self.monitors.append(self.memory_monitor)
self.api = None
self.log_file = None
self.serial_out_path = None
self.metrics_file = None
self._spawned = False
self._killed = False
# device dictionaries
self.iface = {}
self.disks = {}
self.disks_vhost_user = {}
self.vcpus_count = None
self.mem_size_bytes = None
self.cpu_template_name = "None"
# The given custom CPU template will be set in basic_config() but could
# be overwritten via set_cpu_template().
self.custom_cpu_template = custom_cpu_template
self._connections = []
self._pre_cmd = []
if numa_node:
node_str = str(numa_node)
self.add_pre_cmd([["numactl", "-N", node_str, "-m", node_str]])
# MMDS content from file
self.metadata_file = None
self.help = MicrovmHelpers(self)
self.gdb_socket = None
def __repr__(self):
return f"<Microvm id={self.id}>"
def mark_killed(self):
"""
Marks this `Microvm` as killed, meaning test tear down should not try to kill it
raises an exception if the Firecracker process managing this VM is not actually dead
"""
if self.firecracker_pid is not None:
utils.wait_process_termination(self.firecracker_pid)
self._killed = True
def kill(self, might_be_dead=False):
"""All clean up associated with this microVM should go here."""
# pylint: disable=subprocess-run-check
# if it was already killed, return
if self._killed:
return
# Stop any registered monitors
for monitor in self.monitors:
monitor.stop()
# Kill all background SSH connections
for connection in self._connections:
connection.close(strict=not might_be_dead)
# We start with vhost-user backends,
# because if we stop Firecracker first, the backend will want
# to exit as well and this will cause a race condition.
for backend in self.disks_vhost_user.values():
backend.kill()
self.disks_vhost_user.clear()
assert (
"Shutting down VM after intercepting signal" not in self.log_data
or might_be_dead
), self.log_data
# pylint: disable=bare-except
try:
if self.firecracker_pid:
os.kill(self.firecracker_pid, signal.SIGKILL)
if self.screen_pid:
os.kill(self.screen_pid, signal.SIGKILL)
except:
if not might_be_dead:
msg = (
"Failed to kill Firecracker Process. Did it already die (or did the UFFD handler process die and take it down)?"
if self.uffd_handler
else "Failed to kill Firecracker Process. Did it already die?"
)
self._dump_debug_information(msg)
raise
# if microvm was spawned then check if it gets killed
if self._spawned:
# Wait until the Firecracker process is actually dead
utils.wait_process_termination(self.firecracker_pid)
# The following logic guards us against the case where `firecracker_pid` for some
# reason is the wrong PID, e.g. this is a regression test for
# https://github.com/firecracker-microvm/firecracker/pull/4442/commits/d63eb7a65ffaaae0409d15ed55d99ecbd29bc572
# filter ps results for the jailer's unique id
_, stdout, stderr = utils.run_cmd(
f"ps ax -o pid,cmd -ww | grep {self.jailer.jailer_id}"
)
assert not stderr, f"error querying processes using `ps`: {stderr}"
offenders = []
for proc in stdout.splitlines():
_, cmd = proc.lower().split(maxsplit=1)
if "firecracker" in proc and not cmd.startswith("screen"):
offenders.append(proc)
# make sure firecracker was killed
assert not offenders, (
f"Firecracker reported its pid {self.firecracker_pid}, which was killed, but there still exist processes using the supposedly dead Firecracker's jailer_id: \n"
+ "\n".join(offenders)
)
if self.uffd_handler and self.uffd_handler.is_running():
self.uffd_handler.kill()
# Mark the microVM as not spawned, so we avoid trying to kill twice.
self._spawned = False
self._killed = True
if self.time_api_requests:
self._validate_api_response_times()
if self.memory_monitor:
self.memory_monitor.check_samples()
def _validate_api_response_times(self):
"""
Parses the firecracker logs for information regarding api server request processing times, and asserts they
are within acceptable bounds.
"""
# Log messages are either
# 2023-06-16T07:45:41.767987318 [fc44b23e-ce47-4635-9549-5779a6bd9cee:fc_api] The API server received a Get request on "/mmds".
# or
# 2023-06-16T07:47:31.204704732 [2f2427c7-e4de-4226-90e6-e3556402be84:fc_api] The API server received a Put request on "/actions" with body "{\"action_type\": \"InstanceStart\"}".
api_request_regex = re.compile(
r"\] The API server received a (?P<method>\w+) request on \"(?P<url>(/(\w|-)*)+)\"( with body (?P<body>.*))?\."
)
api_request_times_regex = re.compile(
r"\] Total previous API call duration: (?P<execution_time>\d+) us.$"
)
# Note: Processing of api requests is synchronous, so these messages cannot be torn by concurrency effects
log_lines = self.log_data.split("\n")
ApiCall = namedtuple("ApiCall", "method url body")
current_call = None
for log_line in log_lines:
match = api_request_regex.search(log_line)
if match:
if current_call is not None:
raise Exception(
f"API call duration log entry for {current_call.method} {current_call.url} with body {current_call.body} is missing!"
)
current_call = ApiCall(
match.group("method"), match.group("url"), match.group("body")
)
match = api_request_times_regex.search(log_line)
if match:
if current_call is None:
raise Exception(
"Got API call duration log entry before request entry"
)
if current_call.url not in ["/snapshot/create", "/snapshot/load"]:
exec_time = float(match.group("execution_time")) / 1000.0
assert (
exec_time <= MAX_API_CALL_DURATION_MS
), f"{current_call.method} {current_call.url} API call exceeded maximum duration: {exec_time} ms. Body: {current_call.body}"
current_call = None
@property
def firecracker_version(self):
"""Return the version of the Firecracker executable."""
_, stdout, _ = utils.check_output(f"{self.fc_binary_path} --version")
return re.match(r"^Firecracker v(.+)", stdout.partition("\n")[0]).group(1)
@property
def path(self):
"""Return the path on disk used that represents this microVM."""
return self.jailer.chroot_base_with_id()
# some functions use this
fsfiles = path
@property
def id(self):
"""Return the unique identifier of this microVM."""
return self._microvm_id
@property
def log_data(self):
"""Return the log data."""
if self.log_file is None:
return ""
return self.log_file.read_text()
@property
def state(self):
"""Get the InstanceInfo property and return the state field."""
return self.api.describe.get().json()["state"]
@cached_property
def firecracker_pid(self):
"""Return Firecracker's PID
Reads the pid from a file created by jailer.
"""
if not self._spawned:
return None
# Read the PID from Firecracker's pidfile. Retry if
# file doesn't exist yet, or doesn't yet contain an integer
for attempt in Retrying(
stop=stop_after_attempt(5),
wait=wait_fixed(0.1),
reraise=True,
):
with attempt:
return int(self.jailer.pid_file.read_text(encoding="ascii"))
@cached_property
def ps(self):
"""Returns a handle to the psutil.Process for this VM"""
return psutil.Process(self.firecracker_pid)
@property
def dimensions(self):
"""Gets a default set of cloudwatch dimensions describing the configuration of this microvm"""
return {
"instance": global_props.instance,
"cpu_model": global_props.cpu_model,
"host_kernel": f"linux-{global_props.host_linux_version}",
"guest_kernel": self.kernel_file.stem[2:],
"rootfs": self.rootfs_file.name,
"vcpus": str(self.vcpus_count),
"guest_memory": f"{self.mem_size_bytes / (1024 * 1024)}MB",
"pci": f"{self.pci_enabled}",
}
@property
def guest_kernel_version(self):
"""Get the guest kernel version from the filename
It won't work if the file name does not like name-X.Y.Z
"""
splits = self.kernel_file.name.split("-")
if len(splits) < 2:
return None
return tuple(int(x) for x in splits[1].split("."))
def get_metrics(self):
"""Return iterator to metric data points written by FC"""
with self.metrics_file.open() as fd:
for line in fd:
if not line.endswith("}\n"):
LOG.warning("Line is not a proper JSON object. Partial write?")
continue
yield json.loads(line)
def get_all_metrics(self):
"""Return all metric data points written by FC."""
return list(self.get_metrics())
def flush_metrics(self):
"""Flush the microvm metrics and get the latest datapoint"""
self.api.actions.put(action_type="FlushMetrics")
# get the latest metrics
return self.get_all_metrics()[-1]
def create_jailed_resource(self, path):
"""Create a hard link to some resource inside this microvm."""
return self.jailer.jailed_path(path, create=True)
def get_jailed_resource(self, path):
"""Get the relative jailed path to a resource."""
return self.jailer.jailed_path(path, create=False)
def chroot(self):
"""Get the chroot of this microVM."""
return self.jailer.chroot_path()
@property
def screen_session(self):
"""The screen session name
The id of this microVM, which should be unique.
"""
return self.id
@property
def screen_log(self):
"""Get the screen log file."""
return f"/tmp/screen-{self.screen_session}.log"
@property
def screen_pid(self) -> Optional[int]:
"""Get the screen PID."""
if self._screen_pid:
return int(self._screen_pid)
return None
def pin_vmm(self, cpu_id: int) -> bool:
"""Pin the firecracker process VMM thread to a cpu list."""
if self.firecracker_pid:
for thread_name, thread_pids in utils.get_threads(
self.firecracker_pid
).items():
# the firecracker thread should start with firecracker...
if thread_name.startswith("firecracker"):
for pid in thread_pids:
utils.set_cpu_affinity(pid, [cpu_id])
return True
return False
def pin_vcpu(self, vcpu_id: int, cpu_id: int):
"""Pin the firecracker vcpu thread to a cpu list."""
if self.firecracker_pid:
for thread in utils.get_threads(self.firecracker_pid)[f"fc_vcpu {vcpu_id}"]:
utils.set_cpu_affinity(thread, [cpu_id])
return True
return False
def pin_api(self, cpu_id: int):
"""Pin the firecracker process API server thread to a cpu list."""
if self.firecracker_pid:
for thread in utils.get_threads(self.firecracker_pid)["fc_api"]:
utils.set_cpu_affinity(thread, [cpu_id])
return True
return False
def pin_threads(self, first_cpu):
"""
Pins all microvm threads (VMM, API and vCPUs) to consecutive physical cpu core, starting with "first_cpu"
Return next "free" cpu core.
"""
for vcpu, pcpu in enumerate(range(first_cpu, first_cpu + self.vcpus_count)):
assert self.pin_vcpu(
vcpu, pcpu
), f"Failed to pin fc_vcpu {vcpu} thread to core {pcpu}."
# The cores first_cpu,...,first_cpu + self.vcpus_count - 1 are assigned to the individual vCPU threads,
# So the remaining two threads (VMM and API) get first_cpu + self.vcpus_count
# and first_cpu + self.vcpus_count + 1
assert self.pin_vmm(
first_cpu + self.vcpus_count
), "Failed to pin firecracker thread."
assert self.pin_api(
first_cpu + self.vcpus_count + 1
), "Failed to pin fc_api thread."
return first_cpu + self.vcpus_count + 2
def add_pre_cmd(self, pre_cmd):
"""Prepends commands to the command line to launch the microVM
For example, this can be used to pin the VM to a NUMA node or to trace the VM with strace.
"""
self._pre_cmd = pre_cmd + self._pre_cmd
def spawn(
self,
log_file="fc.log",
serial_out_path="serial.log",
log_level="Debug",
log_show_level=False,
log_show_origin=False,
metrics_path="fc.ndjson",
emit_metrics: bool = False,
validate_api: bool = True,
):
"""Start a microVM as a daemon or in a screen session."""
# pylint: disable=subprocess-run-check
# pylint: disable=too-many-branches
self.jailer.setup()
self.api = Api(
self.jailer.api_socket_path(),
validate=validate_api,
on_error=lambda verb, uri, err_msg: self._dump_debug_information(
f"Error during {verb} {uri}: {err_msg}"
),
)
if log_file is not None:
self.log_file = Path(self.path) / log_file
self.log_file.touch()
self.create_jailed_resource(self.log_file)
# The default value for `level`, when configuring the logger via cmd
# line, is `Info`. We set the level to `Debug` to also have the boot
# time printed in the log.
self.jailer.extra_args.update({"log-path": log_file, "level": log_level})
if log_show_level:
self.jailer.extra_args["show-level"] = None
if log_show_origin:
self.jailer.extra_args["show-log-origin"] = None
if serial_out_path is not None:
self.serial_out_path = Path(self.path) / serial_out_path
self.serial_out_path.touch()
self.create_jailed_resource(self.serial_out_path)
if metrics_path is not None:
self.metrics_file = Path(self.path) / metrics_path
self.metrics_file.touch()
self.create_jailed_resource(self.metrics_file)
self.jailer.extra_args.update({"metrics-path": self.metrics_file.name})
else:
assert not emit_metrics
if self.metadata_file:
if os.path.exists(self.metadata_file):
LOG.debug("metadata file exists, adding as a jailed resource")
self.create_jailed_resource(self.metadata_file)
self.jailer.extra_args.update(
{"metadata": os.path.basename(self.metadata_file)}
)
if log_level != "Debug":
# Checking the timings requires DEBUG level log messages
self.time_api_requests = False
cmd = [
*self._pre_cmd,
str(self.jailer_binary_path),
*self.jailer.construct_param_list(),
]
# When the daemonize flag is on, we want to clone-exec into the
# jailer rather than executing it via spawning a shell.
if self.jailer.daemonize:
utils.check_output(cmd, shell=False)
else:
# Run Firecracker under screen. This is used when we want to access
# the serial console. The file will collect the output from
# 'screen'ed Firecracker.
screen_pid = utils.start_screen_process(
self.screen_log,
self.screen_session,
cmd[0],
cmd[1:],
)
self._screen_pid = screen_pid
# If `--new-pid-ns` is used, the Firecracker process will detach from
# the screen and the screen process will exit. We do not want to
# attempt to kill it in that case to avoid a race condition.
if self.jailer.new_pid_ns:
self._screen_pid = None
self._spawned = True
if emit_metrics:
self.monitors.append(FCMetricsMonitor(self))
# Ensure Firecracker is in as good a state as possible wrts guest
# responsiveness / API availability.
# If we are using a config file and it has a network device specified,
# use SSH to wait until guest userspace is available. If we are
# using the API, wait until the log message indicating the API server
# has finished initializing is printed (if logging is enabled), or
# until the API socket file has been created.
# If none of these apply, do a last ditch effort to make sure the
# Firecracker process itself at least came up by checking
# for the startup log message. Otherwise, you're on your own kid.
if "config-file" in self.jailer.extra_args and self.iface:
assert not serial_out_path
self.wait_for_ssh_up()
elif "no-api" not in self.jailer.extra_args:
if self.log_file and log_level in ("Trace", "Debug", "Info"):
self.check_log_message("API server started.")
else:
self._wait_for_api_socket()
if serial_out_path is not None:
self.api.serial.put(serial_out_path=serial_out_path)
elif self.log_file and log_level in ("Trace", "Debug", "Info"):
assert not serial_out_path
self.check_log_message("Running Firecracker")
@retry(wait=wait_fixed(0.2), stop=stop_after_attempt(5), reraise=True)
def _wait_for_api_socket(self):
"""Wait until the API socket and chroot folder are available."""
# We expect the jailer to start within 80 ms. However, we wait for
# 1 sec since we are rechecking the existence of the socket 5 times
# and leave 0.2 delay between them.
os.stat(self.jailer.api_socket_path())
@retry(wait=wait_fixed(0.2), stop=stop_after_attempt(5), reraise=True)
def check_log_message(self, message):
"""Wait until `message` appears in logging output."""
assert (
message in self.log_data
), f'Message ("{message}") not found in log data ("{self.log_data}").'
@retry(wait=wait_fixed(0.2), stop=stop_after_attempt(5), reraise=True)
def get_exit_code(self):
"""Get exit code from logging output"""
exit_msg_pattern = (
r"Firecracker exiting (with error|successfully). exit_code=(\d+)"
)
match = re.search(exit_msg_pattern, self.log_data)
if match:
exit_code = int(match.group(2))
return exit_code
raise AssertionError(f"unable to find exit code from the log: {self.log_data}")
@retry(wait=wait_fixed(0.2), stop=stop_after_attempt(5), reraise=True)
def check_any_log_message(self, messages):
"""Wait until any message in `messages` appears in logging output."""
for message in messages:
if message in self.log_data:
return
raise AssertionError(
f"`{messages}` were not found in this log: {self.log_data}"
)
def serial_input(self, input_string):
"""Send a string to the Firecracker serial console via screen."""
input_cmd = f'screen -S {self.screen_session} -p 0 -X stuff "{input_string}"'
return utils.check_output(input_cmd)
def basic_config(
self,
vcpu_count: int = 2,
smt: bool = None,
mem_size_mib: int = 256,
add_root_device: bool = True,
boot_args: str = None,
use_initrd: bool = False,
track_dirty_pages: bool = False,
huge_pages: HugePagesConfig = None,
rootfs_io_engine=None,
cpu_template: Optional[str] = None,
enable_entropy_device=False,
):
"""Shortcut for quickly configuring a microVM.
It handles:
- CPU and memory.
- Kernel image (will load the one in the microVM allocated path).
- Root File System (will use the one in the microVM allocated path).
- Does not start the microvm.
The function checks the response status code and asserts that
the response is within the interval [200, 300).
If boot_args is None, the default boot_args used in tests is
reboot=k panic=1 nomodule swiotlb=noforce console=ttyS0 [pci=off]
which differs from Firecracker's default only in the enabling of the serial console.
Reference: file:../../src/vmm/src/vmm_config/boot_source.rs::DEFAULT_KERNEL_CMDLINE
"""
self.api.machine_config.put(
vcpu_count=vcpu_count,
smt=smt,
mem_size_mib=mem_size_mib,
track_dirty_pages=track_dirty_pages,
huge_pages=huge_pages,
)
self.vcpus_count = vcpu_count
self.mem_size_bytes = mem_size_mib * 2**20
if self.custom_cpu_template is not None:
self.set_cpu_template(self.custom_cpu_template)
if cpu_template is not None:
self.set_cpu_template(cpu_template)
if self.memory_monitor:
self.memory_monitor.start()
if boot_args is not None:
self.boot_args = boot_args
else:
self.boot_args = "reboot=k panic=1 nomodule swiotlb=noforce console=ttyS0"
if not self.pci_enabled:
self.boot_args += " pci=off"
boot_source_args = {
"kernel_image_path": self.create_jailed_resource(self.kernel_file),
"boot_args": self.boot_args,
}
if use_initrd and self.initrd_file is not None:
boot_source_args.update(
initrd_path=self.create_jailed_resource(self.initrd_file)
)
self.api.boot.put(**boot_source_args)
if add_root_device and self.rootfs_file is not None:
read_only = self.rootfs_file.suffix == ".squashfs"
# Add the root file system
self.add_drive(
drive_id="rootfs",
path_on_host=self.rootfs_file,
is_root_device=True,
is_read_only=read_only,
io_engine=rootfs_io_engine,
)
if enable_entropy_device:
self.enable_entropy_device()
def set_cpu_template(self, cpu_template):
"""Set guest CPU template."""
self.cpu_template_name = get_cpu_template_name(cpu_template)
if cpu_template is None:
return
# static CPU template
if isinstance(cpu_template, str):
self.api.machine_config.patch(cpu_template=cpu_template)
# custom CPU template
elif isinstance(cpu_template, dict):
self.api.cpu_config.put(**cpu_template["template"])
def add_drive(
self,
drive_id,
path_on_host,
is_root_device=False,
is_read_only=False,
partuuid=None,
cache_type=None,
io_engine=None,
):
"""Add a block device."""
path_on_jail = self.create_jailed_resource(path_on_host)
self.api.drive.put(
drive_id=drive_id,
path_on_host=path_on_jail,
is_root_device=is_root_device,
is_read_only=is_read_only,
partuuid=partuuid,
cache_type=cache_type,
io_engine=io_engine,
)
self.disks[drive_id] = path_on_host
def add_vhost_user_drive(
self,
drive_id,
path_on_host,
partuuid=None,
is_root_device=False,
is_read_only=False,
cache_type=None,
backend_type=VhostUserBlkBackendType.CROSVM,
):
"""Add a vhost-user block device."""
# It is possible that the user adds another drive
# with the same ID. In that case, we should clean
# the previous backend up first.
prev = self.disks_vhost_user.pop(drive_id, None)
if prev:
prev.kill()
backend = VhostUserBlkBackend.with_backend(
backend_type, path_on_host, self.chroot(), drive_id, is_read_only
)
socket = backend.spawn(self.jailer.uid, self.jailer.gid)
self.api.drive.put(
drive_id=drive_id,
socket=socket,
partuuid=partuuid,
is_root_device=is_root_device,
cache_type=cache_type,
)
self.disks_vhost_user[drive_id] = backend
def patch_drive(self, drive_id, file=None):
"""Modify/patch an existing block device."""
if file:
self.api.drive.patch(
drive_id=drive_id,
path_on_host=self.create_jailed_resource(file.path),
)
self.disks[drive_id] = Path(file.path)
else:
self.api.drive.patch(drive_id=drive_id)
def add_net_iface(self, iface=None, api=True, **kwargs):
"""Add a network interface"""
if iface is None:
iface = net_tools.NetIfaceConfig.with_id(len(self.iface))
tap = self.netns.add_tap(
iface.tap_name, ip=f"{iface.host_ip}/{iface.netmask_len}"
)
self.iface[iface.dev_name] = {
"iface": iface,
"tap": tap,
}
# If api, call it... there may be cases when we don't want it, for
# example during restore
if api:
self.api.network.put(
iface_id=iface.dev_name,
host_dev_name=iface.tap_name,
guest_mac=iface.guest_mac,
**kwargs,
)
return iface
def add_pmem(
self,
pmem_id,
path_on_host,
root_device=False,
read_only=False,
):
"""Add a pmem device."""