diff --git a/CHANGELOG.md b/CHANGELOG.md index 231891ca92b..e7eb43cf986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,12 @@ and this project adheres to requests to `/mmds/config` to enforce MMDS to always respond plain text contents in the IMDS format regardless of the `Accept` header in requests. Users need to regenerate snapshots. +- [#5350](https://github.com/firecracker-microvm/firecracker/pull/5350): Added a + `/serial` endpoint, which allows setting `serial_out_path` to the path of a + pre-created file into which Firecracker should redirect output from the + guest's serial console. Not configuring it means Firecracker will continue to + print serial output to stdout. Similarly to the logger, this configuration is + not persisted across snapshots. ### Changed diff --git a/src/firecracker/src/api_server/parsed_request.rs b/src/firecracker/src/api_server/parsed_request.rs index 10d5c3d97ea..216e7c7fcf2 100644 --- a/src/firecracker/src/api_server/parsed_request.rs +++ b/src/firecracker/src/api_server/parsed_request.rs @@ -27,6 +27,7 @@ use super::request::net::{parse_patch_net, parse_put_net}; use super::request::snapshot::{parse_patch_vm_state, parse_put_snapshot}; use super::request::version::parse_get_version; use super::request::vsock::parse_put_vsock; +use crate::api_server::request::serial::parse_put_serial; #[derive(Debug)] pub(crate) enum RequestAction { @@ -90,6 +91,7 @@ impl TryFrom<&Request> for ParsedRequest { (Method::Put, "cpu-config", Some(body)) => parse_put_cpu_config(body), (Method::Put, "drives", Some(body)) => parse_put_drive(body, path_tokens.next()), (Method::Put, "logger", Some(body)) => parse_put_logger(body), + (Method::Put, "serial", Some(body)) => parse_put_serial(body), (Method::Put, "machine-config", Some(body)) => parse_put_machine_config(body), (Method::Put, "metrics", Some(body)) => parse_put_metrics(body), (Method::Put, "mmds", Some(body)) => parse_put_mmds(body, path_tokens.next()), diff --git a/src/firecracker/src/api_server/request/mod.rs b/src/firecracker/src/api_server/request/mod.rs index 0c1622798f4..a406842d0a6 100644 --- a/src/firecracker/src/api_server/request/mod.rs +++ b/src/firecracker/src/api_server/request/mod.rs @@ -13,6 +13,7 @@ pub mod machine_configuration; pub mod metrics; pub mod mmds; pub mod net; +pub mod serial; pub mod snapshot; pub mod version; pub mod vsock; diff --git a/src/firecracker/src/api_server/request/serial.rs b/src/firecracker/src/api_server/request/serial.rs new file mode 100644 index 00000000000..a75895cece2 --- /dev/null +++ b/src/firecracker/src/api_server/request/serial.rs @@ -0,0 +1,39 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use micro_http::Body; +use vmm::logger::{IncMetric, METRICS}; +use vmm::rpc_interface::VmmAction; +use vmm::vmm_config::serial::SerialConfig; + +use crate::api_server::parsed_request::{ParsedRequest, RequestError}; + +pub(crate) fn parse_put_serial(body: &Body) -> Result { + METRICS.put_api_requests.serial_count.inc(); + let res = serde_json::from_slice::(body.raw()); + let config = res.inspect_err(|_| { + METRICS.put_api_requests.serial_fails.inc(); + })?; + Ok(ParsedRequest::new_sync(VmmAction::ConfigureSerial(config))) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + use crate::api_server::parsed_request::tests::vmm_action_from_request; + + #[test] + fn test_parse_put_serial_request() { + let body = r#"{"serial_out_path": "serial"}"#; + + let expected_config = SerialConfig { + serial_out_path: Some(PathBuf::from("serial")), + }; + assert_eq!( + vmm_action_from_request(parse_put_serial(&Body::new(body)).unwrap()), + VmmAction::ConfigureSerial(expected_config) + ); + } +} diff --git a/src/firecracker/swagger/firecracker.yaml b/src/firecracker/swagger/firecracker.yaml index 5a101ca204b..3b1a1869239 100644 --- a/src/firecracker/swagger/firecracker.yaml +++ b/src/firecracker/swagger/firecracker.yaml @@ -506,6 +506,27 @@ paths: schema: $ref: "#/definitions/Error" + /serial: + put: + summary: Configures the serial console + operationId: putSerialDevice + description: + Configure the serial console, which the guest can write its kernel logs to. Has no effect if + the serial console is not also enabled on the guest kernel command line + parameters: + - name: body + in: body + description: Serial console properties + required: true + schema: + $ref: "#/definitions/SerialDevice" + responses: + 204: + description: Serial device configured + default: + description: Internal server error + schema: + $ref: "#/definitions/Error" /network-interfaces/{iface_id}: put: @@ -1334,6 +1355,15 @@ definitions: rate_limiter: $ref: "#/definitions/RateLimiter" + SerialDevice: + type: object + description: + The configuration of the serial device + properties: + output_path: + type: string + description: Path to a file or named pipe on the host to which serial output should be written. + FirecrackerVersion: type: object description: diff --git a/src/vmm/src/arch/aarch64/fdt.rs b/src/vmm/src/arch/aarch64/fdt.rs index 6a50c0257a9..9946d3516cc 100644 --- a/src/vmm/src/arch/aarch64/fdt.rs +++ b/src/vmm/src/arch/aarch64/fdt.rs @@ -561,7 +561,7 @@ mod tests { cmdline.insert("console", "/dev/tty0").unwrap(); device_manager - .attach_legacy_devices_aarch64(&vm, &mut event_manager, &mut cmdline) + .attach_legacy_devices_aarch64(&vm, &mut event_manager, &mut cmdline, None) .unwrap(); let dummy = Arc::new(Mutex::new(DummyDevice::new())); device_manager diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index 88d7f56cb4e..b5231f90d8d 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -170,7 +170,12 @@ pub fn build_microvm_for_boot( let (mut vcpus, vcpus_exit_evt) = vm.create_vcpus(vm_resources.machine_config.vcpu_count)?; vm.register_memory_regions(guest_memory)?; - let mut device_manager = DeviceManager::new(event_manager, &vcpus_exit_evt, &vm)?; + let mut device_manager = DeviceManager::new( + event_manager, + &vcpus_exit_evt, + &vm, + vm_resources.serial_out_path.as_ref(), + )?; let vm = Arc::new(vm); @@ -248,7 +253,12 @@ pub fn build_microvm_for_boot( } #[cfg(target_arch = "aarch64")] - device_manager.attach_legacy_devices_aarch64(&vm, event_manager, &mut boot_cmdline)?; + device_manager.attach_legacy_devices_aarch64( + &vm, + event_manager, + &mut boot_cmdline, + vm_resources.serial_out_path.as_ref(), + )?; device_manager.attach_vmgenid_device(vm.guest_memory(), &vm)?; @@ -272,7 +282,6 @@ pub fn build_microvm_for_boot( )?; let vmm = Vmm { - events_observer: Some(std::io::stdin()), instance_info: instance_info.clone(), shutdown_exit_code: None, kvm, @@ -473,7 +482,6 @@ pub fn build_microvm_from_snapshot( DeviceManager::restore(device_ctor_args, µvm_state.device_states)?; let mut vmm = Vmm { - events_observer: Some(std::io::stdin()), instance_info: instance_info.clone(), shutdown_exit_code: None, kvm, @@ -722,7 +730,6 @@ pub(crate) mod tests { let (_, vcpus_exit_evt) = vm.create_vcpus(1).unwrap(); Vmm { - events_observer: Some(std::io::stdin()), instance_info: InstanceInfo::default(), shutdown_exit_code: None, kvm, diff --git a/src/vmm/src/device_manager/legacy.rs b/src/vmm/src/device_manager/legacy.rs index d0194e24e62..77173433d38 100644 --- a/src/vmm/src/device_manager/legacy.rs +++ b/src/vmm/src/device_manager/legacy.rs @@ -104,7 +104,7 @@ impl PortIODeviceManager { SerialEventsWrapper { buffer_ready_event_fd: None, }, - SerialOut::Sink(std::io::sink()), + SerialOut::Sink, ), input: None, })); @@ -114,7 +114,7 @@ impl PortIODeviceManager { SerialEventsWrapper { buffer_ready_event_fd: None, }, - SerialOut::Sink(std::io::sink()), + SerialOut::Sink, ), input: None, })); @@ -249,7 +249,7 @@ mod tests { SerialEventsWrapper { buffer_ready_event_fd: None, }, - SerialOut::Sink(std::io::sink()), + SerialOut::Sink, ), input: None, })), diff --git a/src/vmm/src/device_manager/mod.rs b/src/vmm/src/device_manager/mod.rs index c7f6acabfe1..5cb8a73c2ec 100644 --- a/src/vmm/src/device_manager/mod.rs +++ b/src/vmm/src/device_manager/mod.rs @@ -7,6 +7,8 @@ use std::convert::Infallible; use std::fmt::Debug; +use std::os::unix::prelude::OpenOptionsExt; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; use acpi::ACPIDeviceManager; @@ -125,11 +127,25 @@ impl DeviceManager { /// Sets up the serial device. fn setup_serial_device( event_manager: &mut EventManager, + output: Option<&PathBuf>, ) -> Result>, std::io::Error> { - let serial = Arc::new(Mutex::new(SerialDevice::new( - Some(std::io::stdin()), - SerialOut::Stdout(std::io::stdout()), - )?)); + let (serial_in, serial_out) = match output { + Some(ref path) => ( + None, + std::fs::OpenOptions::new() + .custom_flags(libc::O_NONBLOCK) + .write(true) + .open(path) + .map(SerialOut::File)?, + ), + None => { + Self::set_stdout_nonblocking(); + + (Some(std::io::stdin()), SerialOut::Stdout(std::io::stdout())) + } + }; + + let serial = Arc::new(Mutex::new(SerialDevice::new(serial_in, serial_out)?)); event_manager.add_subscriber(serial.clone()); Ok(serial) } @@ -139,11 +155,10 @@ impl DeviceManager { event_manager: &mut EventManager, vcpus_exit_evt: &EventFd, vm: &Vm, + serial_output: Option<&PathBuf>, ) -> Result { - Self::set_stdout_nonblocking(); - // Create serial device - let serial = Self::setup_serial_device(event_manager)?; + let serial = Self::setup_serial_device(event_manager, serial_output)?; let reset_evt = vcpus_exit_evt .try_clone() .map_err(DeviceManagerCreateError::EventFd)?; @@ -161,9 +176,11 @@ impl DeviceManager { event_manager: &mut EventManager, vcpus_exit_evt: &EventFd, vm: &Vm, + serial_output: Option<&PathBuf>, ) -> Result { #[cfg(target_arch = "x86_64")] - let legacy_devices = Self::create_legacy_devices(event_manager, vcpus_exit_evt, vm)?; + let legacy_devices = + Self::create_legacy_devices(event_manager, vcpus_exit_evt, vm, serial_output)?; Ok(DeviceManager { mmio_devices: MMIODeviceManager::new(), @@ -243,6 +260,7 @@ impl DeviceManager { vm: &Vm, event_manager: &mut EventManager, cmdline: &mut Cmdline, + serial_out_path: Option<&PathBuf>, ) -> Result<(), AttachDeviceError> { // Serial device setup. let cmdline_contains_console = cmdline @@ -253,9 +271,7 @@ impl DeviceManager { .contains("console="); if cmdline_contains_console { - // Make stdout non-blocking. - Self::set_stdout_nonblocking(); - let serial = Self::setup_serial_device(event_manager)?; + let serial = Self::setup_serial_device(event_manager, serial_out_path)?; self.mmio_devices.register_mmio_serial(vm, serial, None)?; self.mmio_devices.add_mmio_serial_to_cmdline(cmdline)?; } @@ -453,6 +469,7 @@ impl<'a> Persist<'a> for DeviceManager { constructor_args.event_manager, constructor_args.vcpus_exit_evt, constructor_args.vm, + constructor_args.vm_resources.serial_out_path.as_ref(), )?; // Restore MMIO devices @@ -555,7 +572,7 @@ pub(crate) mod tests { #[cfg(target_arch = "x86_64")] let legacy_devices = PortIODeviceManager::new( Arc::new(Mutex::new( - SerialDevice::new(None, SerialOut::Sink(std::io::sink())).unwrap(), + SerialDevice::new(None, SerialOut::Sink).unwrap(), )), Arc::new(Mutex::new( I8042Device::new(EventFd::new(libc::EFD_NONBLOCK).unwrap()).unwrap(), @@ -582,7 +599,7 @@ pub(crate) mod tests { let mut cmdline = Cmdline::new(4096).unwrap(); let mut event_manager = EventManager::new().unwrap(); vmm.device_manager - .attach_legacy_devices_aarch64(&vmm.vm, &mut event_manager, &mut cmdline) + .attach_legacy_devices_aarch64(&vmm.vm, &mut event_manager, &mut cmdline, None) .unwrap(); assert!(vmm.device_manager.mmio_devices.rtc.is_some()); assert!(vmm.device_manager.mmio_devices.serial.is_none()); @@ -590,7 +607,7 @@ pub(crate) mod tests { let mut vmm = default_vmm(); cmdline.insert("console", "/dev/blah").unwrap(); vmm.device_manager - .attach_legacy_devices_aarch64(&vmm.vm, &mut event_manager, &mut cmdline) + .attach_legacy_devices_aarch64(&vmm.vm, &mut event_manager, &mut cmdline, None) .unwrap(); assert!(vmm.device_manager.mmio_devices.rtc.is_some()); assert!(vmm.device_manager.mmio_devices.serial.is_some()); diff --git a/src/vmm/src/device_manager/persist.rs b/src/vmm/src/device_manager/persist.rs index eb6bb06e8d6..350a392597c 100644 --- a/src/vmm/src/device_manager/persist.rs +++ b/src/vmm/src/device_manager/persist.rs @@ -16,9 +16,7 @@ use super::mmio::*; use crate::arch::DeviceType; use crate::devices::acpi::vmgenid::{VMGenIDState, VMGenIdConstructorArgs, VmGenId, VmGenIdError}; #[cfg(target_arch = "aarch64")] -use crate::devices::legacy::serial::SerialOut; -#[cfg(target_arch = "aarch64")] -use crate::devices::legacy::{RTCDevice, SerialDevice}; +use crate::devices::legacy::RTCDevice; use crate::devices::virtio::balloon::persist::{BalloonConstructorArgs, BalloonState}; use crate::devices::virtio::balloon::{Balloon, BalloonError}; use crate::devices::virtio::block::BlockError; @@ -358,13 +356,10 @@ impl<'a> Persist<'a> for MMIODeviceManager { { for state in &state.legacy_devices { if state.type_ == DeviceType::Serial { - let serial = Arc::new(Mutex::new(SerialDevice::new( - Some(std::io::stdin()), - SerialOut::Stdout(std::io::stdout()), - )?)); - constructor_args - .event_manager - .add_subscriber(serial.clone()); + let serial = crate::DeviceManager::setup_serial_device( + constructor_args.event_manager, + constructor_args.vm_resources.serial_out_path.as_ref(), + )?; dev_manager.register_mmio_serial(vm, serial, Some(state.device_info))?; } diff --git a/src/vmm/src/devices/legacy/serial.rs b/src/vmm/src/devices/legacy/serial.rs index afc47189c1e..83e84a7bf56 100644 --- a/src/vmm/src/devices/legacy/serial.rs +++ b/src/vmm/src/devices/legacy/serial.rs @@ -7,6 +7,7 @@ //! Implements a wrapper over an UART serial device. use std::fmt::Debug; +use std::fs::File; use std::io::{self, Read, Stdin, Write}; use std::os::unix::io::{AsRawFd, RawFd}; use std::sync::{Arc, Barrier}; @@ -127,20 +128,23 @@ impl SerialEvents for SerialEventsWrapper { #[derive(Debug)] pub enum SerialOut { - Sink(std::io::Sink), + Sink, Stdout(std::io::Stdout), + File(File), } impl std::io::Write for SerialOut { fn write(&mut self, buf: &[u8]) -> std::io::Result { match self { - Self::Sink(sink) => sink.write(buf), + Self::Sink => Ok(buf.len()), Self::Stdout(stdout) => stdout.write(buf), + Self::File(file) => file.write(buf), } } fn flush(&mut self) -> std::io::Result<()> { match self { - Self::Sink(sink) => sink.flush(), + Self::Sink => Ok(()), Self::Stdout(stdout) => stdout.flush(), + Self::File(file) => file.flush(), } } } @@ -407,7 +411,7 @@ mod tests { SerialEventsWrapper { buffer_ready_event_fd: None, }, - SerialOut::Sink(std::io::sink()), + SerialOut::Sink, ), input: None::, }; diff --git a/src/vmm/src/lib.rs b/src/vmm/src/lib.rs index 7bb33411b7e..4bb0a064799 100644 --- a/src/vmm/src/lib.rs +++ b/src/vmm/src/lib.rs @@ -245,10 +245,6 @@ pub enum VmmError { Vm(#[from] vstate::vm::VmError), /// Kvm error: {0} Kvm(#[from] vstate::kvm::KvmError), - /// Error thrown by observer object on Vmm initialization: {0} - VmmObserverInit(vmm_sys_util::errno::Error), - /// Error thrown by observer object on Vmm teardown: {0} - VmmObserverTeardown(vmm_sys_util::errno::Error), /// VMGenID error: {0} VMGenID(#[from] VmGenIdError), /// Failed perform action on device: {0} @@ -293,7 +289,6 @@ pub enum DumpCpuConfigError { /// Contains the state and associated methods required for the Firecracker VMM. #[derive(Debug)] pub struct Vmm { - events_observer: Option, /// The [`InstanceInfo`] state of this [`Vmm`]. pub instance_info: InstanceInfo, shutdown_exit_code: Option, @@ -343,17 +338,16 @@ impl Vmm { let vcpu_count = vcpus.len(); let barrier = Arc::new(Barrier::new(vcpu_count + 1)); - if let Some(stdin) = self.events_observer.as_mut() { - // Set raw mode for stdin. - stdin.lock().set_raw_mode().inspect_err(|&err| { - warn!("Cannot set raw mode for the terminal. {:?}", err); - })?; + let stdin = std::io::stdin().lock(); + // Set raw mode for stdin. + stdin.set_raw_mode().inspect_err(|&err| { + warn!("Cannot set raw mode for the terminal. {:?}", err); + })?; - // Set non blocking stdin. - stdin.lock().set_non_block(true).inspect_err(|&err| { - warn!("Cannot set non block for the terminal. {:?}", err); - })?; - } + // Set non blocking stdin. + stdin.set_non_block(true).inspect_err(|&err| { + warn!("Cannot set non block for the terminal. {:?}", err); + })?; self.vcpus_handles.reserve(vcpu_count); @@ -760,13 +754,8 @@ impl Drop for Vmm { // has already been stopped by the event manager at this point. self.stop(self.shutdown_exit_code.unwrap_or(FcExitCode::Ok)); - if let Some(observer) = self.events_observer.as_mut() { - let res = observer.lock().set_canon_mode().inspect_err(|&err| { - warn!("Cannot set canonical mode for the terminal. {:?}", err); - }); - if let Err(err) = res { - warn!("{}", VmmError::VmmObserverTeardown(err)); - } + if let Err(err) = std::io::stdin().lock().set_canon_mode() { + warn!("Cannot set canonical mode for the terminal. {:?}", err); } // Write the metrics before exiting. diff --git a/src/vmm/src/logger/logging.rs b/src/vmm/src/logger/logging.rs index e5cdd8a33d3..21ede373991 100644 --- a/src/vmm/src/logger/logging.rs +++ b/src/vmm/src/logger/logging.rs @@ -64,7 +64,6 @@ impl Logger { if let Some(log_path) = config.log_path { let file = std::fs::OpenOptions::new() .custom_flags(libc::O_NONBLOCK) - .read(true) .write(true) .open(log_path) .map_err(LoggerUpdateError)?; diff --git a/src/vmm/src/logger/metrics.rs b/src/vmm/src/logger/metrics.rs index f03d196d5fa..d5098cbf748 100644 --- a/src/vmm/src/logger/metrics.rs +++ b/src/vmm/src/logger/metrics.rs @@ -421,6 +421,10 @@ pub struct PutRequestsMetrics { pub vsock_count: SharedIncMetric, /// Number of failures in creating a vsock device. pub vsock_fails: SharedIncMetric, + /// Number of PUTs to /serial + pub serial_count: SharedIncMetric, + /// Number of failed PUTs to /serial + pub serial_fails: SharedIncMetric, } impl PutRequestsMetrics { /// Const default construction. @@ -446,6 +450,8 @@ impl PutRequestsMetrics { mmds_fails: SharedIncMetric::new(), vsock_count: SharedIncMetric::new(), vsock_fails: SharedIncMetric::new(), + serial_count: SharedIncMetric::new(), + serial_fails: SharedIncMetric::new(), } } } diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index d29f76740fc..911ee5e5c99 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -28,6 +28,7 @@ use crate::vmm_config::machine_config::{ use crate::vmm_config::metrics::{MetricsConfig, MetricsConfigError, init_metrics}; use crate::vmm_config::mmds::{MmdsConfig, MmdsConfigError}; use crate::vmm_config::net::*; +use crate::vmm_config::serial::SerialConfig; use crate::vmm_config::vsock::*; use crate::vstate::memory; use crate::vstate::memory::{GuestRegionMmap, MemoryError}; @@ -86,6 +87,8 @@ pub struct VmmConfig { network_interfaces: Vec, vsock: Option, entropy: Option, + #[serde(skip)] + serial_config: Option, } /// A data structure that encapsulates the device configurations @@ -116,6 +119,8 @@ pub struct VmResources { pub boot_timer: bool, /// Whether or not to use PCIe transport for VirtIO devices. pub pci_enabled: bool, + /// Where serial console output should be written to + pub serial_out_path: Option, } impl VmResources { @@ -193,6 +198,10 @@ impl VmResources { resources.build_entropy_device(entropy_device_config)?; } + if let Some(serial_cfg) = vmm_config.serial_config { + resources.serial_out_path = serial_cfg.serial_out_path; + } + Ok(resources) } @@ -506,6 +515,8 @@ impl From<&VmResources> for VmmConfig { network_interfaces: resources.net_builder.configs(), vsock: resources.vsock.config(), entropy: resources.entropy.config(), + // serial_config is marked serde(skip) so that it doesnt end up in snapshots. + serial_config: None, } } } @@ -617,6 +628,7 @@ mod tests { mmds_size_limit: HTTP_MAX_PAYLOAD_SIZE, entropy: Default::default(), pci_enabled: false, + serial_out_path: None, } } diff --git a/src/vmm/src/rpc_interface.rs b/src/vmm/src/rpc_interface.rs index d26b1ba877d..85aab1af826 100644 --- a/src/vmm/src/rpc_interface.rs +++ b/src/vmm/src/rpc_interface.rs @@ -33,6 +33,7 @@ use crate::vmm_config::mmds::{MmdsConfig, MmdsConfigError}; use crate::vmm_config::net::{ NetworkInterfaceConfig, NetworkInterfaceError, NetworkInterfaceUpdateConfig, }; +use crate::vmm_config::serial::SerialConfig; use crate::vmm_config::snapshot::{CreateSnapshotParams, LoadSnapshotParams, SnapshotType}; use crate::vmm_config::vsock::{VsockConfigError, VsockDeviceConfig}; use crate::vmm_config::{self, RateLimiterUpdate}; @@ -50,6 +51,8 @@ pub enum VmmAction { /// Configure the metrics using as input the `MetricsConfig`. This action can only be called /// before the microVM has booted. ConfigureMetrics(MetricsConfig), + /// Configure the serial device. This action can only be called before the microVM has booted. + ConfigureSerial(SerialConfig), /// Create a snapshot using as input the `CreateSnapshotParams`. This action can only be called /// after the microVM has booted and only when the microVM is in `Paused` state. CreateSnapshot(CreateSnapshotParams), @@ -408,6 +411,10 @@ impl<'a> PrebootApiController<'a> { ConfigureMetrics(metrics_cfg) => vmm_config::metrics::init_metrics(metrics_cfg) .map(|()| VmmData::Empty) .map_err(VmmActionError::Metrics), + ConfigureSerial(serial_cfg) => { + self.vm_resources.serial_out_path = serial_cfg.serial_out_path; + Ok(VmmData::Empty) + } GetBalloonConfig => self.balloon_config(), GetFullVmConfig => { warn!( @@ -676,6 +683,7 @@ impl RuntimeApiController { ConfigureBootSource(_) | ConfigureLogger(_) | ConfigureMetrics(_) + | ConfigureSerial(_) | InsertBlockDevice(_) | InsertNetworkDevice(_) | LoadSnapshot(_) diff --git a/src/vmm/src/vmm_config/mod.rs b/src/vmm/src/vmm_config/mod.rs index c7afc5fc65f..0e244ad4328 100644 --- a/src/vmm/src/vmm_config/mod.rs +++ b/src/vmm/src/vmm_config/mod.rs @@ -31,6 +31,7 @@ pub mod mmds; /// Wrapper for configuring the network devices attached to the microVM. pub mod net; /// Wrapper for configuring microVM snapshots and the microVM state. +pub mod serial; pub mod snapshot; /// Wrapper for configuring the vsock devices attached to the microVM. pub mod vsock; diff --git a/src/vmm/src/vmm_config/serial.rs b/src/vmm/src/vmm_config/serial.rs new file mode 100644 index 00000000000..0433df95940 --- /dev/null +++ b/src/vmm/src/vmm_config/serial.rs @@ -0,0 +1,14 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::path::PathBuf; + +use serde::Deserialize; + +/// The body of a PUT /serial request. +#[derive(Debug, PartialEq, Eq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SerialConfig { + /// Named pipe or file used as output for guest serial console. + pub serial_out_path: Option, +} diff --git a/tests/conftest.py b/tests/conftest.py index 96ee285d192..267ef0ffa7a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -378,12 +378,22 @@ def microvm_factory(request, record_property, results_dir, netns_factory): uvm.flush_metrics() except: # pylint: disable=bare-except pass + + try: + uvm.snapshot_full( + mem_path="post_failure.mem", vmstate_path="post_failure.vmstate" + ) + except: # pylint: disable=bare-except + pass + uvm_data = results_dir / uvm.id uvm_data.mkdir() uvm_data.joinpath("host-dmesg.log").write_text( utils.run_cmd(["dmesg", "-dPx"]).stdout ) shutil.copy(f"/firecracker/build/img/{platform.machine()}/id_rsa", uvm_data) + if Path(uvm.screen_log).exists(): + shutil.copy(uvm.screen_log, uvm_data) uvm_root = Path(uvm.chroot()) for item in os.listdir(uvm_root): @@ -392,9 +402,6 @@ def microvm_factory(request, record_property, results_dir, netns_factory): continue dst = uvm_data / item shutil.copy(src, dst) - console_data = uvm.console_data - if console_data: - uvm_data.joinpath("guest-console.log").write_text(console_data) uvm_factory.kill() @@ -598,7 +605,10 @@ def uvm_booted( ): """Return a booted uvm""" uvm = microvm_factory.build(guest_kernel, rootfs, pci=pci_enabled) - uvm.spawn() + if getattr(microvm_factory, "hack_no_serial", False): + uvm.spawn(serial_out_path=None) + else: + uvm.spawn() uvm.basic_config(vcpu_count=vcpu_count, mem_size_mib=mem_size_mib) uvm.set_cpu_template(cpu_template) uvm.add_net_iface() diff --git a/tests/framework/http_api.py b/tests/framework/http_api.py index 7534fe829d2..16990a2a927 100644 --- a/tests/framework/http_api.py +++ b/tests/framework/http_api.py @@ -132,3 +132,4 @@ def __init__(self, api_usocket_full_name, *, on_error=None): self.snapshot_load = Resource(self, "/snapshot/load") self.cpu_config = Resource(self, "/cpu-config") self.entropy = Resource(self, "/entropy") + self.serial = Resource(self, "/serial") diff --git a/tests/framework/microvm.py b/tests/framework/microvm.py index 3c672e82e23..7ab4ea051f7 100644 --- a/tests/framework/microvm.py +++ b/tests/framework/microvm.py @@ -259,6 +259,7 @@ def __init__( self.api = None self.log_file = None + self.serial_out_path = None self.metrics_file = None self._spawned = False self._killed = False @@ -463,16 +464,6 @@ def log_data(self): return "" return self.log_file.read_text() - @property - def console_data(self): - """Return the output of microVM's console""" - if self.screen_log is None: - return None - file = Path(self.screen_log) - if not file.exists(): - return None - return file.read_text(encoding="utf-8") - @property def state(self): """Get the InstanceInfo property and return the state field.""" @@ -634,6 +625,7 @@ def add_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, @@ -664,6 +656,11 @@ def spawn( 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() @@ -728,13 +725,18 @@ def spawn( # 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) @@ -805,12 +807,9 @@ def basic_config( 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 in Firecracker is - reboot=k panic=1 nomodule 8250.nr_uarts=0 i8042.noaux i8042.nomux - i8042.nopnp i8042.dumbkbd swiotlb=noforce - - if PCI is disabled, Firecracker also passes to the guest pci=off - + 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( @@ -834,6 +833,10 @@ def basic_config( 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, @@ -1228,7 +1231,10 @@ def build(self, kernel=None, rootfs=None, **kwargs): def build_from_snapshot(self, snapshot: Snapshot): """Build a microvm from a snapshot""" vm = self.build() - vm.spawn() + if getattr(self, "hack_no_serial", False): + vm.spawn(serial_out_path=None) + else: + vm.spawn() vm.restore_from_snapshot(snapshot, resume=True) return vm @@ -1323,9 +1329,9 @@ def open(self): time.sleep(0.2) attempt += 1 - screen_log_fd = os.open(self._vm.screen_log, os.O_RDONLY) + serial_log_fd = os.open(self._vm.screen_log, os.O_RDONLY) self._poller = select.poll() - self._poller.register(screen_log_fd, select.POLLIN | select.POLLHUP) + self._poller.register(serial_log_fd, select.POLLIN | select.POLLHUP) def tx(self, input_string, end="\n"): # pylint: disable=invalid-name diff --git a/tests/framework/state_machine.py b/tests/framework/state_machine.py deleted file mode 100644 index 1d8dd664e6b..00000000000 --- a/tests/framework/state_machine.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Defines a stream based string matcher and a generic state object.""" - - -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`. - """ - if input_char == "": - return False - 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, serial, 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__ diff --git a/tests/framework/utils_iperf.py b/tests/framework/utils_iperf.py index aa2b663c1c7..9d0d064159c 100644 --- a/tests/framework/utils_iperf.py +++ b/tests/framework/utils_iperf.py @@ -65,12 +65,12 @@ def run_test(self, first_free_cpu): clients = [] for client_idx in range(self._num_clients): - client_mode = self.client_mode(client_idx) - client_mode_flag = self.client_mode_to_iperf3_flag(client_mode) client_future = executor.submit( - self.spawn_iperf3_client, client_idx, client_mode_flag + self.spawn_iperf3_client, + client_idx, + self.client_mode_to_iperf3_flag, ) - clients.append((client_mode, client_future)) + clients.append((self._mode, client_future)) data = {"cpu_load_raw": cpu_load_future.result(), "g2h": [], "h2g": []} @@ -79,31 +79,12 @@ def run_test(self, first_free_cpu): return data - def client_mode(self, client_idx): - """Converts client index into client mode""" - match self._mode: - case "g2h": - client_mode = "g2h" - case "h2g": - client_mode = "h2g" - case "bd": - # in bidirectional mode we alternate - # modes - if client_idx % 2 == 0: - client_mode = "g2h" - else: - client_mode = "h2g" - return client_mode - - @staticmethod - def client_mode_to_iperf3_flag(client_mode): + @property + def client_mode_to_iperf3_flag(self): """Converts client mode into iperf3 mode flag""" - match client_mode: - case "g2h": - client_mode_flag = "" - case "h2g": - client_mode_flag = "-R" - return client_mode_flag + if self._mode == "h2g": + return "-R" + return "" def spawn_iperf3_client(self, client_idx, client_mode_flag): """ diff --git a/tests/host_tools/fcmetrics.py b/tests/host_tools/fcmetrics.py index 1b3cdcb96b1..e2a1862c21f 100644 --- a/tests/host_tools/fcmetrics.py +++ b/tests/host_tools/fcmetrics.py @@ -227,6 +227,8 @@ def validate_fc_metrics(metrics): "mmds_fails", "vsock_count", "vsock_fails", + "serial_count", + "serial_fails", ], "seccomp": [ "num_faults", diff --git a/tests/integration_tests/functional/test_api.py b/tests/integration_tests/functional/test_api.py index 55bb15d5eb4..32527e5c905 100644 --- a/tests/integration_tests/functional/test_api.py +++ b/tests/integration_tests/functional/test_api.py @@ -1136,8 +1136,10 @@ def test_get_full_config_after_restoring_snapshot(microvm_factory, uvm_nano): expected_cfg["boot-source"] = { "kernel_image_path": uvm_nano.get_jailed_resource(uvm_nano.kernel_file), "initrd_path": None, - "boot_args": None, + "boot_args": "reboot=k panic=1 nomodule swiotlb=noforce console=ttyS0", } + if not uvm_nano.pci_enabled: + expected_cfg["boot-source"]["boot_args"] += " pci=off" # no ipv4_address or imds_compat specified during PUT /mmds/config so we expect the default expected_cfg["mmds-config"] = { diff --git a/tests/integration_tests/functional/test_api_server.py b/tests/integration_tests/functional/test_api_server.py index b502dff7e6d..1df880c7f28 100644 --- a/tests/integration_tests/functional/test_api_server.py +++ b/tests/integration_tests/functional/test_api_server.py @@ -23,7 +23,7 @@ def test_api_socket_in_use(uvm_plain): sock = socket.socket(socket.AF_UNIX) sock.bind(microvm.jailer.api_socket_path()) - microvm.spawn(log_level="warn") + microvm.spawn(log_level="warn", serial_out_path=None) msg = "Failed to open the API socket at: /run/firecracker.socket. Check that it is not already used." microvm.check_log_message(msg) diff --git a/tests/integration_tests/functional/test_cmd_line_parameters.py b/tests/integration_tests/functional/test_cmd_line_parameters.py index 9c2c79d9552..59eebf5d42e 100644 --- a/tests/integration_tests/functional/test_cmd_line_parameters.py +++ b/tests/integration_tests/functional/test_cmd_line_parameters.py @@ -29,7 +29,7 @@ def test_describe_snapshot_all_versions( jailer_binary_path=firecracker_release.jailer, ) # FIXME: Once only FC versions >= 1.12 are supported, drop log_level="warn" - vm.spawn(log_level="warn") + vm.spawn(log_level="warn", serial_out_path=None) vm.basic_config(track_dirty_pages=True) vm.start() snapshot = vm.snapshot_diff() diff --git a/tests/integration_tests/functional/test_cmd_line_start.py b/tests/integration_tests/functional/test_cmd_line_start.py index d4c6c270b8d..3d45fa9d694 100644 --- a/tests/integration_tests/functional/test_cmd_line_start.py +++ b/tests/integration_tests/functional/test_cmd_line_start.py @@ -117,7 +117,7 @@ def test_config_start_with_api(uvm_plain, vm_config_file): """ test_microvm = uvm_plain vm_config = _configure_vm_from_json(test_microvm, vm_config_file) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) assert test_microvm.state == "Running" @@ -134,7 +134,7 @@ def test_config_start_no_api(uvm_plain, vm_config_file): test_microvm = uvm_plain _configure_vm_from_json(test_microvm, vm_config_file) test_microvm.jailer.extra_args.update({"no-api": None}) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) # Get names of threads in Firecracker. cmd = f"ps -T --no-headers -p {test_microvm.firecracker_pid} | awk '{{print $5}}'" @@ -165,7 +165,7 @@ def test_config_start_no_api_exit(uvm_plain, vm_config_file): _configure_network_interface(test_microvm) test_microvm.jailer.extra_args.update({"no-api": None}) - test_microvm.spawn() # Start Firecracker and MicroVM + test_microvm.spawn(serial_out_path=None) # Start Firecracker and MicroVM test_microvm.ssh.run("reboot") # Exit test_microvm.mark_killed() # waits for process to terminate @@ -189,7 +189,7 @@ def test_config_bad_machine_config(uvm_plain, vm_config_file): test_microvm = uvm_plain _configure_vm_from_json(test_microvm, vm_config_file) test_microvm.jailer.extra_args.update({"no-api": None}) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) test_microvm.check_log_message("Configuration for VMM from one single json failed") test_microvm.mark_killed() @@ -215,7 +215,7 @@ def test_config_machine_config_params(uvm_plain, test_config): _configure_vm_from_json(test_microvm, vm_config_file) test_microvm.jailer.extra_args.update({"no-api": None}) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) should_fail = False if cpu_template_used and "C3" not in SUPPORTED_CPU_TEMPLATES: @@ -247,7 +247,7 @@ def test_config_start_with_limit(uvm_plain, vm_config_file): _configure_vm_from_json(test_microvm, vm_config_file) test_microvm.jailer.extra_args.update({"http-api-max-payload-size": "250"}) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) assert test_microvm.state == "Running" @@ -277,7 +277,7 @@ def test_config_with_default_limit(uvm_plain, vm_config_file): test_microvm = uvm_plain _configure_vm_from_json(test_microvm, vm_config_file) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) assert test_microvm.state == "Running" @@ -311,7 +311,7 @@ def test_start_with_metadata(uvm_plain): metadata_file = DIR / "metadata.json" _add_metadata_file(test_microvm, metadata_file) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) test_microvm.check_log_message("Successfully added metadata to mmds from file") @@ -332,7 +332,7 @@ def test_start_with_metadata_limit(uvm_plain): metadata_file = DIR / "metadata.json" _add_metadata_file(test_microvm, metadata_file) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) test_microvm.check_log_message( "Populating MMDS from file failed: The MMDS patch request doesn't fit." @@ -352,7 +352,7 @@ def test_start_with_metadata_default_limit(uvm_plain): _add_metadata_file(test_microvm, metadata_file) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) test_microvm.check_log_message( "Populating MMDS from file failed: The MMDS patch request doesn't fit." @@ -372,7 +372,7 @@ def test_start_with_missing_metadata(uvm_plain): test_microvm.metadata_file = vm_metadata_path try: - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) except: # pylint: disable=bare-except pass finally: @@ -395,7 +395,7 @@ def test_start_with_invalid_metadata(uvm_plain): test_microvm.metadata_file = vm_metadata_path try: - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) except: # pylint: disable=bare-except pass finally: @@ -418,7 +418,7 @@ def test_config_start_and_mmds_with_api(uvm_plain, vm_config_file): _configure_network_interface(test_microvm) # Network namespace has already been created. - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) data_store = { "latest": { @@ -480,7 +480,7 @@ def test_with_config_and_metadata_no_api(uvm_plain, vm_config_file, metadata_fil _add_metadata_file(test_microvm, metadata_file) _configure_network_interface(test_microvm) test_microvm.jailer.extra_args.update({"no-api": None}) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) # Get MMDS version and IPv4 address configured from the file. version, ipv4_address, imds_compat = _get_optional_fields_from_file(vm_config_file) diff --git a/tests/integration_tests/functional/test_kernel_cmdline.py b/tests/integration_tests/functional/test_kernel_cmdline.py index e4e4c122aa9..520fd88822e 100644 --- a/tests/integration_tests/functional/test_kernel_cmdline.py +++ b/tests/integration_tests/functional/test_kernel_cmdline.py @@ -14,7 +14,7 @@ def test_init_params(uvm_plain): """ vm = uvm_plain vm.help.enable_console() - vm.spawn() + vm.spawn(serial_out_path=None) vm.memory_monitor = None # We will override the init with /bin/cat so that we try to read the diff --git a/tests/integration_tests/functional/test_max_devices.py b/tests/integration_tests/functional/test_max_devices.py index 7cf9922c77b..54153b27d2d 100644 --- a/tests/integration_tests/functional/test_max_devices.py +++ b/tests/integration_tests/functional/test_max_devices.py @@ -18,9 +18,9 @@ def max_devices(uvm): match platform.machine(): case "aarch64": # On aarch64, IRQs are available from 32 to 127. We always use one IRQ each for - # the VMGenID and RTC devices, so the maximum number of devices supported - # at the same time is 94. - return 94 + # the VMGenID, RTC and serial devices, so the maximum number of devices supported + # at the same time is 93. + return 93 case "x86_64": # IRQs are available from 5 to 23. We always use one IRQ for VMGenID device, so # the maximum number of devices supported at the same time is 18. diff --git a/tests/integration_tests/functional/test_serial_io.py b/tests/integration_tests/functional/test_serial_io.py index 353496576e4..90601cfdf6f 100644 --- a/tests/integration_tests/functional/test_serial_io.py +++ b/tests/integration_tests/functional/test_serial_io.py @@ -11,51 +11,20 @@ from framework import utils from framework.microvm import Serial -from framework.state_machine import TestState PLATFORM = platform.machine() -class WaitTerminal(TestState): - """Initial state when we wait for the login prompt.""" - - def handle_input(self, serial, input_char) -> TestState: - """Handle input and return next state.""" - if self.match(input_char): - serial.tx("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, unused_serial, 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, unused_serial, _) -> TestState: - """Return self since the test is about to end.""" - return self - - def test_serial_after_snapshot(uvm_plain, microvm_factory): """ Serial I/O after restoring from a snapshot. """ microvm = uvm_plain microvm.help.enable_console() - microvm.spawn() + microvm.spawn(serial_out_path=None) microvm.basic_config( vcpu_count=2, mem_size_mib=256, - boot_args="console=ttyS0 reboot=k panic=1 swiotlb=noforce", ) serial = Serial(microvm) serial.open() @@ -72,7 +41,7 @@ def test_serial_after_snapshot(uvm_plain, microvm_factory): # Load microVM clone from snapshot. vm = microvm_factory.build() vm.help.enable_console() - vm.spawn() + vm.spawn(serial_out_path=None) vm.restore_from_snapshot(snapshot, resume=True) serial = Serial(vm) serial.open() @@ -92,26 +61,22 @@ def test_serial_console_login(uvm_plain_any): """ microvm = uvm_plain_any microvm.help.enable_console() - microvm.spawn() + microvm.spawn(serial_out_path=None) # 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_monitor = 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 swiotlb=noforce" - ) + microvm.basic_config(vcpu_count=1) microvm.start() serial = Serial(microvm) serial.open() - current_state = WaitTerminal("ubuntu-fc-uvm:") - - while not isinstance(current_state, TestFinished): - output_char = serial.rx_char() - current_state = current_state.handle_input(serial, output_char) + serial.rx("ubuntu-fc-uvm:") + serial.tx("id") + serial.rx("uid=0(root) gid=0(root) groups=0(root)") def get_total_mem_size(pid): @@ -146,7 +111,6 @@ def test_serial_dos(uvm_plain_any): # 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 swiotlb=noforce", ) microvm.add_net_iface() microvm.start() @@ -174,13 +138,12 @@ def test_serial_block(uvm_plain_any): """ test_microvm = uvm_plain_any test_microvm.help.enable_console() - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) # Set up the microVM with 1 vCPU so we make sure the vCPU thread # responsible for the SSH connection will also run the serial. test_microvm.basic_config( vcpu_count=1, mem_size_mib=512, - boot_args="console=ttyS0 reboot=k panic=1 swiotlb=noforce", ) test_microvm.add_net_iface() test_microvm.start() @@ -233,3 +196,10 @@ def test_no_serial_fd_error_when_daemonized(uvm_plain): test_microvm.start() assert REGISTER_FAILED_WARNING not in test_microvm.log_data + + +def test_serial_file_output(uvm_any): + """Test that redirecting serial console output to a file works for booted and restored VMs""" + uvm_any.ssh.check_output("echo 'hello' > /dev/ttyS0") + + assert b"hello" in uvm_any.serial_out_path.read_bytes() diff --git a/tests/integration_tests/performance/test_block.py b/tests/integration_tests/performance/test_block.py index 8882ee0717c..47b1f466daa 100644 --- a/tests/integration_tests/performance/test_block.py +++ b/tests/integration_tests/performance/test_block.py @@ -174,7 +174,7 @@ def test_block_performance( Execute block device emulation benchmarking scenarios. """ vm = uvm_plain_acpi - vm.spawn(log_level="Info", emit_metrics=True) + vm.spawn(log_level="Info", emit_metrics=True, serial_out_path=None) vm.basic_config(vcpu_count=vcpus, mem_size_mib=GUEST_MEM_MIB) vm.add_net_iface() # Add a secondary block device for benchmark tests. @@ -223,7 +223,7 @@ def test_block_vhost_user_performance( """ vm = uvm_plain_acpi - vm.spawn(log_level="Info", emit_metrics=True) + vm.spawn(log_level="Info", emit_metrics=True, serial_out_path=None) vm.basic_config(vcpu_count=vcpus, mem_size_mib=GUEST_MEM_MIB) vm.add_net_iface() diff --git a/tests/integration_tests/performance/test_boottime.py b/tests/integration_tests/performance/test_boottime.py index d80bf026a39..e8f77f0b62c 100644 --- a/tests/integration_tests/performance/test_boottime.py +++ b/tests/integration_tests/performance/test_boottime.py @@ -100,7 +100,7 @@ def launch_vm_with_boot_timer( """Launches a microVM with guest-timer and returns the reported metrics for it""" vm = microvm_factory.build(guest_kernel_acpi, rootfs_rw, pci=pci_enabled) vm.jailer.extra_args.update({"boot-timer": None}) - vm.spawn() + vm.spawn(serial_out_path=None) vm.basic_config( vcpu_count=vcpu_count, mem_size_mib=mem_size_mib, diff --git a/tests/integration_tests/performance/test_initrd.py b/tests/integration_tests/performance/test_initrd.py index 7b92644efa6..1bc84933fe9 100644 --- a/tests/integration_tests/performance/test_initrd.py +++ b/tests/integration_tests/performance/test_initrd.py @@ -29,7 +29,7 @@ def test_microvm_initrd_with_serial(uvm_with_initrd, huge_pages): """ vm = uvm_with_initrd vm.help.enable_console() - vm.spawn() + vm.spawn(serial_out_path=None) vm.memory_monitor = None vm.basic_config( diff --git a/tests/integration_tests/performance/test_memory_overhead.py b/tests/integration_tests/performance/test_memory_overhead.py index 2f4888c95ea..9e31d106afe 100644 --- a/tests/integration_tests/performance/test_memory_overhead.py +++ b/tests/integration_tests/performance/test_memory_overhead.py @@ -47,7 +47,7 @@ def test_memory_overhead( microvm = microvm_factory.build( guest_kernel_acpi, rootfs, pci=pci_enabled, monitor_memory=False ) - microvm.spawn(emit_metrics=True) + microvm.spawn(emit_metrics=True, serial_out_path=None) microvm.basic_config(vcpu_count=vcpu_count, mem_size_mib=mem_size_mib) microvm.add_net_iface() microvm.start() diff --git a/tests/integration_tests/performance/test_network.py b/tests/integration_tests/performance/test_network.py index 74ad26c26a8..89450e9f996 100644 --- a/tests/integration_tests/performance/test_network.py +++ b/tests/integration_tests/performance/test_network.py @@ -46,7 +46,7 @@ def network_microvm(request, uvm_plain_acpi): guest_vcpus = request.param vm = uvm_plain_acpi - vm.spawn(log_level="Info", emit_metrics=True) + vm.spawn(log_level="Info", emit_metrics=True, serial_out_path=None) vm.basic_config(vcpu_count=guest_vcpus, mem_size_mib=guest_mem_mib) vm.add_net_iface() vm.start() @@ -91,7 +91,7 @@ def test_network_latency(network_microvm, metrics): @pytest.mark.timeout(120) @pytest.mark.parametrize("network_microvm", [1, 2], indirect=True) @pytest.mark.parametrize("payload_length", ["128K", "1024K"], ids=["p128K", "p1024K"]) -@pytest.mark.parametrize("mode", ["g2h", "h2g", "bd"]) +@pytest.mark.parametrize("mode", ["g2h", "h2g"]) def test_network_tcp_throughput( network_microvm, payload_length, @@ -109,12 +109,6 @@ def test_network_tcp_throughput( # Time (in seconds) for which iperf runs after warmup is done runtime_sec = 20 - # We run bi-directional tests only on uVM with more than 2 vCPus - # because we need to pin one iperf3/direction per vCPU, and since we - # have two directions, we need at least two vCPUs. - if mode == "bd" and network_microvm.vcpus_count < 2: - pytest.skip("bidrectional test only done with at least 2 vcpus") - metrics.set_dimensions( { "performance_test": "test_network_tcp_throughput", diff --git a/tests/integration_tests/performance/test_snapshot.py b/tests/integration_tests/performance/test_snapshot.py index b4e9afabb67..38980396107 100644 --- a/tests/integration_tests/performance/test_snapshot.py +++ b/tests/integration_tests/performance/test_snapshot.py @@ -52,7 +52,7 @@ def boot_vm(self, microvm_factory, guest_kernel, rootfs, pci_enabled) -> Microvm monitor_memory=False, pci=pci_enabled, ) - vm.spawn(log_level="Info", emit_metrics=True) + vm.spawn(log_level="Info", emit_metrics=True, serial_out_path=None) vm.time_api_requests = False vm.basic_config( vcpu_count=self.vcpus, @@ -271,7 +271,7 @@ def test_snapshot_create_latency( """Measure the latency of creating a Full snapshot""" vm = uvm_plain - vm.spawn() + vm.spawn(serial_out_path=None) vm.basic_config( vcpu_count=2, mem_size_mib=512, diff --git a/tests/integration_tests/performance/test_vsock.py b/tests/integration_tests/performance/test_vsock.py index 402e7ff66b5..ef7d2ac4ab1 100644 --- a/tests/integration_tests/performance/test_vsock.py +++ b/tests/integration_tests/performance/test_vsock.py @@ -73,7 +73,7 @@ def guest_command(self, port_offset): @pytest.mark.nonci @pytest.mark.parametrize("vcpus", [1, 2], ids=["1vcpu", "2vcpu"]) @pytest.mark.parametrize("payload_length", ["64K", "1024K"], ids=["p64K", "p1024K"]) -@pytest.mark.parametrize("mode", ["g2h", "h2g", "bd"]) +@pytest.mark.parametrize("mode", ["g2h", "h2g"]) def test_vsock_throughput( uvm_plain_acpi, vcpus, @@ -85,15 +85,10 @@ def test_vsock_throughput( """ Test vsock throughput for multiple vm configurations. """ - # We run bi-directional tests only on uVM with more than 2 vCPus - # because we need to pin one iperf3/direction per vCPU, and since we - # have two directions, we need at least two vCPUs. - if mode == "bd" and vcpus < 2: - pytest.skip("bidrectional test only done with at least 2 vcpus") mem_size_mib = 1024 vm = uvm_plain_acpi - vm.spawn(log_level="Info", emit_metrics=True) + vm.spawn(log_level="Info", emit_metrics=True, serial_out_path=None) vm.basic_config(vcpu_count=vcpus, mem_size_mib=mem_size_mib) vm.add_net_iface() # Create a vsock device diff --git a/tests/integration_tests/security/test_custom_seccomp.py b/tests/integration_tests/security/test_custom_seccomp.py index 05f9b9aa96e..ffcedbbd653 100644 --- a/tests/integration_tests/security/test_custom_seccomp.py +++ b/tests/integration_tests/security/test_custom_seccomp.py @@ -123,7 +123,7 @@ def test_invalid_bpf(uvm_plain): test_microvm.create_jailed_resource(bpf_path) test_microvm.jailer.extra_args.update({"seccomp-filter": bpf_path.name}) - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) # give time for the process to get killed time.sleep(1) assert "Seccomp error: Filter deserialization failed" in test_microvm.log_data diff --git a/tests/integration_tests/security/test_jail.py b/tests/integration_tests/security/test_jail.py index 277d33b17b3..e9cdfc2c644 100644 --- a/tests/integration_tests/security/test_jail.py +++ b/tests/integration_tests/security/test_jail.py @@ -165,7 +165,7 @@ def test_arbitrary_usocket_location(uvm_plain): test_microvm = uvm_plain test_microvm.jailer.extra_args = {"api-sock": "api.socket"} - test_microvm.spawn() + test_microvm.spawn(serial_out_path=None) check_stats( os.path.join(test_microvm.jailer.chroot_path(), "api.socket"), diff --git a/tests/integration_tests/security/test_vulnerabilities.py b/tests/integration_tests/security/test_vulnerabilities.py index 01b8e9c595b..606e49758d0 100644 --- a/tests/integration_tests/security/test_vulnerabilities.py +++ b/tests/integration_tests/security/test_vulnerabilities.py @@ -211,6 +211,7 @@ def microvm_factory_a(record_property): bin_dir = git_clone(Path("../build") / revision_a, revision_a).resolve() record_property("firecracker_bin", str(bin_dir / "firecracker")) uvm_factory = MicroVMFactory(bin_dir) + uvm_factory.hack_no_serial = True yield uvm_factory uvm_factory.kill() diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 8824affce78..3fe5721420a 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -56,4 +56,5 @@ disable = [ "too-many-positional-arguments", "too-few-public-methods", "too-many-branches", + "too-many-statements", ]