/ src / bin / cmd / qemu.rs
qemu.rs
  1  use std::path::PathBuf;
  2  
  3  use clap::Parser;
  4  use tempfile::tempdir_in;
  5  
  6  use ambient_ci::{
  7      action::RunnableAction,
  8      action_impl::Shell,
  9      cloud_init::CloudInitError,
 10      git::GitError,
 11      plan::{PlanError, RunnablePlan},
 12      qemu::{QemuError, QemuRunner},
 13      qemu_utils::convert_image,
 14      run::{create_cloud_init_iso, create_executor_vdrive},
 15      runlog::{RunLog, RunLogSource},
 16      util::{mkdir, UtilError},
 17      vdrive::{create_tar, create_tar_with_size, VirtualDriveError},
 18  };
 19  
 20  use super::{AmbientError, Config, Leaf};
 21  
 22  /// Run QEMU with a specific runnable plan. Empty source, cache,
 23  /// dependencies, and artifacts.
 24  #[derive(Debug, Parser)]
 25  pub struct QemuCmd {
 26      /// File with runnable plan.
 27      #[clap(long, required_unless_present = "shell", conflicts_with = "shell")]
 28      plan: Option<PathBuf>,
 29  
 30      /// Execute shell snippet in VM.
 31      #[clap(long, required_unless_present = "plan", conflicts_with = "plan")]
 32      shell: Vec<String>,
 33  
 34      /// Use this virtual machine image as the base image. The base
 35      /// image will not be modified, even if the virtual machine changes
 36      /// things on its disk. The changes are written to a copy-on-write
 37      /// temporary image (but see `--persist`).
 38      #[clap(long)]
 39      image: PathBuf,
 40  
 41      /// Save the image after the VM shuts to this file. This allows
 42      /// capturing changes made inside the virtual machine.
 43      #[clap(long)]
 44      persist: Option<PathBuf>,
 45  
 46      /// Allow network?
 47      #[clap(long)]
 48      network: bool,
 49  
 50      /// Write console log to this file.
 51      #[clap(long)]
 52      console: Option<PathBuf>,
 53  
 54      /// Write run log to this file.
 55      #[clap(long)]
 56      run_log: Option<PathBuf>,
 57  
 58      /// Write artifacts to this file.
 59      #[clap(long)]
 60      artifacts: Option<PathBuf>,
 61  
 62      /// Use UEFI.
 63      #[clap(long)]
 64      uefi: bool,
 65  }
 66  
 67  impl QemuCmd {
 68      fn helper(&self, config: &Config) -> Result<(), QemuCmdError> {
 69          let mut runnable_plan = if let Some(plan) = &self.plan {
 70              RunnablePlan::from_file(plan)?
 71          } else {
 72              let mut runnable_plan = RunnablePlan::default();
 73              for shell in self.shell.iter() {
 74                  let shell = Shell::new(shell);
 75                  runnable_plan.push(RunnableAction::Shell(shell));
 76              }
 77              runnable_plan
 78          };
 79          runnable_plan.set_unset_dirs(".");
 80  
 81          let tmp = tempdir_in(config.tmpdir()).map_err(QemuCmdError::TempDir)?;
 82  
 83          let console_log = self
 84              .console
 85              .clone()
 86              .unwrap_or_else(|| tmp.path().join("console.log"));
 87          let run_log = self
 88              .run_log
 89              .clone()
 90              .unwrap_or_else(|| tmp.path().join("run.log"));
 91  
 92          let empty = tmp.path().join("src");
 93          mkdir(&empty)?;
 94  
 95          let executor = config.executor().ok_or(QemuCmdError::NoExecutor)?;
 96          let executor_drive = create_executor_vdrive(&tmp, &runnable_plan, executor)?;
 97          let source_drive = create_tar(tmp.path().join("src.tar"), &empty)?;
 98          let artifacts_drive = create_tar_with_size(
 99              self.artifacts
100                  .clone()
101                  .unwrap_or_else(|| tmp.path().join("artifacts.tar")),
102              &empty,
103              1024 * 1024 * 1024,
104          )?;
105          let ds = create_cloud_init_iso(self.network)?;
106  
107          let backing_image = self
108              .image
109              .canonicalize()
110              .map_err(|err| QemuCmdError::Canonicalize(self.image.clone(), err))?;
111          let cow_image = tmp.path().join("vm.qcow2");
112  
113          let qemu = QemuRunner::default()
114              .config(config)
115              .base_image(&backing_image)
116              .cow_image(&cow_image)
117              .executor(&executor_drive)
118              .cloud_init(&ds)
119              .source(&source_drive)
120              .artifacts(&artifacts_drive)
121              .cloud_init(&ds)
122              .console_log(&console_log)
123              .raw_log(&run_log)
124              .network(self.network)
125              .uefi(self.uefi || config.uefi());
126  
127          let mut runlog = RunLog::default();
128          qemu.run(RunLogSource::Plan, &mut runlog)?;
129  
130          if let Some(persist) = &self.persist {
131              convert_image(&cow_image, persist).map_err(|err| {
132                  QemuCmdError::ConvertImage(cow_image.clone(), persist.to_path_buf(), err)
133              })?;
134          }
135  
136          Ok(())
137      }
138  }
139  
140  impl Leaf for QemuCmd {
141      fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
142          Ok(self.helper(config)?)
143      }
144  }
145  
146  #[derive(Debug, thiserror::Error)]
147  pub enum QemuCmdError {
148      #[error(transparent)]
149      Qemu(#[from] QemuError),
150  
151      #[error("failed to create a temporary directory")]
152      TempDir(#[source] std::io::Error),
153  
154      #[error(transparent)]
155      Util(#[from] UtilError),
156  
157      #[error(transparent)]
158      VDrive(#[from] VirtualDriveError),
159  
160      #[error(transparent)]
161      CloudInit(#[from] CloudInitError),
162  
163      #[error(transparent)]
164      CreateDrive(#[from] ambient_ci::run::RunError),
165  
166      #[error(transparent)]
167      Plan(#[from] PlanError),
168  
169      #[error(transparent)]
170      Git(#[from] GitError),
171  
172      #[error("no executor specified in configuration")]
173      NoExecutor,
174  
175      #[error("failed to convert image {0} to {1}")]
176      ConvertImage(
177          PathBuf,
178          PathBuf,
179          #[source] ambient_ci::qemu_utils::QemuUtilError,
180      ),
181  
182      #[error("failed to make path absolute: {0}")]
183      Canonicalize(PathBuf, #[source] std::io::Error),
184  }