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
|
||||
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
|
||||
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 {
|
||||
name: entry.name.clone(),
|
||||
template: template.unwrap_or_else(|| entry.brand.clone()),
|
||||
template: template_name.unwrap_or_else(|| entry.brand.clone()),
|
||||
nets,
|
||||
created: String::new(), // unknown for imported zones
|
||||
created: today,
|
||||
};
|
||||
|
||||
zone.write(registry)?;
|
||||
|
|
|
|||
374
src/main.rs
374
src/main.rs
|
|
@ -43,6 +43,10 @@ struct Cli {
|
|||
#[arg(long, short = 'y')]
|
||||
yes: bool,
|
||||
|
||||
/// Output in JSON format (for scripting)
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
|
@ -68,6 +72,16 @@ enum Commands {
|
|||
/// Zone name
|
||||
name: String,
|
||||
},
|
||||
/// Boot a zone
|
||||
Boot {
|
||||
/// Zone name
|
||||
name: String,
|
||||
},
|
||||
/// Halt a zone
|
||||
Halt {
|
||||
/// Zone name
|
||||
name: String,
|
||||
},
|
||||
/// List managed zones
|
||||
List,
|
||||
/// Show zone status (registry + system state)
|
||||
|
|
@ -144,6 +158,7 @@ fn main() -> miette::Result<()> {
|
|||
|
||||
let dry_run = cli.dry_run;
|
||||
let yes = cli.yes;
|
||||
let json = cli.json;
|
||||
|
||||
match cli.command {
|
||||
Commands::Init { force } => cmd_init(®istry, force),
|
||||
|
|
@ -151,20 +166,22 @@ fn main() -> miette::Result<()> {
|
|||
cmd_create(®istry, &name, template.as_deref(), dry_run)
|
||||
}
|
||||
Commands::Destroy { name } => cmd_destroy(®istry, &name, dry_run, yes),
|
||||
Commands::List => cmd_list(®istry),
|
||||
Commands::Status { name } => cmd_status(®istry, name.as_deref()),
|
||||
Commands::Boot { name } => cmd_boot(®istry, &name),
|
||||
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::Validate => cmd_validate(®istry),
|
||||
Commands::Template { action } => match action {
|
||||
TemplateAction::List => cmd_template_list(®istry),
|
||||
TemplateAction::Show { name } => cmd_template_show(®istry, &name),
|
||||
TemplateAction::List => cmd_template_list(®istry, json),
|
||||
TemplateAction::Show { name } => cmd_template_show(®istry, &name, json),
|
||||
},
|
||||
Commands::Pool { action } => match action {
|
||||
PoolAction::List => cmd_pool_list(®istry),
|
||||
PoolAction::Show { name } => cmd_pool_show(®istry, &name),
|
||||
PoolAction::List => cmd_pool_list(®istry, json),
|
||||
PoolAction::Show { name } => cmd_pool_show(®istry, &name, json),
|
||||
},
|
||||
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::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(())
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
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() {
|
||||
println!("No managed zones. Use `zmgr create <name>` or `zmgr import`.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!(
|
||||
"{:<20} {:<10} {:<10} {}",
|
||||
"NAME", "TEMPLATE", "NETS", "ADDRESSES"
|
||||
);
|
||||
for z in &zones {
|
||||
let rows: Vec<Vec<String>> = zones
|
||||
.iter()
|
||||
.map(|z| {
|
||||
let addrs: Vec<&str> = z.nets.iter().map(|n| n.address.as_str()).collect();
|
||||
println!(
|
||||
"{:<20} {:<10} {:<10} {}",
|
||||
z.name,
|
||||
z.template,
|
||||
z.nets.len(),
|
||||
addrs.join(", ")
|
||||
);
|
||||
}
|
||||
vec![
|
||||
z.name.clone(),
|
||||
z.template.clone(),
|
||||
z.nets.len().to_string(),
|
||||
addrs.join(", "),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
print_table(&["NAME", "TEMPLATE", "NETS", "ADDRESSES"], &rows);
|
||||
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)?;
|
||||
|
||||
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 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) {
|
||||
(Some(r), Some(s)) => {
|
||||
println!("Zone: {name}");
|
||||
|
|
@ -648,11 +780,32 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{:<20} {:<10} {:<12} {:<10} {}",
|
||||
"NAME", "TEMPLATE", "STATE", "REGISTRY", "ADDRESSES"
|
||||
);
|
||||
if json {
|
||||
let mut entries = Vec::new();
|
||||
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();
|
||||
|
||||
for r in ®istry_zones {
|
||||
|
|
@ -662,22 +815,29 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
|||
.map(|s| s.state.as_str())
|
||||
.unwrap_or("absent");
|
||||
let addrs: Vec<&str> = r.nets.iter().map(|n| n.address.as_str()).collect();
|
||||
println!(
|
||||
"{:<20} {:<10} {:<12} {:<10} {}",
|
||||
r.name, r.template, state, "yes",
|
||||
addrs.join(", ")
|
||||
);
|
||||
rows.push(vec![
|
||||
r.name.clone(),
|
||||
r.template.clone(),
|
||||
state.to_string(),
|
||||
"yes".to_string(),
|
||||
addrs.join(", "),
|
||||
]);
|
||||
seen.insert(r.name.clone());
|
||||
}
|
||||
|
||||
for s in &system_zones {
|
||||
if !seen.contains(&s.name) && s.brand != "bhyve" {
|
||||
println!(
|
||||
"{:<20} {:<10} {:<12} {:<10} {}",
|
||||
s.name, s.brand, s.state, "no", "-"
|
||||
);
|
||||
rows.push(vec![
|
||||
s.name.clone(),
|
||||
s.brand.clone(),
|
||||
s.state.clone(),
|
||||
"no".to_string(),
|
||||
"-".to_string(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
print_table(&["NAME", "TEMPLATE", "STATE", "REGISTRY", "ADDRESSES"], &rows);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -715,14 +875,35 @@ fn cmd_validate(registry: &Path) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_template_list(registry: &Path) -> Result<()> {
|
||||
fn cmd_template_list(registry: &Path, json: bool) -> Result<()> {
|
||||
config::ensure_initialized(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() {
|
||||
println!("No templates defined.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for name in &names {
|
||||
let tmpl = Template::load(registry, name)?;
|
||||
let net_desc: Vec<String> = tmpl
|
||||
|
|
@ -730,21 +911,36 @@ fn cmd_template_list(registry: &Path) -> Result<()> {
|
|||
.iter()
|
||||
.map(|n| format!("{}:{}", n.name, n.pool))
|
||||
.collect();
|
||||
println!(
|
||||
"{:<10} brand={:<10} nets={:<25} autoboot={}",
|
||||
name,
|
||||
rows.push(vec![
|
||||
name.clone(),
|
||||
tmpl.brand,
|
||||
net_desc.join(", "),
|
||||
tmpl.autoboot
|
||||
);
|
||||
tmpl.autoboot.to_string(),
|
||||
]);
|
||||
}
|
||||
print_table(&["NAME", "BRAND", "NETS", "AUTOBOOT"], &rows);
|
||||
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)?;
|
||||
|
||||
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!(" brand: {}", tmpl.brand);
|
||||
println!(" autoboot: {}", tmpl.autoboot);
|
||||
|
|
@ -756,45 +952,58 @@ fn cmd_template_show(registry: &Path, name: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_pool_list(registry: &Path) -> Result<()> {
|
||||
fn cmd_pool_list(registry: &Path, json: bool) -> Result<()> {
|
||||
config::ensure_initialized(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() {
|
||||
println!("No pools defined.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let zones = Zone::list(registry)?;
|
||||
let used_ips = Zone::all_allocated_ips(&zones);
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for name in &names {
|
||||
let pool = Pool::load(registry, name)?;
|
||||
let allocated = used_ips
|
||||
.iter()
|
||||
.filter(|ip| pool.network.contains(*ip))
|
||||
.count();
|
||||
let allocated = used_ips.iter().filter(|ip| pool.network.contains(*ip)).count();
|
||||
let total = pool.total_addresses();
|
||||
println!(
|
||||
"{:<10} network={:<18} stub={:<12} used={}/{}",
|
||||
name, pool.network, pool.stub, allocated, total
|
||||
);
|
||||
rows.push(vec![
|
||||
name.clone(),
|
||||
pool.network.to_string(),
|
||||
pool.stub.clone(),
|
||||
format!("{}/{}", allocated, total),
|
||||
]);
|
||||
}
|
||||
print_table(&["NAME", "NETWORK", "STUB", "USED"], &rows);
|
||||
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)?;
|
||||
|
||||
let pool = Pool::load(registry, name)?;
|
||||
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();
|
||||
for z in &zones {
|
||||
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() {
|
||||
println!(" allocations: none");
|
||||
} else {
|
||||
|
|
@ -823,18 +1054,35 @@ fn cmd_pool_show(registry: &Path, name: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_publisher_list(registry: &Path) -> Result<()> {
|
||||
fn cmd_publisher_list(registry: &Path, json: bool) -> Result<()> {
|
||||
config::ensure_initialized(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() {
|
||||
println!("No publishers configured.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for name in &names {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -276,7 +276,8 @@ impl Pool {
|
|||
// Example public pool with explicit address list
|
||||
std::fs::write(
|
||||
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" {
|
||||
network "203.0.113.0/28"
|
||||
gateway "203.0.113.1"
|
||||
|
|
|
|||
|
|
@ -92,13 +92,35 @@ impl Publisher {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a publisher by filename stem.
|
||||
/// Remove a publisher by filename stem or publisher name.
|
||||
pub fn remove(registry: &Path, name: &str) -> Result<()> {
|
||||
let dir = registry.join("publishers");
|
||||
|
||||
// Try exact filename stem first
|
||||
let path = dir.join(format!("{name}.kdl"));
|
||||
if path.exists() {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue