Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bin/propolis-server/src/lib/initializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,15 @@ impl MachineInitializer<'_> {
smbios.serial_number.try_into().unwrap_or_default();
smb_type1.version =
smbios.version.to_string().try_into().unwrap_or_default();
// Unset SKU number and family leave the table's default (empty)
// strings so the emitted bytes match pre-v7 behavior.
if let Some(sku_number) = smbios.sku_number {
smb_type1.sku_number =
sku_number.try_into().unwrap_or_default();
}
if let Some(smb_family) = smbios.family {
smb_type1.family = smb_family.try_into().unwrap_or_default();
}
} else {
smb_type1.manufacturer = "Oxide".try_into().unwrap();
smb_type1.product_name = "OxVM".try_into().unwrap();
Expand Down
22 changes: 18 additions & 4 deletions bin/propolis-server/src/lib/migrate/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,14 @@
use serde::{Deserialize, Serialize};

use propolis_api_types_versions::v1::instance::ReplacementComponent;
use propolis_api_types_versions::{v1, v2, v3, v6};
use propolis_api_types_versions::{v1, v2, v3, v6, v7};

use std::collections::BTreeMap;

use crate::migrate::MigrateError;
use crate::spec::{api_spec_v1, api_spec_v2, api_spec_v3, api_spec_v6, Spec};
use crate::spec::{
api_spec_v1, api_spec_v2, api_spec_v3, api_spec_v6, api_spec_v7, Spec,
};

/// A wrapper for one of any supported `InstanceSpec` that describe a
/// to-be-migrated VM.
Expand All @@ -113,6 +115,7 @@ pub(crate) enum VersionedInstanceSpec {
V2(v2::instance_spec::InstanceSpec),
V3(v3::instance_spec::InstanceSpec),
V6(v6::instance_spec::InstanceSpec),
V7(v7::instance_spec::InstanceSpec),
}

impl VersionedInstanceSpec {
Expand Down Expand Up @@ -142,9 +145,13 @@ impl VersionedInstanceSpec {
TryInto::<v3::instance_spec::InstanceSpec>::try_into(spec.clone())
{
VersionedInstanceSpec::V3(v3_spec)
} else if let Ok(v6_spec) =
TryInto::<v6::instance_spec::InstanceSpec>::try_into(spec.clone())
{
VersionedInstanceSpec::V6(v6_spec)
} else {
VersionedInstanceSpec::V6(
Into::<v6::instance_spec::InstanceSpec>::into(spec.clone()),
VersionedInstanceSpec::V7(
Into::<v7::instance_spec::InstanceSpec>::into(spec.clone()),
)
};
Ok(versioned)
Expand Down Expand Up @@ -186,6 +193,13 @@ impl VersionedInstanceSpec {
.map_err(|e| MigrateError::PreambleParse(e.to_string()))?
.finish()
}
VersionedInstanceSpec::V7(mut source_spec) => {
api_spec_v7::amend(&mut source_spec, replacements)?;

api_spec_v7::v7_to_spec_builder(source_spec)
.map_err(|e| MigrateError::PreambleParse(e.to_string()))?
.finish()
}
};

Ok(amended_spec)
Expand Down
40 changes: 40 additions & 0 deletions bin/propolis-server/src/lib/spec/api_spec_latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ mod test {
product_name: "913-0000019".to_string(),
serial_number: "2FAKE000".to_string(),
version: 2,
sku_number: Some("913-0000019".to_string()),
family: Some("Gimlet".to_string()),
}),
};

Expand All @@ -265,5 +267,43 @@ mod test {
assert_eq!(smbios.product_name, "913-0000019");
assert_eq!(smbios.serial_number, "2FAKE000");
assert_eq!(smbios.version, 2);
assert_eq!(smbios.sku_number.as_deref(), Some("913-0000019"));
assert_eq!(smbios.family.as_deref(), Some("Gimlet"));
}

// Empty SKU and family strings normalize to unset at ingress, so a
// pre-v7 view of such a spec stays expressible.
#[test]
fn empty_smbios_strings_normalize_to_unset() {
let api_spec = latest::instance_spec::InstanceSpec {
board: Board {
cpus: 4,
memory_mb: 512,
chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }),
guest_hv_interface: GuestHypervisorInterface::Bhyve,
// Explicit values keep the builder from querying bhyve for
// its default guest CPUID set, which needs VMM device access
// the test runner may lack.
cpuid: Some(Cpuid {
entries: vec![],
vendor: CpuidVendor::Amd,
}),
},
components: Default::default(),
smbios: Some(SmbiosType1Input {
manufacturer: "a4x2".to_string(),
product_name: "913-0000019".to_string(),
serial_number: "2FAKE000".to_string(),
version: 2,
sku_number: Some(String::new()),
family: Some(String::new()),
}),
};

let spec = latest_to_spec_builder(api_spec).unwrap().finish();
let smbios =
spec.smbios_type1_input.expect("SMBIOS type 1 input preserved");
assert_eq!(smbios.sku_number, None);
assert_eq!(smbios.family, None);
}
}
6 changes: 4 additions & 2 deletions bin/propolis-server/src/lib/spec/api_spec_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ impl TryFrom<Spec> for v2::instance_spec::InstanceSpec {
// `smbios_type1_input`. Emptying out the SMBIOS Type 1 input means
// this either can be converted to a V1 spec which we can losslessly
// make V2 by adding the SMBIOS table input back in, or we wouldn't be
// able to get to a V2 InstanceSpec either way.
let smbios = val.smbios_type1_input.take();
// able to get to a V2 InstanceSpec either way. The input itself must
// also downgrade: v7 added fields to it that V2 cannot express.
let smbios =
val.smbios_type1_input.take().map(TryInto::try_into).transpose()?;

let v1::instance_spec::InstanceSpec { board, components } =
val.try_into()?;
Expand Down
56 changes: 34 additions & 22 deletions bin/propolis-server/src/lib/spec/api_spec_v6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,28 @@
use std::collections::BTreeMap;

use propolis_api_types::instance_spec::SpecKey;
use propolis_api_types_versions::{v1::instance::ReplacementComponent, v3, v6};

use super::{builder::SpecBuilder, ApiSpecError, Disk, Spec, StorageDevice};
use propolis_api_types_versions::{
v1::instance::ReplacementComponent, v3, v6, v7,
};

use super::{
builder::SpecBuilder, ApiSpecError, Disk, LegacyApiSpecError, Spec,
StorageDevice,
};
use crate::migrate::MigrateError;
use crate::spec::api_spec_latest;

impl From<Spec> for v6::instance_spec::InstanceSpec {
fn from(mut val: Spec) -> Self {
impl TryFrom<Spec> for v6::instance_spec::InstanceSpec {
type Error = LegacyApiSpecError;

fn try_from(mut val: Spec) -> Result<Self, Self::Error> {
// v6 adds a new field on NvmeDisk. Such disks probably can't be
// converted to v3 components and would cause a conversion from
// Spec->v3::instance_spec::InstanceSpec to fail. So, extract those
// disks and convert the rest of the Spec to a
// v3::instance_spec::InstanceSpec. If this fails, we wouldn't have been
// able to get a v6 spec anyway. If it succeeds, we can add the disks
// back in here.
// v3::instance_spec::InstanceSpec. That conversion fails only if the
// Spec uses post-v6 features (a v7 SMBIOS type 1 input), in which case
// no v6 spec exists either. If it succeeds, add the disks back in.
//
// TODO: could be extract_if once we're on a Rust >= 1.91.0.
let mut nvme_disks = Vec::new();
Expand All @@ -35,15 +42,7 @@ impl From<Spec> for v6::instance_spec::InstanceSpec {
}
val.disks.retain(|_, disk| !v6_only_disk(disk));

let v3_spec: v3::instance_spec::InstanceSpec =
val.try_into().unwrap_or_else(|e| {
unreachable!(
"Converting to Spec without v6 bits to v3 failed: {e}. \
This is currently impossible. When Spec to \
v6::instance_spec::InstanceSpec becomes fallible, \
this should `?`."
);
});
let v3_spec: v3::instance_spec::InstanceSpec = val.try_into()?;

let mut spec: v6::instance_spec::InstanceSpec = v3_spec.into();

Expand Down Expand Up @@ -77,7 +76,7 @@ impl From<Spec> for v6::instance_spec::InstanceSpec {
insert_component(&mut spec, backend_id, backend_component);
}

spec
Ok(spec)
}
}

Expand All @@ -87,7 +86,11 @@ impl From<Spec> for v6::instance_spec::InstanceSpec {
pub(crate) fn v6_to_spec_builder(
value: v6::instance_spec::InstanceSpec,
) -> Result<SpecBuilder, ApiSpecError> {
api_spec_latest::latest_to_spec_builder(value)
// Converting v6 to v7 is lossless so just do that and piggyback on the
// latest `InstanceSpec->SpecBuilder`.
let v7_spec: v7::instance_spec::InstanceSpec = value.into();

api_spec_latest::latest_to_spec_builder(v7_spec)
}

fn amend_component(
Expand Down Expand Up @@ -140,12 +143,14 @@ fn amend_component(
Ok(())
}

pub(crate) fn amend(
spec: &mut v6::instance_spec::InstanceSpec,
/// Applies `replacements` to `components`; shared by the v6 and v7 amend
/// paths, which use the same component type.
pub(crate) fn amend_components(
components: &mut BTreeMap<SpecKey, v6::instance_spec::Component>,
replacements: &BTreeMap<SpecKey, ReplacementComponent>,
) -> Result<(), MigrateError> {
for (id, replacement) in replacements {
let Some(to_amend) = spec.components.get_mut(id) else {
let Some(to_amend) = components.get_mut(id) else {
return Err(MigrateError::InstanceSpecsIncompatible(format!(
"replacement component {id} not in source spec",
)));
Expand All @@ -156,3 +161,10 @@ pub(crate) fn amend(

Ok(())
}

pub(crate) fn amend(
spec: &mut v6::instance_spec::InstanceSpec,
replacements: &BTreeMap<SpecKey, ReplacementComponent>,
) -> Result<(), MigrateError> {
amend_components(&mut spec.components, replacements)
}
55 changes: 55 additions & 0 deletions bin/propolis-server/src/lib/spec/api_spec_v7.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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/.

//! Conversions between [`propolis_api_types_versions::v7`] instance specs and
//! the internal [`super::Spec`] representation.

use std::collections::BTreeMap;

use propolis_api_types::instance_spec::SpecKey;
use propolis_api_types_versions::{v1::instance::ReplacementComponent, v6, v7};

use super::{builder::SpecBuilder, ApiSpecError, Spec};
use crate::migrate::MigrateError;
use crate::spec::{api_spec_latest, api_spec_v6};

impl From<Spec> for v7::instance_spec::InstanceSpec {
fn from(mut val: Spec) -> Self {
// v7 only widens the SMBIOS type 1 input, which would cause the
// conversion to a v6 spec to fail if the new fields are set. Set the
// input aside, convert the rest as v6, and put it back afterwards.
let smbios = val.smbios_type1_input.take();

let v6_spec: v6::instance_spec::InstanceSpec =
val.try_into().unwrap_or_else(|e| {
unreachable!(
"Converting a Spec without v7 bits to v6 failed: {e}. \
This is currently impossible. When Spec to \
v7::instance_spec::InstanceSpec becomes fallible, \
this should `?`."
);
});

let mut spec: v7::instance_spec::InstanceSpec = v6_spec.into();
spec.smbios = smbios;
spec
}
}

/// Parses a v7 instance spec into a [`SpecBuilder`], validating component
/// names, PCI paths, and backend references along the way. Callers can add
/// additional (non-v7) components to the builder before calling `finish()`.
pub(crate) fn v7_to_spec_builder(
value: v7::instance_spec::InstanceSpec,
) -> Result<SpecBuilder, ApiSpecError> {
api_spec_latest::latest_to_spec_builder(value)
}

pub(crate) fn amend(
spec: &mut v7::instance_spec::InstanceSpec,
replacements: &BTreeMap<SpecKey, ReplacementComponent>,
) -> Result<(), MigrateError> {
// v7 reuses the v6 component types, so the v6 amendment logic applies.
api_spec_v6::amend_components(&mut spec.components, replacements)
}
Comment thread
sion42x marked this conversation as resolved.
6 changes: 5 additions & 1 deletion bin/propolis-server/src/lib/spec/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,11 @@ impl SpecBuilder {
}

/// Sets the SMBIOS type 1 table contents to expose to the guest.
pub fn set_smbios_type1_input(&mut self, input: SmbiosType1Input) {
pub fn set_smbios_type1_input(&mut self, mut input: SmbiosType1Input) {
// An empty SKU or family renders identically to unset (string index
// 0), so normalize at ingress and keep downgrades expressible.
input.sku_number = input.sku_number.filter(|s| !s.is_empty());
input.family = input.family.filter(|s| !s.is_empty());
self.spec.smbios_type1_input = Some(input);
}

Expand Down
9 changes: 8 additions & 1 deletion bin/propolis-server/src/lib/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub(crate) mod api_spec_v1;
pub(crate) mod api_spec_v2;
pub(crate) mod api_spec_v3;
pub(crate) mod api_spec_v6;
pub(crate) mod api_spec_v7;
pub(crate) mod builder;

/// An error that can arise in converting
Expand Down Expand Up @@ -117,6 +118,12 @@ pub(crate) enum LegacyApiSpecError {

#[error("spec contains v1-incompatible component: {0}")]
IncompatibleComponent(String),

#[error(transparent)]
SmbiosDowngrade(
#[from]
propolis_api_types_versions::v7::instance_spec::SmbiosDowngradeError,
),
}

/// `propolis-server` relies on `TryInto` to convert the API-provided
Expand All @@ -127,7 +134,7 @@ impl TryFrom<InstanceSpec> for Spec {
type Error = ApiSpecError;

fn try_from(value: InstanceSpec) -> Result<Self, Self::Error> {
Ok(api_spec_v6::v6_to_spec_builder(value)?.finish())
Ok(api_spec_v7::v7_to_spec_builder(value)?.finish())
}
}

Expand Down
14 changes: 7 additions & 7 deletions crates/propolis-api-types-versions/src/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ pub mod instance {
pub use crate::v1::instance::InstanceStateRequested;
pub use crate::v1::instance::ReplacementComponent;

pub use crate::v6::api::InstanceEnsureRequest;
pub use crate::v6::api::InstanceInitializationMethod;
pub use crate::v7::api::InstanceEnsureRequest;
pub use crate::v7::api::InstanceInitializationMethod;
}

pub mod instance_spec {
Expand All @@ -83,12 +83,12 @@ pub mod instance_spec {
pub use crate::v1::instance_spec::SpecKey;
pub use crate::v1::instance_spec::VersionedInstanceSpec;

pub use crate::v2::instance_spec::SmbiosType1Input;

pub use crate::v6::instance_spec::Component;
pub use crate::v6::instance_spec::InstanceSpec;
pub use crate::v6::instance_spec::InstanceSpecGetResponse;
pub use crate::v6::instance_spec::InstanceSpecStatus;

pub use crate::v7::instance_spec::InstanceSpec;
pub use crate::v7::instance_spec::InstanceSpecGetResponse;
pub use crate::v7::instance_spec::InstanceSpecStatus;
pub use crate::v7::instance_spec::SmbiosType1Input;
}

pub mod migration {
Expand Down
2 changes: 2 additions & 0 deletions crates/propolis-api-types-versions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ pub mod v3;
pub mod v5;
#[path = "nvme_write_cache/mod.rs"]
pub mod v6;
#[path = "smbios_sku_family/mod.rs"]
pub mod v7;
Loading