diff --git a/bin/propolis-server/src/lib/spec/api_request.rs b/bin/propolis-server/src/lib/spec/api_request.rs new file mode 100644 index 000000000..d00a9d4c3 --- /dev/null +++ b/bin/propolis-server/src/lib/spec/api_request.rs @@ -0,0 +1,154 @@ +// 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/. + +//! Converts device descriptions from an +//! [`propolis_api_types::InstanceEnsureRequest`] into elements that can be +//! added to a spec. + +use propolis_api_types::{ + instance_spec::{ + components::{ + backends::{ + BlobStorageBackend, CrucibleStorageBackend, + VirtioNetworkBackend, + }, + devices::{NvmeDisk, VirtioDisk, VirtioNic}, + }, + v0::{ + NetworkBackendV0, NetworkDeviceV0, StorageBackendV0, + StorageDeviceV0, + }, + PciPath, + }, + DiskRequest, NetworkInterfaceRequest, Slot, +}; +use thiserror::Error; + +use super::{ParsedNetworkDevice, ParsedStorageDevice}; + +#[derive(Debug, Error)] +pub(crate) enum DeviceRequestError { + #[error("invalid storage interface {0} for disk in slot {1}")] + InvalidStorageInterface(String, u8), + + #[error("invalid PCI slot {0} for device type {1:?}")] + PciSlotInvalid(u8, SlotType), + + #[error("error serializing {0}")] + SerializationError(String, #[source] serde_json::error::Error), +} + +/// A type of PCI device. Device numbers on the PCI bus are partitioned by slot +/// type. If a client asks to attach a device of type X to PCI slot Y, the +/// server will assign the Yth device number in X's partition. The partitioning +/// scheme is defined by the implementation of the `slot_to_pci_path` utility +/// function. +#[derive(Clone, Copy, Debug)] +pub(crate) enum SlotType { + Nic, + Disk, + CloudInit, +} + +/// Translates a device type and PCI slot (as presented in an instance creation +/// request) into a concrete PCI path. See the documentation for [`SlotType`]. +fn slot_to_pci_path( + slot: Slot, + ty: SlotType, +) -> Result { + match ty { + // Slots for NICS: 0x08 -> 0x0F + SlotType::Nic if slot.0 <= 7 => PciPath::new(0, slot.0 + 0x8, 0), + // Slots for Disks: 0x10 -> 0x17 + SlotType::Disk if slot.0 <= 7 => PciPath::new(0, slot.0 + 0x10, 0), + // Slot for CloudInit + SlotType::CloudInit if slot.0 == 0 => PciPath::new(0, slot.0 + 0x18, 0), + _ => return Err(DeviceRequestError::PciSlotInvalid(slot.0, ty)), + } + .map_err(|_| DeviceRequestError::PciSlotInvalid(slot.0, ty)) +} + +pub(super) fn parse_disk_from_request( + disk: &DiskRequest, +) -> Result { + let pci_path = slot_to_pci_path(disk.slot, SlotType::Disk)?; + let device_spec = match disk.device.as_ref() { + "virtio" => StorageDeviceV0::VirtioDisk(VirtioDisk { + backend_name: disk.name.to_string(), + pci_path, + }), + "nvme" => StorageDeviceV0::NvmeDisk(NvmeDisk { + backend_name: disk.name.to_string(), + pci_path, + }), + _ => { + return Err(DeviceRequestError::InvalidStorageInterface( + disk.device.clone(), + disk.slot.0, + )) + } + }; + + let device_name = disk.name.clone(); + let backend_name = format!("{}-backend", disk.name); + let backend_spec = StorageBackendV0::Crucible(CrucibleStorageBackend { + request_json: serde_json::to_string(&disk.volume_construction_request) + .map_err(|e| { + DeviceRequestError::SerializationError(disk.name.clone(), e) + })?, + readonly: disk.read_only, + }); + + Ok(ParsedStorageDevice { + device_name, + device_spec, + backend_name, + backend_spec, + }) +} + +pub(super) fn parse_cloud_init_from_request( + base64: String, +) -> Result { + let name = "cloud-init"; + let pci_path = slot_to_pci_path(Slot(0), SlotType::CloudInit)?; + let backend_name = name.to_string(); + let backend_spec = + StorageBackendV0::Blob(BlobStorageBackend { base64, readonly: true }); + + let device_name = name.to_string(); + let device_spec = StorageDeviceV0::VirtioDisk(VirtioDisk { + backend_name: name.to_string(), + pci_path, + }); + + Ok(ParsedStorageDevice { + device_name, + device_spec, + backend_name, + backend_spec, + }) +} + +pub(super) fn parse_nic_from_request( + nic: &NetworkInterfaceRequest, +) -> Result { + let pci_path = slot_to_pci_path(nic.slot, SlotType::Nic)?; + let (device_name, backend_name) = super::pci_path_to_nic_names(pci_path); + let device_spec = NetworkDeviceV0::VirtioNic(VirtioNic { + backend_name: backend_name.clone(), + pci_path, + }); + + let backend_spec = NetworkBackendV0::Virtio(VirtioNetworkBackend { + vnic_name: nic.name.to_string(), + }); + + Ok(ParsedNetworkDevice { + device_name, + device_spec, + backend_name, + backend_spec, + }) +} diff --git a/bin/propolis-server/src/lib/spec/builder.rs b/bin/propolis-server/src/lib/spec/builder.rs index fb3871ee1..55b39ff35 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -11,20 +11,22 @@ use propolis_api_types::instance_spec::{ board::Board, devices::{PciPciBridge, QemuPvpanic, SerialPort, SerialPortNumber}, }, - v0::{ - DeviceSpecV0, InstanceSpecV0, NetworkBackendV0, NetworkDeviceV0, - StorageBackendV0, StorageDeviceV0, - }, + v0::{DeviceSpecV0, InstanceSpecV0, NetworkDeviceV0, StorageDeviceV0}, PciPath, }; use thiserror::Error; #[cfg(feature = "falcon")] -use propolis_api_types::instance_spec::components::{ - backends::DlpiNetworkBackend, - devices::{P9fs, SoftNpuP9, SoftNpuPciPort, SoftNpuPort}, +use propolis_api_types::instance_spec::{ + components::{ + backends::DlpiNetworkBackend, + devices::{P9fs, SoftNpuP9, SoftNpuPciPort, SoftNpuPort}, + }, + v0::NetworkBackendV0, }; +use super::{ParsedNetworkDevice, ParsedStorageDevice}; + /// Errors that can arise while building an instance spec from component parts. #[allow(clippy::enum_variant_names)] #[derive(Debug, Error)] @@ -46,7 +48,7 @@ pub(crate) enum SpecBuilderError { SoftNpuPortInUse(String), } -pub(super) struct SpecBuilder { +pub(crate) struct SpecBuilder { spec: InstanceSpecV0, pci_paths: BTreeSet, } @@ -100,10 +102,12 @@ impl SpecBuilder { /// Adds a storage device with an associated backend. pub(super) fn add_storage_device( &mut self, - device_name: String, - device_spec: StorageDeviceV0, - backend_name: String, - backend_spec: StorageBackendV0, + ParsedStorageDevice { + device_name, + device_spec, + backend_name, + backend_spec, + }: ParsedStorageDevice, ) -> Result<&Self, SpecBuilderError> { if self.spec.devices.storage_devices.contains_key(&device_name) { return Err(SpecBuilderError::DeviceNameInUse(device_name)); @@ -128,12 +132,14 @@ impl SpecBuilder { } /// Adds a network device with an associated backend. - pub fn add_network_device( + pub(super) fn add_network_device( &mut self, - device_name: String, - device_spec: NetworkDeviceV0, - backend_name: String, - backend_spec: NetworkBackendV0, + ParsedNetworkDevice { + device_name, + device_spec, + backend_name, + backend_spec, + }: ParsedNetworkDevice, ) -> Result<&Self, SpecBuilderError> { if self.spec.devices.network_devices.contains_key(&device_name) { return Err(SpecBuilderError::DeviceNameInUse(device_name)); diff --git a/bin/propolis-server/src/lib/spec/config_toml.rs b/bin/propolis-server/src/lib/spec/config_toml.rs index 0163bd914..08e349c02 100644 --- a/bin/propolis-server/src/lib/spec/config_toml.rs +++ b/bin/propolis-server/src/lib/spec/config_toml.rs @@ -25,6 +25,8 @@ use propolis_api_types::instance_spec::components::devices::{ use crate::config; +use super::{ParsedNetworkDevice, ParsedStorageDevice}; + #[derive(Debug, Error)] pub(crate) enum ConfigTomlError { #[error("unrecognized device type {0:?}")] @@ -48,6 +50,9 @@ pub(crate) enum ConfigTomlError { #[error("invalid storage backend kind {kind:?} for backend {name:?}")] InvalidStorageBackendType { kind: String, name: String }, + #[error("couldn't find storage device {device:?}'s backend {backend:?}")] + StorageDeviceBackendNotFound { device: String, backend: String }, + #[error("couldn't get path for file backend {0:?}")] InvalidFileBackendPath(String), @@ -66,6 +71,119 @@ pub(crate) enum ConfigTomlError { NoP9Target(String), } +#[cfg(feature = "falcon")] +#[derive(Default)] +pub(super) struct ParsedSoftNpu { + pub(super) pci_ports: Vec, + pub(super) ports: Vec, + pub(super) p9_devices: Vec, + pub(super) p9fs: Vec, +} + +#[derive(Default)] +pub(super) struct ParsedConfig { + pub(super) disks: Vec, + pub(super) nics: Vec, + pub(super) pci_bridges: Vec, + + #[cfg(feature = "falcon")] + pub(super) softnpu: ParsedSoftNpu, +} + +impl TryFrom<&config::Config> for ParsedConfig { + type Error = ConfigTomlError; + + fn try_from(config: &config::Config) -> Result { + let mut parsed = Self::default(); + for (device_name, device) in config.devices.iter() { + let driver = device.driver.as_str(); + match driver { + // If this is a storage device, parse its "block_dev" property + // to get the name of its corresponding backend. + "pci-virtio-block" | "pci-nvme" => { + let device_spec = + parse_storage_device_from_config(device_name, device)?; + + let backend_name = match &device_spec { + StorageDeviceV0::VirtioDisk(disk) => { + disk.backend_name.clone() + } + StorageDeviceV0::NvmeDisk(disk) => { + disk.backend_name.clone() + } + }; + + let backend_config = + config.block_devs.get(&backend_name).ok_or_else( + || ConfigTomlError::StorageDeviceBackendNotFound { + device: device_name.to_owned(), + backend: backend_name.to_owned(), + }, + )?; + + let backend_spec = parse_storage_backend_from_config( + &backend_name, + backend_config, + )?; + + parsed.disks.push(ParsedStorageDevice { + device_name: device_name.to_owned(), + device_spec, + backend_name, + backend_spec, + }); + } + "pci-virtio-viona" => { + parsed.nics.push(parse_network_device_from_config( + device_name, + device, + )?); + } + #[cfg(feature = "falcon")] + "softnpu-pci-port" => { + parsed.softnpu.pci_ports.push( + parse_softnpu_pci_port_from_config( + device_name, + device, + )?, + ); + } + #[cfg(feature = "falcon")] + "softnpu-port" => { + parsed.softnpu.ports.push(parse_softnpu_port_from_config( + device_name, + device, + )?); + } + #[cfg(feature = "falcon")] + "softnpu-p9" => { + parsed.softnpu.p9_devices.push( + parse_softnpu_p9_from_config(device_name, device)?, + ); + } + #[cfg(feature = "falcon")] + "pci-virtio-9p" => { + parsed + .softnpu + .p9fs + .push(parse_p9fs_from_config(device_name, device)?); + } + _ => { + return Err(ConfigTomlError::UnrecognizedDeviceType( + driver.to_owned(), + )) + } + } + } + + for bridge in config.pci_bridges.iter() { + parsed.pci_bridges.push(parse_pci_bridge_from_config(bridge)?); + } + + Ok(parsed) + } +} + pub(super) fn parse_storage_backend_from_config( name: &str, backend: &config::BlockDevice, @@ -154,13 +272,6 @@ pub(super) fn parse_storage_device_from_config( }) } -pub(super) struct ParsedNetworkDevice { - pub(super) device_name: String, - pub(super) device_spec: NetworkDeviceV0, - pub(super) backend_name: String, - pub(super) backend_spec: NetworkBackendV0, -} - pub(super) fn parse_network_device_from_config( name: &str, device: &config::Device, diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index a13bf7501..30a4f6a9d 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -5,78 +5,54 @@ //! Helper functions for building instance specs from server parameters. use crate::config; +use api_request::DeviceRequestError; use builder::SpecBuilder; use config_toml::ConfigTomlError; -use propolis_api_types::instance_spec::components::backends::{ - BlobStorageBackend, CrucibleStorageBackend, VirtioNetworkBackend, -}; use propolis_api_types::instance_spec::components::board::{Chipset, I440Fx}; use propolis_api_types::instance_spec::components::devices::{ - NvmeDisk, QemuPvpanic, SerialPortNumber, VirtioDisk, VirtioNic, + QemuPvpanic, SerialPortNumber, }; use propolis_api_types::instance_spec::{ components::board::Board, v0::*, PciPath, }; use propolis_api_types::{ - self as api, DiskRequest, InstanceProperties, NetworkInterfaceRequest, + DiskRequest, InstanceProperties, NetworkInterfaceRequest, }; use thiserror::Error; -mod builder; +mod api_request; +pub(crate) mod builder; mod config_toml; +/// Describes a storage device/backend pair parsed from an input source like an +/// API request or a config TOML entry. +struct ParsedStorageDevice { + device_name: String, + device_spec: StorageDeviceV0, + backend_name: String, + backend_spec: StorageBackendV0, +} + +/// Describes a network device/backend pair parsed from an input source like an +/// API request or a config TOML entry. +struct ParsedNetworkDevice { + device_name: String, + device_spec: NetworkDeviceV0, + backend_name: String, + backend_spec: NetworkBackendV0, +} + /// Errors that can occur while building an instance spec from component parts. #[derive(Debug, Error)] pub(crate) enum ServerSpecBuilderError { #[error(transparent)] InnerBuilderError(#[from] builder::SpecBuilderError), - #[error( - "Could not translate PCI slot {0} for device type {1:?} to a PCI path" - )] - PciSlotInvalid(u8, SlotType), - - #[error("Unrecognized storage device interface {0}")] - UnrecognizedStorageDevice(String), - - #[error("Device {0} requested missing backend {1}")] - DeviceMissingBackend(String, String), - #[error("error parsing config TOML")] ConfigToml(#[from] ConfigTomlError), - #[error("Error serializing {0} into spec element: {1}")] - SerializationError(String, serde_json::error::Error), -} - -/// A type of PCI device. Device numbers on the PCI bus are partitioned by slot -/// type. If a client asks to attach a device of type X to PCI slot Y, the -/// server will assign the Yth device number in X's partition. The partitioning -/// scheme is defined by the implementation of the `slot_to_pci_path` utility -/// function. -#[derive(Clone, Copy, Debug)] -pub enum SlotType { - Nic, - Disk, - CloudInit, -} - -/// Translates a device type and PCI slot (as presented in an instance creation -/// request) into a concrete PCI path. See the documentation for [`SlotType`]. -pub(crate) fn slot_to_pci_path( - slot: api::Slot, - ty: SlotType, -) -> Result { - match ty { - // Slots for NICS: 0x08 -> 0x0F - SlotType::Nic if slot.0 <= 7 => PciPath::new(0, slot.0 + 0x8, 0), - // Slots for Disks: 0x10 -> 0x17 - SlotType::Disk if slot.0 <= 7 => PciPath::new(0, slot.0 + 0x10, 0), - // Slot for CloudInit - SlotType::CloudInit if slot.0 == 0 => PciPath::new(0, slot.0 + 0x18, 0), - _ => return Err(ServerSpecBuilderError::PciSlotInvalid(slot.0, ty)), - } - .map_err(|_| ServerSpecBuilderError::PciSlotInvalid(slot.0, ty)) + #[error("error parsing device in ensure request")] + DeviceRequest(#[from] DeviceRequestError), } /// Generates NIC device and backend names from the NIC's PCI path. This is @@ -136,23 +112,8 @@ impl ServerSpecBuilder { &mut self, nic: &NetworkInterfaceRequest, ) -> Result<(), ServerSpecBuilderError> { - let pci_path = slot_to_pci_path(nic.slot, SlotType::Nic)?; - let (device_name, backend_name) = pci_path_to_nic_names(pci_path); - let device_spec = NetworkDeviceV0::VirtioNic(VirtioNic { - backend_name: backend_name.clone(), - pci_path, - }); - - let backend_spec = NetworkBackendV0::Virtio(VirtioNetworkBackend { - vnic_name: nic.name.to_string(), - }); - - self.builder.add_network_device( - device_name, - device_spec, - backend_name, - backend_spec, - )?; + self.builder + .add_network_device(api_request::parse_nic_from_request(nic)?)?; Ok(()) } @@ -163,42 +124,8 @@ impl ServerSpecBuilder { &mut self, disk: &DiskRequest, ) -> Result<(), ServerSpecBuilderError> { - let pci_path = slot_to_pci_path(disk.slot, SlotType::Disk)?; - let backend_name = disk.name.clone(); - - let backend_spec = StorageBackendV0::Crucible(CrucibleStorageBackend { - request_json: serde_json::to_string( - &disk.volume_construction_request, - ) - .map_err(|e| { - ServerSpecBuilderError::SerializationError(disk.name.clone(), e) - })?, - readonly: disk.read_only, - }); - - let device_name = disk.name.clone(); - let device_spec = match disk.device.as_ref() { - "virtio" => StorageDeviceV0::VirtioDisk(VirtioDisk { - backend_name: disk.name.to_string(), - pci_path, - }), - "nvme" => StorageDeviceV0::NvmeDisk(NvmeDisk { - backend_name: disk.name.to_string(), - pci_path, - }), - _ => { - return Err(ServerSpecBuilderError::UnrecognizedStorageDevice( - disk.device.clone(), - )) - } - }; - - self.builder.add_storage_device( - device_name, - device_spec, - backend_name, - backend_spec, - )?; + self.builder + .add_storage_device(api_request::parse_disk_from_request(disk)?)?; Ok(()) } @@ -209,188 +136,58 @@ impl ServerSpecBuilder { &mut self, base64: String, ) -> Result<(), ServerSpecBuilderError> { - let name = "cloud-init"; - let pci_path = slot_to_pci_path(api::Slot(0), SlotType::CloudInit)?; - let backend_name = name.to_string(); - let backend_spec = StorageBackendV0::Blob(BlobStorageBackend { - base64, - readonly: true, - }); - - let device_name = name.to_string(); - let device_spec = StorageDeviceV0::VirtioDisk(VirtioDisk { - backend_name: name.to_string(), - pci_path, - }); - self.builder.add_storage_device( - device_name, - device_spec, - backend_name, - backend_spec, + api_request::parse_cloud_init_from_request(base64)?, )?; Ok(()) } - fn add_network_device_from_config( - &mut self, - name: &str, - device: &config::Device, - ) -> Result<(), ServerSpecBuilderError> { - let parsed = - config_toml::parse_network_device_from_config(name, device)?; - - self.builder.add_network_device( - parsed.device_name, - parsed.device_spec, - parsed.backend_name, - parsed.backend_spec, - )?; - - Ok(()) - } - - fn add_pci_bridge_from_config( - &mut self, - bridge: &config::PciBridge, - ) -> Result<(), ServerSpecBuilderError> { - let parsed = config_toml::parse_pci_bridge_from_config(bridge)?; - self.builder.add_pci_bridge(parsed.name, parsed.bridge)?; - Ok(()) - } - /// Adds all the devices and backends specified in the supplied /// configuration TOML to the spec under construction. pub fn add_devices_from_config( &mut self, config: &config::Config, ) -> Result<(), ServerSpecBuilderError> { - for (device_name, device) in config.devices.iter() { - let driver = device.driver.as_str(); - match driver { - // If this is a storage device, parse its "block_dev" property - // to get the name of its corresponding backend. - "pci-virtio-block" | "pci-nvme" => { - let device_spec = - config_toml::parse_storage_device_from_config( - device_name, - device, - )?; - - let backend_name = match &device_spec { - StorageDeviceV0::VirtioDisk(disk) => { - disk.backend_name.clone() - } - StorageDeviceV0::NvmeDisk(disk) => { - disk.backend_name.clone() - } - }; - - let backend_config = config - .block_devs - .get(&backend_name) - .ok_or_else(|| { - ServerSpecBuilderError::DeviceMissingBackend( - device_name.clone(), - backend_name.clone(), - ) - })?; - - let backend_spec = - config_toml::parse_storage_backend_from_config( - &backend_name, - backend_config, - )?; - - self.builder.add_storage_device( - device_name.clone(), - device_spec, - backend_name, - backend_spec, - )?; - } - "pci-virtio-viona" => { - self.add_network_device_from_config(device_name, device)? - } - #[cfg(feature = "falcon")] - "softnpu-pci-port" => { - self.add_softnpu_pci_port_from_config(device_name, device)? - } - #[cfg(feature = "falcon")] - "softnpu-port" => { - self.add_softnpu_device_from_config(device_name, device)? - } - #[cfg(feature = "falcon")] - "softnpu-p9" => { - self.add_softnpu_p9_from_config(device_name, device)? - } - #[cfg(feature = "falcon")] - "pci-virtio-9p" => { - self.add_p9fs_from_config(device_name, device)? - } - _ => { - return Err(ServerSpecBuilderError::ConfigToml( - ConfigTomlError::UnrecognizedDeviceType( - driver.to_owned(), - ), - )) - } - } + let parsed = config_toml::ParsedConfig::try_from(config)?; + for disk in parsed.disks { + self.builder.add_storage_device(disk)?; } - for bridge in config.pci_bridges.iter() { - self.add_pci_bridge_from_config(bridge)?; + for nic in parsed.nics { + self.builder.add_network_device(nic)?; } - Ok(()) - } + for bridge in parsed.pci_bridges { + self.builder.add_pci_bridge(bridge.name, bridge.bridge)?; + } - #[cfg(feature = "falcon")] - fn add_softnpu_p9_from_config( - &mut self, - name: &str, - device: &config::Device, - ) -> Result<(), ServerSpecBuilderError> { - self.builder.set_softnpu_p9( - config_toml::parse_softnpu_p9_from_config(name, device)?, - )?; + #[cfg(feature = "falcon")] + self.add_parsed_softnpu_devices(parsed.softnpu)?; Ok(()) } #[cfg(feature = "falcon")] - fn add_softnpu_pci_port_from_config( + fn add_parsed_softnpu_devices( &mut self, - name: &str, - device: &config::Device, + devices: config_toml::ParsedSoftNpu, ) -> Result<(), ServerSpecBuilderError> { - self.builder.set_softnpu_pci_port( - config_toml::parse_softnpu_pci_port_from_config(name, device)?, - )?; + for pci_port in devices.pci_ports { + self.builder.set_softnpu_pci_port(pci_port)?; + } - Ok(()) - } + for port in devices.ports { + self.builder.add_softnpu_port(port.name.clone(), port)?; + } - #[cfg(feature = "falcon")] - fn add_softnpu_device_from_config( - &mut self, - name: &str, - device: &config::Device, - ) -> Result<(), ServerSpecBuilderError> { - let port = config_toml::parse_softnpu_port_from_config(name, device)?; - self.builder.add_softnpu_port(name.to_string(), port)?; - Ok(()) - } + for p9 in devices.p9_devices { + self.builder.set_softnpu_p9(p9)?; + } - #[cfg(feature = "falcon")] - fn add_p9fs_from_config( - &mut self, - name: &str, - device: &config::Device, - ) -> Result<(), ServerSpecBuilderError> { - self.builder - .set_p9fs(config_toml::parse_p9fs_from_config(name, device)?)?; + for p9fs in devices.p9fs { + self.builder.set_p9fs(p9fs)?; + } Ok(()) } @@ -524,7 +321,7 @@ mod test { }, }) .err(), - Some(ServerSpecBuilderError::UnrecognizedStorageDevice(_)) + Some(ServerSpecBuilderError::DeviceRequest(_)) )); } } diff --git a/crates/propolis-api-types/src/instance_spec/v0/mod.rs b/crates/propolis-api-types/src/instance_spec/v0.rs similarity index 100% rename from crates/propolis-api-types/src/instance_spec/v0/mod.rs rename to crates/propolis-api-types/src/instance_spec/v0.rs