Skip to content

Commit 1519297

Browse files
sandreimacatangiu
authored andcommitted
Added serial console login integration test.
- Implemented microvm.serial_input() function to send input to a 'screen'ed jailer stdin - Updated dev container version to v10 Signed-off-by: Andrei Sandu <[email protected]>
1 parent bf38691 commit 1519297

File tree

3 files changed

+176
-6
lines changed

3 files changed

+176
-6
lines changed

tests/framework/microvm.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(
7070
# Create the jailer context associated with this microvm.
7171
self._jailer = JailerContext(
7272
jailer_id=self._microvm_id,
73-
exec_file=self._fc_binary_path
73+
exec_file=self._fc_binary_path,
7474
)
7575
self.jailer_clone_pid = None
7676

@@ -292,7 +292,6 @@ def spawn(self):
292292
# successfully.
293293
if _p.stderr.decode().strip():
294294
raise Exception(_p.stderr.decode())
295-
296295
self.jailer_clone_pid = int(_p.stdout.decode().rstrip())
297296
else:
298297
# This code path is not used at the moment, but I just feel
@@ -306,7 +305,15 @@ def spawn(self):
306305
)
307306
self.jailer_clone_pid = _pid
308307
else:
309-
start_cmd = 'screen -dmS {session} {binary} {params}'
308+
# Delete old screen log if any.
309+
try:
310+
os.unlink('/tmp/screen.log')
311+
except OSError:
312+
pass
313+
# Log screen output to /tmp/screen.log.
314+
# This file will collect any output from 'screen'ed Firecracker.
315+
start_cmd = 'screen -L -Logfile /tmp/screen.log '\
316+
'-dmS {session} {binary} {params}'
310317
start_cmd = start_cmd.format(
311318
session=self._session_name,
312319
binary=self._jailer_binary_path,
@@ -323,6 +330,11 @@ def spawn(self):
323330
.format(screen_pid)
324331
).read().strip()
325332

333+
# Configure screen to flush stdout to file.
334+
flush_cmd = 'screen -S {session} -X colon "logfile flush 0^M"'
335+
run(flush_cmd.format(session=self._session_name),
336+
shell=True, check=True)
337+
326338
# Wait for the jailer to create resources needed, and Firecracker to
327339
# create its API socket.
328340
# We expect the jailer to start within 80 ms. However, we wait for
@@ -335,12 +347,20 @@ def _wait_create(self):
335347
"""Wait until the API socket and chroot folder are available."""
336348
os.stat(self._jailer.api_socket_path())
337349

350+
def serial_input(self, input_string):
351+
"""Send a string to the Firecracker serial console via screen."""
352+
input_cmd = 'screen -S {session} -p 0 -X stuff "{input_string}^M"'
353+
run(input_cmd.format(session=self._session_name,
354+
input_string=input_string),
355+
shell=True, check=True)
356+
338357
def basic_config(
339358
self,
340359
vcpu_count: int = 2,
341360
ht_enabled: bool = False,
342361
mem_size_mib: int = 256,
343-
add_root_device: bool = True
362+
add_root_device: bool = True,
363+
boot_args: str = None,
344364
):
345365
"""Shortcut for quickly configuring a microVM.
346366
@@ -369,7 +389,8 @@ def basic_config(
369389

370390
# Add a kernel to start booting from.
371391
response = self.boot.put(
372-
kernel_image_path=self.create_jailed_resource(self.kernel_file)
392+
kernel_image_path=self.create_jailed_resource(self.kernel_file),
393+
boot_args=boot_args
373394
)
374395
assert self._api_session.is_status_no_content(response.status_code)
375396

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Tests scenario for the Firecracker serial console."""
4+
import os
5+
import time
6+
import select
7+
8+
9+
# Too few public methods (1/2) (too-few-public-methods)
10+
# pylint: disable=R0903
11+
class MatchStaticString:
12+
"""Match a static string versus input."""
13+
14+
# Prevent state objects from being collected by pytest.
15+
__test__ = False
16+
17+
def __init__(self, match_string):
18+
"""Initialize using specified match string."""
19+
self._string = match_string
20+
self._input = ""
21+
22+
def match(self, input_char) -> bool:
23+
"""
24+
Check if `_input` matches the match `_string`.
25+
26+
Process one char at a time and build `_input` string.
27+
Preserve built `_input` if partially matches `_string`.
28+
Return True when `_input` is the same as `_string`.
29+
"""
30+
self._input += str(input_char)
31+
if self._input == self._string[:len(self._input)]:
32+
if len(self._input) == len(self._string):
33+
self._input = ""
34+
return True
35+
return False
36+
37+
self._input = self._input[1:]
38+
return False
39+
40+
41+
class TestState(MatchStaticString):
42+
"""Generic test state object."""
43+
44+
# Prevent state objects from being collected by pytest.
45+
__test__ = False
46+
47+
def __init__(self, match_string=''):
48+
"""Initialize state fields."""
49+
MatchStaticString.__init__(self, match_string)
50+
print('\n*** Current test state: ', str(self), end='')
51+
52+
def handle_input(self, microvm, input_char):
53+
"""Handle input event and return next state."""
54+
55+
def __repr__(self):
56+
"""Leverages the __str__ method to describe the TestState."""
57+
return self.__str__()
58+
59+
def __str__(self):
60+
"""Return state name."""
61+
return self.__class__.__name__
62+
63+
64+
class WaitLogin(TestState):
65+
"""Initial state when we wait for the login prompt."""
66+
67+
def handle_input(self, microvm, input_char) -> TestState:
68+
"""Handle input and return next state."""
69+
if self.match(input_char):
70+
# Send login username.
71+
microvm.serial_input("root")
72+
return WaitPasswordPrompt("Password:")
73+
return self
74+
75+
76+
class WaitPasswordPrompt(TestState):
77+
"""Wait for the password prompt to be shown."""
78+
79+
def handle_input(self, microvm, input_char) -> TestState:
80+
"""Handle input and return next state."""
81+
if self.match(input_char):
82+
microvm.serial_input("root")
83+
# Wait 1 second for shell
84+
time.sleep(1)
85+
microvm.serial_input("id")
86+
return WaitIDResult("uid=0(root) gid=0(root) groups=0(root)")
87+
return self
88+
89+
90+
class WaitIDResult(TestState):
91+
"""Wait for the console to show the result of the 'id' shell command."""
92+
93+
def handle_input(self, microvm, input_char) -> TestState:
94+
"""Handle input and return next state."""
95+
if self.match(input_char):
96+
return TestFinished()
97+
return self
98+
99+
100+
class TestFinished(TestState):
101+
"""Test complete and successful."""
102+
103+
def handle_input(self, microvm, input_char) -> TestState:
104+
"""Return self since the test is about to end."""
105+
return self
106+
107+
108+
def test_serial_console_login(test_microvm_with_ssh):
109+
"""Test serial console login."""
110+
microvm = test_microvm_with_ssh
111+
microvm.jailer.daemonize = False
112+
microvm.spawn()
113+
114+
# We don't need to monitor the memory for this test because we are
115+
# just rebooting and the process dies before pmap gets the RSS.
116+
microvm.memory_events_queue = None
117+
118+
# Set up the microVM with 1 vCPU and a serial console.
119+
microvm.basic_config(vcpu_count=1,
120+
boot_args='console=ttyS0 reboot=k panic=1 pci=off')
121+
122+
microvm.start()
123+
124+
# Screen stdout log
125+
screen_log = "/tmp/screen.log"
126+
127+
# Open the screen log file.
128+
screen_log_fd = os.open(screen_log, os.O_RDONLY)
129+
poller = select.poll()
130+
131+
# Set initial state - wait for 'login:' prompt
132+
current_state = WaitLogin("login:")
133+
134+
poller.register(screen_log_fd, select.POLLIN | select.POLLHUP)
135+
136+
while not isinstance(current_state, TestFinished):
137+
result = poller.poll(0.1)
138+
for fd, flag in result:
139+
if flag & select.POLLIN:
140+
output_char = str(os.read(fd, 1),
141+
encoding='utf-8',
142+
errors='ignore')
143+
# [DEBUG] Uncomment to see the serial console output.
144+
# print(output_char, end='')
145+
current_state = current_state.handle_input(
146+
microvm, output_char)
147+
elif flag & select.POLLHUP:
148+
assert False, "Oh! The console vanished before test completed."
149+
os.close(screen_log_fd)

tools/devtool

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
# Development container image (name:tag)
7474
# This should be updated whenever we upgrade the development container.
7575
# (Yet another step on our way to reproducible builds.)
76-
DEVCTR_IMAGE="fcuvm/dev:v9"
76+
DEVCTR_IMAGE="fcuvm/dev:v10"
7777

7878
# Naming things is hard
7979
MY_NAME="Firecracker $(basename "$0")"

0 commit comments

Comments
 (0)