Skip to content

Commit f2d30c9

Browse files
committed
allow redirecting guest serial output to a file
Add a new field, `serial_out_path`, to the logger configuration (available both via API and CLI) which can be set to the path of a file into which the output of the guest's serial console should be dumped. Have the file behave identically to our log file (e.g. it must already exist, Firecracker does not create it). If we redirect serial output to a log file, disable serial input via stdin. Signed-off-by: Patrick Roy <[email protected]>
1 parent d6d7b7b commit f2d30c9

File tree

16 files changed

+171
-15
lines changed

16 files changed

+171
-15
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ and this project adheres to
3535
requests to `/mmds/config` to enforce MMDS to always respond plain text
3636
contents in the IMDS format regardless of the `Accept` header in requests.
3737
Users need to regenerate snapshots.
38+
- [#5350](https://github.com/firecracker-microvm/firecracker/pull/5350): Added a
39+
`/serial` endpoint, which allows setting `serial_out_path` to the path of a
40+
pre-created file into which Firecracker should redirect output from the
41+
guest's serial console. Not configuring it means Firecracker will continue to
42+
print serial output to stdout. Similarly to the logger, this configuration is
43+
not persisted across snapshots.
3844

3945
### Changed
4046

src/firecracker/src/api_server/parsed_request.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use super::request::net::{parse_patch_net, parse_put_net};
2727
use super::request::snapshot::{parse_patch_vm_state, parse_put_snapshot};
2828
use super::request::version::parse_get_version;
2929
use super::request::vsock::parse_put_vsock;
30+
use crate::api_server::request::serial::parse_put_serial;
3031

3132
#[derive(Debug)]
3233
pub(crate) enum RequestAction {
@@ -90,6 +91,7 @@ impl TryFrom<&Request> for ParsedRequest {
9091
(Method::Put, "cpu-config", Some(body)) => parse_put_cpu_config(body),
9192
(Method::Put, "drives", Some(body)) => parse_put_drive(body, path_tokens.next()),
9293
(Method::Put, "logger", Some(body)) => parse_put_logger(body),
94+
(Method::Put, "serial", Some(body)) => parse_put_serial(body),
9395
(Method::Put, "machine-config", Some(body)) => parse_put_machine_config(body),
9496
(Method::Put, "metrics", Some(body)) => parse_put_metrics(body),
9597
(Method::Put, "mmds", Some(body)) => parse_put_mmds(body, path_tokens.next()),

src/firecracker/src/api_server/request/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod machine_configuration;
1313
pub mod metrics;
1414
pub mod mmds;
1515
pub mod net;
16+
pub mod serial;
1617
pub mod snapshot;
1718
pub mod version;
1819
pub mod vsock;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use micro_http::Body;
5+
use vmm::logger::{IncMetric, METRICS};
6+
use vmm::rpc_interface::VmmAction;
7+
use vmm::vmm_config::serial::SerialConfig;
8+
9+
use crate::api_server::parsed_request::{ParsedRequest, RequestError};
10+
11+
pub(crate) fn parse_put_serial(body: &Body) -> Result<ParsedRequest, RequestError> {
12+
METRICS.put_api_requests.serial_count.inc();
13+
let res = serde_json::from_slice::<SerialConfig>(body.raw());
14+
let config = res.inspect_err(|_| {
15+
METRICS.put_api_requests.serial_fails.inc();
16+
})?;
17+
Ok(ParsedRequest::new_sync(VmmAction::ConfigureSerial(config)))
18+
}
19+
20+
#[cfg(test)]
21+
mod tests {
22+
use std::path::PathBuf;
23+
24+
use super::*;
25+
use crate::api_server::parsed_request::tests::vmm_action_from_request;
26+
27+
#[test]
28+
fn test_parse_put_serial_request() {
29+
let body = r#"{"serial_out_path": "serial"}"#;
30+
31+
let expected_config = SerialConfig {
32+
serial_out_path: Some(PathBuf::from("serial")),
33+
};
34+
assert_eq!(
35+
vmm_action_from_request(parse_put_serial(&Body::new(body)).unwrap()),
36+
VmmAction::ConfigureSerial(expected_config)
37+
);
38+
}
39+
}

src/firecracker/swagger/firecracker.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,27 @@ paths:
506506
schema:
507507
$ref: "#/definitions/Error"
508508

509+
/serial:
510+
put:
511+
summary: Configures the serial console
512+
operationId: putSerialDevice
513+
description:
514+
Configure the serial console, which the guest can write its kernel logs to. Has no effect if
515+
the serial console is not also enabled on the guest kernel command line
516+
parameters:
517+
- name: body
518+
in: body
519+
description: Serial console properties
520+
required: true
521+
schema:
522+
$ref: "#/definitions/SerialDevice"
523+
responses:
524+
204:
525+
description: Serial device configured
526+
default:
527+
description: Internal server error
528+
schema:
529+
$ref: "#/definitions/Error"
509530

510531
/network-interfaces/{iface_id}:
511532
put:
@@ -1334,6 +1355,15 @@ definitions:
13341355
rate_limiter:
13351356
$ref: "#/definitions/RateLimiter"
13361357

1358+
SerialDevice:
1359+
type: object
1360+
description:
1361+
The configuration of the serial device
1362+
properties:
1363+
output_path:
1364+
type: string
1365+
description: Path to a file or named pipe on the host to which serial output should be written.
1366+
13371367
FirecrackerVersion:
13381368
type: object
13391369
description:

src/vmm/src/arch/aarch64/fdt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ mod tests {
561561
cmdline.insert("console", "/dev/tty0").unwrap();
562562

563563
device_manager
564-
.attach_legacy_devices_aarch64(&vm, &mut event_manager, &mut cmdline)
564+
.attach_legacy_devices_aarch64(&vm, &mut event_manager, &mut cmdline, None)
565565
.unwrap();
566566
let dummy = Arc::new(Mutex::new(DummyDevice::new()));
567567
device_manager

src/vmm/src/builder.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,12 @@ pub fn build_microvm_for_boot(
170170
let (mut vcpus, vcpus_exit_evt) = vm.create_vcpus(vm_resources.machine_config.vcpu_count)?;
171171
vm.register_memory_regions(guest_memory)?;
172172

173-
let mut device_manager = DeviceManager::new(event_manager, &vcpus_exit_evt, &vm)?;
173+
let mut device_manager = DeviceManager::new(
174+
event_manager,
175+
&vcpus_exit_evt,
176+
&vm,
177+
vm_resources.serial_out_path.as_ref(),
178+
)?;
174179

175180
let vm = Arc::new(vm);
176181

@@ -248,7 +253,12 @@ pub fn build_microvm_for_boot(
248253
}
249254

250255
#[cfg(target_arch = "aarch64")]
251-
device_manager.attach_legacy_devices_aarch64(&vm, event_manager, &mut boot_cmdline)?;
256+
device_manager.attach_legacy_devices_aarch64(
257+
&vm,
258+
event_manager,
259+
&mut boot_cmdline,
260+
vm_resources.serial_out_path.as_ref(),
261+
)?;
252262

253263
device_manager.attach_vmgenid_device(vm.guest_memory(), &vm)?;
254264

src/vmm/src/device_manager/mod.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
use std::convert::Infallible;
99
use std::fmt::Debug;
10+
use std::os::unix::prelude::OpenOptionsExt;
11+
use std::path::PathBuf;
1012
use std::sync::{Arc, Mutex};
1113

1214
use acpi::ACPIDeviceManager;
@@ -125,13 +127,25 @@ impl DeviceManager {
125127
/// Sets up the serial device.
126128
fn setup_serial_device(
127129
event_manager: &mut EventManager,
130+
output: Option<&PathBuf>,
128131
) -> Result<Arc<Mutex<SerialDevice>>, std::io::Error> {
129-
Self::set_stdout_nonblocking();
132+
let (serial_in, serial_out) = match output {
133+
Some(ref path) => (
134+
None,
135+
std::fs::OpenOptions::new()
136+
.custom_flags(libc::O_NONBLOCK)
137+
.write(true)
138+
.open(path)
139+
.map(SerialOut::File)?,
140+
),
141+
None => {
142+
Self::set_stdout_nonblocking();
143+
144+
(Some(std::io::stdin()), SerialOut::Stdout(std::io::stdout()))
145+
}
146+
};
130147

131-
let serial = Arc::new(Mutex::new(SerialDevice::new(
132-
Some(std::io::stdin()),
133-
SerialOut::Stdout(std::io::stdout()),
134-
)?));
148+
let serial = Arc::new(Mutex::new(SerialDevice::new(serial_in, serial_out)?));
135149
event_manager.add_subscriber(serial.clone());
136150
Ok(serial)
137151
}
@@ -141,9 +155,10 @@ impl DeviceManager {
141155
event_manager: &mut EventManager,
142156
vcpus_exit_evt: &EventFd,
143157
vm: &Vm,
158+
serial_output: Option<&PathBuf>,
144159
) -> Result<PortIODeviceManager, DeviceManagerCreateError> {
145160
// Create serial device
146-
let serial = Self::setup_serial_device(event_manager)?;
161+
let serial = Self::setup_serial_device(event_manager, serial_output)?;
147162
let reset_evt = vcpus_exit_evt
148163
.try_clone()
149164
.map_err(DeviceManagerCreateError::EventFd)?;
@@ -161,9 +176,11 @@ impl DeviceManager {
161176
event_manager: &mut EventManager,
162177
vcpus_exit_evt: &EventFd,
163178
vm: &Vm,
179+
serial_output: Option<&PathBuf>,
164180
) -> Result<Self, DeviceManagerCreateError> {
165181
#[cfg(target_arch = "x86_64")]
166-
let legacy_devices = Self::create_legacy_devices(event_manager, vcpus_exit_evt, vm)?;
182+
let legacy_devices =
183+
Self::create_legacy_devices(event_manager, vcpus_exit_evt, vm, serial_output)?;
167184

168185
Ok(DeviceManager {
169186
mmio_devices: MMIODeviceManager::new(),
@@ -243,6 +260,7 @@ impl DeviceManager {
243260
vm: &Vm,
244261
event_manager: &mut EventManager,
245262
cmdline: &mut Cmdline,
263+
serial_out_path: Option<&PathBuf>,
246264
) -> Result<(), AttachDeviceError> {
247265
// Serial device setup.
248266
let cmdline_contains_console = cmdline
@@ -253,7 +271,7 @@ impl DeviceManager {
253271
.contains("console=");
254272

255273
if cmdline_contains_console {
256-
let serial = Self::setup_serial_device(event_manager)?;
274+
let serial = Self::setup_serial_device(event_manager, serial_out_path)?;
257275
self.mmio_devices.register_mmio_serial(vm, serial, None)?;
258276
self.mmio_devices.add_mmio_serial_to_cmdline(cmdline)?;
259277
}
@@ -451,6 +469,7 @@ impl<'a> Persist<'a> for DeviceManager {
451469
constructor_args.event_manager,
452470
constructor_args.vcpus_exit_evt,
453471
constructor_args.vm,
472+
constructor_args.vm_resources.serial_out_path.as_ref(),
454473
)?;
455474

456475
// Restore MMIO devices
@@ -580,15 +599,15 @@ pub(crate) mod tests {
580599
let mut cmdline = Cmdline::new(4096).unwrap();
581600
let mut event_manager = EventManager::new().unwrap();
582601
vmm.device_manager
583-
.attach_legacy_devices_aarch64(&vmm.vm, &mut event_manager, &mut cmdline)
602+
.attach_legacy_devices_aarch64(&vmm.vm, &mut event_manager, &mut cmdline, None)
584603
.unwrap();
585604
assert!(vmm.device_manager.mmio_devices.rtc.is_some());
586605
assert!(vmm.device_manager.mmio_devices.serial.is_none());
587606

588607
let mut vmm = default_vmm();
589608
cmdline.insert("console", "/dev/blah").unwrap();
590609
vmm.device_manager
591-
.attach_legacy_devices_aarch64(&vmm.vm, &mut event_manager, &mut cmdline)
610+
.attach_legacy_devices_aarch64(&vmm.vm, &mut event_manager, &mut cmdline, None)
592611
.unwrap();
593612
assert!(vmm.device_manager.mmio_devices.rtc.is_some());
594613
assert!(vmm.device_manager.mmio_devices.serial.is_some());

src/vmm/src/device_manager/persist.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,10 @@ impl<'a> Persist<'a> for MMIODeviceManager {
356356
{
357357
for state in &state.legacy_devices {
358358
if state.type_ == DeviceType::Serial {
359-
let serial =
360-
crate::DeviceManager::setup_serial_device(constructor_args.event_manager)?;
359+
let serial = crate::DeviceManager::setup_serial_device(
360+
constructor_args.event_manager,
361+
constructor_args.vm_resources.serial_out_path.as_ref(),
362+
)?;
361363

362364
dev_manager.register_mmio_serial(vm, serial, Some(state.device_info))?;
363365
}

src/vmm/src/devices/legacy/serial.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
//! Implements a wrapper over an UART serial device.
99
use std::fmt::Debug;
10+
use std::fs::File;
1011
use std::io::{self, Read, Stdin, Write};
1112
use std::os::unix::io::{AsRawFd, RawFd};
1213
use std::sync::{Arc, Barrier};
@@ -129,18 +130,21 @@ impl SerialEvents for SerialEventsWrapper {
129130
pub enum SerialOut {
130131
Sink,
131132
Stdout(std::io::Stdout),
133+
File(File),
132134
}
133135
impl std::io::Write for SerialOut {
134136
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
135137
match self {
136138
Self::Sink => Ok(buf.len()),
137139
Self::Stdout(stdout) => stdout.write(buf),
140+
Self::File(file) => file.write(buf),
138141
}
139142
}
140143
fn flush(&mut self) -> std::io::Result<()> {
141144
match self {
142145
Self::Sink => Ok(()),
143146
Self::Stdout(stdout) => stdout.flush(),
147+
Self::File(file) => file.flush(),
144148
}
145149
}
146150
}

0 commit comments

Comments
 (0)