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