mirror of
https://github.com/CloudNebulaProject/zmgr.git
synced 2026-04-10 13:10:42 +00:00
Improve DX: JSON output, boot/halt, tables, publisher fix
Batch 2 of developer experience improvements: - Add --json flag for machine-readable output on list/show/status commands - Add boot and halt subcommands as thin wrappers around zoneadm - Publisher remove now accepts publisher name (not just filename stem) - Tables auto-size columns based on content instead of hardcoded widths - Import uses current date for created field and matches template net names - Default public pool includes RFC 5737 documentation comment - Template list uses proper table format with separate columns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
382823b228
commit
6e2915fdc9
4 changed files with 354 additions and 71 deletions
|
|
@ -46,16 +46,28 @@ pub fn import_zones(registry: &Path, filter_name: Option<&str>) -> Result<Vec<St
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse all network interfaces from zonecfg info
|
// Parse all network interfaces from zonecfg info
|
||||||
let nets = parse_zonecfg_nets(&info, &entry.name, &pools, registry);
|
let mut nets = parse_zonecfg_nets(&info, &entry.name, &pools, registry);
|
||||||
|
|
||||||
// Match to a template by brand
|
// Match to a template by brand
|
||||||
let template = match_template(&entry.brand, &templates, registry);
|
let template_name = match_template(&entry.brand, &templates, registry);
|
||||||
|
|
||||||
|
// Try to use template net names if we matched a template
|
||||||
|
if let Some(ref tname) = template_name {
|
||||||
|
if let Ok(tmpl) = crate::template::Template::load(registry, tname) {
|
||||||
|
if tmpl.nets.len() == nets.len() {
|
||||||
|
for (i, tnet) in tmpl.nets.iter().enumerate() {
|
||||||
|
nets[i].name = tnet.name.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||||
let zone = Zone {
|
let zone = Zone {
|
||||||
name: entry.name.clone(),
|
name: entry.name.clone(),
|
||||||
template: template.unwrap_or_else(|| entry.brand.clone()),
|
template: template_name.unwrap_or_else(|| entry.brand.clone()),
|
||||||
nets,
|
nets,
|
||||||
created: String::new(), // unknown for imported zones
|
created: today,
|
||||||
};
|
};
|
||||||
|
|
||||||
zone.write(registry)?;
|
zone.write(registry)?;
|
||||||
|
|
|
||||||
374
src/main.rs
374
src/main.rs
|
|
@ -43,6 +43,10 @@ struct Cli {
|
||||||
#[arg(long, short = 'y')]
|
#[arg(long, short = 'y')]
|
||||||
yes: bool,
|
yes: bool,
|
||||||
|
|
||||||
|
/// Output in JSON format (for scripting)
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +72,16 @@ enum Commands {
|
||||||
/// Zone name
|
/// Zone name
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
/// Boot a zone
|
||||||
|
Boot {
|
||||||
|
/// Zone name
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Halt a zone
|
||||||
|
Halt {
|
||||||
|
/// Zone name
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
/// List managed zones
|
/// List managed zones
|
||||||
List,
|
List,
|
||||||
/// Show zone status (registry + system state)
|
/// Show zone status (registry + system state)
|
||||||
|
|
@ -144,6 +158,7 @@ fn main() -> miette::Result<()> {
|
||||||
|
|
||||||
let dry_run = cli.dry_run;
|
let dry_run = cli.dry_run;
|
||||||
let yes = cli.yes;
|
let yes = cli.yes;
|
||||||
|
let json = cli.json;
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Init { force } => cmd_init(®istry, force),
|
Commands::Init { force } => cmd_init(®istry, force),
|
||||||
|
|
@ -151,20 +166,22 @@ fn main() -> miette::Result<()> {
|
||||||
cmd_create(®istry, &name, template.as_deref(), dry_run)
|
cmd_create(®istry, &name, template.as_deref(), dry_run)
|
||||||
}
|
}
|
||||||
Commands::Destroy { name } => cmd_destroy(®istry, &name, dry_run, yes),
|
Commands::Destroy { name } => cmd_destroy(®istry, &name, dry_run, yes),
|
||||||
Commands::List => cmd_list(®istry),
|
Commands::Boot { name } => cmd_boot(®istry, &name),
|
||||||
Commands::Status { name } => cmd_status(®istry, name.as_deref()),
|
Commands::Halt { name } => cmd_halt(®istry, &name),
|
||||||
|
Commands::List => cmd_list(®istry, json),
|
||||||
|
Commands::Status { name } => cmd_status(®istry, name.as_deref(), json),
|
||||||
Commands::Import { name } => cmd_import(®istry, name.as_deref()),
|
Commands::Import { name } => cmd_import(®istry, name.as_deref()),
|
||||||
Commands::Validate => cmd_validate(®istry),
|
Commands::Validate => cmd_validate(®istry),
|
||||||
Commands::Template { action } => match action {
|
Commands::Template { action } => match action {
|
||||||
TemplateAction::List => cmd_template_list(®istry),
|
TemplateAction::List => cmd_template_list(®istry, json),
|
||||||
TemplateAction::Show { name } => cmd_template_show(®istry, &name),
|
TemplateAction::Show { name } => cmd_template_show(®istry, &name, json),
|
||||||
},
|
},
|
||||||
Commands::Pool { action } => match action {
|
Commands::Pool { action } => match action {
|
||||||
PoolAction::List => cmd_pool_list(®istry),
|
PoolAction::List => cmd_pool_list(®istry, json),
|
||||||
PoolAction::Show { name } => cmd_pool_show(®istry, &name),
|
PoolAction::Show { name } => cmd_pool_show(®istry, &name, json),
|
||||||
},
|
},
|
||||||
Commands::Publisher { action } => match action {
|
Commands::Publisher { action } => match action {
|
||||||
PublisherAction::List => cmd_publisher_list(®istry),
|
PublisherAction::List => cmd_publisher_list(®istry, json),
|
||||||
PublisherAction::Add { name, origin } => cmd_publisher_add(®istry, &name, &origin),
|
PublisherAction::Add { name, origin } => cmd_publisher_add(®istry, &name, &origin),
|
||||||
PublisherAction::Remove { name } => cmd_publisher_remove(®istry, &name),
|
PublisherAction::Remove { name } => cmd_publisher_remove(®istry, &name),
|
||||||
},
|
},
|
||||||
|
|
@ -574,33 +591,120 @@ fn cmd_destroy(registry: &Path, name: &str, dry_run: bool, yes: bool) -> Result<
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_list(registry: &Path) -> Result<()> {
|
fn cmd_boot(registry: &Path, name: &str) -> Result<()> {
|
||||||
|
config::ensure_initialized(registry)?;
|
||||||
|
if !Zone::exists(registry, name) {
|
||||||
|
return Err(ZmgrError::ZoneNotFound {
|
||||||
|
name: name.to_string(),
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
exec::zoneadm_boot(name)?;
|
||||||
|
println!("Zone '{name}' booted.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_halt(registry: &Path, name: &str) -> Result<()> {
|
||||||
|
config::ensure_initialized(registry)?;
|
||||||
|
if !Zone::exists(registry, name) {
|
||||||
|
return Err(ZmgrError::ZoneNotFound {
|
||||||
|
name: name.to_string(),
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
exec::zoneadm_halt(name)?;
|
||||||
|
println!("Zone '{name}' halted.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Table helper ---
|
||||||
|
|
||||||
|
/// Print a table with auto-sized columns. Last column is unbounded.
|
||||||
|
fn print_table(headers: &[&str], rows: &[Vec<String>]) {
|
||||||
|
let col_count = headers.len();
|
||||||
|
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
for (i, cell) in row.iter().enumerate() {
|
||||||
|
if i < col_count - 1 {
|
||||||
|
// Don't measure last column (unbounded)
|
||||||
|
widths[i] = widths[i].max(cell.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print header
|
||||||
|
for (i, h) in headers.iter().enumerate() {
|
||||||
|
if i < col_count - 1 {
|
||||||
|
print!("{:<width$} ", h, width = widths[i]);
|
||||||
|
} else {
|
||||||
|
print!("{}", h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Print rows
|
||||||
|
for row in rows {
|
||||||
|
for (i, cell) in row.iter().enumerate() {
|
||||||
|
if i < col_count - 1 {
|
||||||
|
print!("{:<width$} ", cell, width = widths[i]);
|
||||||
|
} else {
|
||||||
|
print!("{}", cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_list(registry: &Path, json: bool) -> Result<()> {
|
||||||
config::ensure_initialized(registry)?;
|
config::ensure_initialized(registry)?;
|
||||||
|
|
||||||
let zones = Zone::list(registry)?;
|
let zones = Zone::list(registry)?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let out: Vec<serde_json::Value> = zones
|
||||||
|
.iter()
|
||||||
|
.map(|z| {
|
||||||
|
serde_json::json!({
|
||||||
|
"name": z.name,
|
||||||
|
"template": z.template,
|
||||||
|
"created": z.created,
|
||||||
|
"nets": z.nets.iter().map(|n| serde_json::json!({
|
||||||
|
"name": n.name,
|
||||||
|
"address": n.address,
|
||||||
|
"gateway": n.gateway,
|
||||||
|
"vnic": n.vnic,
|
||||||
|
"stub": n.stub,
|
||||||
|
})).collect::<Vec<_>>(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
println!("{}", serde_json::to_string_pretty(&out).unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if zones.is_empty() {
|
if zones.is_empty() {
|
||||||
println!("No managed zones. Use `zmgr create <name>` or `zmgr import`.");
|
println!("No managed zones. Use `zmgr create <name>` or `zmgr import`.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
let rows: Vec<Vec<String>> = zones
|
||||||
"{:<20} {:<10} {:<10} {}",
|
.iter()
|
||||||
"NAME", "TEMPLATE", "NETS", "ADDRESSES"
|
.map(|z| {
|
||||||
);
|
|
||||||
for z in &zones {
|
|
||||||
let addrs: Vec<&str> = z.nets.iter().map(|n| n.address.as_str()).collect();
|
let addrs: Vec<&str> = z.nets.iter().map(|n| n.address.as_str()).collect();
|
||||||
println!(
|
vec![
|
||||||
"{:<20} {:<10} {:<10} {}",
|
z.name.clone(),
|
||||||
z.name,
|
z.template.clone(),
|
||||||
z.template,
|
z.nets.len().to_string(),
|
||||||
z.nets.len(),
|
addrs.join(", "),
|
||||||
addrs.join(", ")
|
]
|
||||||
);
|
})
|
||||||
}
|
.collect();
|
||||||
|
print_table(&["NAME", "TEMPLATE", "NETS", "ADDRESSES"], &rows);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
fn cmd_status(registry: &Path, name: Option<&str>, json: bool) -> Result<()> {
|
||||||
config::ensure_initialized(registry)?;
|
config::ensure_initialized(registry)?;
|
||||||
|
|
||||||
let system_zones = exec::zoneadm_list()?;
|
let system_zones = exec::zoneadm_list()?;
|
||||||
|
|
@ -610,6 +714,34 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||||
let reg = registry_zones.iter().find(|z| z.name == name);
|
let reg = registry_zones.iter().find(|z| z.name == name);
|
||||||
let sys = system_zones.iter().find(|z| z.name == name);
|
let sys = system_zones.iter().find(|z| z.name == name);
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let mut obj = serde_json::Map::new();
|
||||||
|
obj.insert("name".into(), name.into());
|
||||||
|
if let Some(r) = reg {
|
||||||
|
obj.insert("template".into(), r.template.clone().into());
|
||||||
|
obj.insert("created".into(), r.created.clone().into());
|
||||||
|
obj.insert("in_registry".into(), true.into());
|
||||||
|
let nets: Vec<serde_json::Value> = r.nets.iter().map(|n| serde_json::json!({
|
||||||
|
"name": n.name, "address": n.address,
|
||||||
|
"gateway": n.gateway, "vnic": n.vnic, "stub": n.stub,
|
||||||
|
})).collect();
|
||||||
|
obj.insert("nets".into(), nets.into());
|
||||||
|
} else {
|
||||||
|
obj.insert("in_registry".into(), false.into());
|
||||||
|
}
|
||||||
|
if let Some(s) = sys {
|
||||||
|
obj.insert("brand".into(), s.brand.clone().into());
|
||||||
|
obj.insert("state".into(), s.state.clone().into());
|
||||||
|
obj.insert("path".into(), s.path.clone().into());
|
||||||
|
obj.insert("uuid".into(), s.uuid.clone().into());
|
||||||
|
obj.insert("on_system".into(), true.into());
|
||||||
|
} else {
|
||||||
|
obj.insert("on_system".into(), false.into());
|
||||||
|
}
|
||||||
|
println!("{}", serde_json::to_string_pretty(&serde_json::Value::Object(obj)).unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
match (reg, sys) {
|
match (reg, sys) {
|
||||||
(Some(r), Some(s)) => {
|
(Some(r), Some(s)) => {
|
||||||
println!("Zone: {name}");
|
println!("Zone: {name}");
|
||||||
|
|
@ -648,11 +780,32 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!(
|
if json {
|
||||||
"{:<20} {:<10} {:<12} {:<10} {}",
|
let mut entries = Vec::new();
|
||||||
"NAME", "TEMPLATE", "STATE", "REGISTRY", "ADDRESSES"
|
let mut seen = HashSet::new();
|
||||||
);
|
for r in ®istry_zones {
|
||||||
|
let state = system_zones.iter().find(|s| s.name == r.name)
|
||||||
|
.map(|s| s.state.as_str()).unwrap_or("absent");
|
||||||
|
entries.push(serde_json::json!({
|
||||||
|
"name": r.name, "template": r.template, "state": state,
|
||||||
|
"in_registry": true,
|
||||||
|
"addresses": r.nets.iter().map(|n| &n.address).collect::<Vec<_>>(),
|
||||||
|
}));
|
||||||
|
seen.insert(r.name.clone());
|
||||||
|
}
|
||||||
|
for s in &system_zones {
|
||||||
|
if !seen.contains(&s.name) && s.brand != "bhyve" {
|
||||||
|
entries.push(serde_json::json!({
|
||||||
|
"name": s.name, "brand": s.brand, "state": s.state,
|
||||||
|
"in_registry": false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("{}", serde_json::to_string_pretty(&entries).unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rows = Vec::new();
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
|
|
||||||
for r in ®istry_zones {
|
for r in ®istry_zones {
|
||||||
|
|
@ -662,22 +815,29 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||||
.map(|s| s.state.as_str())
|
.map(|s| s.state.as_str())
|
||||||
.unwrap_or("absent");
|
.unwrap_or("absent");
|
||||||
let addrs: Vec<&str> = r.nets.iter().map(|n| n.address.as_str()).collect();
|
let addrs: Vec<&str> = r.nets.iter().map(|n| n.address.as_str()).collect();
|
||||||
println!(
|
rows.push(vec![
|
||||||
"{:<20} {:<10} {:<12} {:<10} {}",
|
r.name.clone(),
|
||||||
r.name, r.template, state, "yes",
|
r.template.clone(),
|
||||||
addrs.join(", ")
|
state.to_string(),
|
||||||
);
|
"yes".to_string(),
|
||||||
|
addrs.join(", "),
|
||||||
|
]);
|
||||||
seen.insert(r.name.clone());
|
seen.insert(r.name.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
for s in &system_zones {
|
for s in &system_zones {
|
||||||
if !seen.contains(&s.name) && s.brand != "bhyve" {
|
if !seen.contains(&s.name) && s.brand != "bhyve" {
|
||||||
println!(
|
rows.push(vec![
|
||||||
"{:<20} {:<10} {:<12} {:<10} {}",
|
s.name.clone(),
|
||||||
s.name, s.brand, s.state, "no", "-"
|
s.brand.clone(),
|
||||||
);
|
s.state.clone(),
|
||||||
|
"no".to_string(),
|
||||||
|
"-".to_string(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print_table(&["NAME", "TEMPLATE", "STATE", "REGISTRY", "ADDRESSES"], &rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -715,14 +875,35 @@ fn cmd_validate(registry: &Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_template_list(registry: &Path) -> Result<()> {
|
fn cmd_template_list(registry: &Path, json: bool) -> Result<()> {
|
||||||
config::ensure_initialized(registry)?;
|
config::ensure_initialized(registry)?;
|
||||||
|
|
||||||
let names = Template::list(registry)?;
|
let names = Template::list(registry)?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for name in &names {
|
||||||
|
let tmpl = Template::load(registry, name)?;
|
||||||
|
out.push(serde_json::json!({
|
||||||
|
"name": name,
|
||||||
|
"brand": tmpl.brand,
|
||||||
|
"autoboot": tmpl.autoboot,
|
||||||
|
"ip_type": tmpl.ip_type,
|
||||||
|
"nets": tmpl.nets.iter().map(|n| serde_json::json!({
|
||||||
|
"name": n.name, "pool": n.pool,
|
||||||
|
})).collect::<Vec<_>>(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
println!("{}", serde_json::to_string_pretty(&out).unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if names.is_empty() {
|
if names.is_empty() {
|
||||||
println!("No templates defined.");
|
println!("No templates defined.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut rows = Vec::new();
|
||||||
for name in &names {
|
for name in &names {
|
||||||
let tmpl = Template::load(registry, name)?;
|
let tmpl = Template::load(registry, name)?;
|
||||||
let net_desc: Vec<String> = tmpl
|
let net_desc: Vec<String> = tmpl
|
||||||
|
|
@ -730,21 +911,36 @@ fn cmd_template_list(registry: &Path) -> Result<()> {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|n| format!("{}:{}", n.name, n.pool))
|
.map(|n| format!("{}:{}", n.name, n.pool))
|
||||||
.collect();
|
.collect();
|
||||||
println!(
|
rows.push(vec![
|
||||||
"{:<10} brand={:<10} nets={:<25} autoboot={}",
|
name.clone(),
|
||||||
name,
|
|
||||||
tmpl.brand,
|
tmpl.brand,
|
||||||
net_desc.join(", "),
|
net_desc.join(", "),
|
||||||
tmpl.autoboot
|
tmpl.autoboot.to_string(),
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
|
print_table(&["NAME", "BRAND", "NETS", "AUTOBOOT"], &rows);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_template_show(registry: &Path, name: &str) -> Result<()> {
|
fn cmd_template_show(registry: &Path, name: &str, json: bool) -> Result<()> {
|
||||||
config::ensure_initialized(registry)?;
|
config::ensure_initialized(registry)?;
|
||||||
|
|
||||||
let tmpl = Template::load(registry, name)?;
|
let tmpl = Template::load(registry, name)?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let out = serde_json::json!({
|
||||||
|
"name": name,
|
||||||
|
"brand": tmpl.brand,
|
||||||
|
"autoboot": tmpl.autoboot,
|
||||||
|
"ip_type": tmpl.ip_type,
|
||||||
|
"nets": tmpl.nets.iter().map(|n| serde_json::json!({
|
||||||
|
"name": n.name, "pool": n.pool,
|
||||||
|
})).collect::<Vec<_>>(),
|
||||||
|
});
|
||||||
|
println!("{}", serde_json::to_string_pretty(&out).unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
println!("Template: {name}");
|
println!("Template: {name}");
|
||||||
println!(" brand: {}", tmpl.brand);
|
println!(" brand: {}", tmpl.brand);
|
||||||
println!(" autoboot: {}", tmpl.autoboot);
|
println!(" autoboot: {}", tmpl.autoboot);
|
||||||
|
|
@ -756,45 +952,58 @@ fn cmd_template_show(registry: &Path, name: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_pool_list(registry: &Path) -> Result<()> {
|
fn cmd_pool_list(registry: &Path, json: bool) -> Result<()> {
|
||||||
config::ensure_initialized(registry)?;
|
config::ensure_initialized(registry)?;
|
||||||
|
|
||||||
let names = Pool::list(registry)?;
|
let names = Pool::list(registry)?;
|
||||||
|
let zones = Zone::list(registry)?;
|
||||||
|
let used_ips = Zone::all_allocated_ips(&zones);
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for name in &names {
|
||||||
|
let pool = Pool::load(registry, name)?;
|
||||||
|
let allocated = used_ips.iter().filter(|ip| pool.network.contains(*ip)).count();
|
||||||
|
out.push(serde_json::json!({
|
||||||
|
"name": name,
|
||||||
|
"network": pool.network.to_string(),
|
||||||
|
"gateway": pool.gateway.to_string(),
|
||||||
|
"stub": pool.stub,
|
||||||
|
"allocated": allocated,
|
||||||
|
"total": pool.total_addresses(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
println!("{}", serde_json::to_string_pretty(&out).unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if names.is_empty() {
|
if names.is_empty() {
|
||||||
println!("No pools defined.");
|
println!("No pools defined.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let zones = Zone::list(registry)?;
|
let mut rows = Vec::new();
|
||||||
let used_ips = Zone::all_allocated_ips(&zones);
|
|
||||||
|
|
||||||
for name in &names {
|
for name in &names {
|
||||||
let pool = Pool::load(registry, name)?;
|
let pool = Pool::load(registry, name)?;
|
||||||
let allocated = used_ips
|
let allocated = used_ips.iter().filter(|ip| pool.network.contains(*ip)).count();
|
||||||
.iter()
|
|
||||||
.filter(|ip| pool.network.contains(*ip))
|
|
||||||
.count();
|
|
||||||
let total = pool.total_addresses();
|
let total = pool.total_addresses();
|
||||||
println!(
|
rows.push(vec![
|
||||||
"{:<10} network={:<18} stub={:<12} used={}/{}",
|
name.clone(),
|
||||||
name, pool.network, pool.stub, allocated, total
|
pool.network.to_string(),
|
||||||
);
|
pool.stub.clone(),
|
||||||
|
format!("{}/{}", allocated, total),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
print_table(&["NAME", "NETWORK", "STUB", "USED"], &rows);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_pool_show(registry: &Path, name: &str) -> Result<()> {
|
fn cmd_pool_show(registry: &Path, name: &str, json: bool) -> Result<()> {
|
||||||
config::ensure_initialized(registry)?;
|
config::ensure_initialized(registry)?;
|
||||||
|
|
||||||
let pool = Pool::load(registry, name)?;
|
let pool = Pool::load(registry, name)?;
|
||||||
let zones = Zone::list(registry)?;
|
let zones = Zone::list(registry)?;
|
||||||
|
|
||||||
println!("Pool: {name}");
|
|
||||||
println!(" network: {}", pool.network);
|
|
||||||
println!(" gateway: {}", pool.gateway);
|
|
||||||
println!(" stub: {}", pool.stub);
|
|
||||||
println!(" addresses: {}", pool.source_description());
|
|
||||||
|
|
||||||
let mut allocations = Vec::new();
|
let mut allocations = Vec::new();
|
||||||
for z in &zones {
|
for z in &zones {
|
||||||
for net in &z.nets {
|
for net in &z.nets {
|
||||||
|
|
@ -811,6 +1020,28 @@ fn cmd_pool_show(registry: &Path, name: &str) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let out = serde_json::json!({
|
||||||
|
"name": name,
|
||||||
|
"network": pool.network.to_string(),
|
||||||
|
"gateway": pool.gateway.to_string(),
|
||||||
|
"stub": pool.stub,
|
||||||
|
"addresses": pool.source_description(),
|
||||||
|
"total": pool.total_addresses(),
|
||||||
|
"allocations": allocations.iter().map(|(zn, nn, addr)| serde_json::json!({
|
||||||
|
"address": addr, "zone": zn, "net": nn,
|
||||||
|
})).collect::<Vec<_>>(),
|
||||||
|
});
|
||||||
|
println!("{}", serde_json::to_string_pretty(&out).unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Pool: {name}");
|
||||||
|
println!(" network: {}", pool.network);
|
||||||
|
println!(" gateway: {}", pool.gateway);
|
||||||
|
println!(" stub: {}", pool.stub);
|
||||||
|
println!(" addresses: {}", pool.source_description());
|
||||||
|
|
||||||
if allocations.is_empty() {
|
if allocations.is_empty() {
|
||||||
println!(" allocations: none");
|
println!(" allocations: none");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -823,18 +1054,35 @@ fn cmd_pool_show(registry: &Path, name: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_publisher_list(registry: &Path) -> Result<()> {
|
fn cmd_publisher_list(registry: &Path, json: bool) -> Result<()> {
|
||||||
config::ensure_initialized(registry)?;
|
config::ensure_initialized(registry)?;
|
||||||
|
|
||||||
let names = Publisher::list(registry)?;
|
let names = Publisher::list(registry)?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for name in &names {
|
||||||
|
let pub_ = Publisher::load(registry, name)?;
|
||||||
|
out.push(serde_json::json!({
|
||||||
|
"name": pub_.name,
|
||||||
|
"origin": pub_.origin,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
println!("{}", serde_json::to_string_pretty(&out).unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if names.is_empty() {
|
if names.is_empty() {
|
||||||
println!("No publishers configured.");
|
println!("No publishers configured.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut rows = Vec::new();
|
||||||
for name in &names {
|
for name in &names {
|
||||||
let pub_ = Publisher::load(registry, name)?;
|
let pub_ = Publisher::load(registry, name)?;
|
||||||
println!("{:<30} {}", pub_.name, pub_.origin);
|
rows.push(vec![pub_.name, pub_.origin]);
|
||||||
}
|
}
|
||||||
|
print_table(&["NAME", "ORIGIN"], &rows);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,8 @@ impl Pool {
|
||||||
// Example public pool with explicit address list
|
// Example public pool with explicit address list
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
dir.join("public.kdl"),
|
dir.join("public.kdl"),
|
||||||
r#"// Public addresses from hoster — list each usable IP
|
r#"// Example public pool — replace with your hoster's actual addresses
|
||||||
|
// 203.0.113.0/28 is RFC 5737 documentation range, not routable
|
||||||
pool "public" {
|
pool "public" {
|
||||||
network "203.0.113.0/28"
|
network "203.0.113.0/28"
|
||||||
gateway "203.0.113.1"
|
gateway "203.0.113.1"
|
||||||
|
|
|
||||||
|
|
@ -92,13 +92,35 @@ impl Publisher {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a publisher by filename stem.
|
/// Remove a publisher by filename stem or publisher name.
|
||||||
pub fn remove(registry: &Path, name: &str) -> Result<()> {
|
pub fn remove(registry: &Path, name: &str) -> Result<()> {
|
||||||
let dir = registry.join("publishers");
|
let dir = registry.join("publishers");
|
||||||
|
|
||||||
|
// Try exact filename stem first
|
||||||
let path = dir.join(format!("{name}.kdl"));
|
let path = dir.join(format!("{name}.kdl"));
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
std::fs::remove_file(&path).map_err(ZmgrError::Io)?;
|
std::fs::remove_file(&path).map_err(ZmgrError::Io)?;
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try matching by publisher name inside the KDL files
|
||||||
|
if dir.exists() {
|
||||||
|
for entry in std::fs::read_dir(&dir).map_err(ZmgrError::Io)? {
|
||||||
|
let entry = entry.map_err(ZmgrError::Io)?;
|
||||||
|
let fpath = entry.path();
|
||||||
|
if fpath.extension().is_some_and(|e| e == "kdl") {
|
||||||
|
if let Some(stem) = fpath.file_stem() {
|
||||||
|
if let Ok(pub_) = Publisher::load(registry, &stem.to_string_lossy()) {
|
||||||
|
if pub_.name == name {
|
||||||
|
std::fs::remove_file(&fpath).map_err(ZmgrError::Io)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue