From 052afbf38dfbac19a52a37c7a991031036b799b9 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 29 Apr 2026 12:44:51 +0530 Subject: [PATCH 1/3] cli: Add command to create base disk For https://github.com/bootc-dev/bootc/issues/2172 it would be useful for us to create the base disk manually and not delegate it to the `bcvk libvirt run` command Signed-off-by: Pragyan Poudyal --- crates/kit/src/libvirt/base_disks_cli.rs | 36 ++++++++++++++++++++++-- crates/kit/src/libvirt/mod.rs | 4 +++ crates/kit/src/main.rs | 3 ++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crates/kit/src/libvirt/base_disks_cli.rs b/crates/kit/src/libvirt/base_disks_cli.rs index 1771d2f7..2082e476 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 7e1bc17f..49f8a30a 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/main.rs b/crates/kit/src/main.rs index eb8c8777..690c79d0 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)? } From aae5195fb312e06ddb6034ce942796a5e19f12b4 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 5 May 2026 13:47:08 +0530 Subject: [PATCH 2/3] tests: Add integration test for `libvirt to-base-disk` Assisted-by: Claude Code (Opus 4) Signed-off-by: Pragyan Poudyal --- crates/integration-tests/src/main.rs | 1 + .../src/tests/libvirt_to_base_disk.rs | 226 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 crates/integration-tests/src/tests/libvirt_to_base_disk.rs diff --git a/crates/integration-tests/src/main.rs b/crates/integration-tests/src/main.rs index 9623f418..3d64015a 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 00000000..30593e90 --- /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); From 832e23591236b1f2374c4f4c27b55a9a8f384bbf Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 5 May 2026 15:39:49 +0530 Subject: [PATCH 3/3] pool: Handle default pool create race Our tests can race to create a default pool if one doesn't already exist which causes tests to fail as they try to create a pool that already exists If pool creation fails and stderr contains "already exists", don't consider it as an error Signed-off-by: Pragyan Poudyal --- crates/kit/src/libvirt/run.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/kit/src/libvirt/run.rs b/crates/kit/src/libvirt/run.rs index 43e3b786..5dd412fd 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." - )) + )), } } }