diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 1b0c0506f..84b991ed3 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -21,6 +21,7 @@ use propolis::hw::chipset::Chipset; use propolis::hw::ibmpc; use propolis::hw::pci; use propolis::hw::ps2::ctrl::PS2Ctrl; +use propolis::hw::qemu::pvpanic::QemuPvpanic; use propolis::hw::qemu::{debug::QemuDebugPort, fwcfg, ramfb}; use propolis::hw::uart::LpcUart; use propolis::hw::{nvme, virtio}; @@ -34,7 +35,7 @@ use crate::serial::Serial; use crate::server::CrucibleBackendMap; pub use nexus_client::Client as NexusClient; -use anyhow::Result; +use anyhow::{Context, Result}; // Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; @@ -276,6 +277,31 @@ impl<'a> MachineInitializer<'a> { Ok(()) } + pub fn initialize_qemu_pvpanic( + &self, + uuid: uuid::Uuid, + ) -> Result<(), anyhow::Error> { + if let Some(ref spec) = self.spec.devices.qemu_pvpanic { + if spec.enable_isa { + let pvpanic = QemuPvpanic::create( + self.log.new(slog::o!("dev" => "qemu-pvpanic")), + ); + pvpanic.attach_pio(&self.machine.bus_pio); + self.inv.register(&pvpanic)?; + + if let Some(ref registry) = self.producer_registry { + let producer = + crate::stats::PvpanicProducer::new(uuid, pvpanic); + registry.register_producer(producer).context( + "failed to register PVPANIC Oximeter producer", + )?; + } + } + } + + Ok(()) + } + fn create_storage_backend_from_spec( &self, backend_spec: &instance_spec::v0::StorageBackendV0, diff --git a/bin/propolis-server/src/lib/spec.rs b/bin/propolis-server/src/lib/spec.rs index 3b859ce0f..86017c288 100644 --- a/bin/propolis-server/src/lib/spec.rs +++ b/bin/propolis-server/src/lib/spec.rs @@ -222,9 +222,13 @@ impl ServerSpecBuilder { }, )?; - let builder = + let mut builder = SpecBuilder::new(properties.vcpus, properties.memory, enable_pcie); + builder.add_pvpanic_device(components::devices::QemuPvpanic { + enable_isa: true, + })?; + Ok(Self { builder }) } diff --git a/bin/propolis-server/src/lib/stats.rs b/bin/propolis-server/src/lib/stats.rs index 124324428..b0b774562 100644 --- a/bin/propolis-server/src/lib/stats.rs +++ b/bin/propolis-server/src/lib/stats.rs @@ -22,6 +22,9 @@ use uuid::Uuid; use crate::server::MetricsEndpointConfig; +mod pvpanic; +pub use self::pvpanic::PvpanicProducer; + const OXIMETER_STAT_INTERVAL: tokio::time::Duration = tokio::time::Duration::from_secs(30); diff --git a/bin/propolis-server/src/lib/stats/pvpanic.rs b/bin/propolis-server/src/lib/stats/pvpanic.rs new file mode 100644 index 000000000..53d24a717 --- /dev/null +++ b/bin/propolis-server/src/lib/stats/pvpanic.rs @@ -0,0 +1,73 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use super::InstanceUuid; +use oximeter::{ + types::{Cumulative, Sample}, + Metric, MetricsError, Producer, +}; +use propolis::hw::qemu::pvpanic; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Clone, Debug)] +pub struct PvpanicProducer { + /// The name to use as the Oximeter target, i.e. the identifier of the + /// source of these metrics. + stat_name: InstanceUuid, + + /// Kernel panic counts for the relevant instance. + host_handled_panics: PvPanicHostHandled, + guest_handled_panics: PvPanicGuestHandled, + + pvpanic: Arc, +} + +/// An Oximeter `Metric` that specifies the number of times an instance's guest +/// reported a guest-handled kernel panic using the QEMU `pvpanic` device. +#[derive(Debug, Default, Copy, Clone, Metric)] +struct PvPanicGuestHandled { + /// The number of times this instance's guest handled a kernel panic. + #[datum] + pub count: Cumulative, +} + +/// An Oximeter `Metric` that specifies the number of times an instance's guest +/// reported a host-handled kernel panic using the QEMU `pvpanic` device. +#[derive(Debug, Default, Copy, Clone, Metric)] +struct PvPanicHostHandled { + /// The number of times this instance's reported a host-handled kernel panic. + #[datum] + pub count: Cumulative, +} + +impl PvpanicProducer { + pub fn new(id: Uuid, pvpanic: Arc) -> Self { + PvpanicProducer { + stat_name: InstanceUuid { uuid: id }, + host_handled_panics: Default::default(), + guest_handled_panics: Default::default(), + pvpanic, + } + } +} + +impl Producer for PvpanicProducer { + fn produce( + &mut self, + ) -> Result + 'static>, MetricsError> { + let pvpanic::PanicCounts { guest_handled, host_handled } = + self.pvpanic.panic_counts(); + + self.host_handled_panics.datum_mut().set(host_handled as i64); + self.guest_handled_panics.datum_mut().set(guest_handled as i64); + + let data = vec![ + Sample::new(&self.stat_name, &self.guest_handled_panics)?, + Sample::new(&self.stat_name, &self.host_handled_panics)?, + ]; + + Ok(Box::new(data.into_iter())) + } +} diff --git a/bin/propolis-server/src/lib/vm/mod.rs b/bin/propolis-server/src/lib/vm/mod.rs index dfc87784c..8081c0634 100644 --- a/bin/propolis-server/src/lib/vm/mod.rs +++ b/bin/propolis-server/src/lib/vm/mod.rs @@ -458,6 +458,7 @@ impl VmController { let ps2ctrl_id = init.initialize_ps2(&chipset)?; let ps2ctrl: Option> = inv.get_concrete(ps2ctrl_id); init.initialize_qemu_debug_port()?; + init.initialize_qemu_pvpanic(properties.id)?; init.initialize_network_devices(&chipset)?; #[cfg(feature = "falcon")] init.initialize_softnpu_ports(&chipset)?; diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index e9ad90975..ec58fbfba 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -15,15 +15,16 @@ use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Context; use clap::Parser; use futures::future::BoxFuture; +use propolis::hw::qemu::pvpanic::QemuPvpanic; use slog::{o, Drain}; use strum::IntoEnumIterator; use tokio::runtime; use propolis::chardev::{BlockingSource, Sink, Source, UDSock}; use propolis::hw::chipset::{i440fx, Chipset}; -use propolis::hw::ibmpc; use propolis::hw::ps2::ctrl::PS2Ctrl; use propolis::hw::uart::LpcUart; +use propolis::hw::{ibmpc, qemu}; use propolis::intr_pins::FuncPin; use propolis::usdt::register_probes; use propolis::vcpu::Vcpu; @@ -929,6 +930,20 @@ fn setup_instance( chipset.pci_attach(bdf, nvme); } + qemu::pvpanic::DEVICE_NAME => { + let enable_isa = dev + .options + .get("enable_isa") + .and_then(|opt| opt.as_bool()) + .unwrap_or(false); + if enable_isa { + let pvpanic = QemuPvpanic::create( + log.new(slog::o!("dev" => "pvpanic")), + ); + pvpanic.attach_pio(pio); + inv.register(&pvpanic)?; + } + } _ => { slog::error!(log, "unrecognized driver"; "name" => name); return Err(Error::new( diff --git a/crates/propolis-api-types/src/instance_spec/components/devices.rs b/crates/propolis-api-types/src/instance_spec/components/devices.rs index 2a00ccd13..7277de8aa 100644 --- a/crates/propolis-api-types/src/instance_spec/components/devices.rs +++ b/crates/propolis-api-types/src/instance_spec/components/devices.rs @@ -214,6 +214,46 @@ pub enum MigrationCompatibilityError { ComponentConfiguration(String), } +#[derive( + Clone, + Copy, + Deserialize, + Serialize, + Debug, + PartialEq, + Eq, + JsonSchema, + Default, +)] +#[serde(deny_unknown_fields)] +pub struct QemuPvpanic { + /// Enable the QEMU PVPANIC ISA bus device (I/O port 0x505). + pub enable_isa: bool, + // TODO(eliza): add support for the PCI PVPANIC device... +} + +impl MigrationElement for Option { + fn kind(&self) -> &'static str { + "QemuPvpanic" + } + + fn can_migrate_from_element( + &self, + other: &Self, + ) -> Result<(), crate::instance_spec::migration::ElementCompatibilityError> + { + if self != other { + Err(MigrationCompatibilityError::ComponentConfiguration(format!( + "pvpanic configuration mismatch (self: {0:?}, other: {1:?})", + self, other + )) + .into()) + } else { + Ok(()) + } + } +} + // // Structs for Falcon devices. These devices don't support live migration. // @@ -385,4 +425,23 @@ mod test { b2.pci_path = PciPath::new(4, 5, 6).unwrap(); assert!(b1.can_migrate_from_element(&b2).is_err()); } + + #[test] + fn incompatible_qemu_pvpanic() { + let d1 = Some(QemuPvpanic { enable_isa: true }); + let d2 = Some(QemuPvpanic { enable_isa: false }); + assert!(d1.can_migrate_from_element(&d2).is_err()); + assert!(d1.can_migrate_from_element(&None).is_err()); + } + + #[test] + fn compatible_qemu_pvpanic() { + let d1 = Some(QemuPvpanic { enable_isa: true }); + let d2 = Some(QemuPvpanic { enable_isa: true }); + assert!(d1.can_migrate_from_element(&d2).is_ok()); + + let d1 = Some(QemuPvpanic { enable_isa: false }); + let d2 = Some(QemuPvpanic { enable_isa: false }); + assert!(d1.can_migrate_from_element(&d2).is_ok()); + } } diff --git a/crates/propolis-api-types/src/instance_spec/v0/builder.rs b/crates/propolis-api-types/src/instance_spec/v0/builder.rs index 4521fdc91..14765f12f 100644 --- a/crates/propolis-api-types/src/instance_spec/v0/builder.rs +++ b/crates/propolis-api-types/src/instance_spec/v0/builder.rs @@ -176,6 +176,22 @@ impl SpecBuilder { } } + /// Adds a QEMU pvpanic device. + pub fn add_pvpanic_device( + &mut self, + pvpanic: components::devices::QemuPvpanic, + ) -> Result<&Self, SpecBuilderError> { + if self.spec.devices.qemu_pvpanic.is_some() { + return Err(SpecBuilderError::DeviceNameInUse( + "pvpanic".to_string(), + )); + } + + self.spec.devices.qemu_pvpanic = Some(pvpanic); + + Ok(self) + } + #[cfg(feature = "falcon")] pub fn set_softnpu_pci_port( &mut self, diff --git a/crates/propolis-api-types/src/instance_spec/v0/mod.rs b/crates/propolis-api-types/src/instance_spec/v0/mod.rs index 3cd82ebb5..03cab5db8 100644 --- a/crates/propolis-api-types/src/instance_spec/v0/mod.rs +++ b/crates/propolis-api-types/src/instance_spec/v0/mod.rs @@ -114,6 +114,20 @@ pub struct DeviceSpecV0 { pub serial_ports: HashMap, pub pci_pci_bridges: HashMap, + // This field has a default value (`None`) to allow for + // backwards-compatibility when upgrading from a Propolis + // version that does not support this device. If the pvpanic device was not + // present in the spec being deserialized, a `None` will be produced, + // rather than rejecting the spec. + #[serde(default)] + // Skip serializing this field if it is `None`. This is so that Propolis + // versions with support for this device are backwards-compatible with + // older versions that don't, as long as the spec doesn't define a pvpanic + // device --- if there is no panic device, skipping the field from the spec + // means that the older version will still accept the spec. + #[serde(skip_serializing_if = "Option::is_none")] + pub qemu_pvpanic: Option, + #[cfg(feature = "falcon")] pub softnpu_pci_port: Option, #[cfg(feature = "falcon")] @@ -169,6 +183,15 @@ impl DeviceSpecV0 { ) })?; + self.qemu_pvpanic + .can_migrate_from_element(&other.qemu_pvpanic) + .map_err(|e| { + MigrationCompatibilityError::ElementMismatch( + "QEMU PVPANIC device".to_string(), + e, + ) + })?; + Ok(()) } } diff --git a/lib/propolis/src/hw/qemu/mod.rs b/lib/propolis/src/hw/qemu/mod.rs index 266da9899..ea190fe7a 100644 --- a/lib/propolis/src/hw/qemu/mod.rs +++ b/lib/propolis/src/hw/qemu/mod.rs @@ -4,4 +4,5 @@ pub mod debug; pub mod fwcfg; +pub mod pvpanic; pub mod ramfb; diff --git a/lib/propolis/src/hw/qemu/pvpanic.rs b/lib/propolis/src/hw/qemu/pvpanic.rs new file mode 100644 index 000000000..dce12a6aa --- /dev/null +++ b/lib/propolis/src/hw/qemu/pvpanic.rs @@ -0,0 +1,111 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::sync::{Arc, Mutex}; + +use crate::common::*; +use crate::pio::{PioBus, PioFn}; + +/// Implements the QEMU [pvpanic device], which +/// may be used by guests to notify the host when a kernel panic has occurred. +/// +/// QEMU exposes the pvpanic virtual device as a device on the ISA bus (I/O port +/// 0x505), a PCI device, and through ACPI. Currently, Propolis only implements +/// the ISA bus pvpanic device, but the PCI device may be implemented in the +/// future. +/// +/// [pvpanic device]: https://www.qemu.org/docs/master/specs/pvpanic.html +#[derive(Debug)] +pub struct QemuPvpanic { + counts: Mutex, + log: slog::Logger, +} + +/// Counts the number of guest kernel panics reported using the [`QemuPvpanic`] +/// virtual device. +#[derive(Copy, Clone, Debug)] +pub struct PanicCounts { + /// Counts the number of guest kernel panics handled by the host. + pub host_handled: usize, + /// Counts the number of guest kernel panics handled by the guest. + pub guest_handled: usize, +} + +pub const DEVICE_NAME: &str = "qemu-pvpanic"; + +/// Indicates that a guest panic has happened and should be processed by the +/// host +const HOST_HANDLED: u8 = 0b01; +/// Indicates a guest panic has happened and will be handled by the guest; the +/// host should record it or report it, but should not affect the execution of +/// the guest. +const GUEST_HANDLED: u8 = 0b10; + +#[usdt::provider(provider = "propolis")] +mod probes { + fn pvpanic_pio_write(value: u8) {} +} + +impl QemuPvpanic { + const IOPORT: u16 = 0x505; + + pub fn create(log: slog::Logger) -> Arc { + Arc::new(Self { + counts: Mutex::new(PanicCounts { + host_handled: 0, + guest_handled: 0, + }), + log, + }) + } + + /// Attaches this pvpanic device to the provided [`PioBus`]. + pub fn attach_pio(self: &Arc, pio: &PioBus) { + let piodev = self.clone(); + let piofn = Arc::new(move |_port: u16, rwo: RWOp| piodev.pio_rw(rwo)) + as Arc; + pio.register(Self::IOPORT, 1, piofn).unwrap(); + } + + /// Returns the current panic counts reported by the guest. + pub fn panic_counts(&self) -> PanicCounts { + *self.counts.lock().unwrap() + } + + fn pio_rw(&self, rwo: RWOp) { + match rwo { + RWOp::Read(ro) => { + ro.write_u8(HOST_HANDLED | GUEST_HANDLED); + } + RWOp::Write(wo) => { + let value = wo.read_u8(); + probes::pvpanic_pio_write!(|| value); + let host_handled = value & HOST_HANDLED != 0; + let guest_handled = value & GUEST_HANDLED != 0; + slog::debug!( + self.log, + "guest kernel panic"; + "host_handled" => host_handled, + "guest_handled" => guest_handled, + ); + + let mut counts = self.counts.lock().unwrap(); + + if host_handled { + counts.host_handled += 1; + } + + if guest_handled { + counts.guest_handled += 1; + } + } + } + } +} + +impl Entity for QemuPvpanic { + fn type_name(&self) -> &'static str { + DEVICE_NAME + } +} diff --git a/openapi/propolis-server-falcon.json b/openapi/propolis-server-falcon.json index 27b86f16f..50a8db5c4 100644 --- a/openapi/propolis-server-falcon.json +++ b/openapi/propolis-server-falcon.json @@ -631,6 +631,14 @@ "$ref": "#/components/schemas/PciPciBridge" } }, + "qemu_pvpanic": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/QemuPvpanic" + } + ] + }, "serial_ports": { "type": "object", "additionalProperties": { @@ -1405,6 +1413,19 @@ ], "additionalProperties": false }, + "QemuPvpanic": { + "type": "object", + "properties": { + "enable_isa": { + "description": "Enable the QEMU PVPANIC ISA bus device (I/O port 0x505).", + "type": "boolean" + } + }, + "required": [ + "enable_isa" + ], + "additionalProperties": false + }, "SerialPort": { "description": "A serial port device.", "type": "object", diff --git a/openapi/propolis-server.json b/openapi/propolis-server.json index 86c7c6b5a..80b4fc61c 100644 --- a/openapi/propolis-server.json +++ b/openapi/propolis-server.json @@ -623,6 +623,14 @@ "$ref": "#/components/schemas/PciPciBridge" } }, + "qemu_pvpanic": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/QemuPvpanic" + } + ] + }, "serial_ports": { "type": "object", "additionalProperties": { @@ -1340,6 +1348,19 @@ ], "additionalProperties": false }, + "QemuPvpanic": { + "type": "object", + "properties": { + "enable_isa": { + "description": "Enable the QEMU PVPANIC ISA bus device (I/O port 0x505).", + "type": "boolean" + } + }, + "required": [ + "enable_isa" + ], + "additionalProperties": false + }, "SerialPort": { "description": "A serial port device.", "type": "object",