Skip to content

Commit 39e1db2

Browse files
committed
libvirt: Add support for --label
This parallels what we do with `ephemeral`, where it's super natural because containers have labels. With libvirt we already have custom stuff in the XML, so add support for reading/writing labels there, and filtering by them in `bcvk libvirt list --label=foo` etc. This makes it easier to use bcvk in a per-project setting. Signed-off-by: Colin Walters <[email protected]>
1 parent ec54256 commit 39e1db2

File tree

13 files changed

+465
-34
lines changed

13 files changed

+465
-34
lines changed

crates/integration-tests/src/bin/cleanup.rs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::process::Command;
22

3-
/// Label used to identify containers created by integration tests
4-
const INTEGRATION_TEST_LABEL: &str = "bcvk.integration-test=1";
3+
// Import shared constants from the library
4+
use integration_tests::{INTEGRATION_TEST_LABEL, LIBVIRT_INTEGRATION_TEST_LABEL};
55

66
fn cleanup_integration_test_containers() -> Result<(), Box<dyn std::error::Error>> {
77
println!("Cleaning up integration test containers...");
@@ -59,9 +59,68 @@ fn cleanup_integration_test_containers() -> Result<(), Box<dyn std::error::Error
5959
Ok(())
6060
}
6161

62+
fn cleanup_libvirt_integration_test_vms() -> Result<(), Box<dyn std::error::Error>> {
63+
println!("Cleaning up integration test libvirt VMs...");
64+
65+
// Get path to bcvk binary (should be in the same directory as this cleanup binary)
66+
let current_exe = std::env::current_exe()?;
67+
let bcvk_path = current_exe
68+
.parent()
69+
.ok_or("Failed to get parent directory")?
70+
.join("bcvk");
71+
72+
if !bcvk_path.exists() {
73+
println!(
74+
"bcvk binary not found at {:?}, skipping libvirt cleanup",
75+
bcvk_path
76+
);
77+
return Ok(());
78+
}
79+
80+
// Use bcvk libvirt rm-all with label filter
81+
let rm_output = Command::new(&bcvk_path)
82+
.args([
83+
"libvirt",
84+
"rm-all",
85+
"--label",
86+
LIBVIRT_INTEGRATION_TEST_LABEL,
87+
"--force",
88+
"--stop",
89+
])
90+
.output()?;
91+
92+
if !rm_output.status.success() {
93+
let stderr = String::from_utf8_lossy(&rm_output.stderr);
94+
// Don't fail if no VMs found
95+
if stderr.contains("No VMs found") {
96+
println!("No integration test libvirt VMs found to clean up");
97+
return Ok(());
98+
}
99+
eprintln!("Warning: Failed to clean up libvirt VMs: {}", stderr);
100+
return Ok(());
101+
}
102+
103+
let stdout = String::from_utf8_lossy(&rm_output.stdout);
104+
println!("{}", stdout);
105+
106+
Ok(())
107+
}
108+
62109
fn main() {
110+
let mut errors = Vec::new();
111+
63112
if let Err(e) = cleanup_integration_test_containers() {
64-
eprintln!("Error during cleanup: {}", e);
113+
eprintln!("Error during container cleanup: {}", e);
114+
errors.push(format!("containers: {}", e));
115+
}
116+
117+
if let Err(e) = cleanup_libvirt_integration_test_vms() {
118+
eprintln!("Error during libvirt VM cleanup: {}", e);
119+
errors.push(format!("libvirt: {}", e));
120+
}
121+
122+
if !errors.is_empty() {
123+
eprintln!("Cleanup completed with errors: {}", errors.join(", "));
65124
std::process::exit(1);
66125
}
67126
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! Shared library code for integration tests
2+
//!
3+
//! This module contains constants and utilities that are shared between
4+
//! the main test binary and helper binaries like cleanup.
5+
6+
/// Label used to identify containers created by integration tests
7+
pub const INTEGRATION_TEST_LABEL: &str = "bcvk.integration-test=1";
8+
9+
/// Label used to identify libvirt VMs created by integration tests
10+
pub const LIBVIRT_INTEGRATION_TEST_LABEL: &str = "bcvk-integration";

crates/integration-tests/src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use libtest_mimic::{Arguments, Trial};
66
use serde_json::Value;
77
use xshell::{cmd, Shell};
88

9-
/// Label used to identify containers created by integration tests
10-
pub(crate) const INTEGRATION_TEST_LABEL: &str = "bcvk.integration-test=1";
9+
// Re-export constants from lib for internal use
10+
pub(crate) use integration_tests::{INTEGRATION_TEST_LABEL, LIBVIRT_INTEGRATION_TEST_LABEL};
1111

1212
mod tests {
1313
pub mod libvirt_base_disks;
@@ -205,6 +205,10 @@ fn main() {
205205
tests::libvirt_verb::test_libvirt_vm_lifecycle();
206206
Ok(())
207207
}),
208+
Trial::test("libvirt_label_functionality", || {
209+
tests::libvirt_verb::test_libvirt_label_functionality();
210+
Ok(())
211+
}),
208212
Trial::test("libvirt_error_handling", || {
209213
tests::libvirt_verb::test_libvirt_error_handling();
210214
Ok(())

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

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
use std::process::Command;
1111

12-
use crate::{get_bck_command, get_test_image};
12+
use crate::{get_bck_command, get_test_image, LIBVIRT_INTEGRATION_TEST_LABEL};
1313

1414
/// Test libvirt list functionality (lists domains)
1515
pub fn test_libvirt_list_functionality() {
@@ -224,6 +224,8 @@ pub fn test_libvirt_run_ssh_full_workflow() {
224224
"run",
225225
"--name",
226226
&domain_name,
227+
"--label",
228+
LIBVIRT_INTEGRATION_TEST_LABEL,
227229
"--filesystem",
228230
"ext4",
229231
&test_image,
@@ -416,6 +418,8 @@ pub fn test_libvirt_vm_lifecycle() {
416418
"ext4",
417419
"--name",
418420
&domain_name,
421+
"--label",
422+
INTEGRATION_TEST_LABEL,
419423
test_image,
420424
])
421425
.output()
@@ -526,6 +530,8 @@ pub fn test_libvirt_bind_storage_ro() {
526530
"run",
527531
"--name",
528532
&domain_name,
533+
"--label",
534+
INTEGRATION_TEST_LABEL,
529535
"--bind-storage-ro",
530536
"--filesystem",
531537
"ext4",
@@ -699,6 +705,144 @@ pub fn test_libvirt_bind_storage_ro() {
699705
println!("✓ --bind-storage-ro end-to-end test passed");
700706
}
701707

708+
/// Test libvirt label functionality
709+
pub fn test_libvirt_label_functionality() {
710+
let bck = get_bck_command().unwrap();
711+
let test_image = get_test_image();
712+
713+
// Generate unique domain name for this test
714+
let domain_name = format!(
715+
"test-label-{}",
716+
std::time::SystemTime::now()
717+
.duration_since(std::time::UNIX_EPOCH)
718+
.unwrap()
719+
.as_secs()
720+
);
721+
722+
println!(
723+
"Testing libvirt label functionality with domain: {}",
724+
domain_name
725+
);
726+
727+
// Cleanup any existing domain with this name
728+
let _ = Command::new("virsh")
729+
.args(&["destroy", &domain_name])
730+
.output();
731+
let _ = Command::new(&bck)
732+
.args(&["libvirt", "rm", &domain_name, "--force", "--stop"])
733+
.output();
734+
735+
// Create domain with multiple labels
736+
println!("Creating libvirt domain with multiple labels...");
737+
let create_output = Command::new("timeout")
738+
.args([
739+
"300s",
740+
&bck,
741+
"libvirt",
742+
"run",
743+
"--name",
744+
&domain_name,
745+
"--label",
746+
INTEGRATION_TEST_LABEL,
747+
"--label",
748+
"test-env",
749+
"--label",
750+
"temporary",
751+
"--filesystem",
752+
"ext4",
753+
&test_image,
754+
])
755+
.output()
756+
.expect("Failed to run libvirt run with labels");
757+
758+
let create_stdout = String::from_utf8_lossy(&create_output.stdout);
759+
let create_stderr = String::from_utf8_lossy(&create_output.stderr);
760+
761+
println!("Create stdout: {}", create_stdout);
762+
println!("Create stderr: {}", create_stderr);
763+
764+
if !create_output.status.success() {
765+
cleanup_domain(&domain_name);
766+
panic!("Failed to create domain with labels: {}", create_stderr);
767+
}
768+
769+
println!("Successfully created domain with labels: {}", domain_name);
770+
771+
// Verify labels are stored in domain XML
772+
println!("Checking domain XML for labels...");
773+
let dumpxml_output = Command::new("virsh")
774+
.args(&["dumpxml", &domain_name])
775+
.output()
776+
.expect("Failed to dump domain XML");
777+
778+
let domain_xml = String::from_utf8_lossy(&dumpxml_output.stdout);
779+
780+
// Check that labels are in the XML
781+
assert!(
782+
domain_xml.contains("bootc:label") || domain_xml.contains("<label>"),
783+
"Domain XML should contain label metadata"
784+
);
785+
assert!(
786+
domain_xml.contains(LIBVIRT_INTEGRATION_TEST_LABEL),
787+
"Domain XML should contain bcvk-integration label"
788+
);
789+
790+
// Test filtering by label
791+
println!("Testing label filtering with libvirt list...");
792+
let list_output = Command::new(&bck)
793+
.args([
794+
"libvirt",
795+
"list",
796+
"--label",
797+
LIBVIRT_INTEGRATION_TEST_LABEL,
798+
"-a",
799+
])
800+
.output()
801+
.expect("Failed to run libvirt list with label filter");
802+
803+
let list_stdout = String::from_utf8_lossy(&list_output.stdout);
804+
println!("List output: {}", list_stdout);
805+
806+
assert!(
807+
list_output.status.success(),
808+
"libvirt list with label filter should succeed"
809+
);
810+
assert!(
811+
list_stdout.contains(&domain_name),
812+
"Domain should appear in filtered list. Output: {}",
813+
list_stdout
814+
);
815+
816+
// Test filtering by a label that should match
817+
let list_test_env = Command::new(&bck)
818+
.args(["libvirt", "list", "--label", "test-env", "-a"])
819+
.output()
820+
.expect("Failed to run libvirt list with test-env label");
821+
822+
let list_test_env_stdout = String::from_utf8_lossy(&list_test_env.stdout);
823+
assert!(
824+
list_test_env_stdout.contains(&domain_name),
825+
"Domain should appear when filtering by test-env label"
826+
);
827+
828+
// Test filtering by a label that should NOT match
829+
let list_nomatch = Command::new(&bck)
830+
.args(["libvirt", "list", "--label", "nonexistent-label", "-a"])
831+
.output()
832+
.expect("Failed to run libvirt list with nonexistent label");
833+
834+
let list_nomatch_stdout = String::from_utf8_lossy(&list_nomatch.stdout);
835+
assert!(
836+
!list_nomatch_stdout.contains(&domain_name),
837+
"Domain should NOT appear when filtering by nonexistent label"
838+
);
839+
840+
// Cleanup domain
841+
cleanup_domain(&domain_name);
842+
843+
println!("✓ Label functionality test passed");
844+
}
845+
702846
/// Test error handling for invalid configurations
703847
pub fn test_libvirt_error_handling() {
704848
let bck = get_bck_command().unwrap();

crates/kit/src/domain_list.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ pub struct PodmanBootcDomain {
2222
pub created: Option<SystemTime>,
2323
/// Memory allocation in MB
2424
pub memory_mb: Option<u32>,
25-
/// Number of virtual CPUs
25+
/// Number of virtual CPUs
2626
pub vcpus: Option<u32>,
2727
/// Disk path
2828
pub disk_path: Option<String>,
29+
/// User-defined labels for organizing domains
30+
pub labels: Vec<String>,
2931
}
3032

3133
impl PodmanBootcDomain {
@@ -176,6 +178,19 @@ impl DomainLister {
176178
.or_else(|| dom.find("created"))
177179
.map(|node| node.text_content().to_string());
178180

181+
// Extract labels (comma-separated)
182+
let labels = dom
183+
.find("bootc:label")
184+
.or_else(|| dom.find("label"))
185+
.map(|node| {
186+
node.text_content()
187+
.split(',')
188+
.map(|s| s.trim().to_string())
189+
.filter(|s| !s.is_empty())
190+
.collect::<Vec<_>>()
191+
})
192+
.unwrap_or_default();
193+
179194
// Extract memory and vcpu from domain XML
180195
let memory_mb = dom.find("memory").and_then(|node| {
181196
// Memory might have unit attribute, but we'll try to parse the value
@@ -195,6 +210,7 @@ impl DomainLister {
195210
memory_mb,
196211
vcpus,
197212
disk_path,
213+
labels,
198214
}))
199215
}
200216

@@ -223,6 +239,10 @@ impl DomainLister {
223239
memory_mb: metadata.as_ref().and_then(|m| m.memory_mb),
224240
vcpus: metadata.as_ref().and_then(|m| m.vcpus),
225241
disk_path: metadata.as_ref().and_then(|m| m.disk_path.clone()),
242+
labels: metadata
243+
.as_ref()
244+
.map(|m| m.labels.clone())
245+
.unwrap_or_default(),
226246
})
227247
}
228248

@@ -284,6 +304,7 @@ struct PodmanBootcDomainMetadata {
284304
memory_mb: Option<u32>,
285305
vcpus: Option<u32>,
286306
disk_path: Option<String>,
307+
labels: Vec<String>,
287308
}
288309

289310
/// Extract disk path from domain XML using DOM parser
@@ -384,6 +405,7 @@ mod tests {
384405
memory_mb: None,
385406
vcpus: None,
386407
disk_path: None,
408+
labels: vec![],
387409
};
388410

389411
assert!(domain.is_running());
@@ -398,6 +420,7 @@ mod tests {
398420
memory_mb: None,
399421
vcpus: None,
400422
disk_path: None,
423+
labels: vec![],
401424
};
402425

403426
assert!(!stopped_domain.is_running());

0 commit comments

Comments
 (0)