Path: blob/main/tests/integration_tests/functional/test_serial_io.py
1958 views
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.1# SPDX-License-Identifier: Apache-2.02"""Tests scenario for the Firecracker serial console."""34import fcntl5import os6import subprocess7import termios8import time910from framework.microvm import Serial11from framework.state_machine import TestState12import framework.utils as utils13import host_tools.logging as log_tools14import host_tools.network as net_tools # pylint: disable=import-error151617class WaitLogin(TestState): # pylint: disable=too-few-public-methods18"""Initial state when we wait for the login prompt."""1920def handle_input(self, serial, input_char) -> TestState:21"""Handle input and return next state."""22if self.match(input_char):23# Send login username.24serial.tx("root")25return WaitPasswordPrompt("Password:")26return self272829class WaitPasswordPrompt(TestState): # pylint: disable=too-few-public-methods30"""Wait for the password prompt to be shown."""3132def handle_input(self, serial, input_char) -> TestState:33"""Handle input and return next state."""34if self.match(input_char):35serial.tx("root")36# Wait 1 second for shell37time.sleep(1)38serial.tx("id")39return WaitIDResult("uid=0(root) gid=0(root) groups=0(root)")40return self414243class WaitIDResult(TestState): # pylint: disable=too-few-public-methods44"""Wait for the console to show the result of the 'id' shell command."""4546def handle_input(self, unused_serial, input_char) -> TestState:47"""Handle input and return next state."""48if self.match(input_char):49return TestFinished()50return self515253class TestFinished(TestState): # pylint: disable=too-few-public-methods54"""Test complete and successful."""5556def handle_input(self, unused_serial, _) -> TestState:57"""Return self since the test is about to end."""58return self596061def test_serial_console_login(test_microvm_with_ssh):62"""Test serial console login."""63microvm = test_microvm_with_ssh64microvm.jailer.daemonize = False65microvm.spawn()6667# We don't need to monitor the memory for this test because we are68# just rebooting and the process dies before pmap gets the RSS.69microvm.memory_monitor = None7071# Set up the microVM with 1 vCPU and a serial console.72microvm.basic_config(vcpu_count=1,73boot_args='console=ttyS0 reboot=k panic=1 pci=off')7475microvm.start()7677serial = Serial(microvm)78serial.open()79current_state = WaitLogin("login:")8081while not isinstance(current_state, TestFinished):82output_char = serial.rx_char()83current_state = current_state.handle_input(84serial, output_char)858687def get_total_mem_size(pid):88"""Get total memory usage for a process."""89cmd = f"pmap {pid} | tail -n 1 | sed 's/^ //' | tr -s ' ' | cut -d' ' -f2"90rc, stdout, stderr = utils.run_cmd(cmd)91assert rc == 092assert stderr == ""9394return stdout959697def send_bytes(tty, bytes_count, timeout=60):98"""Send data to the terminal."""99start = time.time()100for _ in range(bytes_count):101fcntl.ioctl(tty, termios.TIOCSTI, '\n')102current = time.time()103if current - start > timeout:104break105106107def test_serial_dos(test_microvm_with_ssh):108"""Test serial console behavior under DoS."""109microvm = test_microvm_with_ssh110microvm.jailer.daemonize = False111microvm.spawn()112microvm.memory_events_queue = None113114# Set up the microVM with 1 vCPU and a serial console.115microvm.basic_config(vcpu_count=1,116add_root_device=False,117boot_args='console=ttyS0 reboot=k panic=1 pci=off')118microvm.start()119120# Open an fd for firecracker process terminal.121tty_path = f"/proc/{microvm.jailer_clone_pid}/fd/0"122tty_fd = os.open(tty_path, os.O_RDWR)123124# Check if the total memory size changed.125before_size = get_total_mem_size(microvm.jailer_clone_pid)126send_bytes(tty_fd, 100000000, timeout=1)127after_size = get_total_mem_size(microvm.jailer_clone_pid)128assert before_size == after_size, "The memory size of the " \129"Firecracker process " \130"changed from {} to {}." \131.format(before_size,132after_size)133134135def test_serial_block(test_microvm_with_ssh, network_config):136"""Test that writing to stdout never blocks the vCPU thread."""137test_microvm = test_microvm_with_ssh138test_microvm.jailer.daemonize = False139test_microvm.spawn()140# Set up the microVM with 1 vCPU so we make sure the vCPU thread141# responsible for the SSH connection will also run the serial.142test_microvm.basic_config(143vcpu_count=1,144mem_size_mib=512,145boot_args='console=ttyS0 reboot=k panic=1 pci=off'146)147148_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')149150# Configure the metrics.151metrics_fifo_path = os.path.join(test_microvm.path, 'metrics_fifo')152metrics_fifo = log_tools.Fifo(metrics_fifo_path)153response = test_microvm.metrics.put(154metrics_path=test_microvm.create_jailed_resource(metrics_fifo.path)155)156assert test_microvm.api_session.is_status_no_content(response.status_code)157158test_microvm.start()159ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)160161# Get an initial reading of missed writes to the serial.162fc_metrics = test_microvm.flush_metrics(metrics_fifo)163init_count = fc_metrics['uart']['missed_write_count']164165screen_pid = test_microvm.screen_pid166# Stop `screen` process which captures stdout so we stop consuming stdout.167subprocess.check_call(168"kill -s STOP {}".format(screen_pid),169shell=True170)171172# Generate a random text file.173exit_code, _, _ = ssh_connection.execute_command(174"base64 /dev/urandom | head -c 100000 > file.txt"175)176177# Dump output to terminal178exit_code, _, _ = ssh_connection.execute_command(179"cat file.txt > /dev/ttyS0"180)181assert exit_code == 0182183# Check that the vCPU isn't blocked.184exit_code, _, _ = ssh_connection.execute_command(185"cd /"186)187assert exit_code == 0188189# Check the metrics to see if the serial missed bytes.190fc_metrics = test_microvm.flush_metrics(metrics_fifo)191last_count = fc_metrics['uart']['missed_write_count']192193# Should be significantly more than before the `cat` command.194assert last_count - init_count > 10000195196197