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 }