Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/framework/microvm.py
1956 views
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Classes for working with microVMs.
4
5
This module defines `Microvm`, which can be used to create, test drive, and
6
destroy microvms.
7
8
# TODO
9
10
- Use the Firecracker Open API spec to populate Microvm API resource URLs.
11
"""
12
13
import json
14
import logging
15
import os
16
import re
17
import select
18
import shutil
19
import time
20
import weakref
21
22
from retry import retry
23
from retry.api import retry_call
24
25
import host_tools.logging as log_tools
26
import host_tools.cpu_load as cpu_tools
27
import host_tools.memory as mem_tools
28
import host_tools.network as net_tools
29
30
import framework.utils as utils
31
from framework.defs import MICROVM_KERNEL_RELPATH, MICROVM_FSFILES_RELPATH, \
32
FC_PID_FILE_NAME
33
from framework.http import Session
34
from framework.jailer import JailerContext
35
from framework.resources import Actions, Balloon, BootSource, Drive, \
36
DescribeInstance, FullConfig, Logger, MMDS, MachineConfigure, \
37
Metrics, Network, Vm, Vsock, SnapshotHelper
38
39
LOG = logging.getLogger("microvm")
40
41
42
# pylint: disable=R0904
43
class Microvm:
44
"""Class to represent a Firecracker microvm.
45
46
A microvm is described by a unique identifier, a path to all the resources
47
it needs in order to be able to start and the binaries used to spawn it.
48
Besides keeping track of microvm resources and exposing microvm API
49
methods, `spawn()` and `kill()` can be used to start/end the microvm
50
process.
51
"""
52
53
SCREEN_LOGFILE = "/tmp/screen-{}.log"
54
55
def __init__(
56
self,
57
resource_path,
58
fc_binary_path,
59
jailer_binary_path,
60
microvm_id,
61
monitor_memory=True,
62
bin_cloner_path=None,
63
):
64
"""Set up microVM attributes, paths, and data structures."""
65
# Unique identifier for this machine.
66
self._microvm_id = microvm_id
67
68
# Compose the paths to the resources specific to this microvm.
69
self._path = os.path.join(resource_path, microvm_id)
70
self._kernel_path = os.path.join(self._path, MICROVM_KERNEL_RELPATH)
71
self._fsfiles_path = os.path.join(self._path, MICROVM_FSFILES_RELPATH)
72
self._kernel_file = ''
73
self._rootfs_file = ''
74
self._initrd_file = ''
75
76
# The binaries this microvm will use to start.
77
self._fc_binary_path = fc_binary_path
78
assert os.path.exists(self._fc_binary_path)
79
self._jailer_binary_path = jailer_binary_path
80
assert os.path.exists(self._jailer_binary_path)
81
82
# Create the jailer context associated with this microvm.
83
self._jailer = JailerContext(
84
jailer_id=self._microvm_id,
85
exec_file=self._fc_binary_path,
86
)
87
self.jailer_clone_pid = None
88
self._screen_log = None
89
90
# Copy the /etc/localtime file in the jailer root
91
self.jailer.copy_into_root(
92
"/etc/localtime", create_jail=True)
93
94
# Now deal with the things specific to the api session used to
95
# communicate with this machine.
96
self._api_session = None
97
self._api_socket = None
98
99
# Session name is composed of the last part of the temporary path
100
# allocated by the current test session and the unique id of this
101
# microVM. It should be unique.
102
self._session_name = os.path.basename(os.path.normpath(
103
resource_path
104
)) + self._microvm_id
105
106
# nice-to-have: Put these in a dictionary.
107
self.actions = None
108
self.balloon = None
109
self.boot = None
110
self.desc_inst = None
111
self.drive = None
112
self.full_cfg = None
113
self.logger = None
114
self.metrics = None
115
self.mmds = None
116
self.network = None
117
self.machine_cfg = None
118
self.vm = None
119
self.vsock = None
120
self.snapshot = None
121
122
# Initialize the logging subsystem.
123
self.logging_thread = None
124
self._log_data = ""
125
self._screen_pid = None
126
127
# The ssh config dictionary is populated with information about how
128
# to connect to a microVM that has ssh capability. The path of the
129
# private key is populated by microvms with ssh capabilities and the
130
# hostname is set from the MAC address used to configure the microVM.
131
self._ssh_config = {
132
'username': 'root',
133
'netns_file_path': self._jailer.netns_file_path()
134
}
135
136
# Deal with memory monitoring.
137
if monitor_memory:
138
self._memory_monitor = mem_tools.MemoryMonitor()
139
else:
140
self._memory_monitor = None
141
142
# Cpu load monitoring has to be explicitly enabled using
143
# the `enable_cpu_load_monitor` method.
144
self._cpu_load_monitor = None
145
self._vcpus_count = None
146
147
# External clone/exec tool, because Python can't into clone
148
self.bin_cloner_path = bin_cloner_path
149
150
# Flag checked in destructor to see abnormal signal-induced crashes.
151
self.expect_kill_by_signal = False
152
153
def kill(self):
154
"""All clean up associated with this microVM should go here."""
155
# pylint: disable=subprocess-run-check
156
if self.logging_thread is not None:
157
self.logging_thread.stop()
158
159
if self.expect_kill_by_signal is False and \
160
"Shutting down VM after intercepting signal" in self.log_data:
161
# Too late to assert at this point, pytest will still report the
162
# test as passed. BUT we can dump full logs for debugging,
163
# as well as an intentional eye-sore in the test report.
164
LOG.error(self.log_data)
165
166
if self._jailer.daemonize:
167
if self.jailer_clone_pid:
168
utils.run_cmd(
169
'kill -9 {}'.format(self.jailer_clone_pid),
170
ignore_return_code=True)
171
else:
172
# Killing screen will send SIGHUP to underlying Firecracker.
173
# Needed to avoid false positives in case kill() is called again.
174
self.expect_kill_by_signal = True
175
utils.run_cmd(
176
'kill -9 {} || true'.format(self.screen_pid))
177
178
# Check if Firecracker was launched by the jailer in a new pid ns.
179
fc_pid_in_new_ns = self.pid_in_new_ns
180
181
if fc_pid_in_new_ns:
182
# We need to explicitly kill the Firecracker pid, since it's
183
# different from the jailer pid that was previously killed.
184
utils.run_cmd(f'kill -9 {fc_pid_in_new_ns}',
185
ignore_return_code=True)
186
187
if self._memory_monitor and self._memory_monitor.is_alive():
188
self._memory_monitor.signal_stop()
189
self._memory_monitor.join(timeout=1)
190
self._memory_monitor.check_samples()
191
192
if self._cpu_load_monitor:
193
self._cpu_load_monitor.signal_stop()
194
self._cpu_load_monitor.join()
195
self._cpu_load_monitor.check_samples()
196
197
@property
198
def api_session(self):
199
"""Return the api session associated with this microVM."""
200
return self._api_session
201
202
@property
203
def api_socket(self):
204
"""Return the socket used by this api session."""
205
# TODO: this methods is only used as a workaround for getting
206
# firecracker PID. We should not be forced to make this public.
207
return self._api_socket
208
209
@property
210
def path(self):
211
"""Return the path on disk used that represents this microVM."""
212
return self._path
213
214
@property
215
def id(self):
216
"""Return the unique identifier of this microVM."""
217
return self._microvm_id
218
219
@property
220
def jailer(self):
221
"""Return the jailer context associated with this microVM."""
222
return self._jailer
223
224
@jailer.setter
225
def jailer(self, jailer):
226
"""Setter for associating a different jailer to the default one."""
227
self._jailer = jailer
228
229
@property
230
def kernel_file(self):
231
"""Return the name of the kernel file used by this microVM to boot."""
232
return self._kernel_file
233
234
@kernel_file.setter
235
def kernel_file(self, path):
236
"""Set the path to the kernel file."""
237
self._kernel_file = path
238
239
@property
240
def initrd_file(self):
241
"""Return the name of the initrd file used by this microVM to boot."""
242
return self._initrd_file
243
244
@initrd_file.setter
245
def initrd_file(self, path):
246
"""Set the path to the initrd file."""
247
self._initrd_file = path
248
249
@property
250
def log_data(self):
251
"""Return the log data."""
252
return self._log_data
253
254
@property
255
def rootfs_file(self):
256
"""Return the path to the image this microVM can boot into."""
257
return self._rootfs_file
258
259
@rootfs_file.setter
260
def rootfs_file(self, path):
261
"""Set the path to the image associated."""
262
self._rootfs_file = path
263
264
@property
265
def fsfiles(self):
266
"""Path to filesystem used by this microvm to attach new drives."""
267
return self._fsfiles_path
268
269
@property
270
def ssh_config(self):
271
"""Get the ssh configuration used to ssh into some microVMs."""
272
return self._ssh_config
273
274
@ssh_config.setter
275
def ssh_config(self, key, value):
276
"""Set the dict values inside this configuration."""
277
self._ssh_config.__setattr__(key, value)
278
279
@property
280
def memory_monitor(self):
281
"""Get the memory monitor."""
282
return self._memory_monitor
283
284
@property
285
def state(self):
286
"""Get the InstanceInfo property and return the state field."""
287
return json.loads(self.desc_inst.get().content)["state"]
288
289
@property
290
def started(self):
291
"""Get the InstanceInfo property and return the started field.
292
293
This is kept for legacy snapshot support.
294
"""
295
return json.loads(self.desc_inst.get().content)["started"]
296
297
@memory_monitor.setter
298
def memory_monitor(self, monitor):
299
"""Set the memory monitor."""
300
self._memory_monitor = monitor
301
302
@property
303
def pid_in_new_ns(self):
304
"""Get the pid of the Firecracker process in the new namespace.
305
306
Returns None if Firecracker was not launched in a new pid ns.
307
"""
308
fc_pid = None
309
310
pid_file_path = f"{self.jailer.chroot_path()}/{FC_PID_FILE_NAME}"
311
if os.path.exists(pid_file_path):
312
# Read the PID stored inside the file.
313
with open(pid_file_path) as file:
314
fc_pid = int(file.readline())
315
316
return fc_pid
317
318
def flush_metrics(self, metrics_fifo):
319
"""Flush the microvm metrics.
320
321
Requires specifying the configured metrics file.
322
"""
323
# Empty the metrics pipe.
324
_ = metrics_fifo.sequential_reader(100)
325
326
response = self.actions.put(action_type='FlushMetrics')
327
assert self.api_session.is_status_no_content(response.status_code)
328
329
lines = metrics_fifo.sequential_reader(100)
330
assert len(lines) == 1
331
332
return json.loads(lines[0])
333
334
def get_all_metrics(self, metrics_fifo):
335
"""Return all metric data points written by FC.
336
337
Requires specifying the configured metrics file.
338
"""
339
# Empty the metrics pipe.
340
response = self.actions.put(action_type='FlushMetrics')
341
assert self.api_session.is_status_no_content(response.status_code)
342
343
return metrics_fifo.sequential_reader(1000)
344
345
def append_to_log_data(self, data):
346
"""Append a message to the log data."""
347
self._log_data += data
348
349
def enable_cpu_load_monitor(self, threshold):
350
"""Enable the cpu load monitor."""
351
process_pid = self.jailer_clone_pid
352
# We want to monitor the emulation thread, which is currently
353
# the first one created.
354
# A possible improvement is to find it by name.
355
thread_pid = self.jailer_clone_pid
356
self._cpu_load_monitor = cpu_tools.CpuLoadMonitor(
357
process_pid,
358
thread_pid,
359
threshold
360
)
361
self._cpu_load_monitor.start()
362
363
def copy_to_jail_ramfs(self, src):
364
"""Copy a file to a jail ramfs."""
365
filename = os.path.basename(src)
366
dest_path = os.path.join(self.jailer.chroot_ramfs_path(), filename)
367
jailed_path = os.path.join(
368
'/', self.jailer.ramfs_subdir_name, filename
369
)
370
shutil.copy(src, dest_path)
371
cmd = 'chown {}:{} {}'.format(
372
self.jailer.uid,
373
self.jailer.gid,
374
dest_path
375
)
376
utils.run_cmd(cmd)
377
return jailed_path
378
379
def create_jailed_resource(self, path, create_jail=False):
380
"""Create a hard link to some resource inside this microvm."""
381
return self.jailer.jailed_path(path, create=True,
382
create_jail=create_jail)
383
384
def get_jailed_resource(self, path):
385
"""Get the relative jailed path to a resource."""
386
return self.jailer.jailed_path(path, create=False)
387
388
def chroot(self):
389
"""Get the chroot of this microVM."""
390
return self.jailer.chroot_path()
391
392
def setup(self):
393
"""Create a microvm associated folder on the host.
394
395
The root path of some microvm is `self._path`.
396
Also creates the where essential resources (i.e. kernel and root
397
filesystem) will reside.
398
399
# Microvm Folder Layout
400
401
There is a fixed tree layout for a microvm related folder:
402
403
``` file_tree
404
<microvm_uuid>/
405
kernel/
406
<kernel_file_n>
407
....
408
fsfiles/
409
<fsfile_n>
410
<initrd_file_n>
411
<ssh_key_n>
412
<other fsfiles>
413
...
414
...
415
```
416
"""
417
os.makedirs(self._path, exist_ok=True)
418
os.makedirs(self._kernel_path, exist_ok=True)
419
os.makedirs(self._fsfiles_path, exist_ok=True)
420
421
@property
422
def screen_log(self):
423
"""Get the screen log file."""
424
return self._screen_log
425
426
@property
427
def screen_pid(self):
428
"""Get the screen PID."""
429
return self._screen_pid
430
431
@property
432
def vcpus_count(self):
433
"""Get the vcpus count."""
434
return self._vcpus_count
435
436
@vcpus_count.setter
437
def vcpus_count(self, vcpus_count: int):
438
"""Set the vcpus count."""
439
self._vcpus_count = vcpus_count
440
441
def pin_vmm(self, cpu_id: int) -> bool:
442
"""Pin the firecracker process VMM thread to a cpu list."""
443
if self.jailer_clone_pid:
444
for thread in utils.ProcessManager.get_threads(
445
self.jailer_clone_pid)["firecracker"]:
446
utils.ProcessManager.set_cpu_affinity(thread, [cpu_id])
447
return True
448
return False
449
450
def pin_vcpu(self, vcpu_id: int, cpu_id: int):
451
"""Pin the firecracker vcpu thread to a cpu list."""
452
if self.jailer_clone_pid:
453
for thread in utils.ProcessManager.get_threads(
454
self.jailer_clone_pid)[f"fc_vcpu {vcpu_id}"]:
455
utils.ProcessManager.set_cpu_affinity(thread, [cpu_id])
456
return True
457
return False
458
459
def pin_api(self, cpu_id: int):
460
"""Pin the firecracker process API server thread to a cpu list."""
461
if self.jailer_clone_pid:
462
for thread in utils.ProcessManager.get_threads(
463
self.jailer_clone_pid)["fc_api"]:
464
utils.ProcessManager.set_cpu_affinity(thread, [cpu_id])
465
return True
466
return False
467
468
def spawn(self, create_logger=True,
469
log_file='log_fifo', log_level='Info', use_ramdisk=False):
470
"""Start a microVM as a daemon or in a screen session."""
471
# pylint: disable=subprocess-run-check
472
self._jailer.setup(use_ramdisk=use_ramdisk)
473
self._api_socket = self._jailer.api_socket_path()
474
self._api_session = Session()
475
476
self.actions = Actions(self._api_socket, self._api_session)
477
self.balloon = Balloon(self._api_socket, self._api_session)
478
self.boot = BootSource(self._api_socket, self._api_session)
479
self.desc_inst = DescribeInstance(self._api_socket, self._api_session)
480
self.drive = Drive(self._api_socket, self._api_session)
481
self.full_cfg = FullConfig(self._api_socket, self._api_session)
482
self.logger = Logger(self._api_socket, self._api_session)
483
self.machine_cfg = MachineConfigure(
484
self._api_socket,
485
self._api_session
486
)
487
self.metrics = Metrics(self._api_socket, self._api_session)
488
self.mmds = MMDS(self._api_socket, self._api_session)
489
self.network = Network(self._api_socket, self._api_session)
490
self.snapshot = SnapshotHelper(self._api_socket, self._api_session)
491
self.vm = Vm(self._api_socket, self._api_session)
492
self.vsock = Vsock(self._api_socket, self._api_session)
493
494
if create_logger:
495
log_fifo_path = os.path.join(self.path, log_file)
496
log_fifo = log_tools.Fifo(log_fifo_path)
497
self.create_jailed_resource(log_fifo.path, create_jail=True)
498
# The default value for `level`, when configuring the
499
# logger via cmd line, is `Warning`. We set the level
500
# to `Info` to also have the boot time printed in fifo.
501
self.jailer.extra_args.update({'log-path': log_file,
502
'level': log_level})
503
self.start_console_logger(log_fifo)
504
505
jailer_param_list = self._jailer.construct_param_list()
506
507
# When the daemonize flag is on, we want to clone-exec into the
508
# jailer rather than executing it via spawning a shell. Going
509
# forward, we'll probably switch to this method for running
510
# Firecracker in general, because it represents the way it's meant
511
# to be run by customers (together with CLONE_NEWPID flag).
512
#
513
# We have to use an external tool for CLONE_NEWPID, because
514
# 1) Python doesn't provide a os.clone() interface, and
515
# 2) Python's ctypes libc interface appears to be broken, causing
516
# our clone / exec to deadlock at some point.
517
if self._jailer.daemonize:
518
self.daemonize_jailer(jailer_param_list)
519
else:
520
# This file will collect any output from 'screen'ed Firecracker.
521
self._screen_log = self.SCREEN_LOGFILE.format(self._session_name)
522
start_cmd = 'screen -L -Logfile {logfile} '\
523
'-dmS {session} {binary} {params}'
524
start_cmd = start_cmd.format(
525
logfile=self.screen_log,
526
session=self._session_name,
527
binary=self._jailer_binary_path,
528
params=' '.join(jailer_param_list)
529
)
530
531
utils.run_cmd(start_cmd)
532
533
# Build a regex object to match (number).session_name
534
regex_object = re.compile(
535
r'([0-9]+)\.{}'.format(self._session_name))
536
537
# Run 'screen -ls' in a retry_call loop, 30 times with a one
538
# second delay between calls.
539
# If the output of 'screen -ls' matches the regex object, it will
540
# return the PID. Otherwise a RuntimeError will be raised.
541
screen_pid = retry_call(
542
utils.search_output_from_cmd,
543
fkwargs={
544
"cmd": 'screen -ls',
545
"find_regex": regex_object
546
},
547
exceptions=RuntimeError,
548
tries=30,
549
delay=1).group(1)
550
551
self._screen_pid = screen_pid
552
553
self.jailer_clone_pid = int(open('/proc/{0}/task/{0}/children'
554
.format(screen_pid)
555
).read().strip())
556
557
# Configure screen to flush stdout to file.
558
flush_cmd = 'screen -S {session} -X colon "logfile flush 0^M"'
559
utils.run_cmd(flush_cmd.format(session=self._session_name))
560
561
# Wait for the jailer to create resources needed, and Firecracker to
562
# create its API socket.
563
# We expect the jailer to start within 80 ms. However, we wait for
564
# 1 sec since we are rechecking the existence of the socket 5 times
565
# and leave 0.2 delay between them.
566
if 'no-api' not in self._jailer.extra_args:
567
self._wait_create()
568
if create_logger:
569
self.check_log_message("Running Firecracker")
570
571
@retry(delay=0.2, tries=5)
572
def _wait_create(self):
573
"""Wait until the API socket and chroot folder are available."""
574
os.stat(self._jailer.api_socket_path())
575
576
@retry(delay=0.1, tries=5)
577
def check_log_message(self, message):
578
"""Wait until `message` appears in logging output."""
579
assert message in self._log_data
580
581
def serial_input(self, input_string):
582
"""Send a string to the Firecracker serial console via screen."""
583
input_cmd = 'screen -S {session} -p 0 -X stuff "{input_string}"'
584
utils.run_cmd(input_cmd.format(session=self._session_name,
585
input_string=input_string))
586
587
def basic_config(
588
self,
589
vcpu_count: int = 2,
590
ht_enabled: bool = False,
591
mem_size_mib: int = 256,
592
add_root_device: bool = True,
593
boot_args: str = None,
594
use_initrd: bool = False,
595
track_dirty_pages: bool = False
596
):
597
"""Shortcut for quickly configuring a microVM.
598
599
It handles:
600
- CPU and memory.
601
- Kernel image (will load the one in the microVM allocated path).
602
- Root File System (will use the one in the microVM allocated path).
603
- Does not start the microvm.
604
605
The function checks the response status code and asserts that
606
the response is within the interval [200, 300).
607
"""
608
response = self.machine_cfg.put(
609
vcpu_count=vcpu_count,
610
ht_enabled=ht_enabled,
611
mem_size_mib=mem_size_mib,
612
track_dirty_pages=track_dirty_pages
613
)
614
assert self._api_session.is_status_no_content(response.status_code), \
615
response.text
616
617
if self.memory_monitor:
618
self.memory_monitor.guest_mem_mib = mem_size_mib
619
self.memory_monitor.pid = self.jailer_clone_pid
620
self.memory_monitor.start()
621
622
boot_source_args = {
623
'kernel_image_path': self.create_jailed_resource(self.kernel_file),
624
'boot_args': boot_args
625
}
626
627
if use_initrd and self.initrd_file != '':
628
boot_source_args.update(
629
initrd_path=self.create_jailed_resource(self.initrd_file))
630
631
response = self.boot.put(**boot_source_args)
632
assert self._api_session.is_status_no_content(response.status_code), \
633
response.text
634
635
if add_root_device and self.rootfs_file != '':
636
# Add the root file system with rw permissions.
637
response = self.drive.put(
638
drive_id='rootfs',
639
path_on_host=self.create_jailed_resource(self.rootfs_file),
640
is_root_device=True,
641
is_read_only=False
642
)
643
assert self._api_session \
644
.is_status_no_content(response.status_code), \
645
response.text
646
647
def daemonize_jailer(
648
self,
649
jailer_param_list
650
):
651
"""Daemonize the jailer."""
652
if self.bin_cloner_path:
653
cmd = [self.bin_cloner_path] + \
654
[self._jailer_binary_path] + \
655
jailer_param_list
656
_p = utils.run_cmd(cmd)
657
# Terrible hack to make the tests fail when starting the
658
# jailer fails with a panic. This is needed because we can't
659
# get the exit code of the jailer. In newpid_clone.c we are
660
# not waiting for the process and we always return 0 if the
661
# clone was successful (which in most cases will be) and we
662
# don't do anything if the jailer was not started
663
# successfully.
664
if _p.stderr.strip():
665
raise Exception(_p.stderr)
666
self.jailer_clone_pid = int(_p.stdout.rstrip())
667
else:
668
# This code path is not used at the moment, but I just feel
669
# it's nice to have a fallback mechanism in place, in case
670
# we decide to offload PID namespacing to the jailer.
671
_pid = os.fork()
672
if _pid == 0:
673
os.execv(
674
self._jailer_binary_path,
675
[self._jailer_binary_path] + jailer_param_list
676
)
677
self.jailer_clone_pid = _pid
678
679
def add_drive(
680
self,
681
drive_id,
682
file_path,
683
root_device=False,
684
is_read_only=False,
685
partuuid=None,
686
cache_type=None,
687
use_ramdisk=False,
688
):
689
"""Add a block device."""
690
response = self.drive.put(
691
drive_id=drive_id,
692
path_on_host=(
693
self.copy_to_jail_ramfs(file_path) if
694
use_ramdisk else self.create_jailed_resource(file_path)
695
),
696
is_root_device=root_device,
697
is_read_only=is_read_only,
698
partuuid=partuuid,
699
cache_type=cache_type
700
)
701
assert self.api_session.is_status_no_content(response.status_code)
702
703
def patch_drive(self, drive_id, file):
704
"""Modify/patch an existing block device."""
705
response = self.drive.patch(
706
drive_id=drive_id,
707
path_on_host=self.create_jailed_resource(file.path),
708
)
709
assert self.api_session.is_status_no_content(response.status_code)
710
711
def ssh_network_config(
712
self,
713
network_config,
714
iface_id,
715
allow_mmds_requests=False,
716
tx_rate_limiter=None,
717
rx_rate_limiter=None,
718
tapname=None
719
):
720
"""Create a host tap device and a guest network interface.
721
722
'network_config' is used to generate 2 IPs: one for the tap device
723
and one for the microvm. Adds the hostname of the microvm to the
724
ssh_config dictionary.
725
:param network_config: UniqueIPv4Generator instance
726
:param iface_id: the interface id for the API request
727
:param allow_mmds_requests: specifies whether requests sent from
728
the guest on this interface towards the MMDS address are
729
intercepted and processed by the device model.
730
:param tx_rate_limiter: limit the tx rate
731
:param rx_rate_limiter: limit the rx rate
732
:return: an instance of the tap which needs to be kept around until
733
cleanup is desired, the configured guest and host ips, respectively.
734
"""
735
# Create tap before configuring interface.
736
tapname = tapname or (self.id[:8] + 'tap' + iface_id)
737
(host_ip, guest_ip) = network_config.get_next_available_ips(2)
738
tap = self.create_tap_and_ssh_config(host_ip,
739
guest_ip,
740
network_config.get_netmask_len(),
741
tapname)
742
guest_mac = net_tools.mac_from_ip(guest_ip)
743
744
response = self.network.put(
745
iface_id=iface_id,
746
host_dev_name=tapname,
747
guest_mac=guest_mac,
748
allow_mmds_requests=allow_mmds_requests,
749
tx_rate_limiter=tx_rate_limiter,
750
rx_rate_limiter=rx_rate_limiter
751
)
752
assert self._api_session.is_status_no_content(response.status_code)
753
754
return tap, host_ip, guest_ip
755
756
def create_tap_and_ssh_config(
757
self,
758
host_ip,
759
guest_ip,
760
netmask_len,
761
tapname=None
762
):
763
"""Create tap device and configure ssh."""
764
assert tapname is not None
765
tap = net_tools.Tap(
766
tapname,
767
self._jailer.netns,
768
ip="{}/{}".format(
769
host_ip,
770
netmask_len
771
)
772
)
773
self.config_ssh(guest_ip)
774
return tap
775
776
def config_ssh(self, guest_ip):
777
"""Configure ssh."""
778
self.ssh_config['hostname'] = guest_ip
779
780
def start(self, check=True):
781
"""Start the microvm.
782
783
This function has asserts to validate that the microvm boot success.
784
"""
785
# Check that the VM has not started yet
786
try:
787
assert self.state == "Not started"
788
except KeyError:
789
assert self.started is False
790
791
response = self.actions.put(action_type='InstanceStart')
792
793
if check:
794
assert \
795
self._api_session.is_status_no_content(response.status_code), \
796
response.text
797
798
# Check that the VM has started
799
try:
800
assert self.state == "Running"
801
except KeyError:
802
assert self.started is True
803
804
def pause_to_snapshot(self,
805
mem_file_path=None,
806
snapshot_path=None,
807
diff=False,
808
version=None):
809
"""Pauses the microVM, and creates snapshot.
810
811
This function validates that the microVM pauses successfully and
812
creates a snapshot.
813
"""
814
assert mem_file_path is not None, "Please specify mem_file_path."
815
assert snapshot_path is not None, "Please specify snapshot_path."
816
817
response = self.vm.patch(state='Paused')
818
assert self.api_session.is_status_no_content(response.status_code)
819
820
self.api_session.untime()
821
response = self.snapshot.create(mem_file_path=mem_file_path,
822
snapshot_path=snapshot_path,
823
diff=diff,
824
version=version)
825
assert self.api_session.is_status_no_content(response.status_code), \
826
response.text
827
828
def start_console_logger(self, log_fifo):
829
"""
830
Start a thread that monitors the microVM console.
831
832
The console output will be redirected to the log file.
833
"""
834
def monitor_fd(microvm, path):
835
try:
836
fd = open(path, "r")
837
while True:
838
try:
839
if microvm().logging_thread.stopped():
840
return
841
data = fd.readline()
842
if data:
843
microvm().append_to_log_data(data)
844
except AttributeError as _:
845
# This means that the microvm object was destroyed and
846
# we are using a None reference.
847
return
848
except IOError as error:
849
# pylint: disable=W0150
850
try:
851
LOG.error("[%s] IOError while monitoring fd:"
852
" %s", microvm().id, error)
853
microvm().append_to_log_data(str(error))
854
except AttributeError as _:
855
# This means that the microvm object was destroyed and
856
# we are using a None reference.
857
pass
858
finally:
859
return
860
861
self.logging_thread = utils.StoppableThread(
862
target=monitor_fd,
863
args=(weakref.ref(self), log_fifo.path),
864
daemon=True)
865
self.logging_thread.start()
866
867
def __del__(self):
868
"""Teardown the object."""
869
self.kill()
870
871
872
class Serial:
873
"""Class for serial console communication with a Microvm."""
874
875
RX_TIMEOUT_S = 5
876
877
def __init__(self, vm):
878
"""Initialize a new Serial object."""
879
self._poller = None
880
self._vm = vm
881
882
def open(self):
883
"""Open a serial connection."""
884
# Open the screen log file.
885
if self._poller is not None:
886
# serial already opened
887
return
888
889
screen_log_fd = os.open(self._vm.screen_log, os.O_RDONLY)
890
self._poller = select.poll()
891
self._poller.register(screen_log_fd,
892
select.POLLIN | select.POLLHUP)
893
894
def tx(self, input_string, end='\n'):
895
# pylint: disable=invalid-name
896
# No need to have a snake_case naming style for a single word.
897
r"""Send a string terminated by an end token (defaulting to "\n")."""
898
self._vm.serial_input(input_string + end)
899
900
def rx_char(self):
901
"""Read a single character."""
902
result = self._poller.poll(0.1)
903
904
for fd, flag in result:
905
if flag & select.POLLHUP:
906
assert False, "Oh! The console vanished before test completed."
907
908
if flag & select.POLLIN:
909
output_char = str(os.read(fd, 1),
910
encoding='utf-8',
911
errors='ignore')
912
return output_char
913
914
return ''
915
916
def rx(self, token="\n"):
917
# pylint: disable=invalid-name
918
# No need to have a snake_case naming style for a single word.
919
r"""Read a string delimited by an end token (defaults to "\n")."""
920
rx_str = ''
921
start = time.time()
922
while True:
923
rx_str += self.rx_char()
924
if rx_str.endswith(token):
925
break
926
if (time.time() - start) >= self.RX_TIMEOUT_S:
927
assert False
928
929
return rx_str
930
931