Skip to content

Commit 33b5153

Browse files
committed
libvirt: Standardize table output with comfy_table, add JSON format support
Replace manual printf-style table formatting with comfy_table for consistent, well-formatted output across all list commands. Add --format=json support to base-disks list command to match the pattern used by other list commands, enabling machine-readable output for integration and scripting. Assisted-By: Claude Code Signed-off-by: Colin Walters <[email protected]>
1 parent e4cea7b commit 33b5153

File tree

5 files changed

+103
-74
lines changed

5 files changed

+103
-74
lines changed

crates/kit/src/libvirt/base_disks.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ pub fn list_base_disks(connect_uri: Option<&String>) -> Result<Vec<BaseDiskInfo>
363363
}
364364

365365
/// Information about a base disk
366-
#[derive(Debug)]
366+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
367367
pub struct BaseDiskInfo {
368368
pub path: Utf8PathBuf,
369369
pub image_digest: Option<String>,

crates/kit/src/libvirt/base_disks_cli.rs

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
66
use clap::{Parser, Subcommand};
77
use color_eyre::Result;
8+
use comfy_table::{presets::UTF8_FULL, Table};
9+
use serde_json;
810

911
use super::base_disks::{list_base_disks, prune_base_disks};
12+
use super::OutputFormat;
1013

1114
/// Options for base-disks command
1215
#[derive(Debug, Parser)]
@@ -19,11 +22,19 @@ pub struct LibvirtBaseDisksOpts {
1922
#[derive(Debug, Subcommand)]
2023
pub enum BaseDisksSubcommand {
2124
/// List all base disk images
22-
List,
25+
List(ListOpts),
2326
/// Prune unreferenced base disk images
2427
Prune(PruneOpts),
2528
}
2629

30+
/// Options for list command
31+
#[derive(Debug, Parser)]
32+
pub struct ListOpts {
33+
/// Output format
34+
#[clap(long, value_enum, default_value_t = OutputFormat::Table)]
35+
pub format: OutputFormat,
36+
}
37+
2738
/// Options for prune command
2839
#[derive(Debug, Parser)]
2940
pub struct PruneOpts {
@@ -37,59 +48,69 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtBaseDisksO
3748
let connect_uri = global_opts.connect.as_ref();
3849

3950
match opts.command {
40-
BaseDisksSubcommand::List => run_list(connect_uri),
51+
BaseDisksSubcommand::List(list_opts) => run_list(connect_uri, list_opts),
4152
BaseDisksSubcommand::Prune(prune_opts) => run_prune(connect_uri, prune_opts),
4253
}
4354
}
4455

4556
/// Execute the list subcommand
46-
fn run_list(connect_uri: Option<&String>) -> Result<()> {
57+
fn run_list(connect_uri: Option<&String>, opts: ListOpts) -> Result<()> {
4758
let base_disks = list_base_disks(connect_uri)?;
4859

49-
if base_disks.is_empty() {
50-
println!("No base disk images found");
51-
return Ok(());
60+
match opts.format {
61+
OutputFormat::Table => {
62+
if base_disks.is_empty() {
63+
println!("No base disk images found");
64+
return Ok(());
65+
}
66+
67+
let mut table = Table::new();
68+
table.load_preset(UTF8_FULL);
69+
table.set_header(vec!["NAME", "SIZE", "REFS", "IMAGE DIGEST"]);
70+
71+
for disk in &base_disks {
72+
let name = disk.path.file_name().unwrap_or("unknown");
73+
74+
let size = disk
75+
.size
76+
.map(|bytes| indicatif::BinaryBytes(bytes).to_string())
77+
.unwrap_or_else(|| "unknown".to_string());
78+
79+
let refs = disk.ref_count.to_string();
80+
81+
let digest = disk
82+
.image_digest
83+
.as_ref()
84+
.map(|d| {
85+
// Truncate long digests for display
86+
if d.len() > 56 {
87+
format!("{}...", &d[..53])
88+
} else {
89+
d.clone()
90+
}
91+
})
92+
.unwrap_or_else(|| "<no metadata>".to_string());
93+
94+
table.add_row(vec![name, &size, &refs, &digest]);
95+
}
96+
97+
println!("{}", table);
98+
println!(
99+
"\nFound {} base disk{}",
100+
base_disks.len(),
101+
if base_disks.len() == 1 { "" } else { "s" }
102+
);
103+
}
104+
OutputFormat::Json => {
105+
println!("{}", serde_json::to_string_pretty(&base_disks)?);
106+
}
107+
OutputFormat::Yaml => {
108+
return Err(color_eyre::eyre::eyre!(
109+
"YAML format is not supported for base-disks list command"
110+
))
111+
}
52112
}
53113

54-
// Print table header
55-
println!(
56-
"{:<40} {:<10} {:<10} {:<58}",
57-
"NAME", "SIZE", "REFS", "IMAGE DIGEST"
58-
);
59-
println!("{}", "=".repeat(118));
60-
61-
for disk in &base_disks {
62-
let name = disk.path.file_name().unwrap_or("unknown");
63-
64-
let size = disk
65-
.size
66-
.map(|bytes| indicatif::BinaryBytes(bytes).to_string())
67-
.unwrap_or_else(|| "unknown".to_string());
68-
69-
let refs = disk.ref_count.to_string();
70-
71-
let digest = disk
72-
.image_digest
73-
.as_ref()
74-
.map(|d| {
75-
// Truncate long digests for display
76-
if d.len() > 56 {
77-
format!("{}...", &d[..53])
78-
} else {
79-
d.clone()
80-
}
81-
})
82-
.unwrap_or_else(|| "<no metadata>".to_string());
83-
84-
println!("{:<40} {:<10} {:<10} {:<58}", name, size, refs, digest);
85-
}
86-
87-
println!(
88-
"\nFound {} base disk{}",
89-
base_disks.len(),
90-
if base_disks.len() == 1 { "" } else { "s" }
91-
);
92-
93114
Ok(())
94115
}
95116

crates/kit/src/libvirt/inspect.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
use clap::Parser;
77
use color_eyre::Result;
88

9+
use super::OutputFormat;
10+
911
/// Options for inspecting a libvirt domain
1012
#[derive(Debug, Parser)]
1113
pub struct LibvirtInspectOpts {
1214
/// Name of the domain to inspect
1315
pub name: String,
1416

1517
/// Output format
16-
#[clap(long, default_value = "yaml")]
17-
pub format: String,
18+
#[clap(long, value_enum, default_value_t = OutputFormat::Yaml)]
19+
pub format: OutputFormat,
1820
}
1921

2022
/// Execute the libvirt inspect command
@@ -33,8 +35,8 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtInspectOpt
3335
.get_domain_info(&opts.name)
3436
.map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", opts.name))?;
3537

36-
match opts.format.as_str() {
37-
"yaml" => {
38+
match opts.format {
39+
OutputFormat::Yaml => {
3840
println!("name: {}", vm.name);
3941
if let Some(ref image) = vm.image {
4042
println!("image: {}", image);
@@ -50,17 +52,16 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtInspectOpt
5052
println!("disk_path: {}", disk_path);
5153
}
5254
}
53-
"json" => {
55+
OutputFormat::Json => {
5456
println!(
5557
"{}",
5658
serde_json::to_string_pretty(&vm)
5759
.with_context(|| "Failed to serialize VM as JSON")?
5860
);
5961
}
60-
_ => {
62+
OutputFormat::Table => {
6163
return Err(color_eyre::eyre::eyre!(
62-
"Unsupported format: {}",
63-
opts.format
64+
"Table format is not supported for inspect command"
6465
))
6566
}
6667
}

crates/kit/src/libvirt/list.rs

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
66
use clap::Parser;
77
use color_eyre::Result;
8+
use comfy_table::{presets::UTF8_FULL, Table};
9+
10+
use super::OutputFormat;
811

912
/// Options for listing libvirt domains
1013
#[derive(Debug, Parser)]
1114
pub struct LibvirtListOpts {
1215
/// Output format
13-
#[clap(long, default_value = "table")]
14-
pub format: String,
16+
#[clap(long, value_enum, default_value_t = OutputFormat::Table)]
17+
pub format: OutputFormat,
1518

1619
/// Show all domains including stopped ones
1720
#[clap(long, short = 'a')]
@@ -40,8 +43,8 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtListOpts)
4043
.with_context(|| "Failed to list running bootc domains from libvirt")?
4144
};
4245

43-
match opts.format.as_str() {
44-
"table" => {
46+
match opts.format {
47+
OutputFormat::Table => {
4548
if domains.is_empty() {
4649
if opts.all {
4750
println!("No VMs found");
@@ -54,11 +57,11 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtListOpts)
5457
}
5558
return Ok(());
5659
}
57-
println!(
58-
"{:<20} {:<40} {:<12} {:<20}",
59-
"NAME", "IMAGE", "STATUS", "MEMORY"
60-
);
61-
println!("{}", "=".repeat(92));
60+
61+
let mut table = Table::new();
62+
table.load_preset(UTF8_FULL);
63+
table.set_header(vec!["NAME", "IMAGE", "STATUS", "MEMORY"]);
64+
6265
for domain in &domains {
6366
let image = match &domain.image {
6467
Some(img) => {
@@ -74,31 +77,26 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtListOpts)
7477
Some(mem) => format!("{}MB", mem),
7578
None => "unknown".to_string(),
7679
};
77-
println!(
78-
"{:<20} {:<40} {:<12} {:<20}",
79-
domain.name,
80-
image,
81-
domain.status_string(),
82-
memory
83-
);
80+
table.add_row(vec![&domain.name, &image, &domain.status_string(), &memory]);
8481
}
82+
83+
println!("{}", table);
8584
println!(
8685
"\nFound {} domain{} (source: libvirt)",
8786
domains.len(),
8887
if domains.len() == 1 { "" } else { "s" }
8988
);
9089
}
91-
"json" => {
90+
OutputFormat::Json => {
9291
println!(
9392
"{}",
9493
serde_json::to_string_pretty(&domains)
9594
.with_context(|| "Failed to serialize domains as JSON")?
9695
);
9796
}
98-
_ => {
97+
OutputFormat::Yaml => {
9998
return Err(color_eyre::eyre::eyre!(
100-
"Unsupported format: {}",
101-
opts.format
99+
"YAML format is not supported for list command"
102100
))
103101
}
104102
}

crates/kit/src/libvirt/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
99
use clap::Subcommand;
1010

11+
/// Output format options for libvirt commands
12+
#[derive(Debug, Clone, clap::ValueEnum)]
13+
#[clap(rename_all = "kebab-case")]
14+
pub enum OutputFormat {
15+
Table,
16+
Json,
17+
Yaml,
18+
}
19+
1120
/// Default memory allocation for libvirt VMs
1221
pub const LIBVIRT_DEFAULT_MEMORY: &str = "4G";
1322

0 commit comments

Comments
 (0)