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 }