use std::path::{Path, PathBuf};
use clap::Parser;
use tempfile::tempdir;
use ambient_ci::{
action::UnsafeAction,
cloud_init::{CloudInitError, LocalDataStoreBuilder},
image_store::{ImageStore, ImageStoreError, MetadataBuilder},
plan::RunnablePlan,
qemu::{self, QemuError, QemuRunner},
qemu_utils::{convert_image, QemuUtilError},
run::{create_cloud_init_iso, create_executor_vdrive, create_source_vdrive},
runlog::{RunLog, RunLogSource},
util::{cat_text_file, mkdir, UtilError},
vdrive::VirtualDriveError,
};
use super::{AmbientError, Config, Leaf};
const SECRET: &str = "xyzzy";
#[derive(Debug, Parser)]
pub struct PrepareImage {
#[clap(long)]
base: String,
#[clap(long)]
new: String,
#[clap(long)]
executor: PathBuf,
#[clap(long)]
network: bool,
#[clap(long)]
run_log: Option<PathBuf>,
#[clap(long)]
console_log: Option<PathBuf>,
#[clap(long)]
shell: Option<String>,
}
impl Leaf for PrepareImage {
fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
let mut image_store =
ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?;
if image_store.contains(&self.new) {
return Err(ImageError::ExistsAlready(self.new.clone()).into());
}
if let Some(metadata) = image_store.get_metadata(&self.base) {
let filename = image_store.image_filename(metadata);
if !filename.exists() {
return Err(ImageError::NoSuchFile(filename.clone()).into());
}
let filename = std::fs::canonicalize(&filename)
.map_err(|err| ImageError::Canonicalize(filename.clone(), err))?;
let tmp = tempdir().map_err(ImageError::TempDir)?;
let cow_image = tmp.path().join("new.qcow2");
let src = tmp.path().join("src");
mkdir(&src).map_err(ImageError::Util)?;
let mut plan = RunnablePlan::default();
let shell = self.shell.clone().unwrap_or("echo hello, world".into());
plan.push_unsafe_actions(
[
UnsafeAction::mkdir(Path::new(qemu::WORKSPACE_DIR)),
UnsafeAction::mkdir(Path::new(qemu::SOURCE_DIR)),
UnsafeAction::shell(&shell),
]
.iter(),
);
plan.set_executor_drive(qemu::EXECUTOR_DRIVE);
plan.set_source_drive(qemu::SOURCE_DRIVE);
plan.set_source_dir(qemu::SOURCE_DIR);
let executor_drive = create_executor_vdrive(&tmp, &plan, &self.executor)
.map_err(ImageError::CreateDrive)?;
let source_drive = create_source_vdrive(&tmp, &src).map_err(ImageError::CreateDrive)?;
let run_log = self
.run_log
.clone()
.unwrap_or_else(|| tmp.path().join("run.log"));
let console_log = self
.console_log
.clone()
.unwrap_or_else(|| tmp.path().join("console.log"));
let ds = create_cloud_init_iso(self.network).map_err(ImageError::CreateDrive)?;
let mut runlog = RunLog::default();
let res = QemuRunner::default()
.config(config)
.base_image(&filename)
.cow_image(&cow_image)
.executor(&executor_drive)
.source(&source_drive)
.cloud_init(&ds)
.console_log(&console_log)
.raw_log(&run_log)
.network(self.network)
.run(RunLogSource::Plan, &mut runlog)
.map_err(ImageError::Qemu);
res?;
let full_image = tmp.path().join("full.qcow2");
convert_image(&cow_image, &full_image).map_err(|err| {
ImageError::ConvertImage(cow_image.clone(), full_image.clone(), err)
})?;
let mut new_metadata =
MetadataBuilder::new(&self.new, &full_image).map_err(ImageError::ImageStore)?;
new_metadata.uefi(metadata.uefi());
let new_metadata = new_metadata.build();
image_store
.import(new_metadata)
.map_err(ImageError::ImageStore)?;
image_store.save().map_err(ImageError::ImageStore)?;
Ok(())
} else {
Err(ImageError::NoSuchImage(self.base.clone()).into())
}
}
}
#[derive(Debug, Parser)]
pub struct VerifyImage {
#[clap(long)]
name: String,
#[clap(long)]
executor: PathBuf,
#[clap(long)]
network: bool,
#[clap(long)]
run_log: Option<PathBuf>,
#[clap(long)]
console_log: Option<PathBuf>,
}
impl Leaf for VerifyImage {
fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
let image_store = ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?;
if let Some(metadata) = image_store.get_metadata(&self.name) {
let filename = image_store.image_filename(metadata);
if !filename.exists() {
return Err(ImageError::NoSuchFile(filename.clone()).into());
}
let filename = std::fs::canonicalize(&filename)
.map_err(|err| ImageError::Canonicalize(filename.clone(), err))?;
let tmp = tempdir().map_err(ImageError::TempDir)?;
let src = tmp.path().join("src");
mkdir(&src).map_err(ImageError::Util)?;
let mut plan = RunnablePlan::default();
plan.push_unsafe_actions(
[
UnsafeAction::mkdir(Path::new(qemu::WORKSPACE_DIR)),
UnsafeAction::mkdir(Path::new(qemu::SOURCE_DIR)),
UnsafeAction::shell(&format!("echo {SECRET}")),
]
.iter(),
);
plan.set_executor_drive(qemu::EXECUTOR_DRIVE);
plan.set_source_drive(qemu::SOURCE_DRIVE);
plan.set_source_dir(qemu::SOURCE_DIR);
let executor_drive = create_executor_vdrive(&tmp, &plan, &self.executor)
.map_err(ImageError::CreateDrive)?;
let source_drive = create_source_vdrive(&tmp, &src).map_err(ImageError::CreateDrive)?;
let run_log = self
.run_log
.clone()
.unwrap_or_else(|| tmp.path().join("run.log"));
let console_log = self
.console_log
.clone()
.unwrap_or_else(|| tmp.path().join("console.log"));
let ds = create_cloud_init_iso(self.network).map_err(ImageError::CreateDrive)?;
let mut runlog = RunLog::default();
let res = QemuRunner::default()
.config(config)
.base_image(&filename)
.executor(&executor_drive)
.source(&source_drive)
.cloud_init(&ds)
.console_log(&console_log)
.raw_log(&run_log)
.network(self.network)
.run(RunLogSource::Plan, &mut runlog)
.map_err(ImageError::Qemu);
let log = cat_text_file(&run_log).map_err(ImageError::Util)?;
if !log.contains(SECRET) {
return Err(ImageError::NotAcceptable(filename.clone()).into());
}
println!("image {} is acceptable to Ambient", filename.display());
res?;
Ok(())
} else {
Err(ImageError::NoSuchImage(self.name.clone()).into())
}
}
}
#[derive(Debug, Parser)]
pub struct CloudInit {
#[clap(long)]
filename: PathBuf,
#[clap(long)]
runcmd: Vec<String>,
#[clap(long)]
network: bool,
}
impl Leaf for CloudInit {
fn run(&self, _config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
let mut ds = LocalDataStoreBuilder::default()
.with_network(self.network)
.with_hostname("ambient")
.with_bootcmd("echo xyzzy2")
.with_bootcmd("echo more bootcmd");
for runcmd in self.runcmd.iter() {
ds = ds.with_runcmd(runcmd);
}
let ds = ds.build().map_err(ImageError::CloudInit)?;
ds.iso(&self.filename).map_err(ImageError::CloudInit)?;
Ok(())
}
}
#[derive(Debug, Parser)]
pub struct ListImages {}
impl Leaf for ListImages {
fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
let image_store = ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?;
for name in image_store.image_names().map_err(ImageError::ImageStore)? {
println!("{name}");
}
Ok(())
}
}
#[derive(Debug, Parser)]
pub struct ImportImage {
name: String,
image: PathBuf,
#[clap(short, long)]
description: Option<String>,
#[clap(short, long)]
url: Option<String>,
#[clap(long)]
uefi: bool,
}
impl Leaf for ImportImage {
fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
let mut metadata =
MetadataBuilder::new(&self.name, &self.image).map_err(ImageError::ImageStore)?;
if let Some(d) = &self.description {
metadata.description(d);
}
if let Some(u) = &self.url {
metadata.url(u);
}
metadata.uefi(self.uefi || config.uefi());
let metadata = metadata.build();
let mut image_store =
ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?;
image_store
.import(metadata)
.map_err(ImageError::ImageStore)?;
image_store.save().map_err(ImageError::ImageStore)?;
Ok(())
}
}
#[derive(Debug, Parser)]
pub struct RemoveImages {
images: Vec<String>,
}
impl Leaf for RemoveImages {
fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
let mut image_store =
ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?;
for image in self.images.iter() {
image_store.remove(image).map_err(ImageError::ImageStore)?;
}
image_store.save().map_err(ImageError::ImageStore)?;
Ok(())
}
}
#[derive(Debug, Parser)]
pub struct ShowImage {
image: String,
}
impl Leaf for ShowImage {
fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
let image_store = ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?;
if let Some(image) = image_store.get_metadata(&self.image) {
println!("{}", image.to_json().map_err(ImageError::ImageStore)?);
Ok(())
} else {
Err(ImageError::NoSuchImage(self.image.clone()))?
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ImageError {
#[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("image {0} does not exist")]
NoSuchFile(PathBuf),
#[error("image {0} does not exist in the image store")]
NoSuchImage(String),
#[error("image {0} is NOT an acceptable image for Ambient")]
NotAcceptable(PathBuf),
#[error("image store already contains {0}")]
ExistsAlready(String),
#[error(transparent)]
CreateDrive(#[from] ambient_ci::run::RunError),
#[error(transparent)]
ImageStore(#[from] ImageStoreError),
#[error("failed to make filename canonical: {0}")]
Canonicalize(PathBuf, #[source] std::io::Error),
#[error("failed to convert image {0} to {1}")]
ConvertImage(PathBuf, PathBuf, #[source] QemuUtilError),
}