diff --git a/crates/integration-tests/src/main.rs b/crates/integration-tests/src/main.rs index 5668546..033c0f8 100644 --- a/crates/integration-tests/src/main.rs +++ b/crates/integration-tests/src/main.rs @@ -1,4 +1,5 @@ use camino::Utf8Path; +use std::process::Output; use color_eyre::eyre::{eyre, Context}; use color_eyre::Result; @@ -63,6 +64,58 @@ pub(crate) fn get_alternative_test_image() -> String { } } +/// Captured output from a command with decoded stdout/stderr strings +pub(crate) struct CapturedOutput { + pub output: Output, + pub stdout: String, + pub stderr: String, +} + +impl CapturedOutput { + /// Create from a raw Output + pub fn new(output: Output) -> Self { + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + Self { + output, + stdout, + stderr, + } + } + + /// Assert that the command succeeded, printing debug info on failure + pub fn assert_success(&self, context: &str) { + assert!( + self.output.status.success(), + "{} failed: {}", + context, + self.stderr + ); + } + + /// Get the exit code + pub fn exit_code(&self) -> Option { + self.output.status.code() + } + + /// Check if the command succeeded + pub fn success(&self) -> bool { + self.output.status.success() + } +} + +/// Run a command, capturing output +pub(crate) fn run_command(program: &str, args: &[&str]) -> std::io::Result { + let output = std::process::Command::new(program).args(args).output()?; + Ok(CapturedOutput::new(output)) +} + +/// Run the bcvk command, capturing output +pub(crate) fn run_bcvk(args: &[&str]) -> std::io::Result { + let bck = get_bck_command().expect("Failed to get bcvk command"); + run_command(&bck, args) +} + fn test_images_list() -> Result<()> { println!("Running test: bcvk images list --json"); diff --git a/crates/integration-tests/src/tests/run_ephemeral.rs b/crates/integration-tests/src/tests/run_ephemeral.rs index fc163ff..1240dfc 100644 --- a/crates/integration-tests/src/tests/run_ephemeral.rs +++ b/crates/integration-tests/src/tests/run_ephemeral.rs @@ -18,7 +18,7 @@ use std::process::Command; use tracing::debug; -use crate::{get_bck_command, get_test_image, INTEGRATION_TEST_LABEL}; +use crate::{get_test_image, run_bcvk, INTEGRATION_TEST_LABEL}; pub fn get_container_kernel_version(image: &str) -> String { // Run container to get its kernel version @@ -45,203 +45,115 @@ pub fn get_container_kernel_version(image: &str) -> String { pub fn test_run_ephemeral_correct_kernel() { let image = get_test_image(); - let bck = get_bck_command().unwrap(); - - // Get the kernel version from the container image let container_kernel = get_container_kernel_version(&image); eprintln!("Container kernel version: {}", container_kernel); - // Run the ephemeral VM with poweroff.target - // We can't easily capture the kernel version from inside the VM, - // but we can verify that we're using the container's kernel by - // checking that the kernel files exist and are being used - let output = Command::new("timeout") - .args([ - "120s", - &bck, - "ephemeral", - "run", - "--rm", - "--label", - INTEGRATION_TEST_LABEL, - &image, - "--karg", - "systemd.unit=poweroff.target", - ]) - .output() - .expect("Failed to run bcvk ephemeral run"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - eprintln!("stdout: {}", stdout); - eprintln!("stderr: {}", stderr); - - // Check that the command completed successfully - assert!(output.status.success(), "ephemeral run failed: {}", stderr); - - // The test passing means we successfully booted with the container's kernel - // (since we fixed the code to look in /run/source-image/usr/lib/modules) - eprintln!( - "Successfully booted with container kernel version: {}", - container_kernel - ); + let output = run_bcvk(&[ + "ephemeral", + "run", + "--rm", + "--label", + INTEGRATION_TEST_LABEL, + &image, + "--karg", + "systemd.unit=poweroff.target", + ]) + .expect("Failed to run bcvk ephemeral run"); + + output.assert_success("ephemeral run"); } pub fn test_run_ephemeral_poweroff() { - let bck = get_bck_command().unwrap(); - - // Run the ephemeral VM with poweroff.target - // This should boot the VM and immediately shut it down - // Using timeout command to ensure test doesn't hang - let output = Command::new("timeout") - .args([ - "120s", - &bck, - "ephemeral", - "run", - "--rm", - "--label", - INTEGRATION_TEST_LABEL, - &get_test_image(), - "--karg", - "systemd.unit=poweroff.target", - ]) - .output() - .expect("Failed to run bcvk ephemeral run"); - - // Check that the command completed successfully - assert!( - output.status.success(), - "ephemeral run failed: {}", - String::from_utf8_lossy(&output.stderr) - ); + let output = run_bcvk(&[ + "ephemeral", + "run", + "--rm", + "--label", + INTEGRATION_TEST_LABEL, + &get_test_image(), + "--karg", + "systemd.unit=poweroff.target", + ]) + .expect("Failed to run bcvk ephemeral run"); + + output.assert_success("ephemeral run"); } pub fn test_run_ephemeral_with_memory_limit() { - let bck = get_bck_command().unwrap(); - - // Run with custom memory limit - let output = Command::new("timeout") - .args([ - "120s", - &bck, - "ephemeral", - "run", - "--rm", - "--label", - INTEGRATION_TEST_LABEL, - "--memory", - "1024", - "--karg", - "systemd.unit=poweroff.target", - &get_test_image(), - ]) - .output() - .expect("Failed to run bcvk ephemeral run"); - - assert!( - output.status.success(), - "ephemeral run with memory limit failed: {}", - String::from_utf8_lossy(&output.stderr) - ); + let output = run_bcvk(&[ + "ephemeral", + "run", + "--rm", + "--label", + INTEGRATION_TEST_LABEL, + "--memory", + "1024", + "--karg", + "systemd.unit=poweroff.target", + &get_test_image(), + ]) + .expect("Failed to run bcvk ephemeral run"); + + output.assert_success("ephemeral run with memory limit"); } pub fn test_run_ephemeral_with_vcpus() { - let bck = get_bck_command().unwrap(); - - // Run with custom vcpu count - let output = Command::new("timeout") - .args([ - "120s", - &bck, - "ephemeral", - "run", - "--rm", - "--label", - INTEGRATION_TEST_LABEL, - "--vcpus", - "2", - "--karg", - "systemd.unit=poweroff.target", - &get_test_image(), - ]) - .output() - .expect("Failed to run bcvk ephemeral run"); - - assert!( - output.status.success(), - "ephemeral run with vcpus failed: {}", - String::from_utf8_lossy(&output.stderr) - ); + let output = run_bcvk(&[ + "ephemeral", + "run", + "--rm", + "--label", + INTEGRATION_TEST_LABEL, + "--vcpus", + "2", + "--karg", + "systemd.unit=poweroff.target", + &get_test_image(), + ]) + .expect("Failed to run bcvk ephemeral run"); + + output.assert_success("ephemeral run with vcpus"); } pub fn test_run_ephemeral_execute() { - let bck = get_bck_command().unwrap(); - - // Run with --execute option to run a simple script let script = "/bin/sh -c \"echo 'Hello from VM'; echo 'Current date:'; date; echo 'Script completed successfully'\""; - let output = Command::new("timeout") - .args([ - "120s", - &bck, - "ephemeral", - "run", - "--rm", - "--label", - INTEGRATION_TEST_LABEL, - "--execute", - script, - &get_test_image(), - ]) - .output() - .expect("Failed to run bcvk ephemeral run with --execute"); + let output = run_bcvk(&[ + "ephemeral", + "run", + "--rm", + "--label", + INTEGRATION_TEST_LABEL, + "--execute", + script, + &get_test_image(), + ]) + .expect("Failed to run bcvk ephemeral run with --execute"); - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); + output.assert_success("ephemeral run with --execute"); - eprintln!("execute test stdout: {}", stdout); - eprintln!("execute test stderr: {}", stderr); - - // Check that the command completed successfully assert!( - output.status.success(), - "ephemeral run with --execute failed: {}", - stderr - ); - - // Verify that our script output appears in stdout - assert!( - stdout.contains("Hello from VM"), + output.stdout.contains("Hello from VM"), "Script output 'Hello from VM' not found in stdout: {}", - stdout + output.stdout ); assert!( - stdout.contains("Script completed successfully"), + output.stdout.contains("Script completed successfully"), "Script completion message not found in stdout: {}", - stdout + output.stdout ); - // Verify that the date command output is present assert!( - stdout.contains("Current date:"), + output.stdout.contains("Current date:"), "Date output header not found in stdout: {}", - stdout + output.stdout ); - - eprintln!("Execute test passed: script output captured successfully"); } pub fn test_run_ephemeral_container_ssh_access() { let image = get_test_image(); - let bck = get_bck_command().unwrap(); - - eprintln!("Testing container-based SSH access"); - - // Generate a unique container name let container_name = format!( "ssh-test-{}", std::time::SystemTime::now() @@ -250,74 +162,39 @@ pub fn test_run_ephemeral_container_ssh_access() { .as_secs() ); - eprintln!( - "Starting detached VM with container name: {}", - container_name - ); - - // Start VM with SSH in detached mode - let output = Command::new(&bck) - .args([ - "ephemeral", - "run", - "--ssh-keygen", - "--label", - INTEGRATION_TEST_LABEL, - "--detach", - "--name", - &container_name, - &image, - ]) - .output() - .expect("Failed to start detached VM with SSH"); - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - panic!("Failed to start detached VM: {}", stderr); + let output = run_bcvk(&[ + "ephemeral", + "run", + "--ssh-keygen", + "--label", + INTEGRATION_TEST_LABEL, + "--detach", + "--name", + &container_name, + &image, + ]) + .expect("Failed to start detached VM with SSH"); + + if !output.success() { + panic!("Failed to start detached VM: {}", output.stderr); } - let stdout = String::from_utf8_lossy(&output.stdout); - eprintln!( - "Detached VM started:\nstdout: {}\nstderr: {}", - stdout, - String::from_utf8_lossy(&output.stderr) - ); + let ssh_output = run_bcvk(&[ + "ephemeral", + "ssh", + &container_name, + "echo", + "SSH_TEST_SUCCESS", + ]) + .expect("Failed to run SSH command"); - // Try to SSH into the VM via container (with a simple command) - eprintln!("Attempting SSH connection via container..."); - let ssh_output = Command::new("timeout") - .args([ - "120s", // Give plenty of time for VM boot and SSH to become ready - &bck, - "ephemeral", - "ssh", - &container_name, - "echo", - "SSH_TEST_SUCCESS", - ]) - .output() - .expect("Failed to run SSH command"); - - let ssh_stdout = String::from_utf8_lossy(&ssh_output.stdout); - let ssh_stderr = String::from_utf8_lossy(&ssh_output.stderr); - - debug!("SSH exit status: {:?}", ssh_output.status.code()); - eprintln!("SSH stdout: {}", ssh_stdout); - eprintln!("SSH stderr: {}", ssh_stderr); + debug!("SSH exit status: {:?}", ssh_output.exit_code()); // Cleanup: stop the container - let cleanup_output = Command::new("podman") + let _ = Command::new("podman") .args(["stop", &container_name]) .output(); - if let Ok(cleanup) = cleanup_output { - eprintln!( - "Container cleanup: {}", - String::from_utf8_lossy(&cleanup.stdout) - ); - } - - // Check if SSH worked - assert!(ssh_output.status.success()); - assert!(ssh_stdout.contains("SSH_TEST_SUCCESS")); + assert!(ssh_output.success()); + assert!(ssh_output.stdout.contains("SSH_TEST_SUCCESS")); } diff --git a/crates/integration-tests/src/tests/run_ephemeral_ssh.rs b/crates/integration-tests/src/tests/run_ephemeral_ssh.rs index 58d1e36..65ef2c0 100644 --- a/crates/integration-tests/src/tests/run_ephemeral_ssh.rs +++ b/crates/integration-tests/src/tests/run_ephemeral_ssh.rs @@ -18,92 +18,53 @@ use std::process::Command; use std::thread; use std::time::Duration; -use crate::{get_alternative_test_image, get_bck_command, get_test_image, INTEGRATION_TEST_LABEL}; +use crate::{get_alternative_test_image, get_test_image, run_bcvk, INTEGRATION_TEST_LABEL}; /// Test running a non-interactive command via SSH pub fn test_run_ephemeral_ssh_command() { - let bck = get_bck_command().unwrap(); + let output = run_bcvk(&[ + "ephemeral", + "run-ssh", + "--label", + INTEGRATION_TEST_LABEL, + &get_test_image(), + "--", + "echo", + "hello world from SSH", + ]) + .expect("Failed to run bcvk ephemeral run-ssh"); + + output.assert_success("ephemeral run-ssh"); - eprintln!("Testing ephemeral run-ssh with command execution..."); - - // Run ephemeral SSH with a simple echo command - let output = Command::new("timeout") - .args([ - "60s", - &bck, - "ephemeral", - "run-ssh", - "--label", - INTEGRATION_TEST_LABEL, - &get_test_image(), - "--", - "echo", - "hello world from SSH", - ]) - .output() - .expect("Failed to run bcvk ephemeral run-ssh"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - eprintln!("stdout: {}", stdout); - eprintln!("stderr: {}", stderr); - - // Check that the command completed successfully assert!( - output.status.success(), - "ephemeral run-ssh failed: {}", - stderr - ); - - // Check that we got the expected output - assert!( - stdout.contains("hello world from SSH"), + output.stdout.contains("hello world from SSH"), "Expected output not found. Got: {}", - stdout + output.stdout ); - - eprintln!("Successfully executed command via SSH and received output"); } /// Test that the container is cleaned up when SSH exits pub fn test_run_ephemeral_ssh_cleanup() { - let bck = get_bck_command().unwrap(); - - eprintln!("Testing ephemeral run-ssh cleanup behavior..."); - - // Generate a unique container name for this test let container_name = format!("test-ssh-cleanup-{}", std::process::id()); - // Run ephemeral SSH with a simple command - let output = Command::new("timeout") - .args([ - "60s", - &bck, - "ephemeral", - "run-ssh", - "--name", - &container_name, - "--label", - INTEGRATION_TEST_LABEL, - &get_test_image(), - "--", - "echo", - "testing cleanup", - ]) - .output() - .expect("Failed to run bcvk ephemeral run-ssh"); - - assert!( - output.status.success(), - "ephemeral run-ssh failed: {}", - String::from_utf8_lossy(&output.stderr) - ); + let output = run_bcvk(&[ + "ephemeral", + "run-ssh", + "--name", + &container_name, + "--label", + INTEGRATION_TEST_LABEL, + &get_test_image(), + "--", + "echo", + "testing cleanup", + ]) + .expect("Failed to run bcvk ephemeral run-ssh"); + + output.assert_success("ephemeral run-ssh"); - // Give a moment for cleanup to complete thread::sleep(Duration::from_secs(1)); - // Check that the container no longer exists let check_output = Command::new("podman") .args(["ps", "-a", "--format", "{{.Names}}"]) .output() @@ -116,149 +77,87 @@ pub fn test_run_ephemeral_ssh_cleanup() { container_name, containers ); - - eprintln!("Container was successfully cleaned up after SSH exit"); } /// Test running system commands via SSH pub fn test_run_ephemeral_ssh_system_command() { - let bck = get_bck_command().unwrap(); - - eprintln!("Testing ephemeral run-ssh with system command..."); - - // Run ephemeral SSH with systemctl command - using /bin/sh -c for shell operators - let output = Command::new("timeout") - .args([ - "60s", - &bck, - "ephemeral", - "run-ssh", - "--label", - INTEGRATION_TEST_LABEL, - &get_test_image(), - "--", - "/bin/sh", - "-c", - "systemctl is-system-running || true", // Shell command with operator - ]) - .output() - .expect("Failed to run bcvk ephemeral run-ssh"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - eprintln!("stdout: {}", stdout); - eprintln!("stderr: {}", stderr); - - // The command should complete (even if system is degraded) - assert!( - output.status.success(), - "ephemeral run-ssh failed: {}", - stderr - ); - - eprintln!("Successfully executed system command via SSH"); + let output = run_bcvk(&[ + "ephemeral", + "run-ssh", + "--label", + INTEGRATION_TEST_LABEL, + &get_test_image(), + "--", + "/bin/sh", + "-c", + "systemctl is-system-running || true", + ]) + .expect("Failed to run bcvk ephemeral run-ssh"); + + output.assert_success("ephemeral run-ssh"); } /// Test that ephemeral run-ssh properly forwards exit codes pub fn test_run_ephemeral_ssh_exit_code() { - let bck = get_bck_command().unwrap(); - - eprintln!("Testing ephemeral run-ssh exit code forwarding..."); - - // Run a command that exits with non-zero code - let output = Command::new("timeout") - .args([ - "60s", - &bck, - "ephemeral", - "run-ssh", - "--label", - INTEGRATION_TEST_LABEL, - &get_test_image(), - "--", - "exit", - "42", - ]) - .output() - .expect("Failed to run bcvk ephemeral run-ssh"); - - // Check that the exit code was properly forwarded - let exit_code = output.status.code().expect("Failed to get exit code"); + let output = run_bcvk(&[ + "ephemeral", + "run-ssh", + "--label", + INTEGRATION_TEST_LABEL, + &get_test_image(), + "--", + "exit", + "42", + ]) + .expect("Failed to run bcvk ephemeral run-ssh"); + + let exit_code = output.exit_code().expect("Failed to get exit code"); assert_eq!( exit_code, 42, "Exit code not properly forwarded. Expected 42, got {}", exit_code ); - - eprintln!("Exit code was properly forwarded"); } /// Test SSH functionality across different bootc images (Fedora and CentOS) /// This test verifies that our systemd version compatibility fix works correctly /// with both newer systemd (Fedora) and older systemd (CentOS Stream 9) pub fn test_run_ephemeral_ssh_cross_distro_compatibility() { - let bck = get_bck_command().unwrap(); - - // Test with primary image (usually Fedora) - test_ssh_with_image(&bck, &get_test_image(), "primary"); - - // Test with alternative image (usually CentOS Stream 9) - test_ssh_with_image(&bck, &get_alternative_test_image(), "alternative"); + test_ssh_with_image(&get_test_image(), "primary"); + test_ssh_with_image(&get_alternative_test_image(), "alternative"); } -fn test_ssh_with_image(bck: &str, image: &str, image_type: &str) { - eprintln!( - "Testing SSH functionality with {} image: {}", - image_type, image - ); +fn test_ssh_with_image(image: &str, image_type: &str) { + let output = run_bcvk(&[ + "ephemeral", + "run-ssh", + "--label", + INTEGRATION_TEST_LABEL, + image, + "--", + "systemctl", + "--version", + ]) + .expect("Failed to run bcvk ephemeral run-ssh"); - // Test basic SSH connectivity and systemd status - let output = Command::new("timeout") - .args([ - "90s", // Longer timeout for potentially slower images - bck, - "ephemeral", - "run-ssh", - "--label", - INTEGRATION_TEST_LABEL, - image, - "--", - "systemctl", - "--version", - ]) - .output() - .expect("Failed to run bcvk ephemeral run-ssh"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - eprintln!("=== {} image output ===", image_type); - eprintln!("stdout: {}", stdout); - eprintln!("stderr: {}", stderr); - eprintln!("exit code: {:?}", output.status.code()); - - // Check that the SSH connection was successful assert!( - output.status.success(), + output.success(), "{} image SSH test failed: {}", image_type, - stderr + output.stderr ); - // Verify we got systemd version output assert!( - stdout.contains("systemd"), + output.stdout.contains("systemd"), "{} image: systemd version not found. Got: {}", image_type, - stdout + output.stdout ); - // Extract and log systemd version for compatibility verification - if let Some(version_line) = stdout.lines().next() { + // Log systemd version for diagnostic purposes + if let Some(version_line) = output.stdout.lines().next() { eprintln!("{} image systemd version: {}", image_type, version_line); - // Parse the version number let version_parts: Vec<&str> = version_line.split_whitespace().collect(); if version_parts.len() >= 2 { if let Ok(version_num) = version_parts[1].parse::() { @@ -276,6 +175,4 @@ fn test_ssh_with_image(bck: &str, image: &str, image_type: &str) { } } } - - eprintln!("✓ {} image SSH test passed", image_type); } diff --git a/crates/integration-tests/src/tests/to_disk.rs b/crates/integration-tests/src/tests/to_disk.rs index 5f44e10..f45917a 100644 --- a/crates/integration-tests/src/tests/to_disk.rs +++ b/crates/integration-tests/src/tests/to_disk.rs @@ -18,54 +18,35 @@ use camino::Utf8PathBuf; use std::process::Command; use tempfile::TempDir; -use crate::{get_bck_command, INTEGRATION_TEST_LABEL}; +use crate::{run_bcvk, INTEGRATION_TEST_LABEL}; /// Test actual bootc installation to a disk image pub fn test_to_disk() { - let bck = get_bck_command().unwrap(); - - // Create a temporary disk image file let temp_dir = TempDir::new().expect("Failed to create temp directory"); let disk_path = Utf8PathBuf::try_from(temp_dir.path().join("test-disk.img")) .expect("temp path is not UTF-8"); - println!("Running installation to temporary disk: {}", disk_path); - - // Run the installation with timeout - let output = Command::new("timeout") - .args([ - "600s", // 10 minute timeout for installation - &bck, - "to-disk", - "--label", - INTEGRATION_TEST_LABEL, - "quay.io/centos-bootc/centos-bootc:stream10", - disk_path.as_str(), - ]) - .output() - .expect("Failed to run bcvk to-disk"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - println!("Installation output:"); - println!("stdout:\n{}", stdout); - println!("stderr:\n{}", stderr); + let output = run_bcvk(&[ + "to-disk", + "--label", + INTEGRATION_TEST_LABEL, + "quay.io/centos-bootc/centos-bootc:stream10", + disk_path.as_str(), + ]) + .expect("Failed to run bcvk to-disk"); - // Check that the command completed successfully assert!( - output.status.success(), + output.success(), "to-disk failed with exit code: {:?}. stdout: {}, stderr: {}", - output.status.code(), - stdout, - stderr + output.exit_code(), + output.stdout, + output.stderr ); let metadata = std::fs::metadata(&disk_path).expect("Failed to get disk metadata"); assert!(metadata.len() > 0); // Verify the disk has partitions using sfdisk -l - println!("Verifying disk partitions with sfdisk -l"); let sfdisk_output = Command::new("sfdisk") .arg("-l") .arg(disk_path.as_str()) @@ -73,27 +54,19 @@ pub fn test_to_disk() { .expect("Failed to run sfdisk"); let sfdisk_stdout = String::from_utf8_lossy(&sfdisk_output.stdout); - let sfdisk_stderr = String::from_utf8_lossy(&sfdisk_output.stderr); - println!("sfdisk verification:"); - println!("stdout:\n{}", sfdisk_stdout); - println!("stderr:\n{}", sfdisk_stderr); - - // Check that sfdisk succeeded assert!( sfdisk_output.status.success(), "sfdisk failed with exit code: {:?}", sfdisk_output.status.code() ); - // Verify we have actual partitions (should contain partition table info) assert!( sfdisk_stdout.contains("Disk ") && (sfdisk_stdout.contains("sectors") || sfdisk_stdout.contains("bytes")), "sfdisk output doesn't show expected disk information" ); - // Look for evidence of bootc partitions (EFI, boot, root, etc.) let has_partitions = sfdisk_stdout.lines().any(|line| { line.contains(disk_path.as_str()) && (line.contains("Linux") || line.contains("EFI")) }); @@ -104,208 +77,125 @@ pub fn test_to_disk() { sfdisk_stdout ); - // Most importantly, check for "Installation complete" message from bootc assert!( - stdout.contains("Installation complete") || stderr.contains("Installation complete"), + output.stdout.contains("Installation complete") || output.stderr.contains("Installation complete"), "No 'Installation complete' message found in output. This indicates bootc install did not complete successfully. stdout: {}, stderr: {}", - stdout, stderr - ); - - println!( - "Installation successful - disk contains expected partitions and bootc reported completion" + output.stdout, output.stderr ); } /// Test bootc installation to a qcow2 disk image pub fn test_to_disk_qcow2() { - let bck = get_bck_command().unwrap(); - - // Create a temporary qcow2 disk image file let temp_dir = TempDir::new().expect("Failed to create temp directory"); let disk_path = Utf8PathBuf::try_from(temp_dir.path().join("test-disk.qcow2")) .expect("temp path is not UTF-8"); - println!( - "Running installation to temporary qcow2 disk: {}", - disk_path - ); - - // Run the installation with timeout and qcow2 format - let output = Command::new("timeout") - .args([ - "600s", // 10 minute timeout for installation - &bck, - "to-disk", - "--format=qcow2", - "--label", - INTEGRATION_TEST_LABEL, - "quay.io/centos-bootc/centos-bootc:stream10", - disk_path.as_str(), - ]) - .output() - .expect("Failed to run bcvk to-disk with qcow2 format"); + let output = run_bcvk(&[ + "to-disk", + "--format=qcow2", + "--label", + INTEGRATION_TEST_LABEL, + "quay.io/centos-bootc/centos-bootc:stream10", + disk_path.as_str(), + ]) + .expect("Failed to run bcvk to-disk with qcow2 format"); - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - println!("Installation output:"); - println!("stdout:\n{}", stdout); - println!("stderr:\n{}", stderr); - - // Check that the command completed successfully assert!( - output.status.success(), + output.success(), "to-disk with qcow2 failed with exit code: {:?}. stdout: {}, stderr: {}", - output.status.code(), - stdout, - stderr + output.exit_code(), + output.stdout, + output.stderr ); let metadata = std::fs::metadata(&disk_path).expect("Failed to get disk metadata"); assert!(metadata.len() > 0); // Verify the file is actually qcow2 format using qemu-img info - println!("Verifying qcow2 format with qemu-img info"); let qemu_img_output = Command::new("qemu-img") .args(["info", disk_path.as_str()]) .output() .expect("Failed to run qemu-img info"); let qemu_img_stdout = String::from_utf8_lossy(&qemu_img_output.stdout); - let qemu_img_stderr = String::from_utf8_lossy(&qemu_img_output.stderr); - - println!("qemu-img info output:"); - println!("stdout:\n{}", qemu_img_stdout); - println!("stderr:\n{}", qemu_img_stderr); - // Check that qemu-img succeeded assert!( qemu_img_output.status.success(), "qemu-img info failed with exit code: {:?}", qemu_img_output.status.code() ); - // Verify the format is qcow2 assert!( qemu_img_stdout.contains("file format: qcow2"), "qemu-img info doesn't show qcow2 format. Output was:\n{}", qemu_img_stdout ); - // Verify the disk has partitions - // Note: sfdisk cannot read qcow2 files directly, we need to use qemu-nbd or verify differently - // Since we already verified the format is qcow2 and the installation completed successfully, - // we can skip partition table verification for qcow2 images or use qemu-nbd - - // For qcow2, the key checks are: - // 1. File exists and is non-zero (already checked) - // 2. Format is qcow2 (already checked) - // 3. Installation completed successfully (checked below) - - println!("Skipping partition table verification for qcow2 (sfdisk cannot read qcow2 directly)"); - - // Most importantly, check for "Installation complete" message from bootc assert!( - stdout.contains("Installation complete") || stderr.contains("Installation complete"), + output.stdout.contains("Installation complete") || output.stderr.contains("Installation complete"), "No 'Installation complete' message found in output. This indicates bootc install did not complete successfully. stdout: {}, stderr: {}", - stdout, stderr - ); - - println!( - "qcow2 installation successful - disk contains expected partitions, is in qcow2 format, and bootc reported completion" + output.stdout, output.stderr ); } /// Test disk image caching functionality pub fn test_to_disk_caching() { - let bck = get_bck_command().unwrap(); - - // Create a temporary disk image file let temp_dir = TempDir::new().expect("Failed to create temp directory"); let disk_path = Utf8PathBuf::try_from(temp_dir.path().join("test-disk-cache.img")) .expect("temp path is not UTF-8"); - println!("Testing disk image caching with: {}", disk_path); - // First run: Create the disk image - println!("=== First run: Creating initial disk image ==="); - let output1 = Command::new("timeout") - .args([ - "600s", // 10 minute timeout for installation - &bck, - "to-disk", - "--label", - INTEGRATION_TEST_LABEL, - "quay.io/centos-bootc/centos-bootc:stream10", - disk_path.as_str(), - ]) - .output() - .expect("Failed to run bcvk to-disk (first time)"); - - let stdout1 = String::from_utf8_lossy(&output1.stdout); - let stderr1 = String::from_utf8_lossy(&output1.stderr); + let output1 = run_bcvk(&[ + "to-disk", + "--label", + INTEGRATION_TEST_LABEL, + "quay.io/centos-bootc/centos-bootc:stream10", + disk_path.as_str(), + ]) + .expect("Failed to run bcvk to-disk (first time)"); - println!("First run output:"); - println!("stdout:\n{}", stdout1); - println!("stderr:\n{}", stderr1); - - // Check that the first run completed successfully assert!( - output1.status.success(), + output1.success(), "First to-disk run failed with exit code: {:?}. stdout: {}, stderr: {}", - output1.status.code(), - stdout1, - stderr1 + output1.exit_code(), + output1.stdout, + output1.stderr ); - // Verify the disk was created and has content let metadata1 = std::fs::metadata(&disk_path).expect("Failed to get disk metadata after first run"); assert!(metadata1.len() > 0, "Disk image is empty after first run"); - // Verify installation completed successfully assert!( - stdout1.contains("Installation complete") || stderr1.contains("Installation complete"), + output1.stdout.contains("Installation complete") + || output1.stderr.contains("Installation complete"), "No 'Installation complete' message found in first run output" ); // Second run: Should reuse the cached disk - println!("=== Second run: Should reuse cached disk image ==="); - let output2 = Command::new(&bck) - .args([ - "to-disk", - "--label", - INTEGRATION_TEST_LABEL, - "quay.io/centos-bootc/centos-bootc:stream10", - disk_path.as_str(), - ]) - .output() - .expect("Failed to run bcvk to-disk (second time)"); + let output2 = run_bcvk(&[ + "to-disk", + "--label", + INTEGRATION_TEST_LABEL, + "quay.io/centos-bootc/centos-bootc:stream10", + disk_path.as_str(), + ]) + .expect("Failed to run bcvk to-disk (second time)"); - let stdout2 = String::from_utf8_lossy(&output2.stdout); - let stderr2 = String::from_utf8_lossy(&output2.stderr); - - println!("Second run output:"); - println!("stdout:\n{}", stdout2); - println!("stderr:\n{}", stderr2); - - // Check that the second run completed successfully assert!( - output2.status.success(), + output2.success(), "Second to-disk run failed with exit code: {:?}. stdout: {}, stderr: {}", - output2.status.code(), - stdout2, - stderr2 + output2.exit_code(), + output2.stdout, + output2.stderr ); - // Verify cache was used (should see reusing message) assert!( - stdout2.contains("Reusing existing cached disk image"), + output2.stdout.contains("Reusing existing cached disk image"), "Second run should have reused cached disk, but cache reuse message not found. stdout: {}, stderr: {}", - stdout2, stderr2 + output2.stdout, output2.stderr ); - // Verify the disk metadata didn't change (file wasn't recreated) let metadata2 = std::fs::metadata(&disk_path).expect("Failed to get disk metadata after second run"); assert_eq!( @@ -314,11 +204,8 @@ pub fn test_to_disk_caching() { "Disk size changed between runs, indicating it was recreated instead of reused" ); - // Verify the second run was much faster (no installation should have occurred) assert!( - !stdout2.contains("Installation complete") && !stderr2.contains("Installation complete"), + !output2.stdout.contains("Installation complete") && !output2.stderr.contains("Installation complete"), "Second run should not have performed installation, but found 'Installation complete' message" ); - - println!("Disk image caching test successful - cache was properly reused on second run"); } diff --git a/crates/kit/src/libvirt/base_disks.rs b/crates/kit/src/libvirt/base_disks.rs index ec947ed..ffbf152 100644 --- a/crates/kit/src/libvirt/base_disks.rs +++ b/crates/kit/src/libvirt/base_disks.rs @@ -363,7 +363,7 @@ pub fn list_base_disks(connect_uri: Option<&String>) -> Result } /// Information about a base disk -#[derive(Debug)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct BaseDiskInfo { pub path: Utf8PathBuf, pub image_digest: Option, diff --git a/crates/kit/src/libvirt/base_disks_cli.rs b/crates/kit/src/libvirt/base_disks_cli.rs index a8da0c5..9805a82 100644 --- a/crates/kit/src/libvirt/base_disks_cli.rs +++ b/crates/kit/src/libvirt/base_disks_cli.rs @@ -5,8 +5,11 @@ use clap::{Parser, Subcommand}; 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::OutputFormat; /// Options for base-disks command #[derive(Debug, Parser)] @@ -19,11 +22,19 @@ pub struct LibvirtBaseDisksOpts { #[derive(Debug, Subcommand)] pub enum BaseDisksSubcommand { /// List all base disk images - List, + List(ListOpts), /// Prune unreferenced base disk images Prune(PruneOpts), } +/// Options for list command +#[derive(Debug, Parser)] +pub struct ListOpts { + /// Output format + #[clap(long, value_enum, default_value_t = OutputFormat::Table)] + pub format: OutputFormat, +} + /// Options for prune command #[derive(Debug, Parser)] pub struct PruneOpts { @@ -37,59 +48,69 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtBaseDisksO let connect_uri = global_opts.connect.as_ref(); match opts.command { - BaseDisksSubcommand::List => run_list(connect_uri), + BaseDisksSubcommand::List(list_opts) => run_list(connect_uri, list_opts), BaseDisksSubcommand::Prune(prune_opts) => run_prune(connect_uri, prune_opts), } } /// Execute the list subcommand -fn run_list(connect_uri: Option<&String>) -> Result<()> { +fn run_list(connect_uri: Option<&String>, opts: ListOpts) -> Result<()> { let base_disks = list_base_disks(connect_uri)?; - if base_disks.is_empty() { - println!("No base disk images found"); - return Ok(()); + match opts.format { + OutputFormat::Table => { + if base_disks.is_empty() { + println!("No base disk images found"); + return Ok(()); + } + + let mut table = Table::new(); + table.load_preset(UTF8_FULL); + table.set_header(vec!["NAME", "SIZE", "REFS", "IMAGE DIGEST"]); + + for disk in &base_disks { + let name = disk.path.file_name().unwrap_or("unknown"); + + let size = disk + .size + .map(|bytes| indicatif::BinaryBytes(bytes).to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let refs = disk.ref_count.to_string(); + + let digest = disk + .image_digest + .as_ref() + .map(|d| { + // Truncate long digests for display + if d.len() > 56 { + format!("{}...", &d[..53]) + } else { + d.clone() + } + }) + .unwrap_or_else(|| "".to_string()); + + table.add_row(vec![name, &size, &refs, &digest]); + } + + println!("{}", table); + println!( + "\nFound {} base disk{}", + base_disks.len(), + if base_disks.len() == 1 { "" } else { "s" } + ); + } + OutputFormat::Json => { + println!("{}", serde_json::to_string_pretty(&base_disks)?); + } + OutputFormat::Yaml => { + return Err(color_eyre::eyre::eyre!( + "YAML format is not supported for base-disks list command" + )) + } } - // Print table header - println!( - "{:<40} {:<10} {:<10} {:<58}", - "NAME", "SIZE", "REFS", "IMAGE DIGEST" - ); - println!("{}", "=".repeat(118)); - - for disk in &base_disks { - let name = disk.path.file_name().unwrap_or("unknown"); - - let size = disk - .size - .map(|bytes| indicatif::BinaryBytes(bytes).to_string()) - .unwrap_or_else(|| "unknown".to_string()); - - let refs = disk.ref_count.to_string(); - - let digest = disk - .image_digest - .as_ref() - .map(|d| { - // Truncate long digests for display - if d.len() > 56 { - format!("{}...", &d[..53]) - } else { - d.clone() - } - }) - .unwrap_or_else(|| "".to_string()); - - println!("{:<40} {:<10} {:<10} {:<58}", name, size, refs, digest); - } - - println!( - "\nFound {} base disk{}", - base_disks.len(), - if base_disks.len() == 1 { "" } else { "s" } - ); - Ok(()) } diff --git a/crates/kit/src/libvirt/inspect.rs b/crates/kit/src/libvirt/inspect.rs index 69fec05..7f6187c 100644 --- a/crates/kit/src/libvirt/inspect.rs +++ b/crates/kit/src/libvirt/inspect.rs @@ -6,6 +6,8 @@ use clap::Parser; use color_eyre::Result; +use super::OutputFormat; + /// Options for inspecting a libvirt domain #[derive(Debug, Parser)] pub struct LibvirtInspectOpts { @@ -13,8 +15,8 @@ pub struct LibvirtInspectOpts { pub name: String, /// Output format - #[clap(long, default_value = "yaml")] - pub format: String, + #[clap(long, value_enum, default_value_t = OutputFormat::Yaml)] + pub format: OutputFormat, } /// Execute the libvirt inspect command @@ -33,8 +35,8 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtInspectOpt .get_domain_info(&opts.name) .map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", opts.name))?; - match opts.format.as_str() { - "yaml" => { + match opts.format { + OutputFormat::Yaml => { println!("name: {}", vm.name); if let Some(ref image) = vm.image { println!("image: {}", image); @@ -50,17 +52,16 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtInspectOpt println!("disk_path: {}", disk_path); } } - "json" => { + OutputFormat::Json => { println!( "{}", serde_json::to_string_pretty(&vm) .with_context(|| "Failed to serialize VM as JSON")? ); } - _ => { + OutputFormat::Table => { return Err(color_eyre::eyre::eyre!( - "Unsupported format: {}", - opts.format + "Table format is not supported for inspect command" )) } } diff --git a/crates/kit/src/libvirt/list.rs b/crates/kit/src/libvirt/list.rs index 3569e83..fc2f0ac 100644 --- a/crates/kit/src/libvirt/list.rs +++ b/crates/kit/src/libvirt/list.rs @@ -5,13 +5,16 @@ use clap::Parser; use color_eyre::Result; +use comfy_table::{presets::UTF8_FULL, Table}; + +use super::OutputFormat; /// Options for listing libvirt domains #[derive(Debug, Parser)] pub struct LibvirtListOpts { /// Output format - #[clap(long, default_value = "table")] - pub format: String, + #[clap(long, value_enum, default_value_t = OutputFormat::Table)] + pub format: OutputFormat, /// Show all domains including stopped ones #[clap(long, short = 'a')] @@ -49,8 +52,8 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtListOpts) domains.retain(|d| d.labels.contains(filter_label)); } - match opts.format.as_str() { - "table" => { + match opts.format { + OutputFormat::Table => { if domains.is_empty() { if opts.all { println!("No VMs found"); @@ -63,11 +66,11 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtListOpts) } return Ok(()); } - println!( - "{:<20} {:<40} {:<12} {:<20}", - "NAME", "IMAGE", "STATUS", "MEMORY" - ); - println!("{}", "=".repeat(92)); + + let mut table = Table::new(); + table.load_preset(UTF8_FULL); + table.set_header(vec!["NAME", "IMAGE", "STATUS", "MEMORY"]); + for domain in &domains { let image = match &domain.image { Some(img) => { @@ -83,31 +86,26 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtListOpts) Some(mem) => format!("{}MB", mem), None => "unknown".to_string(), }; - println!( - "{:<20} {:<40} {:<12} {:<20}", - domain.name, - image, - domain.status_string(), - memory - ); + table.add_row(vec![&domain.name, &image, &domain.status_string(), &memory]); } + + println!("{}", table); println!( "\nFound {} domain{} (source: libvirt)", domains.len(), if domains.len() == 1 { "" } else { "s" } ); } - "json" => { + OutputFormat::Json => { println!( "{}", serde_json::to_string_pretty(&domains) .with_context(|| "Failed to serialize domains as JSON")? ); } - _ => { + OutputFormat::Yaml => { return Err(color_eyre::eyre::eyre!( - "Unsupported format: {}", - opts.format + "YAML format is not supported for list command" )) } } diff --git a/crates/kit/src/libvirt/mod.rs b/crates/kit/src/libvirt/mod.rs index 3e0eb7c..1ca0c83 100644 --- a/crates/kit/src/libvirt/mod.rs +++ b/crates/kit/src/libvirt/mod.rs @@ -8,6 +8,15 @@ use clap::Subcommand; +/// Output format options for libvirt commands +#[derive(Debug, Clone, clap::ValueEnum)] +#[clap(rename_all = "kebab-case")] +pub enum OutputFormat { + Table, + Json, + Yaml, +} + /// Default memory allocation for libvirt VMs pub const LIBVIRT_DEFAULT_MEMORY: &str = "4G";