use std::path::PathBuf;
use clap::Parser;
use tempfile::tempdir_in;
use ambient_ci::{
action::RunnableAction,
action_impl::Shell,
cloud_init::CloudInitError,
git::GitError,
plan::{PlanError, RunnablePlan},
qemu::{QemuError, QemuRunner},
qemu_utils::convert_image,
run::{create_cloud_init_iso, create_executor_vdrive},
runlog::{RunLog, RunLogSource},
util::{mkdir, UtilError},
vdrive::{create_tar, create_tar_with_size, VirtualDriveError},
};
use super::{AmbientError, Config, Leaf};
#[derive(Debug, Parser)]
pub struct QemuCmd {
#[clap(long, required_unless_present = "shell", conflicts_with = "shell")]
plan: Option<PathBuf>,
#[clap(long, required_unless_present = "plan", conflicts_with = "plan")]
shell: Vec<String>,
#[clap(long)]
image: PathBuf,
#[clap(long)]
persist: Option<PathBuf>,
#[clap(long)]
network: bool,
#[clap(long)]
console: Option<PathBuf>,
#[clap(long)]
run_log: Option<PathBuf>,
#[clap(long)]
artifacts: Option<PathBuf>,
#[clap(long)]
uefi: bool,
}
impl QemuCmd {
fn helper(&self, config: &Config) -> Result<(), QemuCmdError> {
let mut runnable_plan = if let Some(plan) = &self.plan {
RunnablePlan::from_file(plan)?
} else {
let mut runnable_plan = RunnablePlan::default();
for shell in self.shell.iter() {
let shell = Shell::new(shell);
runnable_plan.push(RunnableAction::Shell(shell));
}
runnable_plan
};
runnable_plan.set_unset_dirs(".");
let tmp = tempdir_in(config.tmpdir()).map_err(QemuCmdError::TempDir)?;
let console_log = self
.console
.clone()
.unwrap_or_else(|| tmp.path().join("console.log"));
let run_log = self
.run_log
.clone()
.unwrap_or_else(|| tmp.path().join("run.log"));
let empty = tmp.path().join("src");
mkdir(&empty)?;
let executor = config.executor().ok_or(QemuCmdError::NoExecutor)?;
let executor_drive = create_executor_vdrive(&tmp, &runnable_plan, executor)?;
let source_drive = create_tar(tmp.path().join("src.tar"), &empty)?;
let artifacts_drive = create_tar_with_size(
self.artifacts
.clone()
.unwrap_or_else(|| tmp.path().join("artifacts.tar")),
&empty,
1024 * 1024 * 1024,
)?;
let ds = create_cloud_init_iso(self.network)?;
let backing_image = self
.image
.canonicalize()
.map_err(|err| QemuCmdError::Canonicalize(self.image.clone(), err))?;
let cow_image = tmp.path().join("vm.qcow2");
let qemu = QemuRunner::default()
.config(config)
.base_image(&backing_image)
.cow_image(&cow_image)
.executor(&executor_drive)
.cloud_init(&ds)
.source(&source_drive)
.artifacts(&artifacts_drive)
.cloud_init(&ds)
.console_log(&console_log)
.raw_log(&run_log)
.network(self.network)
.uefi(self.uefi || config.uefi());
let mut runlog = RunLog::default();
qemu.run(RunLogSource::Plan, &mut runlog)?;
if let Some(persist) = &self.persist {
convert_image(&cow_image, persist).map_err(|err| {
QemuCmdError::ConvertImage(cow_image.clone(), persist.to_path_buf(), err)
})?;
}
Ok(())
}
}
impl Leaf for QemuCmd {
fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
Ok(self.helper(config)?)
}
}
#[derive(Debug, thiserror::Error)]
pub enum QemuCmdError {
#[error(transparent)]
Qemu(#[from] QemuError),
#[error("failed to create a temporary directory")]
TempDir(#[source] std::io::Error),
#[error(transparent)]
Util(#[from] UtilError),
#[error(transparent)]
VDrive(#[from] VirtualDriveError),
#[error(transparent)]
CloudInit(#[from] CloudInitError),
#[error(transparent)]
CreateDrive(#[from] ambient_ci::run::RunError),
#[error(transparent)]
Plan(#[from] PlanError),
#[error(transparent)]
Git(#[from] GitError),
#[error("no executor specified in configuration")]
NoExecutor,
#[error("failed to convert image {0} to {1}")]
ConvertImage(
PathBuf,
PathBuf,
#[source] ambient_ci::qemu_utils::QemuUtilError,
),
#[error("failed to make path absolute: {0}")]
Canonicalize(PathBuf, #[source] std::io::Error),
}