Skip to main content

petri/vm/openvmm/
start.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Methods to start a [`PetriVmConfigOpenVmm`] and produce a running [`PetriVmOpenVmm`].
5
6use 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    /// Launch a fresh worker that restores from `saved_state`, mirroring
39    /// production `--restore-snapshot`. Exercises the path the in-process
40    /// pulse (`verify_save_restore`) cannot reach.
41    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        // Resolve deferred IOMMU assignments.
80        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        // TODO: OpenHCL needs virt_whp support
108        // TODO: PCAT needs vga device support
109        // TODO: arm64 is broken?
110        // TODO: VPCI and some PCIe endpoints (NVMe/GDMA/USB) don't support
111        // TODO: virtio vsock and console don't support save/restore yet
112        // TODO: 9p shares don't support save/restore yet
113        // TODO: nested virt doesn't support save/restore yet
114        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        // Add the GED and VTL 2 settings.
131        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                // Quiet down `hyper_util`'s connection-pool debug spam that
145                // `disk_blob` triggers on every HTTP range request.
146                ("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        // If a memory backing file was requested, open/create it and size
160        // it to match the configured guest RAM.
161        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        // Log the resolved guest RAM backing mode for diagnostics. Read from
176        // the final config so it reflects any backend overrides (e.g.
177        // `with_hugepages` / `with_memory_backing_file` forcing shared). Log
178        // per node, since NUMA nodes can have heterogeneous backings.
179        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        // Resolve the TCP pipette port now, while the VM is starting.
209        // Consomme binds the port during launch, so the oneshot should
210        // be ready.  Caching the resolved port here lets wait_for_agent
211        // reconnect after a reset without needing the oneshot again.
212        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        // Run the basic save/restore pulse if supported. This also verifies
237        // that a restored VM can be saved and restored again.
238        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    /// Run the VM, configuring pipette to automatically start if it is
248    /// included in the config
249    pub async fn run(mut self) -> anyhow::Result<(PetriVmOpenVmm, PetriVmRuntimeConfig)> {
250        // Set up the IMC hive for Windows guests that use pipette in VTL0.
251        // Skip when VMBus is disabled — the no-vmbus prepped image has
252        // pipette pre-configured via offline registry injection.
253        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        // On non-pipette-as-init Linux direct, launch pipette via the serial
274        // agent. (When pipette is PID 1, it auto-starts on boot and the
275        // serial agent is not present.)
276        let launch_via_serial = self.resources.linux_direct_serial_agent.is_some()
277            && self.resources.properties.using_vtl0_pipette;
278
279        // Start the VM.
280        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        // Copy the child's stderr to this process's, since internally this is
296        // wrapped by the test harness.
297        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}