Skip to main content

ambient_ci/
qemu.rs

1//! Run QEMU.
2
3use 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
22/// Path in VM to executor drive.
23pub const EXECUTOR_DRIVE: &str = "/dev/vdb";
24/// Path in VM to source drive.
25pub const SOURCE_DRIVE: &str = "/dev/vdc";
26/// Path in VM to artifact drive.
27pub const ARTIFACT_DRIVE: &str = "/dev/vdd";
28/// Path in VM to cache drive.
29pub const CACHE_DRIVE: &str = "/dev/vde";
30/// Path in VM to dependencies drive.
31pub const DEPS_DRIVE: &str = "/dev/vdf";
32
33/// Path in VM to workspace root directory.
34pub const WORKSPACE_DIR: &str = "/ci";
35/// Path in VM to source directory.
36pub const SOURCE_DIR: &str = "/ci/src";
37/// Path in VM to dependencies directory.
38pub const DEPS_DIR: &str = "/ci/deps";
39/// Path in VM to cache directory.
40pub const CACHE_DIR: &str = "/ci/cache";
41/// Path in VM to artifactsdirectory.
42pub const ARTIFACTS_DIR: &str = "/ci/artifacts";
43
44/// Run QEMU.
45#[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    /// 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: 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/// A QEMU runner.
190#[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    /// Run QEMU in the specified way.
213    #[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())) // ttyS0
252            .with_valued_arg("-serial", &format!("file:{}", raw_log_filename.display())) // ttyS1
253            .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/// Possible errors from running Qemu.
396#[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}