diff --git a/crates/integration-tests/src/main.rs b/crates/integration-tests/src/main.rs index 9623f41..3d64015 100644 --- a/crates/integration-tests/src/main.rs +++ b/crates/integration-tests/src/main.rs @@ -15,6 +15,7 @@ mod tests { pub mod libvirt_base_disks; pub mod libvirt_ignition; pub mod libvirt_port_forward; + pub mod libvirt_to_base_disk; pub mod libvirt_upload_disk; pub mod libvirt_verb; pub mod mount_feature; diff --git a/crates/integration-tests/src/tests/libvirt_to_base_disk.rs b/crates/integration-tests/src/tests/libvirt_to_base_disk.rs new file mode 100644 index 0000000..30593e9 --- /dev/null +++ b/crates/integration-tests/src/tests/libvirt_to_base_disk.rs @@ -0,0 +1,226 @@ +//! Integration tests for libvirt to-base-disk functionality +//! +//! Tests the to-base-disk command which creates base disk images for libvirt VMs: +//! - Basic to-base-disk creation +//! - to-base-disk with different options +//! - to-base-disk reuse behavior +//! - Integration with libvirt base-disks list + +use integration_tests::integration_test; +use itest::TestResult; +use xshell::cmd; + +use regex::Regex; + +use crate::{get_bck_command, get_test_image, shell}; + +/// Test basic to-base-disk command functionality +fn test_to_base_disk_basic() -> TestResult { + let sh = shell()?; + let bck = get_bck_command()?; + let test_image = get_test_image(); + + println!("Testing basic to-base-disk functionality"); + + // Run to-base-disk command + let stdout = cmd!(sh, "{bck} libvirt to-base-disk {test_image}").read()?; + + println!("to-base-disk output: {}", stdout); + + // Should indicate successful creation + assert!( + stdout.contains("Created base disk:") || stdout.contains("Using cached"), + "Should show creation or reuse of base disk, got: {}", + stdout + ); + + // Extract disk path from output + let disk_path_regex = Regex::new(r"Created base disk: (.+)").unwrap(); + let disk_path = if let Some(captures) = disk_path_regex.captures(&stdout) { + captures.get(1).unwrap().as_str() + } else { + // If it's a cached disk, we need to check the list instead + println!("Base disk was cached, checking list..."); + let list_output = cmd!(sh, "{bck} libvirt base-disks list").read()?; + assert!( + !list_output.contains("No base disk"), + "Should have at least one base disk after creation" + ); + return Ok(()); + }; + + println!("Created disk path: {}", disk_path); + + // Verify the disk file exists + assert!( + std::path::Path::new(disk_path).exists(), + "Created disk file should exist at: {}", + disk_path + ); + + // Verify it shows up in base-disks list + let list_output = cmd!(sh, "{bck} libvirt base-disks list").read()?; + println!("base-disks list after creation:\n{}", list_output); + + // Should not be empty and should contain our disk + assert!( + !list_output.contains("No base disk"), + "Should have base disks after creation" + ); + + println!("✓ Basic to-base-disk test passed"); + Ok(()) +} +integration_test!(test_to_base_disk_basic); + +/// Test to-base-disk with filesystem option +fn test_to_base_disk_with_filesystem() -> TestResult { + let sh = shell()?; + let bck = get_bck_command()?; + let test_image = get_test_image(); + + println!("Testing to-base-disk with --filesystem option"); + + // Run to-base-disk command with ext4 filesystem + let stdout = cmd!( + sh, + "{bck} libvirt to-base-disk --filesystem ext4 {test_image}" + ) + .read()?; + + println!("to-base-disk --filesystem ext4 output: {}", stdout); + + // Should indicate successful creation + assert!( + stdout.contains("Created base disk:") || stdout.contains("Using cached"), + "Should show creation or reuse of base disk with filesystem option, got: {}", + stdout + ); + + println!("✓ to-base-disk with filesystem option test passed"); + Ok(()) +} +integration_test!(test_to_base_disk_with_filesystem); + +/// Test to-base-disk reuse behavior +fn test_to_base_disk_reuse() -> TestResult { + let sh = shell()?; + let bck = get_bck_command()?; + let test_image = get_test_image(); + + println!("Testing to-base-disk reuse behavior"); + + // First call - might create or reuse existing + let stdout1 = cmd!(sh, "{bck} libvirt to-base-disk {test_image}").read()?; + println!("First to-base-disk call: {}", stdout1); + + // Second call with same image - should reuse + let stdout2 = cmd!(sh, "{bck} libvirt to-base-disk {test_image}").read()?; + println!("Second to-base-disk call: {}", stdout2); + + // At least one should show base disk creation/usage + assert!( + stdout1.contains("Created base disk:") + || stdout1.contains("Using cached") + || stdout2.contains("Created base disk:") + || stdout2.contains("Using cached"), + "Should show base disk creation or reuse" + ); + + println!("✓ to-base-disk reuse test passed"); + Ok(()) +} +integration_test!(test_to_base_disk_reuse); + +/// Test to-base-disk with root-size option +fn test_to_base_disk_with_root_size() -> TestResult { + let sh = shell()?; + let bck = get_bck_command()?; + let test_image = get_test_image(); + + println!("Testing to-base-disk with --root-size option"); + + // Run to-base-disk command with custom root size + let stdout = cmd!( + sh, + "{bck} libvirt to-base-disk --root-size 15G {test_image}" + ) + .read()?; + + println!("to-base-disk --root-size 15G output: {}", stdout); + + // Should indicate successful creation + assert!( + stdout.contains("Created base disk:") || stdout.contains("Using cached"), + "Should show creation or reuse of base disk with root-size option, got: {}", + stdout + ); + + println!("✓ to-base-disk with root-size option test passed"); + Ok(()) +} +integration_test!(test_to_base_disk_with_root_size); + +/// Test that to-base-disk integrates properly with base-disks commands +fn test_to_base_disk_integration_with_list() -> TestResult { + let sh = shell()?; + let bck = get_bck_command()?; + let test_image = get_test_image(); + + println!("Testing to-base-disk integration with base-disks list"); + + // Get initial count + let initial_list = cmd!(sh, "{bck} libvirt base-disks list").read()?; + let initial_count = if initial_list.contains("No base disk") { + 0 + } else { + // Count lines with disk entries (skip header and summary) + initial_list + .lines() + .filter(|line| { + !line.contains("NAME") && !line.contains("Found") && !line.trim().is_empty() + }) + .count() + }; + + println!("Initial base disk count: {}", initial_count); + + // Create base disk + let stdout = cmd!(sh, "{bck} libvirt to-base-disk {test_image}").read()?; + println!("to-base-disk output: {}", stdout); + + // Check final count + let final_list = cmd!(sh, "{bck} libvirt base-disks list").read()?; + println!("Final base-disks list:\n{}", final_list); + + if stdout.contains("Created base disk:") { + // If we created a new disk, count should increase + let final_count = final_list + .lines() + .filter(|line| { + !line.contains("NAME") && !line.contains("Found") && !line.trim().is_empty() + }) + .count(); + + assert!( + final_count > initial_count, + "Base disk count should increase after creation" + ); + } else { + // If we reused existing, should still have disks listed + assert!( + !final_list.contains("No base disk"), + "Should still have base disks listed after reuse" + ); + } + + // Verify the list shows proper columns + assert!( + final_list.contains("NAME") && final_list.contains("SIZE"), + "List should show proper table headers" + ); + + println!("✓ to-base-disk integration with list test passed"); + Ok(()) +} +integration_test!(test_to_base_disk_integration_with_list); diff --git a/crates/kit/src/libvirt/base_disks_cli.rs b/crates/kit/src/libvirt/base_disks_cli.rs index 1771d2f..2082e47 100644 --- a/crates/kit/src/libvirt/base_disks_cli.rs +++ b/crates/kit/src/libvirt/base_disks_cli.rs @@ -8,8 +8,10 @@ use color_eyre::Result; use comfy_table::{presets::UTF8_FULL, Table}; use serde_json; -use super::base_disks::{list_base_disks, prune_base_disks}; +use super::base_disks::{find_or_create_base_disk, list_base_disks, prune_base_disks}; use super::OutputFormat; +use crate::images; +use crate::install_options::InstallOptions; /// Options for base-disks command #[derive(Debug, Parser)] @@ -18,6 +20,14 @@ pub struct LibvirtBaseDisksOpts { pub command: BaseDisksSubcommand, } +/// Options for base-disks create command +#[derive(Debug, Parser)] +pub struct CreateBaseDiskOpts { + pub source_image: String, + #[clap(flatten)] + pub install_options: InstallOptions, +} + /// Base disk subcommands #[derive(Debug, Subcommand)] pub enum BaseDisksSubcommand { @@ -53,6 +63,26 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtBaseDisksO } } +/// Execute the to-base-disk command (standalone) +pub fn run_create( + global_opts: &crate::libvirt::LibvirtOptions, + opts: CreateBaseDiskOpts, +) -> Result<()> { + let connect_uri = global_opts.connect.as_deref(); + let inspect = images::inspect(&opts.source_image)?; + let image_digest = inspect.digest.to_string(); + + let path = find_or_create_base_disk( + &opts.source_image, + &image_digest, + &opts.install_options, + connect_uri, + )?; + println!("Created base disk: {path}"); + + Ok(()) +} + /// Execute the list subcommand fn run_list(connect_uri: Option<&str>, opts: ListOpts) -> Result<()> { let base_disks = list_base_disks(connect_uri)?; @@ -114,12 +144,12 @@ fn run_list(connect_uri: Option<&str>, opts: ListOpts) -> Result<()> { OutputFormat::Yaml => { return Err(color_eyre::eyre::eyre!( "YAML format is not supported for base-disks list command" - )) + )); } OutputFormat::Xml => { return Err(color_eyre::eyre::eyre!( "XML format is not supported for base-disks list command" - )) + )); } } diff --git a/crates/kit/src/libvirt/mod.rs b/crates/kit/src/libvirt/mod.rs index 7e1bc17..49f8a30 100644 --- a/crates/kit/src/libvirt/mod.rs +++ b/crates/kit/src/libvirt/mod.rs @@ -222,6 +222,10 @@ pub enum LibvirtSubcommands { #[clap(name = "base-disks")] BaseDisks(base_disks_cli::LibvirtBaseDisksOpts), + /// Create a base disk image for libvirt VMs + #[clap(name = "to-base-disk")] + ToBaseDisk(base_disks_cli::CreateBaseDiskOpts), + /// Print detected firmware paths and configuration #[clap(name = "print-firmware", hide = true)] PrintFirmware(print_firmware::LibvirtPrintFirmwareOpts), diff --git a/crates/kit/src/libvirt/run.rs b/crates/kit/src/libvirt/run.rs index 43e3b78..5dd412f 100644 --- a/crates/kit/src/libvirt/run.rs +++ b/crates/kit/src/libvirt/run.rs @@ -683,6 +683,14 @@ fn ensure_default_pool(connect_uri: Option<&str>) -> Result<()> { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let _ = std::fs::remove_file(xml_path); + + // Check if the error is because the pool already exists + if stderr.contains("already exists") { + // Pool was created by race between our tests + info!("Default storage pool already exists, continuing"); + return Ok(()); + } + return Err(color_eyre::eyre::eyre!( "Failed to define default pool: {}", stderr @@ -951,7 +959,7 @@ fn check_libvirt_readonly_support() -> Result<()> { "Could not parse libvirt version. \ The --bind-storage-ro flag requires libvirt 11.0+ with rust-based virtiofsd support. \ Please ensure you have a compatible libvirt version installed." - )) + )), } } } diff --git a/crates/kit/src/main.rs b/crates/kit/src/main.rs index eb8c877..690c79d 100644 --- a/crates/kit/src/main.rs +++ b/crates/kit/src/main.rs @@ -311,6 +311,9 @@ fn main() -> Result<(), Report> { libvirt::LibvirtSubcommands::BaseDisks(opts) => { libvirt::base_disks_cli::run(&options, opts)? } + libvirt::LibvirtSubcommands::ToBaseDisk(opts) => { + libvirt::base_disks_cli::run_create(&options, opts)? + } libvirt::LibvirtSubcommands::PrintFirmware(opts) => { libvirt::print_firmware::run(opts)? } diff --git a/docs/src/man/bcvk-libvirt-run.md b/docs/src/man/bcvk-libvirt-run.md index f62a3f9..1b83383 100644 --- a/docs/src/man/bcvk-libvirt-run.md +++ b/docs/src/man/bcvk-libvirt-run.md @@ -138,6 +138,10 @@ Run a bootable container as a persistent VM Disable TPM 2.0 support (enabled by default) +**--firmware-log** + + Enable firmware debug log (captures OVMF/EDK2 DEBUG output via isa-debugcon) + **--secure-boot-keys**=*SECURE_BOOT_KEYS* Directory containing secure boot keys (required for uefi-secure) @@ -150,6 +154,10 @@ Run a bootable container as a persistent VM Create a transient VM that disappears on shutdown/reboot +**--ignition**=*IGNITION_CONFIG* + + Path to Ignition config file (JSON format) for first-boot provisioning + # EXAMPLES diff --git a/docs/src/man/bcvk-libvirt-to-base-disk.md b/docs/src/man/bcvk-libvirt-to-base-disk.md new file mode 100644 index 0000000..649977e --- /dev/null +++ b/docs/src/man/bcvk-libvirt-to-base-disk.md @@ -0,0 +1,64 @@ +# NAME + +bcvk-libvirt-to-base-disk - Create a base disk image for libvirt VMs + +# SYNOPSIS + +**bcvk libvirt to-base-disk** [*OPTIONS*] + +# DESCRIPTION + +Create a base disk image for libvirt VMs + +# OPTIONS + + +**SOURCE_IMAGE** + + This argument is required. + +**--filesystem**=*FILESYSTEM* + + Root filesystem type (e.g. ext4, xfs, btrfs) + +**--root-size**=*ROOT_SIZE* + + Root filesystem size (e.g., '10G', '5120M') + +**--storage-path**=*STORAGE_PATH* + + Path to host container storage (auto-detected if not specified) + +**--target-transport**=*TARGET_TRANSPORT* + + The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry` + +**--karg**=*KARG* + + Set a kernel argument + +**--composefs-backend** + + Default to composefs-native storage + +**--bootloader**=*BOOTLOADER* + + Which bootloader to use for composefs-native backend + +**--allow-missing-fsverity** + + Allow installation without fs-verity support for composefs-native backend + + + +# EXAMPLES + +TODO: Add practical examples showing how to use this command. + +# SEE ALSO + +**bcvk**(8) + +# VERSION + +