Skip to content

Commit

Permalink
Added serial console login integration test.
Browse files Browse the repository at this point in the history
- 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 <sandreim@amazon.com>
  • Loading branch information
sandreim authored and acatangiu committed Sep 19, 2019
1 parent bf38691 commit 1519297
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 6 deletions.
31 changes: 26 additions & 5 deletions tests/framework/microvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __init__(
# Create the jailer context associated with this microvm.
self._jailer = JailerContext(
jailer_id=self._microvm_id,
exec_file=self._fc_binary_path
exec_file=self._fc_binary_path,
)
self.jailer_clone_pid = None

Expand Down Expand Up @@ -292,7 +292,6 @@ def spawn(self):
# successfully.
if _p.stderr.decode().strip():
raise Exception(_p.stderr.decode())

self.jailer_clone_pid = int(_p.stdout.decode().rstrip())
else:
# This code path is not used at the moment, but I just feel
Expand All @@ -306,7 +305,15 @@ def spawn(self):
)
self.jailer_clone_pid = _pid
else:
start_cmd = 'screen -dmS {session} {binary} {params}'
# Delete old screen log if any.
try:
os.unlink('/tmp/screen.log')
except OSError:
pass
# Log screen output to /tmp/screen.log.
# This file will collect any output from 'screen'ed Firecracker.
start_cmd = 'screen -L -Logfile /tmp/screen.log '\
'-dmS {session} {binary} {params}'
start_cmd = start_cmd.format(
session=self._session_name,
binary=self._jailer_binary_path,
Expand All @@ -323,6 +330,11 @@ def spawn(self):
.format(screen_pid)
).read().strip()

# Configure screen to flush stdout to file.
flush_cmd = 'screen -S {session} -X colon "logfile flush 0^M"'
run(flush_cmd.format(session=self._session_name),
shell=True, check=True)

# Wait for the jailer to create resources needed, and Firecracker to
# create its API socket.
# We expect the jailer to start within 80 ms. However, we wait for
Expand All @@ -335,12 +347,20 @@ def _wait_create(self):
"""Wait until the API socket and chroot folder are available."""
os.stat(self._jailer.api_socket_path())

def serial_input(self, input_string):
"""Send a string to the Firecracker serial console via screen."""
input_cmd = 'screen -S {session} -p 0 -X stuff "{input_string}^M"'
run(input_cmd.format(session=self._session_name,
input_string=input_string),
shell=True, check=True)

def basic_config(
self,
vcpu_count: int = 2,
ht_enabled: bool = False,
mem_size_mib: int = 256,
add_root_device: bool = True
add_root_device: bool = True,
boot_args: str = None,
):
"""Shortcut for quickly configuring a microVM.
Expand Down Expand Up @@ -369,7 +389,8 @@ def basic_config(

# Add a kernel to start booting from.
response = self.boot.put(
kernel_image_path=self.create_jailed_resource(self.kernel_file)
kernel_image_path=self.create_jailed_resource(self.kernel_file),
boot_args=boot_args
)
assert self._api_session.is_status_no_content(response.status_code)

Expand Down
149 changes: 149 additions & 0 deletions tests/integration_tests/functional/test_serial_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Tests scenario for the Firecracker serial console."""
import os
import time
import select


# Too few public methods (1/2) (too-few-public-methods)
# pylint: disable=R0903
class MatchStaticString:
"""Match a static string versus input."""

# Prevent state objects from being collected by pytest.
__test__ = False

def __init__(self, match_string):
"""Initialize using specified match string."""
self._string = match_string
self._input = ""

def match(self, input_char) -> bool:
"""
Check if `_input` matches the match `_string`.
Process one char at a time and build `_input` string.
Preserve built `_input` if partially matches `_string`.
Return True when `_input` is the same as `_string`.
"""
self._input += str(input_char)
if self._input == self._string[:len(self._input)]:
if len(self._input) == len(self._string):
self._input = ""
return True
return False

self._input = self._input[1:]
return False


class TestState(MatchStaticString):
"""Generic test state object."""

# Prevent state objects from being collected by pytest.
__test__ = False

def __init__(self, match_string=''):
"""Initialize state fields."""
MatchStaticString.__init__(self, match_string)
print('\n*** Current test state: ', str(self), end='')

def handle_input(self, microvm, input_char):
"""Handle input event and return next state."""

def __repr__(self):
"""Leverages the __str__ method to describe the TestState."""
return self.__str__()

def __str__(self):
"""Return state name."""
return self.__class__.__name__


class WaitLogin(TestState):
"""Initial state when we wait for the login prompt."""

def handle_input(self, microvm, input_char) -> TestState:
"""Handle input and return next state."""
if self.match(input_char):
# Send login username.
microvm.serial_input("root")
return WaitPasswordPrompt("Password:")
return self


class WaitPasswordPrompt(TestState):
"""Wait for the password prompt to be shown."""

def handle_input(self, microvm, input_char) -> TestState:
"""Handle input and return next state."""
if self.match(input_char):
microvm.serial_input("root")
# Wait 1 second for shell
time.sleep(1)
microvm.serial_input("id")
return WaitIDResult("uid=0(root) gid=0(root) groups=0(root)")
return self


class WaitIDResult(TestState):
"""Wait for the console to show the result of the 'id' shell command."""

def handle_input(self, microvm, input_char) -> TestState:
"""Handle input and return next state."""
if self.match(input_char):
return TestFinished()
return self


class TestFinished(TestState):
"""Test complete and successful."""

def handle_input(self, microvm, input_char) -> TestState:
"""Return self since the test is about to end."""
return self


def test_serial_console_login(test_microvm_with_ssh):
"""Test serial console login."""
microvm = test_microvm_with_ssh
microvm.jailer.daemonize = False
microvm.spawn()

# We don't need to monitor the memory for this test because we are
# just rebooting and the process dies before pmap gets the RSS.
microvm.memory_events_queue = None

# Set up the microVM with 1 vCPU and a serial console.
microvm.basic_config(vcpu_count=1,
boot_args='console=ttyS0 reboot=k panic=1 pci=off')

microvm.start()

# Screen stdout log
screen_log = "/tmp/screen.log"

# Open the screen log file.
screen_log_fd = os.open(screen_log, os.O_RDONLY)
poller = select.poll()

# Set initial state - wait for 'login:' prompt
current_state = WaitLogin("login:")

poller.register(screen_log_fd, select.POLLIN | select.POLLHUP)

while not isinstance(current_state, TestFinished):
result = poller.poll(0.1)
for fd, flag in result:
if flag & select.POLLIN:
output_char = str(os.read(fd, 1),
encoding='utf-8',
errors='ignore')
# [DEBUG] Uncomment to see the serial console output.
# print(output_char, end='')
current_state = current_state.handle_input(
microvm, output_char)
elif flag & select.POLLHUP:
assert False, "Oh! The console vanished before test completed."
os.close(screen_log_fd)
2 changes: 1 addition & 1 deletion tools/devtool
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
# Development container image (name:tag)
# This should be updated whenever we upgrade the development container.
# (Yet another step on our way to reproducible builds.)
DEVCTR_IMAGE="fcuvm/dev:v9"
DEVCTR_IMAGE="fcuvm/dev:v10"

# Naming things is hard
MY_NAME="Firecracker $(basename "$0")"
Expand Down

0 comments on commit 1519297

Please sign in to comment.