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::Firmware;
10use crate::PetriLogFile;
11use crate::PetriLogSource;
12use crate::worker::Worker;
13use anyhow::Context;
14use diag_client::DiagClient;
15use disk_backend_resources::FileDiskHandle;
16use framebuffer::FramebufferAccess;
17use guid::Guid;
18use hvlite_defs::config::DeviceVtl;
19use image::ColorType;
20use mesh_process::Mesh;
21use mesh_process::ProcessConfig;
22use mesh_worker::WorkerHost;
23use pal_async::DefaultDriver;
24use pal_async::pipe::PolledPipe;
25use pal_async::task::Spawn;
26use pal_async::task::Task;
27use pal_async::timer::PolledTimer;
28use petri_artifacts_common::tags::MachineArch;
29use petri_artifacts_common::tags::OsFlavor;
30use pipette_client::PipetteClient;
31use scsidisk_resources::SimpleScsiDiskHandle;
32use std::io::Write;
33use std::path::PathBuf;
34use std::sync::Arc;
35use std::time::Duration;
36use storvsp_resources::ScsiControllerHandle;
37use storvsp_resources::ScsiDeviceAndPath;
38use storvsp_resources::ScsiPath;
39use vm_resource::IntoResource;
40
41impl PetriVmConfigOpenVmm {
42    async fn run_core(self) -> anyhow::Result<PetriVmOpenVmm> {
43        let Self {
44            firmware,
45            arch,
46            mut config,
47
48            mut resources,
49
50            openvmm_log_file,
51
52            ged,
53            vtl2_settings,
54            framebuffer_access,
55        } = self;
56
57        if firmware.is_openhcl() {
58            // Add a pipette disk for VTL 2
59            const UH_CIDATA_SCSI_INSTANCE: Guid =
60                guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
61
62            let uh_agent_disk = resources
63                .openhcl_agent_image
64                .as_ref()
65                .unwrap()
66                .build()
67                .context("failed to build agent image")?;
68
69            config.vmbus_devices.push((
70                DeviceVtl::Vtl2,
71                ScsiControllerHandle {
72                    instance_id: UH_CIDATA_SCSI_INSTANCE,
73                    max_sub_channel_count: 1,
74                    io_queue_depth: None,
75                    devices: vec![ScsiDeviceAndPath {
76                        path: ScsiPath {
77                            path: 0,
78                            target: 0,
79                            lun: 0,
80                        },
81                        device: SimpleScsiDiskHandle {
82                            read_only: true,
83                            parameters: Default::default(),
84                            disk: FileDiskHandle(uh_agent_disk.into_file()).into_resource(),
85                        }
86                        .into_resource(),
87                    }],
88                    requests: None,
89                }
90                .into_resource(),
91            ));
92        }
93
94        // Add the GED and VTL 2 settings.
95        if let Some(mut ged) = ged {
96            ged.vtl2_settings = Some(prost::Message::encode_to_vec(&vtl2_settings.unwrap()));
97            config
98                .vmbus_devices
99                .push((DeviceVtl::Vtl2, ged.into_resource()));
100        }
101
102        let vtl2_vsock_path = config
103            .vtl2_vmbus
104            .as_ref()
105            .and_then(|s| s.vsock_path.as_ref().map(|v| v.into()));
106
107        tracing::debug!(?config, ?firmware, ?arch, "VM config");
108
109        let mesh = Mesh::new("petri_mesh".to_string())?;
110
111        let host = Self::openvmm_host(&mut resources, &mesh, openvmm_log_file)
112            .await
113            .context("failed to create host process")?;
114        let (worker, halt_notif) = Worker::launch(&host, config)
115            .await
116            .context("failed to launch vm worker")?;
117
118        let worker = Arc::new(worker);
119        let watchdog_tasks = Self::start_watchdog_tasks(
120            framebuffer_access,
121            worker.clone(),
122            vtl2_vsock_path,
123            &resources.log_source,
124            &resources.driver,
125        )?;
126
127        let mut vm = PetriVmOpenVmm::new(
128            super::runtime::PetriVmInner {
129                arch,
130                resources,
131                mesh,
132                worker,
133                watchdog_tasks,
134                quirks: firmware.quirks(),
135            },
136            halt_notif,
137        );
138
139        tracing::info!("Resuming VM");
140        vm.resume().await?;
141
142        // Run basic save/restore test that should run on every vm
143        // TODO: OpenHCL needs virt_whp support
144        // TODO: PCAT needs vga device support
145        // TODO: arm64 is broken?
146        if !firmware.is_openhcl()
147            && !matches!(firmware, Firmware::Pcat { .. })
148            && !matches!(arch, MachineArch::Aarch64)
149        {
150            tracing::info!("Testing save/restore");
151            vm.verify_save_restore().await?;
152        }
153
154        tracing::info!("VM ready");
155        Ok(vm)
156    }
157
158    /// Build and boot the requested VM. Does not configure and start pipette.
159    /// Should only be used for testing platforms that pipette does not support.
160    pub async fn run_without_agent(self) -> anyhow::Result<PetriVmOpenVmm> {
161        self.run_core().await
162    }
163
164    /// Run the VM, launching pipette and returning a client to it.
165    pub async fn run(self) -> anyhow::Result<(PetriVmOpenVmm, PipetteClient)> {
166        let mut vm = self.run_with_lazy_pipette().await?;
167        let client = vm.wait_for_agent().await?;
168        Ok((vm, client))
169    }
170
171    /// Run the VM, configuring pipette to automatically start, but do not wait
172    /// for it to connect. This is useful for tests where the first boot attempt
173    /// is expected to not succeed, but pipette functionality is still desired.
174    pub async fn run_with_lazy_pipette(mut self) -> anyhow::Result<PetriVmOpenVmm> {
175        const CIDATA_SCSI_INSTANCE: Guid = guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7b");
176
177        // Construct the agent disk.
178        let agent_disk = self
179            .resources
180            .agent_image
181            .build()
182            .context("failed to build agent image")?;
183
184        // Add a SCSI controller to contain the agent disk. Don't reuse an
185        // existing controller so that we can avoid interfering with
186        // test-specific configuration.
187        self.config.vmbus_devices.push((
188            DeviceVtl::Vtl0,
189            ScsiControllerHandle {
190                instance_id: CIDATA_SCSI_INSTANCE,
191                max_sub_channel_count: 1,
192                io_queue_depth: None,
193                devices: vec![ScsiDeviceAndPath {
194                    path: ScsiPath {
195                        path: 0,
196                        target: 0,
197                        lun: 0,
198                    },
199                    device: SimpleScsiDiskHandle {
200                        read_only: true,
201                        parameters: Default::default(),
202                        disk: FileDiskHandle(agent_disk.into_file()).into_resource(),
203                    }
204                    .into_resource(),
205                }],
206                requests: None,
207            }
208            .into_resource(),
209        ));
210
211        if matches!(self.firmware.os_flavor(), OsFlavor::Windows) {
212            // Make a file for the IMC hive. It's not guaranteed to be at a fixed
213            // location at runtime.
214            let mut imc_hive_file = tempfile::tempfile().context("failed to create temp file")?;
215            imc_hive_file
216                .write_all(include_bytes!("../../../guest-bootstrap/imc.hiv"))
217                .context("failed to write imc hive")?;
218
219            // Add the IMC device.
220            self.config.vmbus_devices.push((
221                DeviceVtl::Vtl0,
222                vmbfs_resources::VmbfsImcDeviceHandle {
223                    file: imc_hive_file,
224                }
225                .into_resource(),
226            ));
227        }
228
229        let is_linux_direct = self.firmware.is_linux_direct();
230
231        // Start the VM.
232        let mut vm = self.run_core().await?;
233
234        if is_linux_direct {
235            vm.launch_linux_direct_pipette().await?;
236        }
237
238        Ok(vm)
239    }
240
241    fn start_watchdog_tasks(
242        framebuffer_access: Option<FramebufferAccess>,
243        worker: Arc<Worker>,
244        vtl2_vsock_path: Option<PathBuf>,
245        log_source: &PetriLogSource,
246        driver: &DefaultDriver,
247    ) -> anyhow::Result<Vec<Task<()>>> {
248        // Our CI environment will kill tests after some time. We want to save
249        // some information about the VM if it's still running at that point.
250        const TIMEOUT_DURATION_MINUTES: u64 = 6;
251        const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60 - 10);
252
253        let mut tasks = Vec::new();
254
255        let mut timer = PolledTimer::new(driver);
256        tasks.push(driver.spawn("petri-watchdog-inspect", {
257            let log_source = log_source.clone();
258            async move {
259                timer.sleep(TIMER_DURATION).await;
260                tracing::warn!(
261                    "Test has been running for almost {TIMEOUT_DURATION_MINUTES} minutes,
262                     saving inspect details."
263                );
264
265                if let Err(e) =
266                    log_source.write_attachment("timeout_inspect.log", worker.inspect_all().await)
267                {
268                    tracing::error!(?e, "Failed to save inspect log");
269                    return;
270                }
271                tracing::info!("Watchdog inspect task finished.");
272            }
273        }));
274
275        if let Some(fba) = framebuffer_access {
276            let mut view = fba.view()?;
277            let mut timer = PolledTimer::new(driver);
278            let log_source = log_source.clone();
279            tasks.push(driver.spawn("petri-watchdog-screenshot", async move {
280                let mut image = Vec::new();
281                let mut last_image = Vec::new();
282                loop {
283                    timer.sleep(Duration::from_secs(2)).await;
284                    tracing::trace!("Taking screenshot.");
285
286                    // Our framebuffer uses 4 bytes per pixel, approximating an
287                    // BGRA image, however it only actually contains BGR data.
288                    // The fourth byte is effectively noise. We can set the 'alpha'
289                    // value to 0xFF to make the image opaque, while we also
290                    // convert it to RGB to output it as a PNG.
291                    const BYTES_PER_PIXEL: usize = 4;
292                    let (width, height) = view.resolution();
293                    let (widthsize, heightsize) = (width as usize, height as usize);
294                    let len = widthsize * heightsize * BYTES_PER_PIXEL;
295
296                    image.resize(len, 0);
297                    for (i, line) in
298                        (0..height).zip(image.chunks_exact_mut(widthsize * BYTES_PER_PIXEL))
299                    {
300                        view.read_line(i, line);
301                        for pixel in line.chunks_exact_mut(BYTES_PER_PIXEL) {
302                            pixel.swap(0, 2);
303                            pixel[3] = 0xFF;
304                        }
305                    }
306
307                    if image == last_image {
308                        tracing::trace!("No change in framebuffer, skipping screenshot.");
309                        continue;
310                    }
311
312                    let r = log_source
313                        .create_attachment("screenshot.png")
314                        .and_then(|mut f| {
315                            image::write_buffer_with_format(
316                                &mut f,
317                                &image,
318                                width.into(),
319                                height.into(),
320                                ColorType::Rgba8,
321                                image::ImageFormat::Png,
322                            )
323                            .map_err(Into::into)
324                        });
325
326                    if let Err(e) = r {
327                        tracing::error!(?e, "Failed to save screenshot");
328                    } else {
329                        tracing::info!("Screenshot saved.");
330                    }
331                    std::mem::swap(&mut image, &mut last_image);
332                }
333            }));
334        }
335
336        if let Some(vtl2_vsock_path) = vtl2_vsock_path {
337            let mut timer = PolledTimer::new(driver);
338            let driver2 = driver.clone();
339            let log_source = log_source.clone();
340            tasks.push(driver.spawn("petri-watchdog-inspect-vtl2", async move {
341                timer.sleep(TIMER_DURATION).await;
342                tracing::warn!(
343                    "Test has been running for almost {TIMEOUT_DURATION_MINUTES} minutes, saving openhcl inspect details."
344                );
345
346                let diag_client =
347                     DiagClient::from_hybrid_vsock(driver2, &vtl2_vsock_path);
348
349                let output = match diag_client.inspect("", None, None).await {
350                    Err(e) => {
351                        tracing::error!(?e, "Failed to inspect vtl2");
352                        return;
353                    }
354                    Ok(output) => output,
355                };
356
357                let formatted_output = format!("{output:#}");
358                if let Err(e) = log_source.write_attachment("timeout_openhcl_inspect.log", formatted_output) {
359                    tracing::error!(?e, "Failed to save ohcldiag-dev inspect log");
360                    return;
361                }
362
363                tracing::info!("Watchdog OpenHCL inspect task finished.");
364            }));
365        }
366
367        Ok(tasks)
368    }
369
370    async fn openvmm_host(
371        resources: &mut PetriVmResourcesOpenVmm,
372        mesh: &Mesh,
373        log_file: PetriLogFile,
374    ) -> anyhow::Result<WorkerHost> {
375        // Copy the child's stderr to this process's, since internally this is
376        // wrapped by the test harness.
377        let (stderr_read, stderr_write) = pal::pipe_pair()?;
378        let task = resources.driver.spawn(
379            "serial log",
380            crate::log_stream(
381                log_file,
382                PolledPipe::new(&resources.driver, stderr_read)
383                    .context("failed to create polled pipe")?,
384            ),
385        );
386        resources.log_stream_tasks.push(task);
387
388        let (host, runner) = mesh_worker::worker_host();
389        mesh.launch_host(
390            ProcessConfig::new("vmm")
391                .process_name(&resources.openvmm_path)
392                .stderr(Some(stderr_write)),
393            hvlite_defs::entrypoint::MeshHostParams { runner },
394        )
395        .await?;
396        Ok(host)
397    }
398}