Skip to content

Commit 049be79

Browse files
authored
Merge pull request #678 from Dstack-TEE/fix/dstack-mr-ovmf-202505-events
dstack-mr: support edk2-stable202505 OVMF event layout
2 parents 53e217c + 82c9bff commit 049be79

10 files changed

Lines changed: 546 additions & 26 deletions

File tree

dstack-mr/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ fs-err.workspace = true
2525
bon.workspace = true
2626
log.workspace = true
2727
scale.workspace = true
28+
dstack-types.workspace = true
2829

2930
[dev-dependencies]
30-
dstack-types.workspace = true
3131
reqwest = { workspace = true, features = ["blocking"] }
3232
flate2.workspace = true
3333
tar.workspace = true

dstack-mr/cli/src/main.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use anyhow::{Context, Result};
66
use clap::{Parser, Subcommand};
7-
use dstack_mr::Machine;
7+
use dstack_mr::{Machine, OvmfVariant, ovmf_variant_for_version};
88
use dstack_types::ImageInfo;
99
use fs_err as fs;
1010
use size_parser::parse_memory_size;
@@ -78,6 +78,12 @@ struct MachineConfig {
7878
#[arg(long)]
7979
qemu_version: Option<String>,
8080

81+
/// dstack OS version (MAJOR.MINOR.PATCH), used to pick the OVMF measurement layout.
82+
/// 0.5.10 <= ver < 0.6.0 and ver >= 0.6.1 use the edk2-stable202505 layout; everything
83+
/// else uses the legacy layout. If omitted, falls back to `image_info.version`.
84+
#[arg(long)]
85+
dstack_os_version: Option<String>,
86+
8187
/// Output JSON
8288
#[arg(long)]
8389
json: bool,
@@ -99,6 +105,21 @@ fn main() -> Result<()> {
99105
let initrd_path = parent_dir.join(&image_info.initrd).display().to_string();
100106
let cmdline = image_info.cmdline + " initrd=initrd";
101107

108+
// CLI flag wins, then the explicit `ovmf_variant` in metadata.json,
109+
// and finally the OS version field. Older metadata.json files may
110+
// carry neither, in which case fall back to the default.
111+
let ovmf_variant = if let Some(v) = config.dstack_os_version.as_deref() {
112+
ovmf_variant_for_version(v)
113+
.with_context(|| format!("invalid dstack OS version: {v}"))?
114+
} else if let Some(variant) = image_info.ovmf_variant {
115+
variant
116+
} else if !image_info.version.is_empty() {
117+
ovmf_variant_for_version(&image_info.version)
118+
.with_context(|| format!("invalid dstack OS version: {}", image_info.version))?
119+
} else {
120+
OvmfVariant::default()
121+
};
122+
102123
let machine = Machine::builder()
103124
.cpu_count(config.cpu)
104125
.memory_size(config.memory)
@@ -116,6 +137,7 @@ fn main() -> Result<()> {
116137
.hotplug_off(config.hotplug_off)
117138
.root_verity(config.root_verity)
118139
.maybe_qemu_version(config.qemu_version.clone())
140+
.ovmf_variant(ovmf_variant)
119141
.build();
120142

121143
let measurements = machine

dstack-mr/src/lib.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5+
use anyhow::{bail, Context, Result};
56
use serde::{Deserialize, Serialize};
67
use serde_human_bytes as hex_bytes;
78

9+
pub use dstack_types::OvmfVariant;
810
pub use machine::{Machine, TdxMeasurementDetails};
911

1012
use util::{measure_log, measure_sha384, utf16_encode};
@@ -17,8 +19,188 @@ mod kernel;
1719
mod machine;
1820
mod num;
1921
mod tdvf;
22+
mod uefi_var;
2023
mod util;
2124

25+
/// Pick the OVMF variant for a given dstack OS version string ("MAJOR.MINOR.PATCH").
26+
///
27+
/// Treats `0.5.10 <= v < 0.6.0` and `v >= 0.6.1` as `Stable202505`, everything else as
28+
/// `Pre202505`. Used as a fallback when `VmConfig::ovmf_variant` is absent.
29+
pub fn ovmf_variant_for_version(version: &str) -> Result<OvmfVariant> {
30+
let parts: Vec<u32> = version
31+
.split('.')
32+
.map(|p| {
33+
p.parse::<u32>()
34+
.with_context(|| format!("invalid version component: {p}"))
35+
})
36+
.collect::<Result<Vec<_>, _>>()?;
37+
if parts.len() != 3 {
38+
bail!("expected MAJOR.MINOR.PATCH, got {version}");
39+
}
40+
let v = (parts[0], parts[1], parts[2]);
41+
let stable = ((0, 5, 10)..(0, 6, 0)).contains(&v) || v >= (0, 6, 1);
42+
Ok(if stable {
43+
OvmfVariant::Stable202505
44+
} else {
45+
OvmfVariant::Pre202505
46+
})
47+
}
48+
49+
/// Extract the `MAJOR.MINOR.PATCH` version suffix from a dstack image name.
50+
///
51+
/// Recognises any `<prefix>-MAJOR.MINOR.PATCH[.SUFFIX]` shape, e.g.
52+
/// `dstack-0.5.10`, `dstack-dev-0.5.10`, `dstack-nvidia-0.5.10`,
53+
/// `dstack-nvidia-dev-0.5.10`, `dstack-0.5.10.rc1`, `dstack-dev-0.6.1.dev`.
54+
///
55+
/// The optional `.SUFFIX` is permitted to be non-numeric (pre-release tag,
56+
/// build label, etc.) and is dropped from the returned slice — only the
57+
/// numeric `X.Y.Z` is needed to pick the OVMF variant.
58+
///
59+
/// Returns `None` when the segment after the last `-` is not at least a valid
60+
/// `X.Y.Z` triple of non-empty numeric components.
61+
pub fn extract_version_from_image_name(image: &str) -> Option<&str> {
62+
let tail = image.rsplit('-').next()?;
63+
let parts: Vec<&str> = tail.split('.').collect();
64+
if !(3..=4).contains(&parts.len()) {
65+
return None;
66+
}
67+
let core_numeric = parts[..3]
68+
.iter()
69+
.all(|p| !p.is_empty() && p.parse::<u32>().is_ok());
70+
let suffix_ok = parts.len() == 3 || !parts[3].is_empty();
71+
if !(core_numeric && suffix_ok) {
72+
return None;
73+
}
74+
// Slice off the optional `.SUFFIX` so callers get just `X.Y.Z`.
75+
let core_len = parts[0].len() + 1 + parts[1].len() + 1 + parts[2].len();
76+
Some(&tail[..core_len])
77+
}
78+
79+
/// Pick the OVMF variant from an image name like `dstack-0.5.10`.
80+
///
81+
/// Falls back to `OvmfVariant::default()` (= `Pre202505`) when the image name is
82+
/// missing or doesn't carry a parseable version suffix. Use this only as a
83+
/// fallback for images that pre-date `VmConfig::ovmf_variant`.
84+
pub fn ovmf_variant_for_image(image: Option<&str>) -> OvmfVariant {
85+
image
86+
.and_then(extract_version_from_image_name)
87+
.and_then(|v| ovmf_variant_for_version(v).ok())
88+
.unwrap_or_default()
89+
}
90+
91+
#[cfg(test)]
92+
mod ovmf_variant_tests {
93+
use super::*;
94+
95+
#[test]
96+
fn pre_202505_for_old_versions() {
97+
for v in ["0.4.99", "0.5.7", "0.5.8", "0.5.9", "0.6.0"] {
98+
assert_eq!(
99+
ovmf_variant_for_version(v).unwrap(),
100+
OvmfVariant::Pre202505,
101+
"{v}"
102+
);
103+
}
104+
}
105+
106+
#[test]
107+
fn stable_202505_for_new_versions() {
108+
for v in ["0.5.10", "0.5.99", "0.6.1", "0.6.2", "0.7.0", "1.0.0"] {
109+
assert_eq!(
110+
ovmf_variant_for_version(v).unwrap(),
111+
OvmfVariant::Stable202505,
112+
"{v}"
113+
);
114+
}
115+
}
116+
117+
#[test]
118+
fn rejects_malformed_version() {
119+
assert!(ovmf_variant_for_version("0.5").is_err());
120+
assert!(ovmf_variant_for_version("0.5.10-dev").is_err());
121+
assert!(ovmf_variant_for_version("v0.5.10").is_err());
122+
}
123+
124+
#[test]
125+
fn parses_version_from_image_name() {
126+
// Three-segment plain versions across the known prefix shapes.
127+
assert_eq!(
128+
extract_version_from_image_name("dstack-0.5.10"),
129+
Some("0.5.10")
130+
);
131+
assert_eq!(
132+
extract_version_from_image_name("dstack-dev-0.5.10"),
133+
Some("0.5.10")
134+
);
135+
assert_eq!(
136+
extract_version_from_image_name("dstack-nvidia-0.5.10"),
137+
Some("0.5.10")
138+
);
139+
assert_eq!(
140+
extract_version_from_image_name("dstack-nvidia-dev-0.6.1"),
141+
Some("0.6.1")
142+
);
143+
// Optional .SUFFIX is allowed and dropped; suffix may be non-numeric.
144+
assert_eq!(
145+
extract_version_from_image_name("dstack-0.5.10.rc1"),
146+
Some("0.5.10")
147+
);
148+
assert_eq!(
149+
extract_version_from_image_name("dstack-dev-0.6.1.dev"),
150+
Some("0.6.1")
151+
);
152+
assert_eq!(
153+
extract_version_from_image_name("dstack-nvidia-dev-0.5.10.1"),
154+
Some("0.5.10")
155+
);
156+
// Rejections.
157+
assert_eq!(extract_version_from_image_name("dstack"), None);
158+
assert_eq!(extract_version_from_image_name("dstack-rc1"), None);
159+
assert_eq!(extract_version_from_image_name("dstack-0.5"), None);
160+
assert_eq!(extract_version_from_image_name("dstack-0.5.10."), None);
161+
assert_eq!(extract_version_from_image_name("dstack-0.5.10.1.2"), None);
162+
assert_eq!(extract_version_from_image_name("dstack-0..10"), None);
163+
assert_eq!(extract_version_from_image_name("dstack-a.b.c"), None);
164+
}
165+
166+
#[test]
167+
fn ovmf_variant_for_image_handles_missing_and_unknown() {
168+
assert_eq!(ovmf_variant_for_image(None), OvmfVariant::Pre202505);
169+
assert_eq!(
170+
ovmf_variant_for_image(Some("dstack")),
171+
OvmfVariant::Pre202505
172+
);
173+
assert_eq!(
174+
ovmf_variant_for_image(Some("dstack-0.5.9")),
175+
OvmfVariant::Pre202505
176+
);
177+
assert_eq!(
178+
ovmf_variant_for_image(Some("dstack-0.5.10")),
179+
OvmfVariant::Stable202505
180+
);
181+
assert_eq!(
182+
ovmf_variant_for_image(Some("dstack-nvidia-dev-0.6.1")),
183+
OvmfVariant::Stable202505
184+
);
185+
}
186+
187+
#[test]
188+
fn serializes_with_snake_case() {
189+
assert_eq!(
190+
serde_json::to_string(&OvmfVariant::Pre202505).unwrap(),
191+
"\"pre202505\""
192+
);
193+
assert_eq!(
194+
serde_json::to_string(&OvmfVariant::Stable202505).unwrap(),
195+
"\"stable202505\""
196+
);
197+
assert_eq!(
198+
serde_json::from_str::<OvmfVariant>("\"stable202505\"").unwrap(),
199+
OvmfVariant::Stable202505
200+
);
201+
}
202+
}
203+
22204
/// Contains all the measurement values for TDX.
23205
#[derive(Debug, Clone, Serialize, Deserialize)]
24206
pub struct TdxMeasurements {

dstack-mr/src/machine.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use crate::acpi::Tables;
66
use crate::tdvf::Tdvf;
77
use crate::util::debug_print_log;
8-
use crate::{kernel, RtmrLogs, TdxMeasurements};
8+
use crate::{kernel, OvmfVariant, RtmrLogs, TdxMeasurements};
99
use crate::{measure_log, measure_sha384};
1010
use anyhow::{bail, Context, Result};
1111
use fs_err as fs;
@@ -32,6 +32,10 @@ pub struct Machine<'a> {
3232
pub root_verity: bool,
3333
#[builder(default)]
3434
pub host_share_mode: String,
35+
/// Selects which OVMF measurement event layout to expect.
36+
/// Defaults to the pre-edk2-stable202505 layout for backwards compatibility.
37+
#[builder(default)]
38+
pub ovmf_variant: OvmfVariant,
3539
}
3640

3741
fn parse_version_tuple(v: &str) -> Result<(u32, u32, u32)> {

0 commit comments

Comments
 (0)