diff --git a/devices/src/debug_console.rs b/devices/src/debug_console.rs new file mode 100644 index 000000000..799c8a67e --- /dev/null +++ b/devices/src/debug_console.rs @@ -0,0 +1,64 @@ +// Copyright © 2023 Cyberus Technology +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Module for [`DebugconState`]. + +use std::io; +use std::io::Write; +use std::sync::{Arc, Barrier}; +use vm_device::BusDevice; +use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; + +/// I/O-port. +pub const DEFAULT_PORT: u64 = 0xe9; + +#[derive(Default)] +pub struct DebugconState {} + +/// Emulates a debug console similar to the QEMU debugcon device. This device +/// is stateless and only prints the bytes (usually text) that are written to +/// it. +/// +/// This device is only available on x86. +/// +/// Reference: +/// - https://github.com/qemu/qemu/blob/master/hw/char/debugcon.c +/// - https://phip1611.de/blog/how-to-use-qemus-debugcon-feature-and-write-to-a-file/ +pub struct DebugConsole { + id: String, + out: Box, +} + +impl DebugConsole { + pub fn new(id: String, out: Box) -> Self { + Self { id, out } + } +} + +impl BusDevice for DebugConsole { + fn read(&mut self, _base: u64, _offset: u64, _data: &mut [u8]) {} + + fn write(&mut self, _base: u64, _offset: u64, data: &[u8]) -> Option> { + if let Err(e) = self.out.write_all(data) { + // unlikely + error!("debug-console: failed writing data: {e:?}"); + } + None + } +} + +impl Snapshottable for DebugConsole { + fn id(&self) -> String { + self.id.clone() + } + + fn snapshot(&mut self) -> Result { + Snapshot::new_from_state(&()) + } +} + +impl Pausable for DebugConsole {} +impl Transportable for DebugConsole {} +impl Migratable for DebugConsole {} diff --git a/devices/src/lib.rs b/devices/src/lib.rs index 79e262020..260b1ecae 100644 --- a/devices/src/lib.rs +++ b/devices/src/lib.rs @@ -15,6 +15,8 @@ extern crate event_monitor; extern crate log; pub mod acpi; +#[cfg(target_arch = "x86_64")] +pub mod debug_console; #[cfg(target_arch = "aarch64")] pub mod gic; pub mod interrupt_controller; diff --git a/docs/debug-port.md b/docs/debug-port.md index 6bafd1c37..5cec79311 100644 --- a/docs/debug-port.md +++ b/docs/debug-port.md @@ -1,7 +1,16 @@ -# `cloud-hypervisor` debug IO port +# `cloud-hypervisor` debug IO ports -`cloud-hypervisor` uses the [`0x80`](https://www.intel.com/content/www/us/en/support/articles/000005500/boards-and-kits.html) -I/O port to trace user defined guest events. +When running x86 guests, `cloud-hypervisor` provides different kinds of debug ports: +- [`0x80` debug port](https://www.intel.com/content/www/us/en/support/articles/000005500/boards-and-kits.html) +- Debug console (by default at `0xe9`). +- Firmware debug port at `0x402`. + +All of them can be used to trace user-defined guest events and all of them can +be used simultaneously. + +## Debug Ports Overview + +### `0x80` I/O port Whenever the guest write one byte between `0x0` and `0xF` on this particular I/O port, `cloud-hypervisor` will log and timestamp that event at the `debug` @@ -30,7 +39,7 @@ guest will have `cloud-hypervisor` generate timestamped logs of all those steps. That provides a basic but convenient way of measuring not only the overall guest boot time but all intermediate steps as well. -## Logging +#### Logging Assuming parts of the guest software stack have been instrumented to use the `cloud-hypervisor` debug I/O port, we may want to gather the related logs. @@ -59,3 +68,29 @@ $ grep "Debug I/O port" /tmp/ch-fw.log cloud-hypervisor: 19.762449ms: DEBUG:vmm/src/vm.rs:510 -- [Debug I/O port: Firmware code 0x0] 0.019004 seconds cloud-hypervisor: 403.499628ms: DEBUG:vmm/src/vm.rs:510 -- [Debug I/O port: Firmware code 0x1] 0.402744 seconds ``` + +### Debug console port + +The debug console is inspired by QEMU and Bochs, which have a similar feature. +By default, the I/O port `0xe9` is used. This port can be configured like a +console. Thus, it can print to a tty, a file, or a pty, for example. + +### Firmware debug port + +The firmware debug port is also a simple port that prints all bytes written to +it. The firmware debug port only prints to stdout. + +## When do I need these ports? + +The ports are on the one hand interesting for firmware or kernel developers, as +they provide an easy way to print debug information from within a guest. +Furthermore, you can patch "normal" software to measure certain events, such as +the boot time of a guest. + +## Which port should I choose? + +The `0x80` debug port and the port of the firmware debug device are always +available. The debug console must be activated via the command line, but +provides more configuration options. + +You can use different ports for different aspect of your logging messages. \ No newline at end of file diff --git a/fuzz/fuzz_targets/http_api.rs b/fuzz/fuzz_targets/http_api.rs index 00b818d4a..1f3c92c47 100644 --- a/fuzz/fuzz_targets/http_api.rs +++ b/fuzz/fuzz_targets/http_api.rs @@ -173,6 +173,8 @@ impl RequestHandler for StubApiRequestHandler { iommu: false, socket: None, }, + #[cfg(target_arch = "x86_64")] + debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, diff --git a/src/main.rs b/src/main.rs index 5320d02f4..36869215f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -418,6 +418,15 @@ fn create_app(default_vcpus: String, default_memory: String, default_rng: String .group("vm-config"), ); + #[cfg(target_arch = "x86_64")] + let app = app.arg( + Arg::new("debug-console") + .long("debug-console") + .help("Debug console: off|pty|tty|file=,iobase=") + .default_value("off,iobase=0xe9") + .group("vm-config"), + ); + #[cfg(feature = "guest_debug")] let app = app.arg( Arg::new("gdb") @@ -792,6 +801,8 @@ mod unit_tests { ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, MemoryConfig, PayloadConfig, RngConfig, VmConfig, VmParams, }; + #[cfg(target_arch = "x86_64")] + use vmm::vm_config::DebugConsoleConfig; fn get_vm_config_from_vec(args: &[&str]) -> VmConfig { let (default_vcpus, default_memory, default_rng) = prepare_default_values(); @@ -881,6 +892,8 @@ mod unit_tests { iommu: false, socket: None, }, + #[cfg(target_arch = "x86_64")] + debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, @@ -1512,6 +1525,30 @@ mod unit_tests { }); } + #[cfg(target_arch = "x86_64")] + #[test] + fn test_valid_vm_config_debug_console() { + [( + vec![ + "cloud-hypervisor", + "--kernel", + "/path/to/kernel", + "--debug-console", + "tty,iobase=0xe9", + ], + // 233 == 0xe9 + r#"{ + "payload": {"kernel": "/path/to/kernel" }, + "debug_console": {"mode": "Tty", "iobase": 233 } + }"#, + true, + )] + .iter() + .for_each(|(cli, openapi, equal)| { + compare_vm_config_cli_vs_json(cli, openapi, *equal); + }); + } + #[test] fn test_valid_vm_config_serial_console() { [ diff --git a/vmm/src/api/openapi/cloud-hypervisor.yaml b/vmm/src/api/openapi/cloud-hypervisor.yaml index cfc4723fe..59db48a57 100644 --- a/vmm/src/api/openapi/cloud-hypervisor.yaml +++ b/vmm/src/api/openapi/cloud-hypervisor.yaml @@ -582,6 +582,8 @@ components: $ref: "#/components/schemas/ConsoleConfig" console: $ref: "#/components/schemas/ConsoleConfig" + debug_console: + $ref: "#/components/schemas/DebugConsoleConfig" devices: type: array items: @@ -999,6 +1001,19 @@ components: type: boolean default: false + DebugConsoleConfig: + required: + - mode + type: object + properties: + file: + type: string + mode: + type: string + enum: ["Off", "Pty", "Tty", "File", "Null"] + iobase: + type: integer + DeviceConfig: required: - path diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 5d82c7ae3..c3c6b4c33 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -61,6 +61,9 @@ pub enum Error { ParsePersistentMemory(OptionParserError), /// Failed parsing console ParseConsole(OptionParserError), + #[cfg(target_arch = "x86_64")] + /// Failed parsing debug-console + ParseDebugConsole(OptionParserError), /// No mode given for console ParseConsoleInvalidModeGiven, /// Failed parsing device parameters @@ -116,6 +119,9 @@ pub enum ValidationError { ConsoleSocketPathMissing, /// Max is less than boot CpusMaxLowerThanBoot, + /// Missing file value for debug-console + #[cfg(target_arch = "x86_64")] + DebugconFileMissing, /// Both socket and path specified DiskSocketAndPath, /// Using vhost user requires shared memory @@ -185,6 +191,9 @@ pub enum ValidationError { DefaultPciSegmentInvalidNode(u32), /// Invalid rate-limiter group InvalidRateLimiterGroup, + /// The specified I/O port was invalid. It should be provided in hex, such as `0xe9`. + #[cfg(target_arch = "x86_64")] + InvalidIoPortHex(String), } type ValidationResult = std::result::Result; @@ -197,6 +206,8 @@ impl fmt::Display for ValidationError { ConsoleFileMissing => write!(f, "Path missing when using file console mode"), ConsoleSocketPathMissing => write!(f, "Path missing when using socket console mode"), CpusMaxLowerThanBoot => write!(f, "Max CPUs lower than boot CPUs"), + #[cfg(target_arch = "x86_64")] + DebugconFileMissing => write!(f, "Path missing when using file mode for debug console"), DiskSocketAndPath => write!(f, "Disk path and vhost socket both provided"), VhostUserRequiresSharedMemory => { write!( @@ -311,6 +322,13 @@ impl fmt::Display for ValidationError { InvalidRateLimiterGroup => { write!(f, "Invalid rate-limiter group") } + #[cfg(target_arch = "x86_64")] + InvalidIoPortHex(s) => { + write!( + f, + "The IO port was not properly provided in hex or a `0x` prefix is missing: {s}" + ) + } } } } @@ -320,6 +338,8 @@ impl fmt::Display for Error { use self::Error::*; match self { ParseConsole(o) => write!(f, "Error parsing --console: {o}"), + #[cfg(target_arch = "x86_64")] + ParseDebugConsole(o) => write!(f, "Error parsing --debug-console: {o}"), ParseConsoleInvalidModeGiven => { write!(f, "Error parsing --console: invalid console mode given") } @@ -399,6 +419,8 @@ pub struct VmParams<'a> { pub pmem: Option>, pub serial: &'a str, pub console: &'a str, + #[cfg(target_arch = "x86_64")] + pub debug_console: &'a str, pub devices: Option>, pub user_devices: Option>, pub vdpa: Option>, @@ -440,6 +462,8 @@ impl<'a> VmParams<'a> { .get_many::("net") .map(|x| x.map(|y| y as &str).collect()); let console = args.get_one::("console").unwrap(); + #[cfg(target_arch = "x86_64")] + let debug_console = args.get_one::("debug-console").unwrap().as_str(); let balloon = args.get_one::("balloon").map(|x| x as &str); let fs: Option> = args .get_many::("fs") @@ -489,6 +513,8 @@ impl<'a> VmParams<'a> { pmem, serial, console, + #[cfg(target_arch = "x86_64")] + debug_console, devices, user_devices, vdpa, @@ -1636,6 +1662,59 @@ impl ConsoleConfig { } } +#[cfg(target_arch = "x86_64")] +impl DebugConsoleConfig { + pub fn parse(debug_console_ops: &str) -> Result { + let mut parser = OptionParser::new(); + parser + .add_valueless("off") + .add_valueless("pty") + .add_valueless("tty") + .add_valueless("null") + .add("file") + .add("iobase"); + parser + .parse(debug_console_ops) + .map_err(Error::ParseConsole)?; + + let mut file: Option = default_consoleconfig_file(); + let mut iobase: Option = None; + let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off; + + if parser.is_set("off") { + } else if parser.is_set("pty") { + mode = ConsoleOutputMode::Pty + } else if parser.is_set("tty") { + mode = ConsoleOutputMode::Tty + } else if parser.is_set("null") { + mode = ConsoleOutputMode::Null + } else if parser.is_set("file") { + mode = ConsoleOutputMode::File; + file = + Some(PathBuf::from(parser.get("file").ok_or( + Error::Validation(ValidationError::ConsoleFileMissing), + )?)); + } else { + return Err(Error::ParseConsoleInvalidModeGiven); + } + + if parser.is_set("iobase") { + if let Some(iobase_opt) = parser.get("iobase") { + if !iobase_opt.starts_with("0x") { + return Err(Error::Validation(ValidationError::InvalidIoPortHex( + iobase_opt, + ))); + } + iobase = Some(u16::from_str_radix(&iobase_opt[2..], 16).map_err(|_| { + Error::Validation(ValidationError::InvalidIoPortHex(iobase_opt)) + })?); + } + } + + Ok(Self { file, mode, iobase }) + } +} + impl DeviceConfig { pub const SYNTAX: &'static str = "Direct device assignment parameters \"path=,iommu=on|off,id=,pci_segment=\""; @@ -2054,9 +2133,20 @@ impl VmConfig { // Using such double tty mode, you need to configure the kernel // properly, such as: // "console=hvc0 earlyprintk=ttyS0" - if self.console.mode == ConsoleOutputMode::Tty && self.serial.mode == ConsoleOutputMode::Tty - { - warn!("Using TTY output for both virtio-console and serial port"); + + let mut tty_consoles = Vec::new(); + if self.console.mode == ConsoleOutputMode::Tty { + tty_consoles.push("virtio-console"); + }; + if self.serial.mode == ConsoleOutputMode::Tty { + tty_consoles.push("serial-console"); + }; + #[cfg(target_arch = "x86_64")] + if self.debug_console.mode == ConsoleOutputMode::Tty { + tty_consoles.push("debug-console"); + }; + if tty_consoles.len() > 1 { + warn!("Using TTY output for multiple consoles: {:?}", tty_consoles); } if self.console.mode == ConsoleOutputMode::File && self.console.file.is_none() { @@ -2375,6 +2465,8 @@ impl VmConfig { let console = ConsoleConfig::parse(vm_params.console)?; let serial = ConsoleConfig::parse(vm_params.serial)?; + #[cfg(target_arch = "x86_64")] + let debug_console = DebugConsoleConfig::parse(vm_params.debug_console)?; let mut devices: Option> = None; if let Some(device_list) = &vm_params.devices { @@ -2482,6 +2574,8 @@ impl VmConfig { pmem, serial, console, + #[cfg(target_arch = "x86_64")] + debug_console, devices, user_devices, vdpa, @@ -2606,6 +2700,8 @@ impl Clone for VmConfig { pmem: self.pmem.clone(), serial: self.serial.clone(), console: self.console.clone(), + #[cfg(target_arch = "x86_64")] + debug_console: self.debug_console.clone(), devices: self.devices.clone(), user_devices: self.user_devices.clone(), vdpa: self.vdpa.clone(), @@ -3341,6 +3437,8 @@ mod tests { iommu: false, socket: None, }, + #[cfg(target_arch = "x86_64")] + debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, diff --git a/vmm/src/device_manager.rs b/vmm/src/device_manager.rs index c7bdf86a7..f0a3bbea0 100644 --- a/vmm/src/device_manager.rs +++ b/vmm/src/device_manager.rs @@ -41,14 +41,14 @@ use block::{ }; #[cfg(feature = "io_uring")] use block::{fixed_vhd_async::FixedVhdDiskAsync, raw_async::RawFileDisk}; +#[cfg(target_arch = "x86_64")] +use devices::debug_console::DebugConsole; #[cfg(target_arch = "aarch64")] use devices::gic; #[cfg(target_arch = "x86_64")] use devices::ioapic; #[cfg(target_arch = "aarch64")] use devices::legacy::Pl011; -#[cfg(target_arch = "x86_64")] -use devices::legacy::Serial; use devices::{ interrupt_controller, interrupt_controller::InterruptController, AcpiNotificationFlags, }; @@ -103,6 +103,8 @@ use vm_migration::{ use vm_virtio::AccessPlatform; use vm_virtio::VirtioDeviceType; use vmm_sys_util::eventfd::EventFd; +#[cfg(target_arch = "x86_64")] +use {devices::debug_console, devices::legacy::Serial}; #[cfg(target_arch = "aarch64")] const MMIO_LEN: u64 = 0x1000; @@ -111,6 +113,8 @@ const MMIO_LEN: u64 = 0x1000; #[cfg(target_arch = "x86_64")] const IOAPIC_DEVICE_NAME: &str = "__ioapic"; const SERIAL_DEVICE_NAME: &str = "__serial"; +#[cfg(target_arch = "x86_64")] +const DEBUGCON_DEVICE_NAME: &str = "__debug_console"; #[cfg(target_arch = "aarch64")] const GPIO_DEVICE_NAME: &str = "__gpio"; const RNG_DEVICE_NAME: &str = "__rng"; @@ -249,6 +253,10 @@ pub enum DeviceManagerError { /// Error creating serial output file SerialOutputFileOpen(io::Error), + #[cfg(target_arch = "x86_64")] + /// Error creating debug-console output file + DebugconOutputFileOpen(io::Error), + /// Error creating console output file ConsoleOutputFileOpen(io::Error), @@ -258,6 +266,9 @@ pub enum DeviceManagerError { /// Error creating console pty ConsolePtyOpen(io::Error), + /// Error creating console pty + DebugconPtyOpen(io::Error), + /// Error setting pty raw mode SetPtyRaw(vmm_sys_util::errno::Error), @@ -819,6 +830,9 @@ pub struct DeviceManager { // serial PTY serial_pty: Option>>, + // debug-console PTY + debug_console_pty: Option>>, + // Serial Manager serial_manager: Option>, @@ -1166,6 +1180,7 @@ impl DeviceManager { serial_pty: None, serial_manager: None, console_pty: None, + debug_console_pty: None, console_resize_pipe: None, original_termios_opt: Arc::new(Mutex::new(None)), virtio_mem_devices: Vec::new(), @@ -1209,6 +1224,12 @@ impl DeviceManager { .map(|pty| pty.lock().unwrap().clone()) } + pub fn debug_console_pty(&self) -> Option { + self.debug_console_pty + .as_ref() + .map(|pty| pty.lock().unwrap().clone()) + } + pub fn console_resize_pipe(&self) -> Option> { self.console_resize_pipe.as_ref().map(Arc::clone) } @@ -1217,6 +1238,7 @@ impl DeviceManager { &mut self, serial_pty: Option, console_pty: Option, + debug_console_pty: Option, console_resize_pipe: Option, original_termios_opt: Arc>>, ) -> DeviceManagerResult<()> { @@ -1276,11 +1298,12 @@ impl DeviceManager { self.original_termios_opt = original_termios_opt; - self.console = self.add_console_device( + self.console = self.add_console_devices( &legacy_interrupt_manager, &mut virtio_devices, serial_pty, console_pty, + debug_console_pty, console_resize_pipe, )?; @@ -1799,6 +1822,53 @@ impl DeviceManager { Ok(()) } + #[cfg(target_arch = "x86_64")] + fn add_debug_console_device( + &mut self, + debug_console_writer: Box, + ) -> DeviceManagerResult>> { + let id = String::from(DEBUGCON_DEVICE_NAME); + let debug_console = Arc::new(Mutex::new(DebugConsole::new( + id.clone(), + debug_console_writer, + ))); + + let port = self + .config + .lock() + .unwrap() + .debug_console + .clone() + .iobase + .map(|port| port as u64) + .unwrap_or(debug_console::DEFAULT_PORT); + + self.bus_devices + .push(Arc::clone(&debug_console) as Arc>); + + self.address_manager + .allocator + .lock() + .unwrap() + .allocate_io_addresses(Some(GuestAddress(port)), 0x1, None) + .ok_or(DeviceManagerError::AllocateIoPort)?; + + self.address_manager + .io_bus + .insert(debug_console.clone(), port, 0x1) + .map_err(DeviceManagerError::BusError)?; + + // Fill the device tree with a new node. In case of restore, we + // know there is nothing to do, so we can simply override the + // existing entry. + self.device_tree + .lock() + .unwrap() + .insert(id.clone(), device_node!(id, debug_console)); + + Ok(debug_console) + } + #[cfg(target_arch = "x86_64")] fn add_serial_device( &mut self, @@ -2086,12 +2156,19 @@ impl DeviceManager { }) } - fn add_console_device( + /// Adds all devices that behave like a console with respect to the VM + /// configuration. This includes: + /// - debug-console + /// - serial-console + /// - virtio-console + fn add_console_devices( &mut self, interrupt_manager: &Arc>, virtio_devices: &mut Vec, serial_pty: Option, console_pty: Option, + #[cfg(target_arch = "x86_64")] debug_console_pty: Option, + #[cfg(not(target_arch = "x86_64"))] _: Option, console_resize_pipe: Option, ) -> DeviceManagerResult> { let serial_config = self.config.lock().unwrap().serial.clone(); @@ -2101,7 +2178,7 @@ impl DeviceManager { .map_err(DeviceManagerError::SerialOutputFileOpen)?, )), ConsoleOutputMode::Pty => { - if let Some(pty) = serial_pty { + if let Some(pty) = serial_pty.clone() { self.config.lock().unwrap().serial.file = Some(pty.path.clone()); self.serial_pty = Some(Arc::new(Mutex::new(pty))); } else { @@ -2149,6 +2226,44 @@ impl DeviceManager { }; } + #[cfg(target_arch = "x86_64")] + { + let debug_console_config = self.config.lock().unwrap().debug_console.clone(); + let debug_console_writer: Option> = match debug_console_config + .mode + { + ConsoleOutputMode::File => Some(Box::new( + File::create(debug_console_config.file.as_ref().unwrap()) + .map_err(DeviceManagerError::DebugconOutputFileOpen)?, + )), + ConsoleOutputMode::Pty => { + if let Some(pty) = debug_console_pty { + self.config.lock().unwrap().debug_console.file = Some(pty.path.clone()); + self.debug_console_pty = Some(Arc::new(Mutex::new(pty))); + } else { + let (main, sub, path) = + create_pty().map_err(DeviceManagerError::DebugconPtyOpen)?; + self.set_raw_mode(&sub) + .map_err(DeviceManagerError::SetPtyRaw)?; + self.config.lock().unwrap().debug_console.file = Some(path.clone()); + self.debug_console_pty = Some(Arc::new(Mutex::new(PtyPair { main, path }))); + } + None + } + ConsoleOutputMode::Tty => { + let out = stdout(); + let _ = self.set_raw_mode(&out); + Some(Box::new(out)) + } + ConsoleOutputMode::Off | ConsoleOutputMode::Null | ConsoleOutputMode::Socket => { + None + } + }; + if let Some(writer) = debug_console_writer { + let _ = self.add_debug_console_device(writer)?; + } + } + let console_resizer = self.add_virtio_console_device(virtio_devices, console_pty, console_resize_pipe)?; diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 2fa07ffc8..121f14a52 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -801,6 +801,7 @@ impl Vmm { None, None, None, + None, Arc::clone(&self.original_termios_opt), Some(snapshot), ) @@ -1244,6 +1245,7 @@ impl RequestHandler for Vmm { None, None, None, + None, Arc::clone(&self.original_termios_opt), None, None, @@ -1343,6 +1345,7 @@ impl RequestHandler for Vmm { None, None, None, + None, Arc::clone(&self.original_termios_opt), Some(snapshot), Some(source_url), @@ -1377,17 +1380,24 @@ impl RequestHandler for Vmm { fn vm_reboot(&mut self) -> result::Result<(), VmError> { // First we stop the current VM - let (config, serial_pty, console_pty, console_resize_pipe) = + let (config, serial_pty, console_pty, debug_console_pty, console_resize_pipe) = if let Some(mut vm) = self.vm.take() { let config = vm.get_config(); let serial_pty = vm.serial_pty(); let console_pty = vm.console_pty(); + let debug_console_pty = vm.debug_console_pty(); let console_resize_pipe = vm .console_resize_pipe() .as_ref() .map(|pipe| pipe.try_clone().unwrap()); vm.shutdown()?; - (config, serial_pty, console_pty, console_resize_pipe) + ( + config, + serial_pty, + console_pty, + debug_console_pty, + console_resize_pipe, + ) } else { return Err(VmError::VmNotCreated); }; @@ -1423,6 +1433,7 @@ impl RequestHandler for Vmm { activate_evt, serial_pty, console_pty, + debug_console_pty, console_resize_pipe, Arc::clone(&self.original_termios_opt), None, @@ -2020,6 +2031,8 @@ const DEVICE_MANAGER_SNAPSHOT_ID: &str = "device-manager"; #[cfg(test)] mod unit_tests { use super::*; + #[cfg(target_arch = "x86_64")] + use crate::config::DebugConsoleConfig; use config::{ ConsoleConfig, ConsoleOutputMode, CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, RngConfig, VmConfig, @@ -2094,6 +2107,8 @@ mod unit_tests { iommu: false, socket: None, }, + #[cfg(target_arch = "x86_64")] + debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index ab0f41774..c3346b1ff 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -479,6 +479,7 @@ impl Vm { timestamp: Instant, serial_pty: Option, console_pty: Option, + debug_console_pty: Option, console_resize_pipe: Option, original_termios: Arc>>, snapshot: Option, @@ -631,6 +632,7 @@ impl Vm { .create_devices( serial_pty, console_pty, + debug_console_pty, console_resize_pipe, original_termios, ) @@ -786,6 +788,7 @@ impl Vm { activate_evt: EventFd, serial_pty: Option, console_pty: Option, + debug_console_pty: Option, console_resize_pipe: Option, original_termios: Arc>>, snapshot: Option, @@ -865,6 +868,7 @@ impl Vm { timestamp, serial_pty, console_pty, + debug_console_pty, console_resize_pipe, original_termios, snapshot, @@ -1299,6 +1303,10 @@ impl Vm { self.device_manager.lock().unwrap().console_pty() } + pub fn debug_console_pty(&self) -> Option { + self.device_manager.lock().unwrap().debug_console_pty() + } + pub fn console_resize_pipe(&self) -> Option> { self.device_manager.lock().unwrap().console_resize_pipe() } diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index 677bf61c0..6e5ec9ad7 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -396,6 +396,27 @@ pub fn default_consoleconfig_file() -> Option { None } +#[cfg(target_arch = "x86_64")] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct DebugConsoleConfig { + #[serde(default)] + pub file: Option, + pub mode: ConsoleOutputMode, + /// Optionally dedicated I/O-port, if the default port should not be used. + pub iobase: Option, +} + +#[cfg(target_arch = "x86_64")] +impl Default for DebugConsoleConfig { + fn default() -> Self { + Self { + file: None, + mode: ConsoleOutputMode::Off, + iobase: Some(devices::debug_console::DEFAULT_PORT as u16), + } + } +} + #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct DeviceConfig { pub path: PathBuf, @@ -537,6 +558,9 @@ pub struct VmConfig { pub serial: ConsoleConfig, #[serde(default = "default_console")] pub console: ConsoleConfig, + #[cfg(target_arch = "x86_64")] + #[serde(default)] + pub debug_console: DebugConsoleConfig, pub devices: Option>, pub user_devices: Option>, pub vdpa: Option>,