refraction-forger/crates/forge-engine/src/phase2/qcow2.rs

151 lines
4.9 KiB
Rust
Raw Normal View History

use std::path::Path;
use spec_parser::schema::Target;
use tracing::info;
use crate::error::ForgeError;
use crate::tools::ToolRunner;
/// Build a QCOW2 VM image from the staged rootfs.
///
/// Pipeline:
/// 1. Create raw disk image of specified size
/// 2. Attach loopback device
/// 3. Create ZFS pool with spec properties
/// 4. Create boot environment structure (rpool/ROOT/be-1)
/// 5. Copy staging rootfs into mounted BE
/// 6. Install bootloader via chroot
/// 7. Set bootfs property
/// 8. Export pool, detach loopback
/// 9. Convert raw -> qcow2
pub async fn build_qcow2(
target: &Target,
staging_root: &Path,
output_dir: &Path,
runner: &dyn ToolRunner,
) -> Result<(), ForgeError> {
let disk_size = target
.disk_size
.as_deref()
.ok_or(ForgeError::MissingDiskSize)?;
let bootloader_type = target.bootloader.as_deref().unwrap_or("uefi");
let raw_path = output_dir.join(format!("{}.raw", target.name));
let qcow2_path = output_dir.join(format!("{}.qcow2", target.name));
let raw_str = raw_path.to_str().unwrap();
let qcow2_str = qcow2_path.to_str().unwrap();
// Collect pool properties
let pool_props: Vec<(&str, &str)> = target
.pool
.as_ref()
.map(|p| {
p.properties
.iter()
.map(|prop| (prop.name.as_str(), prop.value.as_str()))
.collect()
})
.unwrap_or_default();
let pool_name = "rpool";
let be_dataset = format!("{pool_name}/ROOT/be-1");
info!(disk_size, "Step 1: Creating raw disk image");
crate::tools::qemu_img::create_raw(runner, raw_str, disk_size).await?;
info!("Step 2: Attaching loopback device");
let device = crate::tools::loopback::attach(runner, raw_str).await?;
// Wrap the rest in a closure-like structure so we can clean up on error
let result = async {
info!(device = %device, "Step 3: Creating ZFS pool");
crate::tools::zpool::create(runner, pool_name, &device, &pool_props).await?;
info!("Step 4: Creating boot environment structure");
crate::tools::zfs::create(
runner,
&format!("{pool_name}/ROOT"),
&[("canmount", "off"), ("mountpoint", "legacy")],
)
.await?;
let staging_str = staging_root.to_str().unwrap_or(".");
crate::tools::zfs::create(
runner,
&be_dataset,
&[("canmount", "noauto"), ("mountpoint", staging_str)],
)
.await?;
crate::tools::zfs::mount(runner, &be_dataset).await?;
info!("Step 5: Copying staging rootfs into boot environment");
copy_rootfs(staging_root, staging_root)?;
info!("Step 6: Installing bootloader");
crate::tools::bootloader::install(runner, staging_str, pool_name, bootloader_type).await?;
info!("Step 7: Setting bootfs property");
crate::tools::zpool::set(runner, pool_name, "bootfs", &be_dataset).await?;
info!("Step 8: Exporting ZFS pool");
crate::tools::zfs::unmount(runner, &be_dataset).await?;
crate::tools::zpool::export(runner, pool_name).await?;
Ok::<(), ForgeError>(())
}
.await;
// Always try to detach loopback, even on error
info!("Detaching loopback device");
let detach_result = crate::tools::loopback::detach(runner, &device).await;
// Return the original error if there was one
result?;
detach_result?;
info!("Step 9: Converting raw -> qcow2");
crate::tools::qemu_img::convert_to_qcow2(runner, raw_str, qcow2_str).await?;
// Clean up raw file
std::fs::remove_file(&raw_path).ok();
info!(path = %qcow2_path.display(), "QCOW2 image created");
Ok(())
}
/// Copy the staging rootfs into the mounted BE.
/// Since the BE is mounted at the staging root mountpoint, we use a recursive
/// copy approach for files that need relocation.
fn copy_rootfs(src: &Path, dest: &Path) -> Result<(), ForgeError> {
// In the actual build, the ZFS dataset is mounted at the staging_root path,
// so the files are already in place after package installation. This function
// handles the case where we need to copy from a temp staging dir into the
// mounted ZFS dataset.
if src == dest {
return Ok(());
}
for entry in walkdir::WalkDir::new(src).follow_links(false) {
let entry = entry.map_err(|e| ForgeError::Qcow2Build {
step: "copy_rootfs".to_string(),
detail: e.to_string(),
})?;
let rel = entry.path().strip_prefix(src).unwrap_or(entry.path());
let target = dest.join(rel);
if entry.path().is_dir() {
std::fs::create_dir_all(&target)?;
} else if entry.path().is_file() {
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(entry.path(), &target)?;
}
}
Ok(())
}