From 004df03945fbef7f69c1cd623a4fc6d85dc89eff Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Tue, 17 Sep 2024 13:11:22 -0700 Subject: [PATCH] instance spec rework: tighten up component naming (#761) Fix a few problems with the way components are named in propolis-server `Spec`s: - Don't duplicate backend names in disk and NIC elements - Use names as keys in the serial port map instead of deriving names from serial port numbers - Properly register the names of bridges when adding them to the spec builder - Make the builder reject requests to add a device/backend pair where the device and backend have the same name (and stop requesting this for cloud-init disks...) --- bin/propolis-server/src/lib/initializer.rs | 27 +--- bin/propolis-server/src/lib/server.rs | 14 +- .../src/lib/spec/api_request.rs | 85 ++-------- .../src/lib/spec/api_spec_v0.rs | 35 ++-- bin/propolis-server/src/lib/spec/builder.rs | 150 +++++++++++++++--- .../src/lib/spec/config_toml.rs | 16 +- bin/propolis-server/src/lib/spec/mod.rs | 17 +- 7 files changed, 185 insertions(+), 159 deletions(-) diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 941c8cd7d..58b156d49 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -298,34 +298,23 @@ impl<'a> MachineInitializer<'a> { chipset: &RegisteredChipset, ) -> Result, Error> { let mut com1 = None; - - // Create UART devices for all of the serial ports in the spec that - // requested one. - for (num, user) in self.spec.serial.iter() { - if *user != spec::SerialPortDevice::Uart { + for (name, desc) in self.spec.serial.iter() { + if desc.device != spec::SerialPortDevice::Uart { continue; } - let (name, irq, port) = match num { - SerialPortNumber::Com1 => { - ("com1", ibmpc::IRQ_COM1, ibmpc::PORT_COM1) - } - SerialPortNumber::Com2 => { - ("com2", ibmpc::IRQ_COM2, ibmpc::PORT_COM2) - } - SerialPortNumber::Com3 => { - ("com3", ibmpc::IRQ_COM3, ibmpc::PORT_COM3) - } - SerialPortNumber::Com4 => { - ("com4", ibmpc::IRQ_COM4, ibmpc::PORT_COM4) - } + let (irq, port) = match desc.num { + SerialPortNumber::Com1 => (ibmpc::IRQ_COM1, ibmpc::PORT_COM1), + SerialPortNumber::Com2 => (ibmpc::IRQ_COM2, ibmpc::PORT_COM2), + SerialPortNumber::Com3 => (ibmpc::IRQ_COM3, ibmpc::PORT_COM3), + SerialPortNumber::Com4 => (ibmpc::IRQ_COM4, ibmpc::PORT_COM4), }; let dev = LpcUart::new(chipset.irq_pin(irq).unwrap()); dev.set_autodiscard(true); LpcUart::attach(&dev, &self.machine.bus_pio, port); self.devices.insert(name.to_owned(), dev.clone()); - if *num == SerialPortNumber::Com1 { + if desc.num == SerialPortNumber::Com1 { assert!(com1.is_none()); com1 = Some(dev); } diff --git a/bin/propolis-server/src/lib/server.rs b/bin/propolis-server/src/lib/server.rs index 2440e18a3..18700dd3c 100644 --- a/bin/propolis-server/src/lib/server.rs +++ b/bin/propolis-server/src/lib/server.rs @@ -133,19 +133,19 @@ fn instance_spec_from_request( spec_builder.add_cloud_init_from_request(base64.clone())?; } - for port in [ - instance_spec::components::devices::SerialPortNumber::Com1, - instance_spec::components::devices::SerialPortNumber::Com2, - instance_spec::components::devices::SerialPortNumber::Com3, + for (name, port) in [ + ("com1", instance_spec::components::devices::SerialPortNumber::Com1), + ("com2", instance_spec::components::devices::SerialPortNumber::Com2), + ("com3", instance_spec::components::devices::SerialPortNumber::Com3), // SoftNpu uses this port for ASIC management. #[cfg(not(feature = "falcon"))] - instance_spec::components::devices::SerialPortNumber::Com4, + ("com4", instance_spec::components::devices::SerialPortNumber::Com4), ] { - spec_builder.add_serial_port(port)?; + spec_builder.add_serial_port(name.to_owned(), port)?; } #[cfg(feature = "falcon")] - spec_builder.set_softnpu_com4()?; + spec_builder.set_softnpu_com4("com4".to_owned())?; spec_builder.add_pvpanic_device(spec::QemuPvpanic { name: "pvpanic".to_string(), diff --git a/bin/propolis-server/src/lib/spec/api_request.rs b/bin/propolis-server/src/lib/spec/api_request.rs index b778a485f..3cc115976 100644 --- a/bin/propolis-server/src/lib/spec/api_request.rs +++ b/bin/propolis-server/src/lib/spec/api_request.rs @@ -72,16 +72,13 @@ pub(super) fn parse_disk_from_request( disk: &DiskRequest, ) -> Result { let pci_path = slot_to_pci_path(disk.slot, SlotType::Disk)?; + let device_name = disk.name.clone(); let backend_name = format!("{}-backend", disk.name); let device_spec = match disk.device.as_ref() { - "virtio" => StorageDevice::Virtio(VirtioDisk { - backend_name: backend_name.clone(), - pci_path, - }), - "nvme" => StorageDevice::Nvme(NvmeDisk { - backend_name: backend_name.clone(), - pci_path, - }), + "virtio" => { + StorageDevice::Virtio(VirtioDisk { backend_name, pci_path }) + } + "nvme" => StorageDevice::Nvme(NvmeDisk { backend_name, pci_path }), _ => { return Err(DeviceRequestError::InvalidStorageInterface( disk.device.clone(), @@ -90,7 +87,6 @@ pub(super) fn parse_disk_from_request( } }; - let device_name = disk.name.clone(); let backend_spec = StorageBackend::Crucible(CrucibleStorageBackend { request_json: serde_json::to_string(&disk.volume_construction_request) .map_err(|e| { @@ -101,7 +97,7 @@ pub(super) fn parse_disk_from_request( Ok(ParsedDiskRequest { name: device_name, - disk: Disk { device_spec, backend_name, backend_spec }, + disk: Disk { device_spec, backend_spec }, }) } @@ -110,18 +106,16 @@ pub(super) fn parse_cloud_init_from_request( ) -> Result { let name = "cloud-init"; let pci_path = slot_to_pci_path(Slot(0), SlotType::CloudInit)?; - let backend_name = name.to_string(); + let backend_name = "cloud-init-backend".to_string(); let backend_spec = StorageBackend::Blob(BlobStorageBackend { base64, readonly: true }); - let device_spec = StorageDevice::Virtio(VirtioDisk { - backend_name: name.to_string(), - pci_path, - }); + let device_spec = + StorageDevice::Virtio(VirtioDisk { backend_name, pci_path }); Ok(ParsedDiskRequest { name: name.to_owned(), - disk: Disk { device_spec, backend_name, backend_spec }, + disk: Disk { device_spec, backend_spec }, }) } @@ -139,63 +133,6 @@ pub(super) fn parse_nic_from_request( let backend_spec = VirtioNetworkBackend { vnic_name: nic.name.to_string() }; Ok(ParsedNicRequest { name: device_name, - nic: Nic { device_spec, backend_name, backend_spec }, + nic: Nic { device_spec, backend_spec }, }) } - -#[cfg(test)] -mod test { - use propolis_api_types::VolumeConstructionRequest; - use uuid::Uuid; - - use super::*; - - fn check_parsed_storage_device_backend_pointer(parsed: &ParsedDiskRequest) { - let device_to_backend = match &parsed.disk.device_spec { - StorageDevice::Virtio(d) => d.backend_name.clone(), - StorageDevice::Nvme(d) => d.backend_name.clone(), - }; - - assert_eq!(device_to_backend, parsed.disk.backend_name); - } - - #[test] - fn parsed_disk_devices_point_to_backends() { - let vcr = VolumeConstructionRequest::File { - id: Uuid::nil(), - block_size: 512, - path: "".to_string(), - }; - - let req = DiskRequest { - name: "my-disk".to_string(), - slot: Slot(0), - read_only: false, - device: "nvme".to_string(), - volume_construction_request: vcr, - }; - - let parsed = parse_disk_from_request(&req).unwrap(); - check_parsed_storage_device_backend_pointer(&parsed); - } - - #[test] - fn parsed_network_devices_point_to_backends() { - let req = NetworkInterfaceRequest { - name: "vnic".to_string(), - interface_id: uuid::Uuid::new_v4(), - slot: Slot(0), - }; - - let parsed = parse_nic_from_request(&req).unwrap(); - let VirtioNic { backend_name, .. } = &parsed.nic.device_spec; - assert_eq!(*backend_name, parsed.nic.backend_name); - } - - #[test] - fn parsed_cloud_init_devices_point_to_backends() { - let base64 = "AAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(); - let parsed = parse_cloud_init_from_request(base64).unwrap(); - check_parsed_storage_device_backend_pointer(&parsed); - } -} diff --git a/bin/propolis-server/src/lib/spec/api_spec_v0.rs b/bin/propolis-server/src/lib/spec/api_spec_v0.rs index a41b5185e..67aef2e05 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use propolis_api_types::instance_spec::{ - components::devices::{SerialPort as SerialPortDesc, SerialPortNumber}, + components::devices::SerialPort as SerialPortDesc, v0::{InstanceSpecV0, NetworkBackendV0, NetworkDeviceV0, StorageDeviceV0}, }; use thiserror::Error; @@ -68,6 +68,7 @@ impl From for InstanceSpecV0 { let mut spec = InstanceSpecV0::default(); spec.devices.board = val.board; for (disk_name, disk) in val.disks { + let backend_name = disk.device_spec.backend_name().to_owned(); insert_component( &mut spec.devices.storage_devices, disk_name, @@ -76,12 +77,13 @@ impl From for InstanceSpecV0 { insert_component( &mut spec.backends.storage_backends, - disk.backend_name, + backend_name, disk.backend_spec.into(), ); } for (nic_name, nic) in val.nics { + let backend_name = nic.device_spec.backend_name.clone(); insert_component( &mut spec.devices.network_devices, nic_name, @@ -90,24 +92,17 @@ impl From for InstanceSpecV0 { insert_component( &mut spec.backends.network_backends, - nic.backend_name, + backend_name, NetworkBackendV0::Virtio(nic.backend_spec), ); } - for (num, user) in val.serial.iter() { - if *user == SerialPortDevice::Uart { - let name = match num { - SerialPortNumber::Com1 => "com1", - SerialPortNumber::Com2 => "com2", - SerialPortNumber::Com3 => "com3", - SerialPortNumber::Com4 => "com4", - }; - + for (name, desc) in val.serial { + if desc.device == SerialPortDevice::Uart { insert_component( &mut spec.devices.serial_ports, - name.to_owned(), - SerialPortDesc { num: *num }, + name, + SerialPortDesc { num: desc.num }, ); } } @@ -163,7 +158,7 @@ impl TryFrom for Spec { StorageDeviceV0::NvmeDisk(disk) => &disk.backend_name, }; - let (backend_name, backend_spec) = value + let (_, backend_spec) = value .backends .storage_backends .remove_entry(backend_name) @@ -176,7 +171,6 @@ impl TryFrom for Spec { device_name, Disk { device_spec: device_spec.into(), - backend_name, backend_spec: backend_spec.into(), }, )?; @@ -192,7 +186,7 @@ impl TryFrom for Spec { for (device_name, device_spec) in value.devices.network_devices { let NetworkDeviceV0::VirtioNic(device_spec) = device_spec; let backend_name = &device_spec.backend_name; - let (backend_name, backend_spec) = value + let (_, backend_spec) = value .backends .network_backends .remove_entry(backend_name) @@ -207,7 +201,7 @@ impl TryFrom for Spec { builder.add_network_device( device_name, - Nic { device_spec, backend_name, backend_spec }, + Nic { device_spec, backend_spec }, )?; } @@ -253,9 +247,8 @@ impl TryFrom for Spec { return Err(ApiSpecError::BackendNotUsed(backend.to_owned())); } - // TODO(#735): Serial ports need to have names like other devices. - for serial_port in value.devices.serial_ports.values() { - builder.add_serial_port(serial_port.num)?; + for (name, serial_port) in value.devices.serial_ports { + builder.add_serial_port(name, serial_port.num)?; } for (name, bridge) in value.devices.pci_pci_bridges { diff --git a/bin/propolis-server/src/lib/spec/builder.rs b/bin/propolis-server/src/lib/spec/builder.rs index d13f25adc..fa4b3b830 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -4,7 +4,7 @@ //! A builder for instance specs. -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashSet}; use propolis_api_types::{ instance_spec::{ @@ -28,7 +28,7 @@ use crate::{config, spec::SerialPortDevice}; use super::{ api_request::{self, DeviceRequestError}, config_toml::{ConfigTomlError, ParsedConfig}, - Disk, Nic, QemuPvpanic, + Disk, Nic, QemuPvpanic, SerialPort, }; #[cfg(feature = "falcon")] @@ -43,6 +43,9 @@ pub(crate) enum SpecBuilderError { #[error("error parsing device in ensure request")] DeviceRequest(#[from] DeviceRequestError), + #[error("device {0} has the same name as its backend")] + DeviceAndBackendNamesIdentical(String), + #[error("A component with name {0} already exists")] ComponentNameInUse(String), @@ -60,6 +63,7 @@ pub(crate) enum SpecBuilderError { pub(crate) struct SpecBuilder { spec: super::Spec, pci_paths: BTreeSet, + serial_ports: HashSet, component_names: BTreeSet, } @@ -190,19 +194,25 @@ impl SpecBuilder { disk_name: String, disk: Disk, ) -> Result<&Self, SpecBuilderError> { + if disk_name == disk.device_spec.backend_name() { + return Err(SpecBuilderError::DeviceAndBackendNamesIdentical( + disk_name, + )); + } + if self.component_names.contains(&disk_name) { return Err(SpecBuilderError::ComponentNameInUse(disk_name)); } - if self.component_names.contains(&disk.backend_name) { + if self.component_names.contains(disk.device_spec.backend_name()) { return Err(SpecBuilderError::ComponentNameInUse( - disk.backend_name, + disk.device_spec.backend_name().to_owned(), )); } self.register_pci_device(disk.device_spec.pci_path())?; self.component_names.insert(disk_name.clone()); - self.component_names.insert(disk.backend_name.clone()); + self.component_names.insert(disk.device_spec.backend_name().to_owned()); let _old = self.spec.disks.insert(disk_name, disk); assert!(_old.is_none()); Ok(self) @@ -214,17 +224,25 @@ impl SpecBuilder { nic_name: String, nic: Nic, ) -> Result<&Self, SpecBuilderError> { + if nic_name == nic.device_spec.backend_name { + return Err(SpecBuilderError::DeviceAndBackendNamesIdentical( + nic_name, + )); + } + if self.component_names.contains(&nic_name) { return Err(SpecBuilderError::ComponentNameInUse(nic_name)); } - if self.component_names.contains(&nic.backend_name) { - return Err(SpecBuilderError::ComponentNameInUse(nic.backend_name)); + if self.component_names.contains(&nic.device_spec.backend_name) { + return Err(SpecBuilderError::ComponentNameInUse( + nic.device_spec.backend_name, + )); } self.register_pci_device(nic.device_spec.pci_path)?; self.component_names.insert(nic_name.clone()); - self.component_names.insert(nic.backend_name.clone()); + self.component_names.insert(nic.device_spec.backend_name.clone()); let _old = self.spec.nics.insert(nic_name, nic); assert!(_old.is_none()); Ok(self) @@ -241,6 +259,7 @@ impl SpecBuilder { } self.register_pci_device(bridge.pci_path)?; + self.component_names.insert(name.clone()); let _old = self.spec.pci_pci_bridges.insert(name, bridge); assert!(_old.is_none()); Ok(self) @@ -249,35 +268,60 @@ impl SpecBuilder { /// Adds a serial port. pub fn add_serial_port( &mut self, - port: SerialPortNumber, + name: String, + num: SerialPortNumber, ) -> Result<&Self, SpecBuilderError> { - if self.spec.serial.insert(port, SerialPortDevice::Uart).is_some() { - Err(SpecBuilderError::SerialPortInUse(port)) - } else { - Ok(self) + if self.component_names.contains(&name) { + return Err(SpecBuilderError::ComponentNameInUse(name)); } + + if self.serial_ports.contains(&num) { + return Err(SpecBuilderError::SerialPortInUse(num)); + } + + let desc = SerialPort { num, device: SerialPortDevice::Uart }; + self.spec.serial.insert(name.clone(), desc); + self.component_names.insert(name); + self.serial_ports.insert(num); + Ok(self) } pub fn add_pvpanic_device( &mut self, pvpanic: QemuPvpanic, ) -> Result<&Self, SpecBuilderError> { + if self.component_names.contains(&pvpanic.name) { + return Err(SpecBuilderError::ComponentNameInUse(pvpanic.name)); + } + if self.spec.pvpanic.is_some() { return Err(SpecBuilderError::PvpanicInUse); } + self.component_names.insert(pvpanic.name.clone()); self.spec.pvpanic = Some(pvpanic); Ok(self) } #[cfg(feature = "falcon")] - pub fn set_softnpu_com4(&mut self) -> Result<&Self, SpecBuilderError> { - let port = SerialPortNumber::Com4; - if self.spec.serial.insert(port, SerialPortDevice::SoftNpu).is_some() { - Err(SpecBuilderError::SerialPortInUse(port)) - } else { - Ok(self) + pub fn set_softnpu_com4( + &mut self, + name: String, + ) -> Result<&Self, SpecBuilderError> { + if self.component_names.contains(&name) { + return Err(SpecBuilderError::ComponentNameInUse(name)); + } + + let num = SerialPortNumber::Com4; + if self.serial_ports.contains(&num) { + return Err(SpecBuilderError::SerialPortInUse(num)); } + + let desc = SerialPort { num, device: SerialPortDevice::SoftNpu }; + self.spec.serial.insert(name.clone(), desc); + self.component_names.insert(name); + self.serial_ports.insert(num); + Ok(self) } #[cfg(feature = "falcon")] @@ -313,6 +357,12 @@ impl SpecBuilder { port_name: String, port: SoftNpuPort, ) -> Result<&Self, SpecBuilderError> { + if port_name == port.backend_name { + return Err(SpecBuilderError::DeviceAndBackendNamesIdentical( + port_name, + )); + } + if self.component_names.contains(&port_name) { return Err(SpecBuilderError::ComponentNameInUse(port_name)); } @@ -337,10 +387,16 @@ impl SpecBuilder { #[cfg(test)] mod test { use propolis_api_types::{ + instance_spec::components::{ + backends::{BlobStorageBackend, VirtioNetworkBackend}, + devices::{VirtioDisk, VirtioNic}, + }, InstanceMetadata, Slot, VolumeConstructionRequest, }; use uuid::Uuid; + use crate::spec::{StorageBackend, StorageDevice}; + use super::*; fn test_metadata() -> InstanceMetadata { @@ -403,11 +459,21 @@ mod test { #[test] fn duplicate_serial_port() { let mut builder = test_builder(); - assert!(builder.add_serial_port(SerialPortNumber::Com1).is_ok()); - assert!(builder.add_serial_port(SerialPortNumber::Com2).is_ok()); - assert!(builder.add_serial_port(SerialPortNumber::Com3).is_ok()); - assert!(builder.add_serial_port(SerialPortNumber::Com4).is_ok()); - assert!(builder.add_serial_port(SerialPortNumber::Com1).is_err()); + assert!(builder + .add_serial_port("com1".to_owned(), SerialPortNumber::Com1) + .is_ok()); + assert!(builder + .add_serial_port("com2".to_owned(), SerialPortNumber::Com2) + .is_ok()); + assert!(builder + .add_serial_port("com3".to_owned(), SerialPortNumber::Com3) + .is_ok()); + assert!(builder + .add_serial_port("com4".to_owned(), SerialPortNumber::Com4) + .is_ok()); + assert!(builder + .add_serial_port("com1".to_owned(), SerialPortNumber::Com1) + .is_err()); } #[test] @@ -427,4 +493,40 @@ mod test { }) .is_err()); } + + #[test] + fn device_with_same_name_as_backend() { + let mut builder = test_builder(); + assert!(builder + .add_storage_device( + "storage".to_owned(), + Disk { + device_spec: StorageDevice::Virtio(VirtioDisk { + backend_name: "storage".to_owned(), + pci_path: PciPath::new(0, 4, 0).unwrap() + }), + backend_spec: StorageBackend::Blob(BlobStorageBackend { + base64: "".to_string(), + readonly: false + }) + } + ) + .is_err()); + + assert!(builder + .add_network_device( + "network".to_owned(), + Nic { + device_spec: VirtioNic { + backend_name: "network".to_owned(), + interface_id: Uuid::nil(), + pci_path: PciPath::new(0, 5, 0).unwrap() + }, + backend_spec: VirtioNetworkBackend { + vnic_name: "vnic0".to_owned() + } + } + ) + .is_err()); + } } diff --git a/bin/propolis-server/src/lib/spec/config_toml.rs b/bin/propolis-server/src/lib/spec/config_toml.rs index 2de57ad8f..141c26630 100644 --- a/bin/propolis-server/src/lib/spec/config_toml.rs +++ b/bin/propolis-server/src/lib/spec/config_toml.rs @@ -113,15 +113,9 @@ impl TryFrom<&config::Config> for ParsedConfig { let device_spec = parse_storage_device_from_config(device_name, device)?; - let backend_name = match &device_spec { - StorageDevice::Virtio(disk) => { - disk.backend_name.clone() - } - StorageDevice::Nvme(disk) => disk.backend_name.clone(), - }; - + let backend_name = device_spec.backend_name(); let backend_config = - config.block_devs.get(&backend_name).ok_or_else( + config.block_devs.get(backend_name).ok_or_else( || ConfigTomlError::StorageDeviceBackendNotFound { device: device_name.to_owned(), backend: backend_name.to_owned(), @@ -129,13 +123,13 @@ impl TryFrom<&config::Config> for ParsedConfig { )?; let backend_spec = parse_storage_backend_from_config( - &backend_name, + backend_name, backend_config, )?; parsed.disks.push(ParsedDiskRequest { name: device_name.to_owned(), - disk: Disk { device_spec, backend_name, backend_spec }, + disk: Disk { device_spec, backend_spec }, }); } "pci-virtio-viona" => { @@ -298,7 +292,7 @@ pub(super) fn parse_network_device_from_config( Ok(ParsedNicRequest { name: device_name, - nic: Nic { device_spec, backend_name, backend_spec }, + nic: Nic { device_spec, backend_spec }, }) } diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 706bef589..e26f51757 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -58,7 +58,7 @@ pub(crate) struct Spec { pub disks: HashMap, pub nics: HashMap, - pub serial: HashMap, + pub serial: HashMap, pub pci_pci_bridges: HashMap, pub pvpanic: Option, @@ -81,6 +81,13 @@ impl StorageDevice { StorageDevice::Nvme(disk) => disk.pci_path, } } + + pub fn backend_name(&self) -> &str { + match self { + StorageDevice::Virtio(disk) => &disk.backend_name, + StorageDevice::Nvme(disk) => &disk.backend_name, + } + } } impl From for StorageDeviceV0 { @@ -132,14 +139,12 @@ impl From for StorageBackend { #[derive(Clone, Debug)] pub struct Disk { pub device_spec: StorageDevice, - pub backend_name: String, pub backend_spec: StorageBackend, } #[derive(Clone, Debug)] pub struct Nic { pub device_spec: VirtioNic, - pub backend_name: String, pub backend_spec: VirtioNetworkBackend, } @@ -152,6 +157,12 @@ pub enum SerialPortDevice { SoftNpu, } +#[derive(Clone, Debug)] +pub struct SerialPort { + pub num: SerialPortNumber, + pub device: SerialPortDevice, +} + #[derive(Clone, Debug)] pub struct QemuPvpanic { #[allow(dead_code)]