Skip to content

Commit 21cb048

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 <[email protected]>
1 parent 6b3aa05 commit 21cb048

File tree

10 files changed

+436
-18
lines changed

10 files changed

+436
-18
lines changed

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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,63 @@ 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+
let output = run_bcvk(&[
240+
"ephemeral",
241+
"run",
242+
"--rm",
243+
"--label",
244+
INTEGRATION_TEST_LABEL,
245+
"--itype",
246+
"u1.nano",
247+
"--karg",
248+
"systemd.unit=poweroff.target",
249+
&get_test_image(),
250+
])?;
251+
252+
output.assert_success("ephemeral run with instance type u1.nano");
253+
Ok(())
254+
}
255+
256+
#[distributed_slice(INTEGRATION_TESTS)]
257+
static TEST_RUN_EPHEMERAL_INSTANCETYPE_INVALID: IntegrationTest = IntegrationTest::new(
258+
"run_ephemeral_instancetype_invalid",
259+
test_run_ephemeral_instancetype_invalid,
260+
);
261+
262+
fn test_run_ephemeral_instancetype_invalid() -> Result<()> {
263+
let output = run_bcvk(&[
264+
"ephemeral",
265+
"run",
266+
"--rm",
267+
"--label",
268+
INTEGRATION_TEST_LABEL,
269+
"--itype",
270+
"invalid.type",
271+
"--karg",
272+
"systemd.unit=poweroff.target",
273+
&get_test_image(),
274+
])?;
275+
276+
// Should fail with invalid instance type
277+
assert!(
278+
!output.success(),
279+
"Expected failure with invalid instance type, but succeeded"
280+
);
281+
282+
// Error message should mention the invalid type
283+
assert!(
284+
output.stderr.contains("invalid.type") || output.stderr.contains("Unknown instance type"),
285+
"Error message should mention invalid instance type: {}",
286+
output.stderr
287+
);
288+
289+
Ok(())
290+
}

crates/kit/src/instancetypes.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
)]
27+
#[non_exhaustive]
28+
pub enum InstanceType {
29+
/// u1.nano - 1 vCPU, 512 MiB memory
30+
#[strum(serialize = "u1.nano")]
31+
U1Nano,
32+
/// u1.micro - 1 vCPU, 1 GiB memory
33+
#[strum(serialize = "u1.micro")]
34+
U1Micro,
35+
/// u1.small - 1 vCPU, 2 GiB memory
36+
#[strum(serialize = "u1.small")]
37+
U1Small,
38+
/// u1.medium - 1 vCPU, 4 GiB memory
39+
#[strum(serialize = "u1.medium")]
40+
U1Medium,
41+
/// u1.2xmedium - 2 vCPU, 4 GiB memory
42+
#[strum(serialize = "u1.2xmedium")]
43+
U1TwoXMedium,
44+
/// u1.large - 2 vCPU, 8 GiB memory
45+
#[strum(serialize = "u1.large")]
46+
U1Large,
47+
/// u1.xlarge - 4 vCPU, 16 GiB memory
48+
#[strum(serialize = "u1.xlarge")]
49+
U1XLarge,
50+
/// u1.2xlarge - 8 vCPU, 32 GiB memory
51+
#[strum(serialize = "u1.2xlarge")]
52+
U1TwoXLarge,
53+
/// u1.4xlarge - 16 vCPU, 64 GiB memory
54+
#[strum(serialize = "u1.4xlarge")]
55+
U1FourXLarge,
56+
/// u1.8xlarge - 32 vCPU, 128 GiB memory
57+
#[strum(serialize = "u1.8xlarge")]
58+
U1EightXLarge,
59+
}
60+
61+
impl InstanceType {
62+
/// Get the number of vCPUs for this instance type
63+
pub const fn vcpus(self) -> u32 {
64+
match self {
65+
Self::U1Nano => 1,
66+
Self::U1Micro => 1,
67+
Self::U1Small => 1,
68+
Self::U1Medium => 1,
69+
Self::U1TwoXMedium => 2,
70+
Self::U1Large => 2,
71+
Self::U1XLarge => 4,
72+
Self::U1TwoXLarge => 8,
73+
Self::U1FourXLarge => 16,
74+
Self::U1EightXLarge => 32,
75+
}
76+
}
77+
78+
/// Get the memory in megabytes for this instance type
79+
pub const fn memory_mb(self) -> u32 {
80+
match self {
81+
Self::U1Nano => 512,
82+
Self::U1Micro => 1024,
83+
Self::U1Small => 2048,
84+
Self::U1Medium => 4096,
85+
Self::U1TwoXMedium => 4096,
86+
Self::U1Large => 8192,
87+
Self::U1XLarge => 16384,
88+
Self::U1TwoXLarge => 32768,
89+
Self::U1FourXLarge => 65536,
90+
Self::U1EightXLarge => 131072,
91+
}
92+
}
93+
}
94+
95+
#[cfg(test)]
96+
mod tests {
97+
use super::*;
98+
use std::str::FromStr;
99+
100+
#[test]
101+
fn test_parse_valid_instancetype() {
102+
let itype = InstanceType::from_str("u1.nano").unwrap();
103+
assert_eq!(itype, InstanceType::U1Nano);
104+
assert_eq!(itype.vcpus(), 1);
105+
assert_eq!(itype.memory_mb(), 512);
106+
}
107+
108+
#[test]
109+
fn test_parse_all_variants() {
110+
assert_eq!(
111+
InstanceType::from_str("u1.nano").unwrap(),
112+
InstanceType::U1Nano
113+
);
114+
assert_eq!(
115+
InstanceType::from_str("u1.micro").unwrap(),
116+
InstanceType::U1Micro
117+
);
118+
assert_eq!(
119+
InstanceType::from_str("u1.small").unwrap(),
120+
InstanceType::U1Small
121+
);
122+
assert_eq!(
123+
InstanceType::from_str("u1.medium").unwrap(),
124+
InstanceType::U1Medium
125+
);
126+
assert_eq!(
127+
InstanceType::from_str("u1.2xmedium").unwrap(),
128+
InstanceType::U1TwoXMedium
129+
);
130+
assert_eq!(
131+
InstanceType::from_str("u1.large").unwrap(),
132+
InstanceType::U1Large
133+
);
134+
assert_eq!(
135+
InstanceType::from_str("u1.xlarge").unwrap(),
136+
InstanceType::U1XLarge
137+
);
138+
assert_eq!(
139+
InstanceType::from_str("u1.2xlarge").unwrap(),
140+
InstanceType::U1TwoXLarge
141+
);
142+
assert_eq!(
143+
InstanceType::from_str("u1.4xlarge").unwrap(),
144+
InstanceType::U1FourXLarge
145+
);
146+
assert_eq!(
147+
InstanceType::from_str("u1.8xlarge").unwrap(),
148+
InstanceType::U1EightXLarge
149+
);
150+
}
151+
152+
#[test]
153+
fn test_parse_invalid_instancetype() {
154+
let result = InstanceType::from_str("invalid");
155+
assert!(result.is_err());
156+
}
157+
158+
#[test]
159+
fn test_display() {
160+
assert_eq!(InstanceType::U1Nano.to_string(), "u1.nano");
161+
assert_eq!(InstanceType::U1Small.to_string(), "u1.small");
162+
assert_eq!(InstanceType::U1EightXLarge.to_string(), "u1.8xlarge");
163+
}
164+
165+
#[test]
166+
fn test_vcpus() {
167+
assert_eq!(InstanceType::U1Nano.vcpus(), 1);
168+
assert_eq!(InstanceType::U1TwoXMedium.vcpus(), 2);
169+
assert_eq!(InstanceType::U1XLarge.vcpus(), 4);
170+
assert_eq!(InstanceType::U1EightXLarge.vcpus(), 32);
171+
}
172+
173+
#[test]
174+
fn test_memory_mb() {
175+
assert_eq!(InstanceType::U1Nano.memory_mb(), 512);
176+
assert_eq!(InstanceType::U1Small.memory_mb(), 2048);
177+
assert_eq!(InstanceType::U1Large.memory_mb(), 8192);
178+
assert_eq!(InstanceType::U1EightXLarge.memory_mb(), 131072);
179+
}
180+
181+
#[test]
182+
fn test_roundtrip() {
183+
for variant in [
184+
InstanceType::U1Nano,
185+
InstanceType::U1Micro,
186+
InstanceType::U1Small,
187+
InstanceType::U1Medium,
188+
InstanceType::U1TwoXMedium,
189+
InstanceType::U1Large,
190+
InstanceType::U1XLarge,
191+
InstanceType::U1TwoXLarge,
192+
InstanceType::U1FourXLarge,
193+
InstanceType::U1EightXLarge,
194+
] {
195+
let s = variant.to_string();
196+
let parsed = InstanceType::from_str(&s).unwrap();
197+
assert_eq!(parsed, variant);
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)