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::BootDeviceType;
10use crate::Firmware;
11use crate::OpenvmmLogConfig;
12use crate::PetriLogFile;
13use crate::PetriVmRuntimeConfig;
14use crate::worker::Worker;
15use anyhow::Context;
16use disk_backend_resources::FileDiskHandle;
17use guid::Guid;
18use mesh_process::Mesh;
19use mesh_process::ProcessConfig;
20use mesh_worker::WorkerHost;
21use openvmm_defs::config::DeviceVtl;
22use pal_async::pipe::PolledPipe;
23use pal_async::task::Spawn;
24use petri_artifacts_common::tags::MachineArch;
25use petri_artifacts_common::tags::OsFlavor;
26use scsidisk_resources::SimpleScsiDiskHandle;
27use std::collections::BTreeMap;
28use std::ffi::OsString;
29use std::io::Write;
30use std::sync::Arc;
31use storvsp_resources::ScsiControllerHandle;
32use storvsp_resources::ScsiDeviceAndPath;
33use storvsp_resources::ScsiPath;
34use vm_resource::IntoResource;
35
36impl PetriVmConfigOpenVmm {
37    async fn run_core(self) -> anyhow::Result<(PetriVmOpenVmm, PetriVmRuntimeConfig)> {
38        let Self {
39            firmware,
40            arch,
41            host_log_levels,
42            mut config,
43            boot_device_type,
44
45            mesh,
46
47            mut resources,
48
49            openvmm_log_file,
50
51            petri_vtl0_scsi,
52            ged,
53            framebuffer_view,
54
55            mut vtl2_settings,
56        } = self;
57
58        tracing::debug!(?firmware, ?arch, "Petri VM firmware configuration");
59
60        let has_pcie = !config.pcie_root_complexes.is_empty();
61
62        // TODO: OpenHCL needs virt_whp support
63        // TODO: PCAT needs vga device support
64        // TODO: arm64 is broken?
65        // TODO: VPCI and NVMe don't support save/restore
66        // TODO: PCIe emulators don't support save/restore yet
67        let supports_save_restore = !firmware.is_openhcl()
68            && !matches!(firmware, Firmware::Pcat { .. })
69            && !matches!(arch, MachineArch::Aarch64)
70            && !matches!(boot_device_type, BootDeviceType::Nvme)
71            && !has_pcie;
72
73        if firmware.is_openhcl() {
74            // Add a pipette disk for VTL 2
75            const UH_CIDATA_SCSI_INSTANCE: Guid =
76                guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
77
78            if let Some(openhcl_agent_disk) = resources
79                .openhcl_agent_image
80                .as_ref()
81                .unwrap()
82                .build()
83                .context("failed to build agent image")?
84            {
85                config.vmbus_devices.push((
86                    DeviceVtl::Vtl2,
87                    ScsiControllerHandle {
88                        instance_id: UH_CIDATA_SCSI_INSTANCE,
89                        max_sub_channel_count: 1,
90                        io_queue_depth: None,
91                        devices: vec![ScsiDeviceAndPath {
92                            path: ScsiPath {
93                                path: 0,
94                                target: 0,
95                                lun: crate::vm::PETRI_VTL0_SCSI_BOOT_LUN,
96                            },
97                            device: SimpleScsiDiskHandle {
98                                read_only: true,
99                                parameters: Default::default(),
100                                disk: FileDiskHandle(openhcl_agent_disk.into_file())
101                                    .into_resource(),
102                            }
103                            .into_resource(),
104                        }],
105                        requests: None,
106                        poll_mode_queue_depth: None,
107                    }
108                    .into_resource(),
109                ));
110            }
111        }
112
113        // Add the Petri SCSI controller to VTL0 now that all the disks are on it.
114        if !petri_vtl0_scsi.devices.is_empty() {
115            config
116                .vmbus_devices
117                .push((DeviceVtl::Vtl0, petri_vtl0_scsi.into_resource()));
118        }
119
120        // Apply custom VTL2 settings
121        if let Some(f) = firmware
122            .into_openhcl_config()
123            .and_then(|c| c.modify_vtl2_settings)
124        {
125            f.0(vtl2_settings.as_mut().unwrap())
126        };
127
128        // Add the GED and VTL 2 settings.
129        if let Some(mut ged) = ged {
130            ged.vtl2_settings = Some(prost::Message::encode_to_vec(
131                vtl2_settings.as_ref().unwrap(),
132            ));
133            config
134                .vmbus_devices
135                .push((DeviceVtl::Vtl2, ged.into_resource()));
136        }
137
138        tracing::debug!(?config, "OpenVMM config");
139
140        let log_env = match host_log_levels {
141            None | Some(OpenvmmLogConfig::TestDefault) => BTreeMap::<OsString, OsString>::from([
142                ("OPENVMM_LOG".into(), "debug".into()),
143                ("OPENVMM_SHOW_SPANS".into(), "true".into()),
144            ]),
145            Some(OpenvmmLogConfig::BuiltInDefault) => BTreeMap::new(),
146            Some(OpenvmmLogConfig::Custom(levels)) => levels
147                .iter()
148                .map(|(k, v)| (OsString::from(k), OsString::from(v)))
149                .collect::<BTreeMap<OsString, OsString>>(),
150        };
151
152        let host = Self::openvmm_host(&mut resources, &mesh, openvmm_log_file, log_env)
153            .await
154            .context("failed to create host process")?;
155        let (worker, halt_notif) = Worker::launch(&host, config)
156            .await
157            .context("failed to launch vm worker")?;
158
159        let worker = Arc::new(worker);
160
161        let mut vm = PetriVmOpenVmm::new(
162            super::runtime::PetriVmInner {
163                resources,
164                mesh,
165                worker,
166                framebuffer_view,
167            },
168            halt_notif,
169        );
170
171        tracing::info!("Resuming VM");
172        vm.resume().await?;
173
174        // Run basic save/restore test if it is supported
175        if supports_save_restore {
176            tracing::info!("Testing save/restore");
177            vm.verify_save_restore().await?;
178        }
179
180        tracing::info!("VM ready");
181        Ok((vm, PetriVmRuntimeConfig { vtl2_settings }))
182    }
183
184    /// Run the VM, configuring pipette to automatically start if it is
185    /// included in the config
186    pub async fn run(mut self) -> anyhow::Result<(PetriVmOpenVmm, PetriVmRuntimeConfig)> {
187        let launch_linux_direct_pipette = if let Some(agent_image) = &self.resources.agent_image {
188            // Construct the agent disk.
189            if let Some(agent_disk) = agent_image.build().context("failed to build agent image")? {
190                self.petri_vtl0_scsi.devices.push(ScsiDeviceAndPath {
191                    path: ScsiPath {
192                        path: 0,
193                        target: 0,
194                        lun: crate::vm::PETRI_VTL0_SCSI_PIPETTE_LUN,
195                    },
196                    device: SimpleScsiDiskHandle {
197                        read_only: true,
198                        parameters: Default::default(),
199                        disk: FileDiskHandle(agent_disk.into_file()).into_resource(),
200                    }
201                    .into_resource(),
202                });
203            }
204
205            if matches!(self.firmware.os_flavor(), OsFlavor::Windows)
206                && self.firmware.isolation().is_none()
207            {
208                // Make a file for the IMC hive. It's not guaranteed to be at a fixed
209                // location at runtime.
210                let mut imc_hive_file =
211                    tempfile::tempfile().context("failed to create temp file")?;
212                imc_hive_file
213                    .write_all(include_bytes!("../../../guest-bootstrap/imc.hiv"))
214                    .context("failed to write imc hive")?;
215
216                // Add the IMC device.
217                self.config.vmbus_devices.push((
218                    DeviceVtl::Vtl0,
219                    vmbfs_resources::VmbfsImcDeviceHandle {
220                        file: imc_hive_file,
221                    }
222                    .into_resource(),
223                ));
224            }
225
226            self.firmware.is_linux_direct() && agent_image.contains_pipette()
227        } else {
228            false
229        };
230
231        // Start the VM.
232        let (mut vm, config) = self.run_core().await?;
233
234        if launch_linux_direct_pipette {
235            vm.launch_linux_direct_pipette().await?;
236        }
237
238        Ok((vm, config))
239    }
240
241    async fn openvmm_host(
242        resources: &mut PetriVmResourcesOpenVmm,
243        mesh: &Mesh,
244        log_file: PetriLogFile,
245        vmm_env: BTreeMap<OsString, OsString>,
246    ) -> anyhow::Result<WorkerHost> {
247        // Copy the child's stderr to this process's, since internally this is
248        // wrapped by the test harness.
249        let (stderr_read, stderr_write) = pal::pipe_pair()?;
250        let task = resources.driver.spawn(
251            "serial log",
252            crate::log_task(
253                log_file,
254                PolledPipe::new(&resources.driver, stderr_read)
255                    .context("failed to create polled pipe")?,
256                "openvmm stderr",
257            ),
258        );
259        resources.log_stream_tasks.push(task);
260
261        let (host, runner) = mesh_worker::worker_host();
262        mesh.launch_host(
263            ProcessConfig::new("vmm")
264                .process_name(&resources.openvmm_path)
265                .stderr(Some(stderr_write))
266                .env(vmm_env.into_iter()),
267            openvmm_defs::entrypoint::MeshHostParams { runner },
268        )
269        .await?;
270        Ok(host)
271    }
272}