Skip to content

Commit c39e70b

Browse files
committed
feat: Add KubeVirt common-instancetypes support
Add support for KubeVirt common-instancetypes to provide standardized VM sizing. Basically it's annoying to have to specify cpus and memory separately always since they're often *related*, and that's the whole idea of the generic instance type. We only expose the U1 (Universal) series instancetype definitions from kubevirt/common-instancetypes because we're not a true cloud. Both `bcvk ephemeral run` and `bcvk libvirt run` now accept an `--itype` flag that overrides `--vcpus` and `--memory` settings, making it easier to create consistently-sized VMs across different environments. For libvirt VMs, the instance type is stored in domain metadata as `bootc:instance-type` for reference. Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 6b3aa05 commit c39e70b

10 files changed

Lines changed: 419 additions & 18 deletions

File tree

crates/integration-tests/src/tests/libvirt_verb.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,97 @@ fn test_libvirt_ssh_integration() -> Result<()> {
363363
Ok(())
364364
}
365365

366+
#[distributed_slice(INTEGRATION_TESTS)]
367+
static TEST_LIBVIRT_RUN_WITH_INSTANCETYPE: IntegrationTest = IntegrationTest::new(
368+
"test_libvirt_run_with_instancetype",
369+
test_libvirt_run_with_instancetype,
370+
);
371+
372+
/// Test libvirt run with instancetype
373+
fn test_libvirt_run_with_instancetype() -> Result<()> {
374+
let test_image = get_test_image();
375+
376+
// Generate unique domain name for this test
377+
let domain_name = format!(
378+
"test-itype-{}",
379+
std::time::SystemTime::now()
380+
.duration_since(std::time::UNIX_EPOCH)
381+
.unwrap()
382+
.as_secs()
383+
);
384+
385+
println!(
386+
"Testing libvirt run with instancetype for domain: {}",
387+
domain_name
388+
);
389+
390+
// Cleanup any existing domain with this name
391+
cleanup_domain(&domain_name);
392+
393+
// Create domain with instancetype
394+
println!("Creating libvirt domain with instancetype u1.small...");
395+
let create_output = run_bcvk(&[
396+
"libvirt",
397+
"run",
398+
"--name",
399+
&domain_name,
400+
"--label",
401+
LIBVIRT_INTEGRATION_TEST_LABEL,
402+
"--itype",
403+
"u1.small",
404+
"--filesystem",
405+
"ext4",
406+
&test_image,
407+
])
408+
.expect("Failed to run libvirt run");
409+
410+
println!("Create stdout: {}", create_output.stdout);
411+
println!("Create stderr: {}", create_output.stderr);
412+
413+
if !create_output.success() {
414+
cleanup_domain(&domain_name);
415+
panic!(
416+
"Failed to create domain with instancetype: {}",
417+
create_output.stderr
418+
);
419+
}
420+
421+
println!("Successfully created domain: {}", domain_name);
422+
423+
// Inspect the domain to verify instancetype was set
424+
let inspect_output =
425+
run_bcvk(&["libvirt", "inspect", &domain_name]).expect("Failed to run libvirt inspect");
426+
427+
let inspect_stdout = inspect_output.stdout;
428+
println!("Inspect output: {}", inspect_stdout);
429+
430+
// Parse XML to verify memory and vcpus match u1.small (1 vcpu, 2048 MB)
431+
let dom = parse_xml_dom(&inspect_stdout).expect("Failed to parse domain XML");
432+
433+
// Check vCPUs (should be 1 for u1.small)
434+
let vcpu_node = dom.find("vcpu").expect("vcpu element not found");
435+
let vcpus: u32 = vcpu_node.text.parse().expect("Failed to parse vcpu count");
436+
assert_eq!(vcpus, 1, "u1.small should have 1 vCPU, got {}", vcpus);
437+
println!("✓ vCPUs correctly set to: {}", vcpus);
438+
439+
// Check memory (should be 2048 MB = 2097152 KB for u1.small)
440+
let memory_node = dom.find("memory").expect("memory element not found");
441+
let memory_kb: u64 = memory_node.text.parse().expect("Failed to parse memory");
442+
let memory_mb = memory_kb / 1024;
443+
assert_eq!(
444+
memory_mb, 2048,
445+
"u1.small should have 2048 MB, got {} MB",
446+
memory_mb
447+
);
448+
println!("✓ Memory correctly set to: {} MB", memory_mb);
449+
450+
// Cleanup domain
451+
cleanup_domain(&domain_name);
452+
453+
println!("✓ libvirt run with instancetype test passed");
454+
Ok(())
455+
}
456+
366457
/// Helper function to cleanup domain
367458
fn cleanup_domain(domain_name: &str) {
368459
println!("Cleaning up domain: {}", domain_name);

crates/integration-tests/src/tests/run_ephemeral.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,100 @@ fn test_run_ephemeral_container_ssh_access() -> Result<()> {
228228
assert!(ssh_output.stdout.contains("SSH_TEST_SUCCESS"));
229229
Ok(())
230230
}
231+
232+
#[distributed_slice(INTEGRATION_TESTS)]
233+
static TEST_RUN_EPHEMERAL_WITH_INSTANCETYPE: IntegrationTest = IntegrationTest::new(
234+
"run_ephemeral_with_instancetype",
235+
test_run_ephemeral_with_instancetype,
236+
);
237+
238+
fn test_run_ephemeral_with_instancetype() -> Result<()> {
239+
// Test u1.nano: 1 vCPU, 512 MiB memory
240+
let output = run_bcvk(&[
241+
"ephemeral",
242+
"run",
243+
"--rm",
244+
"--label",
245+
INTEGRATION_TEST_LABEL,
246+
"--itype",
247+
"u1.nano",
248+
"--execute",
249+
"/bin/sh -c 'echo CPUs:$(grep -c ^processor /proc/cpuinfo); echo MemTotal:$(grep MemTotal /proc/meminfo | awk \"{print \\$2}\")'",
250+
&get_test_image(),
251+
])?;
252+
253+
output.assert_success("ephemeral run with instance type u1.nano");
254+
255+
// Verify vCPUs (should be 1)
256+
assert!(
257+
output.stdout.contains("CPUs:1"),
258+
"Expected 1 vCPU for u1.nano, output: {}",
259+
output.stdout
260+
);
261+
262+
// Verify memory (should be ~512 MiB = ~524288 kB, allow some tolerance for kernel overhead)
263+
// MemTotal is typically slightly less than allocated due to kernel reservations
264+
let memtotal_line = output
265+
.stdout
266+
.lines()
267+
.find(|line| line.contains("MemTotal:"))
268+
.expect("MemTotal line not found in output");
269+
270+
let memtotal_kb: u32 = memtotal_line
271+
.split(':')
272+
.nth(1)
273+
.expect("Could not parse MemTotal")
274+
.trim()
275+
.parse()
276+
.expect("Could not parse MemTotal as number");
277+
278+
// Allow 10% tolerance for kernel overhead (512 MiB = 524288 kB)
279+
let expected_kb = 512 * 1024;
280+
let lower_bound = expected_kb * 9 / 10; // 90% of expected
281+
let upper_bound = expected_kb * 11 / 10; // 110% of expected
282+
283+
assert!(
284+
memtotal_kb >= lower_bound && memtotal_kb <= upper_bound,
285+
"Expected memory around {} kB for u1.nano, got {} kB",
286+
expected_kb,
287+
memtotal_kb
288+
);
289+
290+
Ok(())
291+
}
292+
293+
#[distributed_slice(INTEGRATION_TESTS)]
294+
static TEST_RUN_EPHEMERAL_INSTANCETYPE_INVALID: IntegrationTest = IntegrationTest::new(
295+
"run_ephemeral_instancetype_invalid",
296+
test_run_ephemeral_instancetype_invalid,
297+
);
298+
299+
fn test_run_ephemeral_instancetype_invalid() -> Result<()> {
300+
let output = run_bcvk(&[
301+
"ephemeral",
302+
"run",
303+
"--rm",
304+
"--label",
305+
INTEGRATION_TEST_LABEL,
306+
"--itype",
307+
"invalid.type",
308+
"--karg",
309+
"systemd.unit=poweroff.target",
310+
&get_test_image(),
311+
])?;
312+
313+
// Should fail with invalid instance type
314+
assert!(
315+
!output.success(),
316+
"Expected failure with invalid instance type, but succeeded"
317+
);
318+
319+
// Error message should mention the invalid type
320+
assert!(
321+
output.stderr.contains("invalid.type") || output.stderr.contains("Unknown instance type"),
322+
"Error message should mention invalid instance type: {}",
323+
output.stderr
324+
);
325+
326+
Ok(())
327+
}

crates/kit/src/instancetypes.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! KubeVirt common-instancetypes support
2+
//!
3+
//! This module vendors the KubeVirt common-instancetypes definitions,
4+
//! specifically the U series (Universal/General Purpose) instance types.
5+
//! These provide standardized VM sizing with predefined vCPU and memory
6+
//! configurations.
7+
//!
8+
//! Instance types follow the format: u1.{size}
9+
//! Examples: u1.nano, u1.micro, u1.small, u1.medium, u1.large, etc.
10+
//!
11+
//! Source: https://github.com/kubevirt/common-instancetypes
12+
13+
/// Instance type variants with associated vCPU and memory specifications
14+
///
15+
/// Source: https://github.com/kubevirt/common-instancetypes/blob/main/instancetypes/u/1/sizes.yaml
16+
#[derive(
17+
Debug,
18+
Clone,
19+
Copy,
20+
PartialEq,
21+
Eq,
22+
serde::Serialize,
23+
serde::Deserialize,
24+
strum::Display,
25+
strum::EnumString,
26+
strum::EnumIter,
27+
)]
28+
#[non_exhaustive]
29+
pub enum InstanceType {
30+
/// u1.nano - 1 vCPU, 512 MiB memory
31+
#[strum(serialize = "u1.nano")]
32+
U1Nano,
33+
/// u1.micro - 1 vCPU, 1 GiB memory
34+
#[strum(serialize = "u1.micro")]
35+
U1Micro,
36+
/// u1.small - 1 vCPU, 2 GiB memory
37+
#[strum(serialize = "u1.small")]
38+
U1Small,
39+
/// u1.medium - 1 vCPU, 4 GiB memory
40+
#[strum(serialize = "u1.medium")]
41+
U1Medium,
42+
/// u1.2xmedium - 2 vCPU, 4 GiB memory
43+
#[strum(serialize = "u1.2xmedium")]
44+
U1TwoXMedium,
45+
/// u1.large - 2 vCPU, 8 GiB memory
46+
#[strum(serialize = "u1.large")]
47+
U1Large,
48+
/// u1.xlarge - 4 vCPU, 16 GiB memory
49+
#[strum(serialize = "u1.xlarge")]
50+
U1XLarge,
51+
/// u1.2xlarge - 8 vCPU, 32 GiB memory
52+
#[strum(serialize = "u1.2xlarge")]
53+
U1TwoXLarge,
54+
/// u1.4xlarge - 16 vCPU, 64 GiB memory
55+
#[strum(serialize = "u1.4xlarge")]
56+
U1FourXLarge,
57+
/// u1.8xlarge - 32 vCPU, 128 GiB memory
58+
#[strum(serialize = "u1.8xlarge")]
59+
U1EightXLarge,
60+
}
61+
62+
impl InstanceType {
63+
/// Get the number of vCPUs for this instance type
64+
pub const fn vcpus(self) -> u32 {
65+
match self {
66+
Self::U1Nano => 1,
67+
Self::U1Micro => 1,
68+
Self::U1Small => 1,
69+
Self::U1Medium => 1,
70+
Self::U1TwoXMedium => 2,
71+
Self::U1Large => 2,
72+
Self::U1XLarge => 4,
73+
Self::U1TwoXLarge => 8,
74+
Self::U1FourXLarge => 16,
75+
Self::U1EightXLarge => 32,
76+
}
77+
}
78+
79+
/// Get the memory in megabytes for this instance type
80+
pub const fn memory_mb(self) -> u32 {
81+
match self {
82+
Self::U1Nano => 512,
83+
Self::U1Micro => 1024,
84+
Self::U1Small => 2048,
85+
Self::U1Medium => 4096,
86+
Self::U1TwoXMedium => 4096,
87+
Self::U1Large => 8192,
88+
Self::U1XLarge => 16384,
89+
Self::U1TwoXLarge => 32768,
90+
Self::U1FourXLarge => 65536,
91+
Self::U1EightXLarge => 131072,
92+
}
93+
}
94+
}
95+
96+
#[cfg(test)]
97+
mod tests {
98+
use super::*;
99+
use std::str::FromStr;
100+
use strum::IntoEnumIterator;
101+
102+
#[test]
103+
fn test_properties() {
104+
for variant in InstanceType::iter() {
105+
let (expected_vcpus, expected_memory_mb) = match variant {
106+
InstanceType::U1Nano => (1, 512),
107+
InstanceType::U1Micro => (1, 1024),
108+
InstanceType::U1Small => (1, 2048),
109+
InstanceType::U1Medium => (1, 4096),
110+
InstanceType::U1TwoXMedium => (2, 4096),
111+
InstanceType::U1Large => (2, 8192),
112+
InstanceType::U1XLarge => (4, 16384),
113+
InstanceType::U1TwoXLarge => (8, 32768),
114+
InstanceType::U1FourXLarge => (16, 65536),
115+
InstanceType::U1EightXLarge => (32, 131072),
116+
};
117+
assert_eq!(
118+
variant.vcpus(),
119+
expected_vcpus,
120+
"Mismatch in vcpus for {:?}",
121+
variant
122+
);
123+
assert_eq!(
124+
variant.memory_mb(),
125+
expected_memory_mb,
126+
"Mismatch in memory_mb for {:?}",
127+
variant
128+
);
129+
}
130+
}
131+
132+
#[test]
133+
fn test_parse_invalid_instancetype() {
134+
let result = InstanceType::from_str("invalid");
135+
assert!(result.is_err());
136+
}
137+
138+
#[test]
139+
fn test_roundtrip() {
140+
for variant in InstanceType::iter() {
141+
let s = variant.to_string();
142+
let parsed = InstanceType::from_str(&s).unwrap();
143+
assert_eq!(parsed, variant);
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)