Skip to content

Commit fd36368

Browse files
authored
Add support for programmable SMBIOS Type 1 table (#977)
1 parent cae89ab commit fd36368

File tree

19 files changed

+2458
-76
lines changed

19 files changed

+2458
-76
lines changed

bin/propolis-cli/src/main.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ use futures::{future, SinkExt};
1818
use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
1919
use propolis_client::instance_spec::{
2020
BlobStorageBackend, Board, Chipset, ComponentV0, CrucibleStorageBackend,
21-
GuestHypervisorInterface, HyperVFeatureFlag, I440Fx, InstanceSpecV0,
22-
NvmeDisk, PciPath, QemuPvpanic, ReplacementComponent, SerialPort,
23-
SerialPortNumber, SpecKey, VirtioDisk,
21+
GuestHypervisorInterface, HyperVFeatureFlag, I440Fx, InstanceMetadata,
22+
InstanceProperties, InstanceSpec, InstanceSpecGetResponse, NvmeDisk,
23+
PciPath, QemuPvpanic, ReplacementComponent, SerialPort, SerialPortNumber,
24+
SpecKey, VirtioDisk,
2425
};
2526
use propolis_client::support::nvme_serial_from_str;
2627
use propolis_client::types::{
27-
InstanceEnsureRequest, InstanceInitializationMethod, InstanceMetadata,
28-
InstanceSpecGetResponse,
28+
InstanceEnsureRequest, InstanceInitializationMethod,
2929
};
3030
use propolis_config_toml::spec::toml_cpuid_to_spec_cpuid;
3131
use propolis_config_toml::spec::SpecConfig;
@@ -40,10 +40,7 @@ use uuid::Uuid;
4040

4141
use propolis_client::{
4242
support::{InstanceSerialConsoleHelper, WSClientOffset},
43-
types::{
44-
InstanceProperties, InstanceStateRequested, InstanceVcrReplace,
45-
MigrationState,
46-
},
43+
types::{InstanceStateRequested, InstanceVcrReplace, MigrationState},
4744
Client,
4845
};
4946

@@ -208,7 +205,7 @@ struct VmConfig {
208205
}
209206

210207
fn add_component_to_spec(
211-
spec: &mut InstanceSpecV0,
208+
spec: &mut InstanceSpec,
212209
id: SpecKey,
213210
component: ComponentV0,
214211
) -> anyhow::Result<()> {
@@ -290,7 +287,7 @@ impl DiskRequest {
290287
}
291288

292289
impl VmConfig {
293-
fn instance_spec(&self) -> anyhow::Result<InstanceSpecV0> {
290+
fn instance_spec(&self) -> anyhow::Result<InstanceSpec> {
294291
// If the configuration specifies an instance spec path, just read the
295292
// spec from that path and return it. Otherwise, construct a spec from
296293
// this configuration's component parts.
@@ -328,7 +325,7 @@ impl VmConfig {
328325
})
329326
.transpose()?;
330327

331-
let mut spec = InstanceSpecV0 {
328+
let mut spec = InstanceSpec {
332329
board: Board {
333330
chipset: Chipset::I440Fx(I440Fx { enable_pcie }),
334331
cpuid: cpuid_profile,
@@ -345,6 +342,7 @@ impl VmConfig {
345342
},
346343
},
347344
components: Default::default(),
345+
smbios: None,
348346
};
349347

350348
if let Some(from_toml) = from_toml {
@@ -519,7 +517,7 @@ async fn new_instance(
519517
client: &Client,
520518
name: String,
521519
id: Uuid,
522-
spec: InstanceSpecV0,
520+
spec: InstanceSpec,
523521
metadata: InstanceMetadata,
524522
) -> anyhow::Result<()> {
525523
let properties = InstanceProperties {

bin/propolis-server/src/lib/initializer.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,21 +1002,26 @@ impl MachineInitializer<'_> {
10021002
..Default::default()
10031003
};
10041004

1005-
let smb_type1 = smbios::table::Type1 {
1006-
manufacturer: "Oxide".try_into().unwrap(),
1007-
product_name: "OxVM".try_into().unwrap(),
1008-
1009-
serial_number: self
1010-
.properties
1011-
.id
1012-
.to_string()
1013-
.try_into()
1014-
.unwrap_or_default(),
1015-
uuid: self.properties.id.to_bytes_le(),
1016-
1017-
wake_up_type: type1::WakeUpType::PowerSwitch,
1018-
..Default::default()
1005+
// If `spec` contains smbios_type1_input then use it. Otherwise use
1006+
// defaults.
1007+
let mut smb_type1 = smbios::table::Type1::default();
1008+
if let Some(smbios) = self.spec.smbios_type1_input.clone() {
1009+
smb_type1.manufacturer =
1010+
smbios.manufacturer.try_into().unwrap_or_default();
1011+
smb_type1.product_name =
1012+
smbios.product_name.try_into().unwrap_or_default();
1013+
smb_type1.serial_number =
1014+
smbios.serial_number.try_into().unwrap_or_default();
1015+
smb_type1.version =
1016+
smbios.version.to_string().try_into().unwrap_or_default();
1017+
} else {
1018+
smb_type1.manufacturer = "Oxide".try_into().unwrap();
1019+
smb_type1.product_name = "OxVM".try_into().unwrap();
1020+
smb_type1.serial_number =
1021+
self.properties.id.to_string().try_into().unwrap_or_default();
10191022
};
1023+
smb_type1.uuid = self.properties.id.to_bytes_le();
1024+
smb_type1.wake_up_type = type1::WakeUpType::PowerSwitch;
10201025

10211026
// The processor vendor, family/model/stepping, and brand string should
10221027
// correspond to the values the guest will see if it queries CPUID.

bin/propolis-server/src/lib/server.rs

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ use internal_dns_types::names::ServiceName;
3737
pub use nexus_client::Client as NexusClient;
3838
use oximeter::types::ProducerRegistry;
3939
use propolis_api_types as api;
40-
use propolis_api_types::instance_spec::SpecKey;
40+
use propolis_api_types::instance_spec::{
41+
v0::InstanceSpecGetResponseV0, SpecKey,
42+
};
43+
use propolis_api_types::v0::InstanceInitializationMethodV0;
4144
use propolis_api_types::InstanceInitializationMethod;
4245
use propolis_server_api::PropolisServerApi;
4346
use rfb::tungstenite::BinaryWs;
@@ -190,7 +193,15 @@ async fn find_local_nexus_client(
190193
}
191194
}
192195

193-
async fn instance_get_common(
196+
// DEPRECATED
197+
async fn v0_instance_get(
198+
rqctx: &RequestContext<Arc<DropshotEndpointContext>>,
199+
) -> Result<InstanceSpecGetResponseV0, HttpError> {
200+
let ctx = rqctx.context();
201+
ctx.vm.v0_get().await.ok_or_else(not_created_error)
202+
}
203+
204+
async fn instance_get(
194205
rqctx: &RequestContext<Arc<DropshotEndpointContext>>,
195206
) -> Result<api::InstanceSpecGetResponse, HttpError> {
196207
let ctx = rqctx.context();
@@ -291,16 +302,111 @@ impl PropolisServerApi for PropolisServerImpl {
291302
})
292303
}
293304

305+
async fn v0_instance_ensure(
306+
rqctx: RequestContext<Self::Context>,
307+
request: TypedBody<api::v0::InstanceEnsureRequestV0>,
308+
) -> Result<HttpResponseCreated<api::InstanceEnsureResponse>, HttpError>
309+
{
310+
let server_context = rqctx.context();
311+
let api::v0::InstanceEnsureRequestV0 { properties, init } =
312+
request.into_inner();
313+
let oximeter_registry = server_context
314+
.static_config
315+
.metrics
316+
.as_ref()
317+
.map(|_| ProducerRegistry::with_id(properties.id));
318+
319+
let nexus_client =
320+
find_local_nexus_client(rqctx.server.local_addr, rqctx.log.clone())
321+
.await;
322+
323+
let ensure_options = crate::vm::EnsureOptions {
324+
bootrom_path: server_context.static_config.bootrom_path.clone(),
325+
bootrom_version: server_context
326+
.static_config
327+
.bootrom_version
328+
.clone(),
329+
use_reservoir: server_context.static_config.use_reservoir,
330+
metrics_config: server_context.static_config.metrics.clone(),
331+
oximeter_registry,
332+
nexus_client,
333+
vnc_server: server_context.vnc_server.clone(),
334+
local_server_addr: rqctx.server.local_addr,
335+
};
336+
337+
let vm_init = match init {
338+
InstanceInitializationMethodV0::Spec { spec } => spec
339+
.try_into()
340+
.map(|s| VmInitializationMethod::Spec(Box::new(s)))
341+
.map_err(|e| {
342+
if let Some(s) = e.source() {
343+
format!("{e}: {s}")
344+
} else {
345+
e.to_string()
346+
}
347+
}),
348+
InstanceInitializationMethodV0::MigrationTarget {
349+
migration_id,
350+
src_addr,
351+
replace_components,
352+
} => Ok(VmInitializationMethod::Migration(MigrationTargetInfo {
353+
migration_id,
354+
src_addr,
355+
replace_components,
356+
})),
357+
}
358+
.map_err(|e| {
359+
HttpError::for_bad_request(
360+
None,
361+
format!("failed to generate internal instance spec: {e}"),
362+
)
363+
})?;
364+
365+
let request = VmEnsureRequest { properties, init: vm_init };
366+
server_context
367+
.vm
368+
.ensure(&server_context.log, request, ensure_options)
369+
.await
370+
.map(HttpResponseCreated)
371+
.map_err(|e| match e {
372+
VmError::ResultChannelClosed => HttpError::for_internal_error(
373+
"state driver unexpectedly dropped result channel"
374+
.to_string(),
375+
),
376+
VmError::WaitingToInitialize
377+
| VmError::AlreadyInitialized
378+
| VmError::RundownInProgress => HttpError::for_client_error(
379+
Some(api::ErrorCode::AlreadyInitialized.to_string()),
380+
ClientErrorStatusCode::CONFLICT,
381+
"instance already initialized".to_string(),
382+
),
383+
VmError::InitializationFailed(e) => {
384+
HttpError::for_internal_error(format!(
385+
"VM initialization failed: {e}"
386+
))
387+
}
388+
_ => HttpError::for_internal_error(format!(
389+
"unexpected error from VM controller: {e}"
390+
)),
391+
})
392+
}
393+
294394
async fn instance_spec_get(
295395
rqctx: RequestContext<Self::Context>,
296396
) -> Result<HttpResponseOk<api::InstanceSpecGetResponse>, HttpError> {
297-
Ok(HttpResponseOk(instance_get_common(&rqctx).await?))
397+
Ok(HttpResponseOk(instance_get(&rqctx).await?))
398+
}
399+
400+
async fn v0_instance_spec_get(
401+
rqctx: RequestContext<Self::Context>,
402+
) -> Result<HttpResponseOk<InstanceSpecGetResponseV0>, HttpError> {
403+
Ok(HttpResponseOk(v0_instance_get(&rqctx).await?))
298404
}
299405

300406
async fn instance_get(
301407
rqctx: RequestContext<Self::Context>,
302408
) -> Result<HttpResponseOk<api::InstanceGetResponse>, HttpError> {
303-
instance_get_common(&rqctx).await.map(|full| {
409+
instance_get(&rqctx).await.map(|full| {
304410
HttpResponseOk(api::InstanceGetResponse {
305411
instance: api::Instance {
306412
properties: full.properties,

bin/propolis-server/src/lib/spec/api_spec_v0.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ impl From<Spec> for InstanceSpecV0 {
6969
migration_failure,
7070
#[cfg(feature = "falcon")]
7171
softnpu,
72+
73+
// Not part of `InstanceSpecV0`. Added in `InstanceSpec` in API
74+
// Version 2.0.0.
75+
smbios_type1_input: _,
7276
} = val;
7377

7478
// Inserts a component entry into the supplied map, asserting first that

bin/propolis-server/src/lib/spec/mod.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
1717
use std::collections::BTreeMap;
1818

19+
use crate::spec::api_spec_v0::ApiSpecError;
1920
use cpuid_utils::CpuidSet;
2021
use propolis_api_types::instance_spec::{
2122
components::{
@@ -29,9 +30,10 @@ use propolis_api_types::instance_spec::{
2930
SerialPortNumber, VirtioDisk, VirtioNic,
3031
},
3132
},
32-
v0::ComponentV0,
33+
v0::{ComponentV0, InstanceSpecV0},
3334
PciPath, SpecKey,
3435
};
36+
use propolis_api_types::{InstanceSpec, SmbiosType1Input};
3537
use thiserror::Error;
3638

3739
#[cfg(feature = "failure-injection")]
@@ -47,6 +49,28 @@ use propolis_api_types::instance_spec::components::{
4749
pub(crate) mod api_spec_v0;
4850
pub(crate) mod builder;
4951

52+
/// The code related to latest types does not go into a versioned module
53+
impl From<Spec> for InstanceSpec {
54+
fn from(val: Spec) -> Self {
55+
let smbios = val.smbios_type1_input.clone();
56+
let InstanceSpecV0 { board, components } = InstanceSpecV0::from(val);
57+
InstanceSpec { board, components, smbios }
58+
}
59+
}
60+
61+
/// The code related to latest types does not go into a versioned module
62+
impl TryFrom<InstanceSpec> for Spec {
63+
type Error = ApiSpecError;
64+
65+
fn try_from(value: InstanceSpec) -> Result<Self, Self::Error> {
66+
let InstanceSpec { board, components, smbios } = value;
67+
let v0 = InstanceSpecV0 { board, components };
68+
let mut spec: Spec = v0.try_into()?;
69+
spec.smbios_type1_input = smbios;
70+
Ok(spec)
71+
}
72+
}
73+
5074
#[derive(Debug, Error)]
5175
#[error("input component type can't convert to output type")]
5276
pub struct ComponentTypeMismatch;
@@ -78,6 +102,18 @@ pub(crate) struct Spec {
78102

79103
#[cfg(feature = "falcon")]
80104
pub softnpu: SoftNpu,
105+
106+
// TODO: This is an option because there is no good way to generate a
107+
// default implementation of `SmbiosType1Input`. The default `serial_number`
108+
// field of `SmbiosType1Input` should be equivalent to the VM UUID for
109+
// backwards compatibility, but that isn't currently possible.
110+
//
111+
// One way to fix this would be to remove the `Builder` and directly
112+
// construct `Spec` from a function that takes an `InstanceSpecV0` and the
113+
// VM UUID. This would replace `impl TryFrom<InstanceSpecV0> for Spec`, and
114+
// would allow removing the `Default` derive on `Spec`, and the `Option`
115+
// from the `smbios_type1_input` field.
116+
pub smbios_type1_input: Option<SmbiosType1Input>,
81117
}
82118

83119
/// The VM's mainboard.

0 commit comments

Comments
 (0)