1use super::PetriVmConfigOpenVmm;
7use super::PetriVmOpenVmm;
8use super::PetriVmResourcesOpenVmm;
9use crate::OpenvmmLogConfig;
10use crate::PetriLogFile;
11use crate::PetriVmRuntimeConfig;
12use crate::worker::Worker;
13use anyhow::Context;
14use mesh::payload::message::ProtobufMessage;
15use mesh_process::Mesh;
16use mesh_process::ProcessConfig;
17use mesh_worker::WorkerHost;
18use openvmm_defs::config::DeviceVtl;
19use pal_async::pipe::PolledPipe;
20use pal_async::task::Spawn;
21use petri_artifacts_common::tags::MachineArch;
22use petri_artifacts_common::tags::OsFlavor;
23use std::collections::BTreeMap;
24use std::ffi::OsString;
25use std::io::Write;
26use std::sync::Arc;
27use vm_resource::IntoResource;
28
29fn pcie_device_supports_save_restore(id: &str) -> bool {
30 !matches!(id, "nvme" | "gdma" | "usb_xhci" | "usb_xhci_config")
31}
32
33impl PetriVmConfigOpenVmm {
34 pub(super) async fn run_core(self) -> anyhow::Result<(PetriVmOpenVmm, PetriVmRuntimeConfig)> {
35 self.run_core_inner(None).await
36 }
37
38 pub(crate) async fn run_restore(
42 self,
43 saved_state: &[u8],
44 ) -> anyhow::Result<(PetriVmOpenVmm, PetriVmRuntimeConfig)> {
45 let saved_state: ProtobufMessage = mesh::payload::decode(saved_state)
46 .context("failed to decode serialized saved state")?;
47 self.run_core_inner(Some(saved_state)).await
48 }
49
50 async fn run_core_inner(
51 self,
52 saved_state: Option<ProtobufMessage>,
53 ) -> anyhow::Result<(PetriVmOpenVmm, PetriVmRuntimeConfig)> {
54 let has_guest_shares = !self.guest_shares.is_empty();
55 let Self {
56 guest_shares: _,
57 private_virtio_devices: _,
58 runtime_config,
59 arch,
60 host_log_levels,
61 mut config,
62 skip_save_restore,
63
64 mesh,
65
66 mut resources,
67
68 openvmm_log_file,
69
70 memory_backing_file,
71 requested_private_memory: _,
72
73 ged,
74 framebuffer_view,
75
76 pending_iommu,
77 } = self.with_private_virtio_devices();
78
79 for (name, iommu_config) in &pending_iommu {
81 let rc = config
82 .pcie_root_complexes
83 .iter_mut()
84 .find(|rc| rc.name == *name)
85 .with_context(|| format!("IOMMU configured for unknown root complex '{name}'"))?;
86 rc.iommu = Some(iommu_config.clone());
87 }
88
89 let enable_legacy_pci_config_io =
90 arch == MachineArch::X86_64 && !config.chipset.with_piix4_pci_bus;
91 for root_complex in &mut config.pcie_root_complexes {
92 root_complex.legacy_pci_config_io = enable_legacy_pci_config_io
93 && root_complex.segment == 0
94 && root_complex.start_bus == 0
95 && root_complex.end_bus == u8::MAX;
96 }
97 if config
98 .pcie_root_complexes
99 .iter()
100 .any(|root_complex| root_complex.legacy_pci_config_io)
101 {
102 config
103 .chipset_devices
104 .retain(|device| device.name != "missing-pci");
105 }
106
107 let has_unsupported_pcie_save_restore_device = config
115 .pcie_devices
116 .iter()
117 .any(|device| !pcie_device_supports_save_restore(device.resource.id()));
118 let supports_save_restore = !skip_save_restore
119 && !resources.properties.is_openhcl
120 && !resources.properties.is_isolated
121 && !resources.properties.is_pcat
122 && !matches!(arch, MachineArch::Aarch64)
123 && !resources.properties.using_vpci
124 && !has_unsupported_pcie_save_restore_device
125 && !resources.properties.use_virtio_vsock
126 && !resources.properties.use_virtio_console
127 && !has_guest_shares
128 && !config.hypervisor.nested_virt;
129
130 if let Some(mut ged) = ged {
132 ged.vtl2_settings = Some(prost::Message::encode_to_vec(
133 runtime_config.vtl2_settings.as_ref().unwrap(),
134 ));
135 config
136 .vmbus_devices
137 .push((DeviceVtl::Vtl2, ged.into_resource()));
138 }
139
140 tracing::debug!(?config, "OpenVMM config");
141
142 let log_env = match host_log_levels {
143 None | Some(OpenvmmLogConfig::TestDefault) => BTreeMap::<OsString, OsString>::from([
144 ("OPENVMM_LOG".into(), "debug,hyper_util=info".into()),
147 ("OPENVMM_SHOW_SPANS".into(), "false".into()),
148 ]),
149 Some(OpenvmmLogConfig::BuiltInDefault) => BTreeMap::new(),
150 Some(OpenvmmLogConfig::Custom(levels)) => levels
151 .iter()
152 .map(|(k, v)| (OsString::from(k), OsString::from(v)))
153 .collect::<BTreeMap<OsString, OsString>>(),
154 };
155
156 let (host, pid) = Self::openvmm_host(&mut resources, &mesh, openvmm_log_file, log_env)
157 .await
158 .context("failed to create host process")?;
159 let shared_memory = memory_backing_file
162 .as_ref()
163 .map(|mem_path| {
164 let total_mem_size: u64 = config
165 .numa
166 .nodes
167 .iter()
168 .filter_map(|n| n.mem.as_ref())
169 .map(|m| m.mem_size)
170 .sum();
171 openvmm_helpers::shared_memory::open_memory_backing_file(mem_path, total_mem_size)
172 })
173 .transpose()?;
174
175 for (node, mem) in config
180 .numa
181 .nodes
182 .iter()
183 .enumerate()
184 .filter_map(|(i, n)| n.mem.as_ref().map(|m| (i, m)))
185 {
186 tracing::info!(
187 node,
188 mem_size = mem.mem_size,
189 backing_mode = if mem.private_memory {
190 "private"
191 } else {
192 "shared"
193 },
194 transparent_hugepages = mem.transparent_hugepages,
195 hugepages = mem.hugepages,
196 "guest RAM backing"
197 );
198 }
199
200 let (worker, halt_notif) = Worker::launch(&host, config, shared_memory, saved_state)
201 .await
202 .context("failed to launch vm worker")?;
203
204 let worker = Arc::new(worker);
205
206 let is_minimal = resources.properties.minimal_mode;
207
208 let tcp_pipette_port = match resources.tcp_pipette_port.take() {
213 Some(recv) => Some(
214 recv.await
215 .context("failed to receive TCP pipette port from consomme")?,
216 ),
217 None => None,
218 };
219
220 let mut vm = PetriVmOpenVmm::new(
221 super::runtime::PetriVmInner {
222 resources,
223 mesh,
224 worker,
225 framebuffer_view,
226 cidata_mounted: false,
227 tcp_pipette_port,
228 pid,
229 },
230 halt_notif,
231 );
232
233 tracing::info!("Resuming VM");
234 vm.resume().await?;
235
236 if supports_save_restore && !is_minimal {
239 tracing::info!("Testing save/restore");
240 vm.verify_save_restore().await?;
241 }
242
243 tracing::info!("VM ready");
244 Ok((vm, runtime_config))
245 }
246
247 pub async fn run(mut self) -> anyhow::Result<(PetriVmOpenVmm, PetriVmRuntimeConfig)> {
250 if self.resources.properties.using_vtl0_pipette
254 && !self.resources.properties.external_pipette
255 && matches!(self.resources.properties.os_flavor, OsFlavor::Windows)
256 && !self.resources.properties.is_isolated
257 && !self.resources.properties.no_vmbus
258 {
259 let mut imc_hive_file = tempfile::tempfile().context("failed to create temp file")?;
260 imc_hive_file
261 .write_all(include_bytes!("../../../guest-bootstrap/imc.hiv"))
262 .context("failed to write imc hive")?;
263
264 self.config.vmbus_devices.push((
265 DeviceVtl::Vtl0,
266 vmbfs_resources::VmbfsImcDeviceHandle {
267 file: imc_hive_file,
268 }
269 .into_resource(),
270 ));
271 }
272
273 let launch_via_serial = self.resources.linux_direct_serial_agent.is_some()
277 && self.resources.properties.using_vtl0_pipette;
278
279 let (mut vm, config) = self.run_core().await?;
281
282 if launch_via_serial {
283 vm.launch_linux_direct_pipette().await?;
284 }
285
286 Ok((vm, config))
287 }
288
289 async fn openvmm_host(
290 resources: &mut PetriVmResourcesOpenVmm,
291 mesh: &Mesh,
292 log_file: PetriLogFile,
293 vmm_env: BTreeMap<OsString, OsString>,
294 ) -> anyhow::Result<(WorkerHost, i32)> {
295 let (stderr_read, stderr_write) = pal::pipe_pair()?;
298 let task = resources.driver.spawn(
299 "serial log",
300 crate::log_task(
301 log_file,
302 PolledPipe::new(&resources.driver, stderr_read)
303 .context("failed to create polled pipe")?,
304 "openvmm stderr",
305 ),
306 );
307 resources.log_stream_tasks.push(task);
308
309 let (host, runner) = mesh_worker::worker_host();
310 let pid = mesh
311 .launch_host(
312 ProcessConfig::new("vmm")
313 .process_name(&resources.openvmm_path)
314 .stderr(Some(stderr_write))
315 .env(vmm_env),
316 openvmm_defs::entrypoint::MeshHostParams { runner },
317 )
318 .await?;
319 Ok((host, pid))
320 }
321}