Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions crates/integration-tests/src/tests/libvirt_verb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,97 @@ fn test_libvirt_ssh_integration() -> Result<()> {
Ok(())
}

#[distributed_slice(INTEGRATION_TESTS)]
static TEST_LIBVIRT_RUN_WITH_INSTANCETYPE: IntegrationTest = IntegrationTest::new(
"test_libvirt_run_with_instancetype",
test_libvirt_run_with_instancetype,
);

/// Test libvirt run with instancetype
fn test_libvirt_run_with_instancetype() -> Result<()> {
let test_image = get_test_image();

// Generate unique domain name for this test
let domain_name = format!(
"test-itype-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
);

println!(
"Testing libvirt run with instancetype for domain: {}",
domain_name
);

// Cleanup any existing domain with this name
cleanup_domain(&domain_name);

// Create domain with instancetype
println!("Creating libvirt domain with instancetype u1.small...");
let create_output = run_bcvk(&[
"libvirt",
"run",
"--name",
&domain_name,
"--label",
LIBVIRT_INTEGRATION_TEST_LABEL,
"--itype",
"u1.small",
"--filesystem",
"ext4",
&test_image,
])
.expect("Failed to run libvirt run");

println!("Create stdout: {}", create_output.stdout);
println!("Create stderr: {}", create_output.stderr);

if !create_output.success() {
cleanup_domain(&domain_name);
panic!(
"Failed to create domain with instancetype: {}",
create_output.stderr
);
}

println!("Successfully created domain: {}", domain_name);

// Inspect the domain to verify instancetype was set
let inspect_output = run_bcvk(&["libvirt", "inspect", "--format", "xml", &domain_name])
.expect("Failed to run libvirt inspect");

let inspect_stdout = inspect_output.stdout;
println!("Inspect output: {}", inspect_stdout);

// Parse XML to verify memory and vcpus match u1.small (1 vcpu, 2048 MB)
let dom = parse_xml_dom(&inspect_stdout).expect("Failed to parse domain XML");

// Check vCPUs (should be 1 for u1.small)
let vcpu_node = dom.find("vcpu").expect("vcpu element not found");
let vcpus: u32 = vcpu_node.text.parse().expect("Failed to parse vcpu count");
assert_eq!(vcpus, 1, "u1.small should have 1 vCPU, got {}", vcpus);
println!("✓ vCPUs correctly set to: {}", vcpus);

// Check memory (should be 2048 MB = 2097152 KB for u1.small)
let memory_node = dom.find("memory").expect("memory element not found");
let memory_kb: u64 = memory_node.text.parse().expect("Failed to parse memory");
let memory_mb = memory_kb / 1024;
assert_eq!(
memory_mb, 2048,
"u1.small should have 2048 MB, got {} MB",
memory_mb
);
println!("✓ Memory correctly set to: {} MB", memory_mb);

// Cleanup domain
cleanup_domain(&domain_name);

println!("✓ libvirt run with instancetype test passed");
Ok(())
}

/// Helper function to cleanup domain
fn cleanup_domain(domain_name: &str) {
println!("Cleaning up domain: {}", domain_name);
Expand Down
104 changes: 104 additions & 0 deletions crates/integration-tests/src/tests/run_ephemeral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,107 @@ fn test_run_ephemeral_container_ssh_access() -> Result<()> {
assert!(ssh_output.stdout.contains("SSH_TEST_SUCCESS"));
Ok(())
}

#[distributed_slice(INTEGRATION_TESTS)]
static TEST_RUN_EPHEMERAL_WITH_INSTANCETYPE: IntegrationTest = IntegrationTest::new(
"run_ephemeral_with_instancetype",
test_run_ephemeral_with_instancetype,
);

fn test_run_ephemeral_with_instancetype() -> Result<()> {
// Test u1.nano: 1 vCPU, 512 MiB memory
// Calculate physical memory from /sys/firmware/memmap (System RAM regions)
let script = "/bin/sh -c 'echo CPUs:$(grep -c ^processor /proc/cpuinfo); total=0; for dir in /sys/firmware/memmap/*; do type=$(cat \"$dir/type\" 2>/dev/null); if [ \"$type\" = \"System RAM\" ]; then start=$(cat \"$dir/start\"); end=$(cat \"$dir/end\"); start_dec=$((start)); end_dec=$((end)); size=$((end_dec - start_dec + 1)); total=$((total + size)); fi; done; total_kb=$((total / 1024)); echo PhysicalMemKB:$total_kb'";

let output = run_bcvk(&[
"ephemeral",
"run",
"--rm",
"--label",
INTEGRATION_TEST_LABEL,
"--itype",
"u1.nano",
"--execute",
script,
&get_test_image(),
])?;

output.assert_success("ephemeral run with instance type u1.nano");

// Verify vCPUs (should be 1)
assert!(
output.stdout.contains("CPUs:1"),
"Expected 1 vCPU for u1.nano, output: {}",
output.stdout
);

// Verify physical memory (should be exactly 512 MiB = 524288 kB)
let mem_line = output
.stdout
.lines()
.find(|line| line.contains("PhysicalMemKB:"))
.expect("PhysicalMemKB line not found in output");

let mem_kb: u32 = mem_line
.split(':')
.nth(1)
.expect("Could not parse PhysicalMemKB")
.trim()
.parse()
.expect("Could not parse PhysicalMemKB as number");

// Physical memory should be close to 512 MiB = 524288 kB
// QEMU reserves small memory regions (BIOS, VGA, ACPI, etc.) so actual may be slightly less
// Allow 1% tolerance to account for hypervisor overhead
let expected_kb = 512 * 1024;
let tolerance_kb = expected_kb / 100; // 1% tolerance
let diff = if mem_kb > expected_kb {
mem_kb - expected_kb
} else {
expected_kb - mem_kb
};

assert!(
diff <= tolerance_kb,
"Expected physical memory ~{} kB for u1.nano, got {} kB (diff: {} kB, max allowed: {} kB [1%])",
expected_kb, mem_kb, diff, tolerance_kb
);

Ok(())
}

#[distributed_slice(INTEGRATION_TESTS)]
static TEST_RUN_EPHEMERAL_INSTANCETYPE_INVALID: IntegrationTest = IntegrationTest::new(
"run_ephemeral_instancetype_invalid",
test_run_ephemeral_instancetype_invalid,
);

fn test_run_ephemeral_instancetype_invalid() -> Result<()> {
let output = run_bcvk(&[
"ephemeral",
"run",
"--rm",
"--label",
INTEGRATION_TEST_LABEL,
"--itype",
"invalid.type",
"--karg",
"systemd.unit=poweroff.target",
&get_test_image(),
])?;

// Should fail with invalid instance type
assert!(
!output.success(),
"Expected failure with invalid instance type, but succeeded"
);

// Error message should mention the invalid type
assert!(
output.stderr.contains("invalid.type") || output.stderr.contains("Unknown instance type"),
"Error message should mention invalid instance type: {}",
output.stderr
);

Ok(())
}
146 changes: 146 additions & 0 deletions crates/kit/src/instancetypes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! KubeVirt common-instancetypes support
//!
//! This module vendors the KubeVirt common-instancetypes definitions,
//! specifically the U series (Universal/General Purpose) instance types.
//! These provide standardized VM sizing with predefined vCPU and memory
//! configurations.
//!
//! Instance types follow the format: u1.{size}
//! Examples: u1.nano, u1.micro, u1.small, u1.medium, u1.large, etc.
//!
//! Source: https://github.com/kubevirt/common-instancetypes

/// Instance type variants with associated vCPU and memory specifications
///
/// Source: https://github.com/kubevirt/common-instancetypes/blob/main/instancetypes/u/1/sizes.yaml
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
strum::Display,
strum::EnumString,
strum::EnumIter,
)]
#[non_exhaustive]
pub enum InstanceType {
/// u1.nano - 1 vCPU, 512 MiB memory
#[strum(serialize = "u1.nano")]
U1Nano,
/// u1.micro - 1 vCPU, 1 GiB memory
#[strum(serialize = "u1.micro")]
U1Micro,
/// u1.small - 1 vCPU, 2 GiB memory
#[strum(serialize = "u1.small")]
U1Small,
/// u1.medium - 1 vCPU, 4 GiB memory
#[strum(serialize = "u1.medium")]
U1Medium,
/// u1.2xmedium - 2 vCPU, 4 GiB memory
#[strum(serialize = "u1.2xmedium")]
U1TwoXMedium,
/// u1.large - 2 vCPU, 8 GiB memory
#[strum(serialize = "u1.large")]
U1Large,
/// u1.xlarge - 4 vCPU, 16 GiB memory
#[strum(serialize = "u1.xlarge")]
U1XLarge,
/// u1.2xlarge - 8 vCPU, 32 GiB memory
#[strum(serialize = "u1.2xlarge")]
U1TwoXLarge,
/// u1.4xlarge - 16 vCPU, 64 GiB memory
#[strum(serialize = "u1.4xlarge")]
U1FourXLarge,
/// u1.8xlarge - 32 vCPU, 128 GiB memory
#[strum(serialize = "u1.8xlarge")]
U1EightXLarge,
}

impl InstanceType {
/// Get the number of vCPUs for this instance type
pub const fn vcpus(self) -> u32 {
match self {
Self::U1Nano => 1,
Self::U1Micro => 1,
Self::U1Small => 1,
Self::U1Medium => 1,
Self::U1TwoXMedium => 2,
Self::U1Large => 2,
Self::U1XLarge => 4,
Self::U1TwoXLarge => 8,
Self::U1FourXLarge => 16,
Self::U1EightXLarge => 32,
}
}

/// Get the memory in megabytes for this instance type
pub const fn memory_mb(self) -> u32 {
match self {
Self::U1Nano => 512,
Self::U1Micro => 1024,
Self::U1Small => 2048,
Self::U1Medium => 4096,
Self::U1TwoXMedium => 4096,
Self::U1Large => 8192,
Self::U1XLarge => 16384,
Self::U1TwoXLarge => 32768,
Self::U1FourXLarge => 65536,
Self::U1EightXLarge => 131072,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use strum::IntoEnumIterator;

#[test]
fn test_properties() {
for variant in InstanceType::iter() {
let (expected_vcpus, expected_memory_mb) = match variant {
InstanceType::U1Nano => (1, 512),
InstanceType::U1Micro => (1, 1024),
InstanceType::U1Small => (1, 2048),
InstanceType::U1Medium => (1, 4096),
InstanceType::U1TwoXMedium => (2, 4096),
InstanceType::U1Large => (2, 8192),
InstanceType::U1XLarge => (4, 16384),
InstanceType::U1TwoXLarge => (8, 32768),
InstanceType::U1FourXLarge => (16, 65536),
InstanceType::U1EightXLarge => (32, 131072),
};
assert_eq!(
variant.vcpus(),
expected_vcpus,
"Mismatch in vcpus for {:?}",
variant
);
assert_eq!(
variant.memory_mb(),
expected_memory_mb,
"Mismatch in memory_mb for {:?}",
variant
);
}
}

#[test]
fn test_parse_invalid_instancetype() {
let result = InstanceType::from_str("invalid");
assert!(result.is_err());
}

#[test]
fn test_roundtrip() {
for variant in InstanceType::iter() {
let s = variant.to_string();
let parsed = InstanceType::from_str(&s).unwrap();
assert_eq!(parsed, variant);
}
}
}
5 changes: 5 additions & 0 deletions crates/kit/src/libvirt/base_disks_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ fn run_list(connect_uri: Option<&str>, opts: ListOpts) -> Result<()> {
"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"
))
}
}

Ok(())
Expand Down
17 changes: 17 additions & 0 deletions crates/kit/src/libvirt/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtInspectOpt
.with_context(|| "Failed to serialize VM as JSON")?
);
}
OutputFormat::Xml => {
// Output raw domain XML using virsh dumpxml
let mut cmd = global_opts.virsh_command();
cmd.args(["dumpxml", &opts.name]);
let output = cmd
.output()
.with_context(|| format!("Failed to run virsh dumpxml for {}", opts.name))?;

if !output.status.success() {
return Err(color_eyre::eyre::eyre!(
"Failed to get domain XML: {}",
String::from_utf8_lossy(&output.stderr)
));
}

print!("{}", String::from_utf8_lossy(&output.stdout));
}
OutputFormat::Table => {
return Err(color_eyre::eyre::eyre!(
"Table format is not supported for inspect command"
Expand Down
Loading
Loading