/ src / qemu.rs
qemu.rs
  1  //! Run QEMU.
  2  
  3  use std::{
  4      ffi::{OsStr, OsString},
  5      path::{Path, PathBuf},
  6      process::Command,
  7  };
  8  
  9  use bytesize::MIB;
 10  use clingwrap::runner::{CommandError, CommandRunner};
 11  use tempfile::{tempdir_in, TempDir};
 12  
 13  use crate::{
 14      cloud_init::LocalDataStore,
 15      config::Config,
 16      qemu_utils::create_cow_image,
 17      runlog::{RunLog, RunLogError, RunLogSource},
 18      util::{copy_file_rw, create_file},
 19      vdrive::{VirtualDrive, VirtualDriveError},
 20  };
 21  
 22  /// Path in VM to executor drive.
 23  pub const EXECUTOR_DRIVE: &str = "/dev/vdb";
 24  /// Path in VM to source drive.
 25  pub const SOURCE_DRIVE: &str = "/dev/vdc";
 26  /// Path in VM to artifact drive.
 27  pub const ARTIFACT_DRIVE: &str = "/dev/vdd";
 28  /// Path in VM to cache drive.
 29  pub const CACHE_DRIVE: &str = "/dev/vde";
 30  /// Path in VM to dependencies drive.
 31  pub const DEPS_DRIVE: &str = "/dev/vdf";
 32  
 33  /// Path in VM to workspace root directory.
 34  pub const WORKSPACE_DIR: &str = "/ci";
 35  /// Path in VM to source directory.
 36  pub const SOURCE_DIR: &str = "/ci/src";
 37  /// Path in VM to dependencies directory.
 38  pub const DEPS_DIR: &str = "/ci/deps";
 39  /// Path in VM to cache directory.
 40  pub const CACHE_DIR: &str = "/ci/cache";
 41  /// Path in VM to artifactsdirectory.
 42  pub const ARTIFACTS_DIR: &str = "/ci/artifacts";
 43  
 44  /// Run QEMU.
 45  #[derive(Default)]
 46  pub struct QemuRunner<'a> {
 47      config: Option<&'a Config>,
 48      base_image: Option<PathBuf>,
 49      cow_image: Option<PathBuf>,
 50      cloud_init: Option<LocalDataStore>,
 51      executor: Option<&'a VirtualDrive>,
 52      source: Option<&'a VirtualDrive>,
 53      dependencies: Option<&'a VirtualDrive>,
 54      cache: Option<&'a VirtualDrive>,
 55      artifacts: Option<&'a VirtualDrive>,
 56      console_log: Option<PathBuf>,
 57      raw_log: Option<PathBuf>,
 58      network: bool,
 59      uefi: bool,
 60  }
 61  
 62  impl<'a> QemuRunner<'a> {
 63      /// Set configuration.
 64      pub fn config(mut self, value: &'a Config) -> Self {
 65          self.config = Some(value);
 66          self
 67      }
 68  
 69      /// Set base image.
 70      pub fn base_image(mut self, filename: &Path) -> Self {
 71          self.base_image = Some(filename.into());
 72          self
 73      }
 74  
 75      /// Set copy-on-write image.
 76      pub fn cow_image(mut self, filename: &Path) -> Self {
 77          self.cow_image = Some(filename.into());
 78          self
 79      }
 80  
 81      /// Set `cloud-init` data store.
 82      pub fn cloud_init(mut self, ds: &LocalDataStore) -> Self {
 83          self.cloud_init = Some(ds.clone());
 84          self
 85      }
 86  
 87      /// Set executor drive.
 88      pub fn executor(mut self, value: &'a VirtualDrive) -> Self {
 89          self.executor = Some(value);
 90          self
 91      }
 92  
 93      /// Set source drive.
 94      pub fn source(mut self, value: &'a VirtualDrive) -> Self {
 95          self.source = Some(value);
 96          self
 97      }
 98  
 99      /// Set dependencies drive.
100      pub fn dependencies(mut self, value: &'a VirtualDrive) -> Self {
101          self.dependencies = Some(value);
102          self
103      }
104  
105      /// Set cache drive.
106      pub fn cache(mut self, value: &'a VirtualDrive) -> Self {
107          self.cache = Some(value);
108          self
109      }
110  
111      /// Set artifacts drive.
112      pub fn artifacts(mut self, value: &'a VirtualDrive) -> Self {
113          self.artifacts = Some(value);
114          self
115      }
116  
117      /// Set console log file.
118      pub fn console_log(mut self, value: &'a Path) -> Self {
119          self.console_log = Some(value.into());
120          self
121      }
122  
123      /// Set run log file.
124      pub fn raw_log(mut self, value: &'a Path) -> Self {
125          self.raw_log = Some(value.into());
126          self
127      }
128  
129      /// Allow network?
130      pub fn network(mut self, network: bool) -> Self {
131          self.network = network;
132          self
133      }
134  
135      /// Use UEFI?
136      pub fn uefi(mut self, uefi: bool) -> Self {
137          self.uefi = uefi;
138          self
139      }
140  
141      /// Run QEMU.
142      pub fn run(&self, source: RunLogSource, runlog: &mut RunLog) -> Result<i32, QemuError> {
143          let config = self.config.ok_or(QemuError::Missing("config"))?;
144          let image = self.base_image.clone().ok_or(QemuError::Missing("image"))?;
145          let cloud_init = self
146              .cloud_init
147              .clone()
148              .ok_or(QemuError::Missing("cloud_init"))?;
149          let executor_drive = self.executor.ok_or(QemuError::Missing("executor_drive"))?;
150  
151          let raw_log = self.raw_log.clone().ok_or(QemuError::Missing("raw_log"))?;
152          let console_log = self
153              .console_log
154              .clone()
155              .ok_or(QemuError::Missing("console_log"))?;
156  
157          let qemu = Qemu {
158              kvm_binary: config.kvm_binary(),
159              ovmf_code: config.ovmf_code_file(),
160              ovmf_vars: config.ovmf_vars_file(),
161              image,
162              cow_image: self.cow_image.clone(),
163              tmpdir: tempdir_in(config.tmpdir()).map_err(QemuError::TempDir)?,
164              console_log,
165              raw_log,
166              cloud_init,
167              executor: executor_drive.clone(),
168              source: self.source,
169              artifact: self.artifacts,
170              dependencies: self.dependencies,
171              cache: self.cache,
172              cpus: config.cpus(),
173              memory: config.memory().as_u64(),
174              network: self.network,
175          };
176          qemu.run(source, runlog)
177      }
178  }
179  
180  /// A QEMU runner.
181  #[derive(Debug)]
182  struct Qemu<'a> {
183      kvm_binary: PathBuf,
184      ovmf_code: PathBuf,
185      ovmf_vars: PathBuf,
186      image: PathBuf,
187      cow_image: Option<PathBuf>,
188      cloud_init: LocalDataStore,
189      tmpdir: TempDir,
190      console_log: PathBuf,
191      raw_log: PathBuf,
192      executor: VirtualDrive,
193      source: Option<&'a VirtualDrive>,
194      artifact: Option<&'a VirtualDrive>,
195      dependencies: Option<&'a VirtualDrive>,
196      cache: Option<&'a VirtualDrive>,
197      cpus: usize,
198      memory: u64,
199      network: bool,
200  }
201  
202  impl Qemu<'_> {
203      /// Run QEMU in the specified way.
204      #[allow(clippy::unwrap_used)]
205      fn run(&self, source: RunLogSource, runlog: &mut RunLog) -> Result<i32, QemuError> {
206          let tmp = tempdir_in(&self.tmpdir).map_err(QemuError::TempDir)?;
207  
208          let cow_image = self
209              .cow_image
210              .clone()
211              .unwrap_or_else(|| tmp.path().join("vm.qcow2"));
212          let iso = tmp.path().join("cloud_init.iso");
213          let vars = tmp.path().join("vars.fd");
214  
215          create_cow_image(&self.image, &cow_image).map_err(QemuError::COW)?;
216  
217          copy_file_rw(&self.ovmf_vars, &vars)
218              .map_err(|e| QemuError::Copy(self.ovmf_vars.to_path_buf(), Box::new(e)))?;
219          assert!(!std::fs::metadata(&vars).unwrap().permissions().readonly());
220  
221          self.cloud_init
222              .iso(&iso)
223              .map_err(|err| QemuError::Iso(iso.clone(), err))?;
224  
225          create_file(&self.console_log).map_err(QemuError::CreateFile)?;
226  
227          let raw_log_filename = create_file(&self.raw_log).map_err(QemuError::CreateFile)?;
228  
229          let cpus = format!("cpus={}", self.cpus);
230          let memory = format!("{}", self.memory / MIB);
231          let mut args = QemuArgs::default()
232              .with_valued_arg("-m", &memory)
233              .with_valued_arg("-smp", &cpus)
234              .with_valued_arg("-cpu", "kvm64")
235              .with_valued_arg("-machine", "type=q35,accel=kvm,usb=off")
236              .with_valued_arg("-uuid", "a85c9de7-edc0-4e54-bead-112e5733582c")
237              .with_valued_arg("-boot", "strict=on")
238              .with_valued_arg("-name", "ambient-ci-vm")
239              .with_valued_arg("-rtc", "base=utc,driftfix=slew")
240              .with_valued_arg("-display", "none")
241              .with_valued_arg("-device", "virtio-rng-pci")
242              .with_valued_arg("-serial", &format!("file:{}", self.console_log.display())) // ttyS0
243              .with_valued_arg("-serial", &format!("file:{}", raw_log_filename.display())) // ttyS1
244              .with_qcow2(&cow_image.to_string_lossy())
245              .with_raw(self.executor.filename(), true)
246              .with_valued_arg("-cdrom", &iso.display().to_string());
247  
248          args = args.with_ipflash(0, &self.ovmf_code.to_string_lossy(), true);
249          args = args.with_ipflash(1, &vars.to_string_lossy(), false);
250  
251          if let Some(drive) = self.source {
252              args = args.with_raw(drive.filename(), true);
253          }
254          if let Some(drive) = self.artifact {
255              args = args.with_raw(drive.filename(), false);
256          }
257          if let Some(drive) = self.cache {
258              args = args.with_raw(drive.filename(), false);
259          }
260          if let Some(drive) = self.dependencies {
261              args = args.with_raw(drive.filename(), true);
262          }
263          if self.network {
264              args = args.with_valued_arg("-nic", "user,model=virtio");
265          }
266          args = args.with_arg("-nodefaults").with_arg("-no-user-config");
267  
268          let mut cmd = Command::new(&self.kvm_binary);
269          cmd.args(args.iter());
270  
271          runlog.start_qemu(source, &cmd);
272          let runner = CommandRunner::new(cmd);
273          let result = runner.execute();
274          let (vm_runlog, exit) = Self::parse_raw_log(&raw_log_filename)?;
275          for msg in vm_runlog.msgs() {
276              runlog.write(msg);
277          }
278          match &result {
279              Ok(output) => {
280                  runlog.qemu_succeeded(source, output);
281                  Ok(exit)
282              }
283              Err(CommandError::KilledBySignal { .. }) => {
284                  let err = result.unwrap_err();
285                  runlog.qemu_failed(source, &err);
286                  Err(QemuError::Qemu(err))
287              }
288              Err(CommandError::CommandFailed { exit_code, .. }) => {
289                  let exit_code = *exit_code;
290                  let err = result.unwrap_err();
291                  runlog.qemu_failed(source, &err);
292                  Ok(exit_code)
293              }
294              Err(_) => {
295                  let err = result.unwrap_err();
296                  runlog.qemu_failed(source, &err);
297                  Err(QemuError::Qemu(err))
298              }
299          }
300      }
301  
302      fn parse_raw_log(filename: &Path) -> Result<(RunLog, i32), QemuError> {
303          const BEGIN: &str = "====================== BEGIN ======================";
304          const EXIT: &str = "\nEXIT CODE: ";
305  
306          let raw = std::fs::read(filename).map_err(|e| QemuError::ReadLog(filename.into(), e))?;
307          let runlog = RunLog::from_raw(raw.clone()).map_err(QemuError::ParseRaw)?;
308  
309          let log = String::from_utf8_lossy(&raw);
310          if let Some((_, log)) = log.split_once(BEGIN) {
311              if let Some((_, rest)) = log.split_once(EXIT) {
312                  if let Some((exit, _)) = rest.split_once('\n') {
313                      let exit = exit.trim();
314                      let exit = exit
315                          .parse::<i32>()
316                          .or(Err(QemuError::ParseExit(exit.to_string())))?;
317                      Ok((runlog, exit))
318                  } else {
319                      Err(QemuError::BadExitCode)
320                  }
321              } else {
322                  Err(QemuError::NoBeginMarker)
323              }
324          } else {
325              Err(QemuError::NoExit)
326          }
327      }
328  }
329  
330  #[derive(Debug, Default)]
331  struct QemuArgs {
332      args: Vec<OsString>,
333  }
334  
335  impl QemuArgs {
336      fn with_arg(mut self, arg: &str) -> Self {
337          self.args.push(arg.into());
338          self
339      }
340  
341      fn with_valued_arg(mut self, arg: &str, value: &str) -> Self {
342          self.args.push(arg.into());
343          self.args.push(value.into());
344          self
345      }
346  
347      fn with_ipflash(mut self, unit: usize, path: &str, readonly: bool) -> Self {
348          self.args.push("-drive".into());
349          self.args.push(
350              format!(
351                  "if=pflash,format=raw,unit={},file={}{}",
352                  unit,
353                  path,
354                  if readonly { ",readonly=on" } else { "" },
355              )
356              .into(),
357          );
358          self
359      }
360  
361      fn with_qcow2(mut self, path: &str) -> Self {
362          self.args.push("-drive".into());
363          self.args
364              .push(format!("format=qcow2,if=virtio,file={path}").into());
365          self
366      }
367  
368      fn with_raw(mut self, path: &Path, readonly: bool) -> Self {
369          self.args.push("-drive".into());
370          self.args.push(
371              format!(
372                  "format=raw,if=virtio,file={}{}",
373                  path.display(),
374                  if readonly { ",readonly=on" } else { "" },
375              )
376              .into(),
377          );
378          self
379      }
380  
381      fn iter(&self) -> impl Iterator<Item = &OsStr> {
382          self.args.iter().map(|s| s.as_os_str())
383      }
384  }
385  
386  /// Possible errors from running Qemu.
387  #[allow(missing_docs)]
388  #[derive(Debug, thiserror::Error)]
389  pub enum QemuError {
390      #[error("missing field in QemuRunner: {0}")]
391      Missing(&'static str),
392  
393      #[error("failed to create a temporary directory")]
394      TempDir(#[source] std::io::Error),
395  
396      #[error("failed to copy to temporary directory: {0}")]
397      Copy(PathBuf, #[source] Box<crate::util::UtilError>),
398  
399      #[error("failed to read log file {0}")]
400      ReadLog(PathBuf, #[source] std::io::Error),
401  
402      #[error("failed to parse raw log for JSON Lines")]
403      ParseRaw(#[source] RunLogError),
404  
405      #[error("failed to create a tar archive from {0}")]
406      Tar(PathBuf, #[source] Box<VirtualDriveError>),
407  
408      #[error("failed to extract cache drive to {0}")]
409      ExtractCache(PathBuf, #[source] Box<VirtualDriveError>),
410  
411      #[error("failed to read temporary file for logging")]
412      TemporaryLog(#[source] std::io::Error),
413  
414      #[error("run log lacks exit code of run")]
415      NoExit,
416  
417      #[error("failed to get length of file {0}")]
418      Metadata(PathBuf, #[source] std::io::Error),
419  
420      #[error("failed to set length of file to {0}: {1}")]
421      SetLen(u64, PathBuf, #[source] std::io::Error),
422  
423      #[error("failed to run actions in QEMU")]
424      QemuFailed(i32),
425  
426      #[error("failed to create cloud-init ISO file {0}")]
427      Iso(PathBuf, #[source] crate::cloud_init::CloudInitError),
428  
429      #[error(transparent)]
430      COW(#[from] crate::qemu_utils::QemuUtilError),
431  
432      #[error("failed to run QEMU")]
433      Qemu(#[source] CommandError),
434  
435      #[error(transparent)]
436      CreateFile(#[from] crate::util::UtilError),
437  
438      #[error("failed to parse exist code {0:?}")]
439      ParseExit(String),
440  
441      #[error("run log from QEMU does not have a BEGIN marker")]
442      NoBeginMarker,
443  
444      #[error("run log from QEMU has malformed exit code marker")]
445      BadExitCode,
446  }