1use petri_artifacts_common::capabilities;
7use std::collections::BTreeSet;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ExecutionEnvironment {
12 Baremetal,
14 Nested,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Vendor {
21 Amd,
23 Intel,
25 Arm,
27}
28
29impl Vendor {
30 pub fn host() -> Self {
32 #[cfg(target_arch = "x86_64")]
34 {
35 let result =
36 safe_intrinsics::cpuid(x86defs::cpuid::CpuidFunction::VendorAndMaxFunction.0, 0);
37 let vendor =
38 x86defs::cpuid::Vendor::from_ebx_ecx_edx(result.ebx, result.ecx, result.edx);
39 if vendor.is_amd_compatible() {
40 Vendor::Amd
41 } else {
42 assert!(vendor.is_intel_compatible());
43 Vendor::Intel
44 }
45 }
46 #[cfg(not(target_arch = "x86_64"))]
48 {
49 Vendor::Arm
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq)]
56pub enum IsolationType {
57 Vbs,
59 Snp,
61 Tdx,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum VmmType {
68 OpenVmm,
70 HyperV,
72 Qemu,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum OpenVmmHypervisor {
79 Mshv,
81 Kvm,
83 Whp,
85 Hvf,
87}
88
89#[derive(Debug, Clone)]
92pub struct VmHostInfo {
93 pub vbs_supported: bool,
95 pub snp_status: bool,
97 pub tdx_status: bool,
99}
100
101#[derive(Debug, Clone)]
103pub struct HostContext {
104 pub vm_host_info: Option<VmHostInfo>,
106 pub vendor: Vendor,
108 pub execution_environment: ExecutionEnvironment,
110 pub vpci_supported: bool,
112 pub openvmm_hypervisor: Option<OpenVmmHypervisor>,
114}
115
116impl HostContext {
117 pub async fn new() -> Self {
119 let is_nested = {
120 #[cfg(target_arch = "x86_64")]
122 {
123 let result = safe_intrinsics::cpuid(
124 hvdef::HV_CPUID_FUNCTION_MS_HV_ENLIGHTENMENT_INFORMATION,
125 0,
126 );
127 hvdef::HvEnlightenmentInformation::from(
128 result.eax as u128
129 | (result.ebx as u128) << 32
130 | (result.ecx as u128) << 64
131 | (result.edx as u128) << 96,
132 )
133 .nested()
134 }
135 #[cfg(not(target_arch = "x86_64"))]
137 {
138 false
139 }
140 };
141
142 let vendor = Vendor::host();
143
144 let vpci_supported = cfg!(windows);
146
147 let openvmm_hypervisor = if cfg!(target_os = "linux") {
148 if fs_err::File::open("/dev/mshv").is_ok() {
149 Some(OpenVmmHypervisor::Mshv)
150 } else if fs_err::File::options()
151 .read(true)
152 .write(true)
153 .open("/dev/kvm")
154 .is_ok()
155 {
156 Some(OpenVmmHypervisor::Kvm)
157 } else {
158 None
159 }
160 } else if cfg!(windows) {
161 Some(OpenVmmHypervisor::Whp)
162 } else if cfg!(target_os = "macos") {
163 Some(OpenVmmHypervisor::Hvf)
164 } else {
165 None
166 };
167
168 let vm_host_info = {
169 #[cfg(windows)]
170 {
171 crate::vm::hyperv::powershell::run_get_vm_host()
172 .await
173 .ok()
174 .map(|info| VmHostInfo {
175 vbs_supported: info.guest_isolation_types.contains(
176 &crate::vm::hyperv::powershell::HyperVGuestStateIsolationType::Vbs,
177 ),
178 snp_status: info.snp_status,
179 tdx_status: info.tdx_status,
180 })
181 }
182 #[cfg(not(windows))]
183 {
184 (vendor == Vendor::Amd && openvmm_hypervisor == Some(OpenVmmHypervisor::Mshv)).then(
187 || VmHostInfo {
188 vbs_supported: false,
189 snp_status: openvmm_hypervisors::mshv_snp_available(),
190 tdx_status: false,
191 },
192 )
193 }
194 };
195
196 Self {
197 vm_host_info,
198 vendor,
199 execution_environment: if is_nested {
200 ExecutionEnvironment::Nested
201 } else {
202 ExecutionEnvironment::Baremetal
203 },
204 vpci_supported,
205 openvmm_hypervisor,
206 }
207 }
208}
209
210pub enum TestRequirement {
212 ExecutionEnvironment(ExecutionEnvironment),
214 Vendor(Vendor),
216 OpenVmmHypervisor(OpenVmmHypervisor),
218 Isolation(IsolationType),
220 RequiresCapability {
231 name: &'static str,
233 vmm: VmmType,
235 },
236 And(Box<TestRequirement>, Box<TestRequirement>),
238 Or(Box<TestRequirement>, Box<TestRequirement>),
240 Not(Box<TestRequirement>),
242 Any,
244}
245
246impl TestRequirement {
247 pub fn and(self, other: TestRequirement) -> TestRequirement {
249 TestRequirement::And(Box::new(self), Box::new(other))
250 }
251
252 pub fn or(self, other: TestRequirement) -> TestRequirement {
254 TestRequirement::Or(Box::new(self), Box::new(other))
255 }
256
257 #[expect(clippy::should_implement_trait)]
259 pub fn not(self) -> TestRequirement {
260 TestRequirement::Not(Box::new(self))
261 }
262
263 pub fn is_satisfied(&self, context: &HostContext) -> bool {
265 match self {
266 TestRequirement::ExecutionEnvironment(env) => context.execution_environment == *env,
267 TestRequirement::Vendor(vendor) => context.vendor == *vendor,
268 TestRequirement::OpenVmmHypervisor(hypervisor) => {
269 context.openvmm_hypervisor == Some(*hypervisor)
270 }
271 TestRequirement::Isolation(isolation_type) => {
272 if let Some(vm_host_info) = &context.vm_host_info {
273 match isolation_type {
274 IsolationType::Vbs => vm_host_info.vbs_supported,
275 IsolationType::Snp => vm_host_info.snp_status,
276 IsolationType::Tdx => vm_host_info.tdx_status,
277 }
278 } else {
279 false
280 }
281 }
282 TestRequirement::RequiresCapability { name, vmm } => {
283 available_capabilities(context, *vmm).contains(name)
284 }
285 TestRequirement::And(req1, req2) => {
286 req1.is_satisfied(context) && req2.is_satisfied(context)
287 }
288 TestRequirement::Or(req1, req2) => {
289 req1.is_satisfied(context) || req2.is_satisfied(context)
290 }
291 TestRequirement::Not(req) => !req.is_satisfied(context),
292 TestRequirement::Any => true,
293 }
294 }
295}
296
297pub fn known_capability(name: &str) -> Option<&'static str> {
299 capabilities::known(name)
300}
301
302pub fn is_known_capability(name: &str) -> bool {
304 known_capability(name).is_some()
305}
306
307fn available_capabilities(context: &HostContext, vmm: VmmType) -> BTreeSet<&'static str> {
308 let mut capabilities = BTreeSet::new();
309
310 if context.vpci_supported {
311 capabilities.insert(capabilities::VPCI);
312 }
313
314 if !matches!(
320 (vmm, context.openvmm_hypervisor),
321 (VmmType::OpenVmm, Some(OpenVmmHypervisor::Mshv))
322 ) {
323 capabilities.insert(capabilities::WINDOWS_PARTITION_RESET);
324 }
325
326 if matches!(
328 (vmm, context.openvmm_hypervisor),
329 (
330 VmmType::OpenVmm,
331 Some(OpenVmmHypervisor::Kvm | OpenVmmHypervisor::Whp)
332 ) | (VmmType::Qemu, _)
333 ) {
334 capabilities.insert(capabilities::NESTED_VIRT);
335 }
336
337 match std::env::var("PETRI_CAPABILITIES") {
338 Ok(env_capabilities) => {
339 for capability in env_capabilities.split(',').map(str::trim) {
340 if capability.is_empty() {
341 continue;
342 }
343 let capability = known_capability(capability)
344 .unwrap_or_else(|| panic!("unknown PETRI_CAPABILITIES entry: {capability}"));
345 capabilities.insert(capability);
346 }
347 }
348 Err(std::env::VarError::NotPresent) => {}
349 Err(std::env::VarError::NotUnicode(_)) => {
350 panic!("PETRI_CAPABILITIES is not valid UTF-8")
351 }
352 }
353
354 capabilities
355}
356
357pub struct TestCaseRequirements {
359 requirements: TestRequirement,
360}
361
362impl TestCaseRequirements {
363 pub fn new(requirements: TestRequirement) -> Self {
365 Self { requirements }
366 }
367}
368
369pub fn can_run_test_with_context(
371 config: Option<&TestCaseRequirements>,
372 context: &HostContext,
373) -> bool {
374 if let Some(config) = config {
375 config.requirements.is_satisfied(context)
376 } else {
377 true
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 fn host_context(openvmm_hypervisor: OpenVmmHypervisor) -> HostContext {
386 HostContext {
387 vm_host_info: None,
388 vendor: Vendor::Intel,
389 execution_environment: ExecutionEnvironment::Baremetal,
390 vpci_supported: false,
391 openvmm_hypervisor: Some(openvmm_hypervisor),
392 }
393 }
394
395 #[test]
396 fn capabilities_are_evaluated_for_the_selected_vmm() {
397 let requirement = |vmm| TestRequirement::RequiresCapability {
398 name: capabilities::WINDOWS_PARTITION_RESET,
399 vmm,
400 };
401 let mshv = host_context(OpenVmmHypervisor::Mshv);
402
403 assert!(!requirement(VmmType::OpenVmm).is_satisfied(&mshv));
404 assert!(requirement(VmmType::HyperV).is_satisfied(&mshv));
405 assert!(requirement(VmmType::OpenVmm).is_satisfied(&host_context(OpenVmmHypervisor::Kvm)));
406 }
407
408 #[test]
409 fn openvmm_hypervisor_requirement_matches_the_selected_backend() {
410 let requirement = TestRequirement::OpenVmmHypervisor(OpenVmmHypervisor::Mshv);
411 assert!(requirement.is_satisfied(&host_context(OpenVmmHypervisor::Mshv)));
412 assert!(!requirement.is_satisfied(&host_context(OpenVmmHypervisor::Kvm)));
413 let mut without_hypervisor = host_context(OpenVmmHypervisor::Mshv);
414 without_hypervisor.openvmm_hypervisor = None;
415 assert!(!requirement.is_satisfied(&without_hypervisor));
416 }
417}