diff --git a/Cargo.toml b/Cargo.toml index 63d6293df..65f34dfa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ p9ds = { git = "https://github.com/oxidecomputer/p9fs" } softnpu = { git = "https://github.com/oxidecomputer/softnpu" } # Omicron-related +illumos-utils = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } internal-dns = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } nexus-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } @@ -96,7 +97,7 @@ bitflags = "2.4" bitstruct = "0.1" bitvec = "1.0" byteorder = "1" -bytes = "1.1" +bytes = "1.7.1" camino = "1.1.6" cargo_metadata = "0.18.1" cc = "1.0.73" @@ -144,9 +145,9 @@ serde_json = "1.0" serde_test = "1.0.138" slog = "2.7" slog-async = "2.8" -slog-bunyan = "2.4.0" +slog-bunyan = "2.5" slog-dtrace = "0.3" -slog-term = "2.8" +slog-term = "2.9.1" strum = "0.26" syn = "1.0" tar = "0.4" @@ -164,3 +165,22 @@ tracing-subscriber = "0.3.14" usdt = { version = "0.5", default-features = false } uuid = "1.3.2" zerocopy = "0.7.34" + + +# +# It's common during development to use a local copy of various complex +# dependencies. If you want to use those, uncomment one of these blocks. +# +# [patch."https://github.com/oxidecomputer/omicron"] +# illumos-utils = { path = "../omicron/illumos-utils" } +# internal-dns = { path = "../omicron/internal-dns" } +# nexus-client = { path = "../omicron/clients/nexus-client" } +# omicron-common = { path = "../omicron/common" } +# omicron-zone-package = "0.9.0" +# oximeter-instruments = { path = "../omicron/oximeter/instruments", default-features = false, features = ["kstat"] } +# oximeter-producer = { path = "../omicron/oximeter/producer" } +# oximeter = { path = "../omicron/oximeter/oximeter" } +# sled-agent-client = { path = "../omicron/clients/sled-agent-client" } +# [patch."https://github.com/oxidecomputer/crucible"] +# crucible = { path = "../crucible/upstairs" } +# crucible-client-types = { path = "../crucible/crucible-client-types" } diff --git a/bin/propolis-cli/src/main.rs b/bin/propolis-cli/src/main.rs index ebf010858..b68501e83 100644 --- a/bin/propolis-cli/src/main.rs +++ b/bin/propolis-cli/src/main.rs @@ -16,14 +16,6 @@ use clap::{Parser, Subcommand}; use futures::{future, SinkExt}; use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag}; use propolis_client::types::InstanceMetadata; -use slog::{o, Drain, Level, Logger}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio_tungstenite::tungstenite::{ - protocol::{frame::coding::CloseCode, CloseFrame}, - Message, -}; -use uuid::Uuid; - use propolis_client::{ support::{InstanceSerialConsoleHelper, WSClientOffset}, types::{ @@ -33,6 +25,13 @@ use propolis_client::{ }, Client, }; +use slog::{o, Drain, Level, Logger}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_tungstenite::tungstenite::{ + protocol::{frame::coding::CloseCode, CloseFrame}, + Message, +}; +use uuid::Uuid; #[derive(Debug, Parser)] #[clap(about, version)] diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 060934d74..4f98dac85 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -10,14 +10,10 @@ use std::os::unix::fs::FileTypeExt; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::serial::Serial; -use crate::stats::virtual_machine::VirtualMachine; -use crate::vm::{BlockBackendMap, CrucibleBackendMap, DeviceMap}; use anyhow::{Context, Result}; use crucible_client_types::VolumeConstructionRequest; pub use nexus_client::Client as NexusClient; use oximeter::types::ProducerRegistry; - use propolis::block; use propolis::chardev::{self, BlockingSource, Source}; use propolis::common::{Lifecycle, GB, MB, PAGE_SIZE}; @@ -37,7 +33,14 @@ use propolis_api_types::instance_spec::{self, v0::InstanceSpecV0}; use propolis_api_types::InstanceProperties; use slog::info; -// Arbitrary ROM limit for now +use crate::serial::Serial; +use crate::stats::VirtualMachine; +use crate::vm::BlockBackendMap; +use crate::vm::CrucibleBackendMap; +use crate::vm::DeviceMap; +use crate::vm::InterfaceIdentifiers; + +/// Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; fn get_spec_guest_ram_limits(spec: &InstanceSpecV0) -> (usize, usize) { @@ -111,6 +114,7 @@ pub struct MachineInitializer<'a> { pub(crate) log: slog::Logger, pub(crate) machine: &'a Machine, pub(crate) devices: DeviceMap, + pub(crate) interface_ids: InterfaceIdentifiers, pub(crate) block_backends: BlockBackendMap, pub(crate) crucible_backends: CrucibleBackendMap, pub(crate) spec: &'a InstanceSpecV0, @@ -633,6 +637,8 @@ impl<'a> MachineInitializer<'a> { )?; self.devices .insert(format!("pci-virtio-viona-{}", bdf), viona.clone()); + self.interface_ids + .push((vnic_spec.interface_id, viona.instance_id()?)); chipset.pci_attach(bdf, viona); } Ok(()) diff --git a/bin/propolis-server/src/lib/spec/api_request.rs b/bin/propolis-server/src/lib/spec/api_request.rs index d00a9d4c3..35ddec28f 100644 --- a/bin/propolis-server/src/lib/spec/api_request.rs +++ b/bin/propolis-server/src/lib/spec/api_request.rs @@ -138,6 +138,7 @@ pub(super) fn parse_nic_from_request( let (device_name, backend_name) = super::pci_path_to_nic_names(pci_path); let device_spec = NetworkDeviceV0::VirtioNic(VirtioNic { backend_name: backend_name.clone(), + interface_id: nic.interface_id, pci_path, }); diff --git a/bin/propolis-server/src/lib/spec/config_toml.rs b/bin/propolis-server/src/lib/spec/config_toml.rs index 08e349c02..14c1245f5 100644 --- a/bin/propolis-server/src/lib/spec/config_toml.rs +++ b/bin/propolis-server/src/lib/spec/config_toml.rs @@ -17,6 +17,7 @@ use propolis_api_types::instance_spec::{ PciPath, }; use thiserror::Error; +use uuid::Uuid; #[cfg(feature = "falcon")] use propolis_api_types::instance_spec::components::devices::{ @@ -291,6 +292,9 @@ pub(super) fn parse_network_device_from_config( let device_spec = NetworkDeviceV0::VirtioNic(VirtioNic { backend_name: backend_name.clone(), + // We don't allow for configuration to specify the interface_id, so we + // generate a new one. + interface_id: Uuid::new_v4(), pci_path, }); diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 30a4f6a9d..4ceb13475 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -114,7 +114,6 @@ impl ServerSpecBuilder { ) -> Result<(), ServerSpecBuilderError> { self.builder .add_network_device(api_request::parse_nic_from_request(nic)?)?; - Ok(()) } diff --git a/bin/propolis-server/src/lib/stats.rs b/bin/propolis-server/src/lib/stats.rs index 326dfd76b..8f5be761f 100644 --- a/bin/propolis-server/src/lib/stats.rs +++ b/bin/propolis-server/src/lib/stats.rs @@ -10,31 +10,38 @@ use oximeter::{ types::{Cumulative, ProducerRegistry, Sample}, Metric, MetricsError, Producer, }; -use oximeter_producer::{Config, Error, Server}; -use slog::Logger; - -use std::net::SocketAddr; -use std::sync::{Arc, Mutex}; -use uuid::Uuid; - -use crate::server::MetricsEndpointConfig; -use crate::stats::virtual_machine::VirtualMachine; - // Propolis is built and some tests are run on non-illumos systems. The real // `kstat` infrastructure cannot be built there, so some conditional compilation // tricks are needed #[cfg(all(not(test), target_os = "illumos"))] use oximeter_instruments::kstat::KstatSampler; +use oximeter_producer::{self, Config, Server}; +use slog::Logger; +use std::{ + net::SocketAddr, + sync::{Arc, Mutex}, +}; +use crate::server::MetricsEndpointConfig; + +mod network_interface; mod pvpanic; -pub(crate) mod virtual_machine; -pub use self::pvpanic::PvpanicProducer; +mod virtual_machine; + +pub(crate) use self::network_interface::InstanceNetworkInterfaces; +pub(crate) use self::pvpanic::PvpanicProducer; +pub(crate) use self::virtual_machine::VirtualMachine; // Interval on which we ask `oximeter` to poll us for metric data. const OXIMETER_STAT_INTERVAL: tokio::time::Duration = tokio::time::Duration::from_secs(30); -// Interval on which we produce vCPU metrics. +/// Interval on which we sample instance/guest network interface metrics. +#[cfg(all(not(test), target_os = "illumos"))] +const NETWORK_INTERFACE_SAMPLE_INTERVAL: std::time::Duration = + std::time::Duration::from_secs(10); + +/// Interval on which we produce vCPU metrics. #[cfg(all(not(test), target_os = "illumos"))] const VCPU_KSTAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5); @@ -132,11 +139,11 @@ impl Producer for ServerStatsOuter { /// task, and will periodically renew that registration. The returned server is /// running, and need not be poked or renewed to successfully serve metric data. pub fn start_oximeter_server( - id: Uuid, + id: uuid::Uuid, config: &MetricsEndpointConfig, log: &Logger, registry: &ProducerRegistry, -) -> Result { +) -> Result { // Request an ephemeral port on which to serve metrics. let producer_address = SocketAddr::new(config.listen_addr, 0); let registration_address = config.registration_addr; @@ -181,16 +188,16 @@ pub fn start_oximeter_server( #[cfg_attr(not(all(not(test), target_os = "illumos")), allow(unused_variables))] pub async fn register_server_metrics( registry: &ProducerRegistry, + nics: InstanceNetworkInterfaces, virtual_machine: VirtualMachine, log: &Logger, ) -> anyhow::Result { let stats = ServerStats::new(virtual_machine.clone()); - let stats_outer = ServerStatsOuter { server_stats_wrapped: Arc::new(Mutex::new(stats)), // Setup the collection of kstats for this instance. #[cfg(all(not(test), target_os = "illumos"))] - kstat_sampler: setup_kstat_tracking(log, virtual_machine).await, + kstat_sampler: setup_kstat_tracking(log, nics, virtual_machine).await, }; registry.register_producer(stats_outer.clone())?; @@ -201,34 +208,175 @@ pub async fn register_server_metrics( #[cfg(all(not(test), target_os = "illumos"))] async fn setup_kstat_tracking( log: &Logger, + nics: InstanceNetworkInterfaces, virtual_machine: VirtualMachine, ) -> Option { - let kstat_limit = - usize::try_from(virtual_machine.n_vcpus() * KSTAT_LIMIT_PER_VCPU) - .unwrap(); + let n_interfaces = nics.interface_ids.len() as u32; + let kstat_limit = usize::try_from( + (virtual_machine.n_vcpus() * KSTAT_LIMIT_PER_VCPU) + + (n_interfaces * KSTAT_LIMIT_PER_VCPU), + ) + .unwrap(); match KstatSampler::with_sample_limit(log, kstat_limit) { Ok(sampler) => { - let details = oximeter_instruments::kstat::CollectionDetails::never( - VCPU_KSTAT_INTERVAL, - ); - if let Err(e) = sampler.add_target(virtual_machine, details).await { + let vcpu_details = + oximeter_instruments::kstat::CollectionDetails::never( + VCPU_KSTAT_INTERVAL, + ); + if let Err(err) = + sampler.add_target(virtual_machine, vcpu_details).await + { slog::error!( log, "failed to add VirtualMachine target, \ vCPU stats will be unavailable"; - "error" => ?e, + "error" => ?err, ); } - Some(sampler) + + let interface_id = nics.target.interface_id; + let nic_details = + oximeter_instruments::kstat::CollectionDetails::never( + NETWORK_INTERFACE_SAMPLE_INTERVAL, + ); + match sampler.add_target(nics, nic_details).await { + Ok(_) => { + slog::debug!( + log, + "Added new network interface to kstat sampler"; + "network_interface_id" => %interface_id, + ); + Some(sampler) + } + Err(err) => { + slog::error!( + log, + "failed to create KstatSampler, \ + network interface stats will be unavailable"; + "network_interface_id" => %interface_id, + "error" => ?err, + ); + + Some(sampler) + } + } } Err(e) => { slog::error!( log, "failed to create KstatSampler, \ - vCPU stats will be unavailable"; + vCPU and link stats will be unavailable"; "error" => ?e, ); None } } } + +#[cfg(all(not(test), target_os = "illumos"))] +mod kstat_types { + pub(crate) use kstat_rs::{Data, Kstat, Named, NamedData}; + pub(crate) use oximeter_instruments::kstat::{ + hrtime_to_utc, ConvertNamedData, Error, KstatList, KstatTarget, + }; +} + +/// Mock the relevant subset of `kstat-rs` types needed for tests. +#[cfg(not(all(not(test), target_os = "illumos")))] +mod kstat_types { + use chrono::DateTime; + use chrono::Utc; + use oximeter::{Sample, Target}; + + pub(crate) type KstatList<'a, 'k> = + &'a [(DateTime, Kstat<'k>, Data<'k>)]; + + pub(crate) trait KstatTarget: + Target + Send + Sync + 'static + std::fmt::Debug + { + /// Return true for any kstat you're interested in. + fn interested(&self, kstat: &Kstat<'_>) -> bool; + + /// Convert from a kstat and its data to a list of samples. + fn to_samples( + &self, + kstats: KstatList<'_, '_>, + ) -> Result, Error>; + } + + #[derive(Debug, Clone)] + pub(crate) enum Data<'a> { + Named(Vec>), + #[allow(dead_code)] + Null, + } + + #[derive(Debug, Clone)] + pub(crate) enum NamedData<'a> { + UInt32(u32), + UInt64(u64), + String(&'a str), + } + + #[derive(Debug)] + pub(crate) struct Kstat<'a> { + pub ks_module: &'a str, + pub ks_instance: i32, + pub ks_name: &'a str, + pub ks_snaptime: i64, + } + + #[derive(Debug, Clone)] + pub(crate) struct Named<'a> { + pub name: &'a str, + pub value: NamedData<'a>, + } + + #[allow(unused)] + pub(crate) trait ConvertNamedData { + fn as_i32(&self) -> Result; + fn as_u32(&self) -> Result; + fn as_i64(&self) -> Result; + fn as_u64(&self) -> Result; + } + + impl<'a> ConvertNamedData for NamedData<'a> { + fn as_i32(&self) -> Result { + unimplemented!() + } + + fn as_u32(&self) -> Result { + if let NamedData::UInt32(x) = self { + Ok(*x) + } else { + panic!() + } + } + + fn as_i64(&self) -> Result { + unimplemented!() + } + + fn as_u64(&self) -> Result { + if let NamedData::UInt64(x) = self { + Ok(*x) + } else { + panic!() + } + } + } + + #[derive(thiserror::Error, Clone, Debug)] + pub(crate) enum Error { + #[error("No such kstat")] + NoSuchKstat, + #[error("Expected a named kstat")] + ExpectedNamedKstat, + #[error("Sample error")] + Sample(#[from] oximeter::MetricsError), + } + + pub(crate) fn hrtime_to_utc(_: i64) -> Result, Error> { + Ok(Utc::now()) + } +} diff --git a/bin/propolis-server/src/lib/stats/network_interface.rs b/bin/propolis-server/src/lib/stats/network_interface.rs new file mode 100644 index 000000000..cac0dffc8 --- /dev/null +++ b/bin/propolis-server/src/lib/stats/network_interface.rs @@ -0,0 +1,380 @@ +// 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/. + +// Copyright 2024 Oxide Computer Company + +//! Report metrics about instance data links from the `viona` module. + +use chrono::{DateTime, Utc}; +use oximeter::{types::Cumulative, FieldType, FieldValue, Sample, Target}; +use propolis_api_types::InstanceProperties; + +use super::kstat_types::{ + hrtime_to_utc, ConvertNamedData, Data, Error, Kstat, KstatList, + KstatTarget, Named, +}; +use crate::vm::InterfaceIdentifiers; + +// NOTE: TOML definitions of timeseries are centralized in Omicron, so this file +// lives in that repo, at +// `./omicron/oximeter/oximeter/schema/instance-network-interface.toml`. +oximeter::use_timeseries!("instance-network-interface.toml"); +use self::instance_network_interface::{ + BytesReceived, BytesSent, InstanceNetworkInterface, PacketsDropped, + PacketsReceived, PacketsSent, +}; + +const KSTAT_RX_BYTES: &str = "rx_bytes"; +const KSTAT_TX_BYTES: &str = "tx_bytes"; +const KSTAT_RX_PACKETS: &str = "rx_packets"; +const KSTAT_TX_PACKETS: &str = "tx_packets"; +const KSTAT_RX_DROPS: &str = "rx_drops"; + +/// The names of the kstat fields that represent the instance network interface metrics +/// we are interested in tracking. +const KSTAT_FIELDS: &[&str] = &[ + KSTAT_RX_BYTES, + KSTAT_TX_BYTES, + KSTAT_RX_PACKETS, + KSTAT_TX_PACKETS, + KSTAT_RX_DROPS, +]; + +/// The name of the kstat module that contains the instance network interface metrics. +const KSTAT_MODULE_NAME: &str = "viona"; + +/// Helper function to extract the same kstat metrics from all link targets. +/// +/// TODO: Match up the kstat names with the metrics they actually represent, +/// once https://www.illumos.org/issues/16699 is resolved. +fn extract_nic_kstats( + target: &InstanceNetworkInterface, + named_data: &Named, + creation_time: DateTime, + snapshot_time: DateTime, +) -> Option> { + let Named { name, value } = named_data; + if *name == KSTAT_RX_BYTES { + Some(value.as_u64().and_then(|x| { + let metric = BytesReceived { + datum: Cumulative::with_start_time(creation_time, x), + }; + Sample::new_with_timestamp(snapshot_time, target, &metric) + .map_err(Error::Sample) + })) + } else if *name == KSTAT_TX_BYTES { + Some(value.as_u64().and_then(|x| { + let metric = BytesSent { + datum: Cumulative::with_start_time(creation_time, x), + }; + Sample::new_with_timestamp(snapshot_time, target, &metric) + .map_err(Error::Sample) + })) + } else if *name == KSTAT_RX_PACKETS { + Some(value.as_u64().and_then(|x| { + let metric = PacketsReceived { + datum: Cumulative::with_start_time(creation_time, x), + }; + Sample::new_with_timestamp(snapshot_time, target, &metric) + .map_err(Error::Sample) + })) + } else if *name == KSTAT_TX_PACKETS { + Some(value.as_u64().and_then(|x| { + let metric = PacketsSent { + datum: Cumulative::with_start_time(creation_time, x), + }; + Sample::new_with_timestamp(snapshot_time, target, &metric) + .map_err(Error::Sample) + })) + } else if *name == KSTAT_RX_DROPS { + Some(value.as_u64().and_then(|x| { + let metric = PacketsDropped { + datum: Cumulative::with_start_time(creation_time, x.into()), + }; + Sample::new_with_timestamp(snapshot_time, target, &metric) + .map_err(Error::Sample) + })) + } else { + None + } +} + +/// A wrapper around the `oximeter::Target` representing all instance network interfaces. +#[derive(Clone, Debug)] +pub struct InstanceNetworkInterfaces { + /// The `oximeter::Target` itself, storing the metric fields for the + /// timeseries. + /// + /// **NOTE**: While this struct represents multiple instance network interfaces, + /// they all share the same target fields. + /// + /// We default `interface_id` by generating a `uuid::Uuid::nil()` on first + /// creation, before creating multiple tragets in the `to_samples` method. + pub(crate) target: InstanceNetworkInterface, + + /// A tuple-mapping of the interface UUIDs to the device instance IDs. + pub(crate) interface_ids: InterfaceIdentifiers, +} + +impl InstanceNetworkInterfaces { + /// Create a new instance network interface metrics target from the given + /// instance properties and add the interface_ids to match and gather + /// metrics from. + pub(crate) fn new( + properties: &InstanceProperties, + interface_ids: &InterfaceIdentifiers, + ) -> Self { + Self { + target: InstanceNetworkInterface { + // Default `interface_id` to a new UUID, as we will create + // multiple targets in the `to_samples` method and override + // this. + interface_id: uuid::Uuid::nil(), + instance_id: properties.id, + project_id: properties.metadata.project_id, + silo_id: properties.metadata.silo_id, + sled_id: properties.metadata.sled_id, + sled_serial: properties.metadata.sled_serial.clone().into(), + sled_model: properties.metadata.sled_model.clone().into(), + sled_revision: properties.metadata.sled_revision, + }, + interface_ids: interface_ids.to_vec(), + } + } +} + +impl KstatTarget for InstanceNetworkInterfaces { + fn interested(&self, kstat: &Kstat<'_>) -> bool { + kstat.ks_module == KSTAT_MODULE_NAME + && self.interface_ids.iter().any(|(_id, device_instance_id)| { + kstat.ks_instance as u32 == *device_instance_id + }) + } + + fn to_samples( + &self, + kstats: KstatList<'_, '_>, + ) -> Result, Error> { + let kstats_for_links = kstats + .iter() + .filter_map(|(creation_time, kstat, data)| { + self.interface_ids.iter().find_map( + |(id, device_instance_id)| { + if kstat.ks_instance as u32 == *device_instance_id { + Some((*creation_time, kstat, data, *id)) + } else { + None + } + }, + ) + }) + .map(|(creation_time, kstat, data, interface_id)| { + let target = InstanceNetworkInterface { + interface_id, + instance_id: self.target.instance_id, + project_id: self.target.project_id, + silo_id: self.target.silo_id, + sled_id: self.target.sled_id, + sled_serial: self.target.sled_serial.clone(), + sled_model: self.target.sled_model.clone(), + sled_revision: self.target.sled_revision, + }; + (creation_time, kstat, data, target) + }); + + // Capacity is determined by the number of interfaces times the number + // of kstat fields we track. + let mut out = + Vec::with_capacity(self.interface_ids.len() * KSTAT_FIELDS.len()); + for (creation_time, kstat, data, target) in kstats_for_links { + let snapshot_time = hrtime_to_utc(kstat.ks_snaptime)?; + if let Data::Named(named) = data { + named + .iter() + .filter_map(|nd| { + extract_nic_kstats( + &target, + nd, + creation_time, + snapshot_time, + ) + .and_then(|opt_result| opt_result.ok()) + }) + .for_each(|sample| out.push(sample)); + } + } + + Ok(out) + } +} + +// Implement the `oximeter::Target` trait for `InstanceNetworkInterfaces` using +// the single `InstanceNetworkInterface` target as it represents all the same fields. +impl Target for InstanceNetworkInterfaces { + fn name(&self) -> &'static str { + self.target.name() + } + fn field_names(&self) -> &'static [&'static str] { + self.target.field_names() + } + + fn field_types(&self) -> Vec { + self.target.field_types() + } + + fn field_values(&self) -> Vec { + self.target.field_values() + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use uuid::Uuid; + + use super::*; + use crate::stats::kstat_types::NamedData; + + fn test_network_interface() -> InstanceNetworkInterface { + const INTERFACE_ID: Uuid = + uuid::uuid!("f4b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b"); + const INSTANCE_ID: Uuid = + uuid::uuid!("96d6ec78-543a-4188-830e-37e2a0eeff16"); + const PROJECT_ID: Uuid = + uuid::uuid!("7b61df02-0794-4b37-93bc-89f03c7289ca"); + const SILO_ID: Uuid = + uuid::uuid!("6a4bd4b6-e9aa-44d1-b616-399d48baa173"); + const SLED_ID: Uuid = + uuid::uuid!("aa144342-94d7-46d3-9eaa-51d84f7574b5"); + const SLED_MODEL: &str = "fake-gimlet"; + const SLED_REVISION: u32 = 1; + const SLED_SERIAL: &str = "fake-serial"; + + InstanceNetworkInterface { + interface_id: INTERFACE_ID, + instance_id: INSTANCE_ID, + project_id: PROJECT_ID, + silo_id: SILO_ID, + sled_id: SLED_ID, + sled_serial: SLED_SERIAL.into(), + sled_model: SLED_MODEL.into(), + sled_revision: SLED_REVISION, + } + } + + #[test] + fn test_kstat_interested() { + let target = InstanceNetworkInterfaces { + target: test_network_interface(), + interface_ids: vec![(Uuid::new_v4(), 2), (Uuid::new_v4(), 3)], + }; + + let ks_interested2 = Kstat { + ks_module: KSTAT_MODULE_NAME, + ks_instance: 2, + ks_snaptime: 0, + ks_name: KSTAT_MODULE_NAME, + }; + + assert!(target.interested(&ks_interested2)); + + let ks_interested3 = Kstat { + ks_module: KSTAT_MODULE_NAME, + ks_instance: 3, + ks_snaptime: 0, + ks_name: KSTAT_MODULE_NAME, + }; + + assert!(target.interested(&ks_interested3)); + + let ks_not_interested_module = Kstat { + ks_module: "not-viona", + ks_instance: 2, + ks_snaptime: 0, + ks_name: KSTAT_MODULE_NAME, + }; + assert!(!target.interested(&ks_not_interested_module)); + + let ks_not_interested_instance = Kstat { + ks_module: KSTAT_MODULE_NAME, + ks_instance: 4, + ks_snaptime: 0, + ks_name: KSTAT_MODULE_NAME, + }; + assert!(!target.interested(&ks_not_interested_instance)); + } + + #[test] + fn test_kstat_to_samples() { + let target = InstanceNetworkInterfaces { + target: test_network_interface(), + interface_ids: vec![(Uuid::new_v4(), 2), (Uuid::new_v4(), 3)], + }; + + let fields_names = vec![ + "interface_id", + "instance_id", + "project_id", + "silo_id", + "sled_id", + "sled_serial", + "sled_model", + "sled_revision", + ]; + + let kstat2 = Kstat { + ks_module: KSTAT_MODULE_NAME, + ks_instance: 2, + ks_snaptime: 0, + ks_name: KSTAT_MODULE_NAME, + }; + + let kstat3 = Kstat { + ks_module: KSTAT_MODULE_NAME, + ks_instance: 3, + ks_snaptime: 0, + ks_name: KSTAT_MODULE_NAME, + }; + + let bytes_received = + Named { name: KSTAT_RX_BYTES, value: NamedData::UInt64(100) }; + let bytes_sent = + Named { name: KSTAT_TX_BYTES, value: NamedData::UInt64(200) }; + let packets_received = + Named { name: KSTAT_RX_PACKETS, value: NamedData::UInt64(4) }; + let packets_sent = + Named { name: KSTAT_TX_PACKETS, value: NamedData::UInt64(4) }; + let packets_dropped = + Named { name: KSTAT_RX_DROPS, value: NamedData::UInt64(0) }; + + let data = Data::Named(vec![ + bytes_received, + bytes_sent, + packets_received, + packets_sent, + packets_dropped, + ]); + + let kstat_list = vec![ + (Utc::now(), kstat2, data.clone()), + (Utc::now(), kstat3, data), + ]; + let samples = target.to_samples(kstat_list.as_slice()).unwrap(); + assert_eq!(samples.len(), 2 * KSTAT_FIELDS.len()); + + let mut interface_uuids = HashSet::new(); + for sample in samples { + assert_eq!(sample.target_name(), "instance_network_interface"); + for field in sample.fields() { + assert!(fields_names.contains(&field.name.as_str())); + if field.name == "interface_id" { + interface_uuids.insert(field.value); + } + } + } + // We should have two unique interface UUIDs. + assert_eq!(interface_uuids.len(), 2); + } +} diff --git a/bin/propolis-server/src/lib/stats/virtual_machine.rs b/bin/propolis-server/src/lib/stats/virtual_machine.rs index 7dea2eeec..be9dd5722 100644 --- a/bin/propolis-server/src/lib/stats/virtual_machine.rs +++ b/bin/propolis-server/src/lib/stats/virtual_machine.rs @@ -13,6 +13,11 @@ // code with cfg directives. #![cfg_attr(any(test, not(target_os = "illumos")), allow(dead_code))] +use super::kstat_types::{ + hrtime_to_utc, ConvertNamedData, Data, Error, Kstat, NamedData, +}; +#[cfg(all(not(test), target_os = "illumos"))] +use super::kstat_types::{KstatList, KstatTarget}; use chrono::{DateTime, Utc}; use oximeter::{types::Cumulative, FieldType, FieldValue, Sample, Target}; use std::borrow::Cow; @@ -26,102 +31,6 @@ use self::virtual_machine::{ VcpuUsage, VirtualMachine as VirtualMachineTarget, }; -#[cfg(all(not(test), target_os = "illumos"))] -mod kstat_types { - pub use kstat_rs::Data; - pub use kstat_rs::Kstat; - pub use kstat_rs::NamedData; - pub use oximeter_instruments::kstat::hrtime_to_utc; - pub use oximeter_instruments::kstat::ConvertNamedData; - pub use oximeter_instruments::kstat::Error; - pub use oximeter_instruments::kstat::KstatList; - pub use oximeter_instruments::kstat::KstatTarget; -} - -// Mock the relevant subset of `kstat-rs` types needed for tests. -#[cfg(not(all(not(test), target_os = "illumos")))] -mod kstat_types { - use chrono::DateTime; - use chrono::Utc; - - #[derive(Debug)] - pub enum Data<'a> { - Named(Vec>), - #[allow(dead_code)] - Null, - } - - #[derive(Debug)] - pub enum NamedData<'a> { - UInt32(u32), - UInt64(u64), - String(&'a str), - } - - #[derive(Debug)] - pub struct Kstat<'a> { - pub ks_module: &'a str, - pub ks_instance: i32, - pub ks_name: &'a str, - pub ks_snaptime: i64, - } - - #[derive(Debug)] - pub struct Named<'a> { - pub name: &'a str, - pub value: NamedData<'a>, - } - - pub trait ConvertNamedData { - fn as_i32(&self) -> Result; - fn as_u32(&self) -> Result; - fn as_i64(&self) -> Result; - fn as_u64(&self) -> Result; - } - - impl<'a> ConvertNamedData for NamedData<'a> { - fn as_i32(&self) -> Result { - unimplemented!() - } - - fn as_u32(&self) -> Result { - if let NamedData::UInt32(x) = self { - Ok(*x) - } else { - panic!() - } - } - - fn as_i64(&self) -> Result { - unimplemented!() - } - - fn as_u64(&self) -> Result { - if let NamedData::UInt64(x) = self { - Ok(*x) - } else { - panic!() - } - } - } - - #[derive(thiserror::Error, Clone, Debug)] - pub enum Error { - #[error("No such kstat")] - NoSuchKstat, - #[error("Expected a named kstat")] - ExpectedNamedKstat, - #[error("Metrics error")] - Metrics(#[from] oximeter::MetricsError), - } - - pub fn hrtime_to_utc(_: i64) -> Result, Error> { - Ok(Utc::now()) - } -} - -pub use kstat_types::*; - /// A wrapper around the `oximeter::Target` representing a VM instance. /// /// This is used to combine the "real" target, @@ -129,21 +38,24 @@ pub use kstat_types::*; /// collect data via kstats. It's not currently possible to attach fields like /// this to the code generated by the `oximeter::use_timeseries!()` macro. #[derive(Clone, Debug)] -pub struct VirtualMachine { +pub(crate) struct VirtualMachine { /// The `oximeter::Target` itself, storing the metric fields for the /// timeseries. - pub target: VirtualMachineTarget, - - // This field is not published as part of the target field definitions. It - // is needed because the hypervisor currently creates kstats for each vCPU, - // regardless of whether they're activated. There is no way to tell from - // userland today which vCPU kstats are "real". We include this value here, - // and implement `oximeter::Target` manually, so that this field is not - // published as a field on the timeseries. + pub(crate) target: VirtualMachineTarget, + + /// This field is needed because the hypervisor currently creates kstats for + /// each vCPU, regardless of whether they're activated. There is no way to + /// tell from userland today which vCPU kstats are "real". + /// + /// This field is not published as part of the target field definitions. + /// We include this value here, and implement `oximeter::Target` manually, + /// so that this field is not published as a field on the timeseries. n_vcpus: u32, - // Same for this field, not published as part of the target, but used to - // find the right kstats. + /// Used to find the right kstats for this VM instance. + /// + /// This field is also not published as part of the target, but used to + /// find the right kstats. vm_name: String, } @@ -400,10 +312,6 @@ mod test { use super::kstat_instance_from_instance_id; use super::kstat_microstate_to_state_name; use super::produce_vcpu_usage; - use super::Data; - use super::Kstat; - use super::Named; - use super::NamedData; use super::Utc; use super::VcpuUsage; use super::VirtualMachine; @@ -412,6 +320,10 @@ mod test { use super::VMM_KSTAT_MODULE_NAME; use super::VM_KSTAT_NAME; use super::VM_NAME_KSTAT; + use crate::stats::kstat_types::Data; + use crate::stats::kstat_types::Kstat; + use crate::stats::kstat_types::Named; + use crate::stats::kstat_types::NamedData; use crate::stats::virtual_machine::N_VCPU_MICROSTATES; use crate::stats::virtual_machine::OXIMETER_EMULATION_STATE; use crate::stats::virtual_machine::OXIMETER_IDLE_STATE; diff --git a/bin/propolis-server/src/lib/vm/ensure.rs b/bin/propolis-server/src/lib/vm/ensure.rs index 75232e9f7..76a9e9883 100644 --- a/bin/propolis-server/src/lib/vm/ensure.rs +++ b/bin/propolis-server/src/lib/vm/ensure.rs @@ -25,29 +25,26 @@ //! its current phase to unwind the whole operation and drive the VM state //! machine to the correct resting state. -use std::sync::Arc; - -use propolis_api_types::{ - instance_spec::{v0::InstanceSpecV0, VersionedInstanceSpec}, - InstanceEnsureResponse, InstanceMigrateInitiateResponse, - InstanceSpecEnsureRequest, InstanceState, +use super::{ + objects::{InputVmObjects, VmObjects}, + services::VmServices, + state_driver::InputQueue, + state_publisher::{ExternalStateUpdate, StatePublisher}, + EnsureOptions, InstanceEnsureResponseTx, VmError, }; -use slog::{debug, info}; - use crate::{ initializer::{ build_instance, MachineInitializer, MachineInitializerState, }, vm::request_queue::InstanceAutoStart, }; - -use super::{ - objects::{InputVmObjects, VmObjects}, - services::VmServices, - state_driver::InputQueue, - state_publisher::{ExternalStateUpdate, StatePublisher}, - EnsureOptions, InstanceEnsureResponseTx, VmError, +use propolis_api_types::{ + instance_spec::{v0::InstanceSpecV0, VersionedInstanceSpec}, + InstanceEnsureResponse, InstanceMigrateInitiateResponse, + InstanceSpecEnsureRequest, InstanceState, }; +use slog::{debug, info}; +use std::sync::Arc; /// Holds state about an instance ensure request that has not yet produced any /// VM objects or driven the VM state machine to the `ActiveVm` state. @@ -171,6 +168,7 @@ impl<'a> VmEnsureNotStarted<'a> { log: self.log.clone(), machine: &machine, devices: Default::default(), + interface_ids: Default::default(), block_backends: Default::default(), crucible_backends: Default::default(), spec: v0_spec, @@ -225,6 +223,7 @@ impl<'a> VmEnsureNotStarted<'a> { devices, block_backends, crucible_backends, + interface_ids, .. } = init; @@ -233,6 +232,7 @@ impl<'a> VmEnsureNotStarted<'a> { vcpu_tasks, machine, devices, + interface_ids, block_backends, crucible_backends, com1, diff --git a/bin/propolis-server/src/lib/vm/mod.rs b/bin/propolis-server/src/lib/vm/mod.rs index 9e17c5672..4e9aff028 100644 --- a/bin/propolis-server/src/lib/vm/mod.rs +++ b/bin/propolis-server/src/lib/vm/mod.rs @@ -111,6 +111,9 @@ pub(crate) mod state_publisher; pub(crate) type DeviceMap = BTreeMap>; +/// Mapping of NIC identifiers to viona device instance IDs. +pub(crate) type InterfaceIdentifiers = Vec<(uuid::Uuid, DeviceInstanceId)>; + /// Maps component names to block backend trait objects. pub(crate) type BlockBackendMap = BTreeMap>; @@ -137,6 +140,9 @@ pub(crate) type CrucibleReplaceResult = pub(crate) type CrucibleReplaceResultTx = oneshot::Sender; +/// PCI device instance ID type. +type DeviceInstanceId = u32; + /// Type alias for the sender side of a channel that receives the results of /// instance-ensure API calls. type InstanceEnsureResponseTx = diff --git a/bin/propolis-server/src/lib/vm/objects.rs b/bin/propolis-server/src/lib/vm/objects.rs index 79e9897ee..16930deb6 100644 --- a/bin/propolis-server/src/lib/vm/objects.rs +++ b/bin/propolis-server/src/lib/vm/objects.rs @@ -21,11 +21,11 @@ use propolis_api_types::instance_spec::v0::InstanceSpecV0; use slog::{error, info}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use crate::{serial::Serial, vcpu_tasks::VcpuTaskController}; - use super::{ - state_driver::VmStartReason, BlockBackendMap, CrucibleBackendMap, DeviceMap, + state_driver::VmStartReason, BlockBackendMap, CrucibleBackendMap, + DeviceMap, InterfaceIdentifiers, }; +use crate::{serial::Serial, vcpu_tasks::VcpuTaskController}; /// A collection of components that make up a Propolis VM instance. pub(crate) struct VmObjects { @@ -48,6 +48,7 @@ pub(super) struct InputVmObjects { pub vcpu_tasks: Box, pub machine: Machine, pub devices: DeviceMap, + pub interface_ids: InterfaceIdentifiers, pub block_backends: BlockBackendMap, pub crucible_backends: CrucibleBackendMap, pub com1: Arc>, @@ -73,6 +74,9 @@ pub(crate) struct VmObjectsLocked { /// operations (e.g. pause and resume) for eligible components. devices: DeviceMap, + /// The set of NIC identifiers that are currently attached to this VM. + interface_ids: InterfaceIdentifiers, + /// Maps from component names to trait objects that implement the block /// storage backend trait. block_backends: BlockBackendMap, @@ -123,6 +127,7 @@ impl VmObjectsLocked { vcpu_tasks: input.vcpu_tasks, machine: input.machine, devices: input.devices, + interface_ids: input.interface_ids, block_backends: input.block_backends, crucible_backends: input.crucible_backends, com1: input.com1, @@ -168,6 +173,11 @@ impl VmObjectsLocked { self.devices.get(name).cloned() } + /// Yields the VM's current NIC identifiers. + pub(crate) fn interface_ids(&self) -> &InterfaceIdentifiers { + &self.interface_ids + } + /// Yields the VM's current Crucible backend map. pub(crate) fn crucible_backends(&self) -> &CrucibleBackendMap { &self.crucible_backends diff --git a/bin/propolis-server/src/lib/vm/services.rs b/bin/propolis-server/src/lib/vm/services.rs index fb60c6fe7..013bfacf3 100644 --- a/bin/propolis-server/src/lib/vm/services.rs +++ b/bin/propolis-server/src/lib/vm/services.rs @@ -5,19 +5,19 @@ //! Services visible to consumers outside this Propolis that depend on //! functionality supplied by an extant VM. -use std::sync::Arc; - use oximeter::types::ProducerRegistry; use propolis_api_types::InstanceProperties; use slog::{error, info, Logger}; +use std::sync::Arc; +use super::objects::{VmObjects, VmObjectsShared}; use crate::{ - serial::SerialTaskControlMessage, server::MetricsEndpointConfig, - stats::virtual_machine::VirtualMachine, vnc::VncServer, + serial::SerialTaskControlMessage, + server::MetricsEndpointConfig, + stats::{InstanceNetworkInterfaces, VirtualMachine}, + vnc::VncServer, }; -use super::objects::{VmObjects, VmObjectsShared}; - /// Information used to serve Oximeter metrics. #[derive(Default)] pub(crate) struct OximeterState { @@ -51,16 +51,24 @@ impl VmServices { vm_properties: &InstanceProperties, ensure_options: &super::EnsureOptions, ) -> Self { + let vm_objects = vm_objects.lock_shared().await; + let oximeter_state = if let Some(cfg) = &ensure_options.metrics_config { let registry = ensure_options.oximeter_registry.as_ref().expect( "should have a producer registry if metrics are configured", ); - register_oximeter_producer(log, cfg, registry, vm_properties).await + register_oximeter_producer( + log, + cfg, + registry, + vm_properties, + &vm_objects, + ) + .await } else { OximeterState::default() }; - let vm_objects = vm_objects.lock_shared().await; let vnc_server = ensure_options.vnc_server.clone(); if let Some(ramfb) = vm_objects.framebuffer() { vnc_server.attach(vm_objects.ps2ctrl().clone(), ramfb.clone()); @@ -105,10 +113,15 @@ async fn register_oximeter_producer( log: &slog::Logger, cfg: &MetricsEndpointConfig, registry: &ProducerRegistry, - vm_properties: &InstanceProperties, + instance_properties: &InstanceProperties, + vm_objects: &VmObjectsShared<'_>, ) -> OximeterState { let mut oximeter_state = OximeterState::default(); - let virtual_machine = VirtualMachine::from(vm_properties); + let network_interfaces = InstanceNetworkInterfaces::new( + instance_properties, + vm_objects.interface_ids(), + ); + let virtual_machine = VirtualMachine::from(instance_properties); // Create the server itself. // @@ -140,6 +153,7 @@ async fn register_oximeter_producer( // polled. oximeter_state.stats = match crate::stats::register_server_metrics( registry, + network_interfaces, virtual_machine, log, ) diff --git a/crates/propolis-api-types/Cargo.toml b/crates/propolis-api-types/Cargo.toml index e5ef2913f..18644eb6c 100644 --- a/crates/propolis-api-types/Cargo.toml +++ b/crates/propolis-api-types/Cargo.toml @@ -9,6 +9,7 @@ doctest = false [dependencies] crucible-client-types.workspace = true +illumos-utils.workspace = true propolis_types.workspace = true schemars.workspace = true serde.workspace = true 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 7277de8aa..0ee8f5bbe 100644 --- a/crates/propolis-api-types/src/instance_spec/components/devices.rs +++ b/crates/propolis-api-types/src/instance_spec/components/devices.rs @@ -9,6 +9,7 @@ use crate::instance_spec::{migration::MigrationElement, PciPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thiserror::Error; +use uuid::Uuid; fn backend_name_matches( this: &str, @@ -96,6 +97,9 @@ pub struct VirtioNic { /// The name of the device's backend. pub backend_name: String, + /// The interface ID of the device. + pub interface_id: Uuid, + /// The PCI path at which to attach this device. pub pci_path: PciPath, } @@ -361,6 +365,7 @@ mod test { fn compatible_virtio_nic() { let d1 = VirtioNic { backend_name: "storage_backend".to_string(), + interface_id: uuid::Uuid::new_v4(), pci_path: PciPath::new(0, 5, 0).unwrap(), }; assert!(d1.can_migrate_from_element(&d1).is_ok()); @@ -370,6 +375,7 @@ mod test { fn incompatible_virtio_nic() { let d1 = VirtioNic { backend_name: "storage_backend".to_string(), + interface_id: uuid::Uuid::new_v4(), pci_path: PciPath::new(0, 5, 0).unwrap(), }; diff --git a/crates/propolis-api-types/src/lib.rs b/crates/propolis-api-types/src/lib.rs index aea3938e0..a70705cf4 100644 --- a/crates/propolis-api-types/src/lib.rs +++ b/crates/propolis-api-types/src/lib.rs @@ -4,10 +4,10 @@ //! Definitions for types exposed by the propolis-server API -use std::{fmt, net::SocketAddr}; - +use illumos_utils::zone::PROPOLIS_ZONE_PREFIX; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::{fmt, net::SocketAddr}; use uuid::Uuid; // Re-export types that are of a public struct @@ -159,12 +159,12 @@ pub struct InstanceSpecGetResponse { #[derive(Clone, Deserialize, Serialize, JsonSchema)] pub struct InstanceStateMonitorRequest { - pub gen: u64, + pub r#gen: u64, } #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct InstanceStateMonitorResponse { - pub gen: u64, + pub r#gen: u64, pub state: InstanceState, pub migration: InstanceMigrateStatusResponse, } @@ -209,7 +209,6 @@ pub struct InstanceProperties { pub description: String, /// Metadata used to track statistics for this Instance. pub metadata: InstanceMetadata, - /// ID of the image used to initialize this Instance. pub image_id: Uuid, /// ID of the bootrom used to initialize this Instance. @@ -225,13 +224,17 @@ impl InstanceProperties { pub fn vm_name(&self) -> String { self.id.to_string() } + + /// Return the zone name of instance. + pub fn zone_name(&self) -> String { + format!("{}{}", PROPOLIS_ZONE_PREFIX, self.id) + } } #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct Instance { pub properties: InstanceProperties, pub state: InstanceState, - pub disks: Vec, pub nics: Vec, } @@ -394,6 +397,7 @@ pub struct Slot(pub u8); #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct NetworkInterfaceRequest { + pub interface_id: Uuid, pub name: String, pub slot: Slot, } diff --git a/crates/viona-api/src/lib.rs b/crates/viona-api/src/lib.rs index 7e2a54551..e2793b7e8 100644 --- a/crates/viona-api/src/lib.rs +++ b/crates/viona-api/src/lib.rs @@ -5,6 +5,7 @@ use std::fs::{File, OpenOptions}; use std::io::{Error, ErrorKind, Result}; use std::os::fd::*; +use std::os::unix::fs::MetadataExt; pub use viona_api_sys::*; @@ -65,6 +66,14 @@ impl VionaFd { Ok(vers as u32) } + /// Retrieve the minor number of the viona device instance. + pub fn instance(&self) -> Result { + let meta = self.0.metadata()?; + let rdev = meta.rdev(); + let minor = unsafe { libc::minor(rdev) }; + Ok(minor) + } + /// Check VMM ioctl command against those known to not require any /// copyin/copyout to function. const fn ioctl_usize_safe(cmd: i32) -> bool { diff --git a/lib/propolis/src/hw/virtio/viona.rs b/lib/propolis/src/hw/virtio/viona.rs index 4276aac51..21c3ac30c 100644 --- a/lib/propolis/src/hw/virtio/viona.rs +++ b/lib/propolis/src/hw/virtio/viona.rs @@ -91,7 +91,6 @@ impl Inner { pub struct PciVirtioViona { virtio_state: PciVirtioState, pci_state: pci::DeviceState, - dev_features: u32, mac_addr: [u8; ETHERADDRL], mtu: Option, @@ -157,6 +156,11 @@ impl PciVirtioViona { Ok(this) } + /// Get the minor instance number of the viona device. + pub fn instance_id(&self) -> io::Result { + self.hdl.instance() + } + fn process_interrupts(&self) { if let Some(mem) = self.pci_state.acc_mem.access() { self.hdl @@ -647,6 +651,11 @@ impl VionaHdl { self.0.ioctl_usize(viona_api::VNA_IOC_RING_INTR_CLR, idx as usize)?; Ok(()) } + /// Get the minor instance number of the viona device. + fn instance(&self) -> io::Result { + let id = self.0.instance()?; + Ok(id) + } /// Set the desired promiscuity level on this interface. #[cfg(feature = "falcon")] diff --git a/openapi/propolis-server.json b/openapi/propolis-server.json index 9d4eee36e..46de309b2 100644 --- a/openapi/propolis-server.json +++ b/openapi/propolis-server.json @@ -1354,6 +1354,10 @@ "NetworkInterfaceRequest": { "type": "object", "properties": { + "interface_id": { + "type": "string", + "format": "uuid" + }, "name": { "type": "string" }, @@ -1362,6 +1366,7 @@ } }, "required": [ + "interface_id", "name", "slot" ] @@ -1669,6 +1674,11 @@ "description": "The name of the device's backend.", "type": "string" }, + "interface_id": { + "description": "The interface ID of the device.", + "type": "string", + "format": "uuid" + }, "pci_path": { "description": "The PCI path at which to attach this device.", "allOf": [ @@ -1680,6 +1690,7 @@ }, "required": [ "backend_name", + "interface_id", "pci_path" ], "additionalProperties": false diff --git a/rustfmt.toml b/rustfmt.toml index 606073501..59816574a 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -3,3 +3,4 @@ # --------------------------------------------------------------------------- max_width = 80 use_small_heuristics = "max" +edition = "2024"