use format_table::format_table;
use garage_util::error::*;
use garage_api::admin::api::*;
use crate::cli::structs::*;
use crate::cli_v2::util::*;
use crate::cli_v2::*;
impl Cli {
pub async fn cmd_status(&self) -> Result<(), Error> {
let status = self.api_request(GetClusterStatusRequest).await?;
let layout = self.api_request(GetClusterLayoutRequest).await?;
// TODO: layout history
println!("==== HEALTHY NODES ====");
let mut healthy_nodes =
vec!["ID\tHostname\tAddress\tTags\tZone\tCapacity\tDataAvail".to_string()];
for adv in status.nodes.iter().filter(|adv| adv.is_up) {
let host = adv.hostname.as_deref().unwrap_or("?");
let addr = match adv.addr {
Some(addr) => addr.to_string(),
None => "N/A".to_string(),
};
if let Some(cfg) = &adv.role {
let data_avail = match &adv.data_partition {
_ if cfg.capacity.is_none() => "N/A".into(),
Some(FreeSpaceResp { available, total }) => {
let pct = (*available as f64) / (*total as f64) * 100.;
let avail_str = bytesize::ByteSize::b(*available);
format!("{} ({:.1}%)", avail_str, pct)
}
None => "?".into(),
};
healthy_nodes.push(format!(
"{id:.16}\t{host}\t{addr}\t[{tags}]\t{zone}\t{capacity}\t{data_avail}",
id = adv.id,
host = host,
addr = addr,
tags = cfg.tags.join(","),
zone = cfg.zone,
capacity = capacity_string(cfg.capacity),
data_avail = data_avail,
));
} else {
/*
let prev_role = layout
.versions
.iter()
.rev()
.find_map(|x| match x.roles.get(&adv.id) {
Some(NodeRoleV(Some(cfg))) => Some(cfg),
_ => None,
});
*/
let prev_role = Option::<NodeRoleResp>::None; //TODO
if let Some(cfg) = prev_role {
healthy_nodes.push(format!(
"{id:.16}\t{host}\t{addr}\t[{tags}]\t{zone}\tdraining metadata...",
id = adv.id,
host = host,
addr = addr,
tags = cfg.tags.join(","),
zone = cfg.zone,
));
} else {
let new_role = match layout.staged_role_changes.iter().find(|x| x.id == adv.id)
{
Some(_) => "pending...",
_ => "NO ROLE ASSIGNED",
};
healthy_nodes.push(format!(
"{id:?}\t{h}\t{addr}\t\t\t{new_role}",
id = adv.id,
h = host,
addr = addr,
new_role = new_role,
));
}
}
}
format_table(healthy_nodes);
// Determine which nodes are unhealthy and print that to stdout
// TODO: do we need this, or can it be done in the GetClusterStatus handler?
let status_map = status
.nodes
.iter()
.map(|adv| (&adv.id, adv))
.collect::<HashMap<_, _>>();
let tf = timeago::Formatter::new();
let mut drain_msg = false;
let mut failed_nodes = vec!["ID\tHostname\tTags\tZone\tCapacity\tLast seen".to_string()];
let mut listed = HashSet::new();
//for ver in layout.versions.iter().rev() {
for ver in [&layout].iter() {
for cfg in ver.roles.iter() {
let node = &cfg.id;
if listed.contains(node.as_str()) {
continue;
}
listed.insert(node.as_str());
let adv = status_map.get(node);
if adv.map(|x| x.is_up).unwrap_or(false) {
continue;
}
// Node is in a layout version, is not a gateway node, and is not up:
// it is in a failed state, add proper line to the output
let (host, last_seen) = match adv {
Some(adv) => (
adv.hostname.as_deref().unwrap_or("?"),
adv.last_seen_secs_ago
.map(|s| tf.convert(Duration::from_secs(s)))
.unwrap_or_else(|| "never seen".into()),
),
None => ("??", "never seen".into()),
};
/*
let capacity = if ver.version == layout.current().version {
cfg.capacity_string()
} else {
drain_msg = true;
"draining metadata...".to_string()
};
*/
let capacity = capacity_string(cfg.capacity);
failed_nodes.push(format!(
"{id:?}\t{host}\t[{tags}]\t{zone}\t{capacity}\t{last_seen}",
id = node,
host = host,
tags = cfg.tags.join(","),
zone = cfg.zone,
capacity = capacity,
last_seen = last_seen,
));
}
}
if failed_nodes.len() > 1 {
println!("\n==== FAILED NODES ====");
format_table(failed_nodes);
if drain_msg {
println!();
println!("Your cluster is expecting to drain data from nodes that are currently unavailable.");
println!(
"If these nodes are definitely dead, please review the layout history with"
);
println!(
"`garage layout history` and use `garage layout skip-dead-nodes` to force progress."
);
}
}
if print_staging_role_changes(&layout) {
println!();
println!(
"Please use `garage layout show` to check the proposed new layout and apply it."
);
println!();
}
Ok(())
}
pub async fn cmd_connect(&self, opt: ConnectNodeOpt) -> Result<(), Error> {
let res = self
.api_request(ConnectClusterNodesRequest(vec![opt.node]))
.await?;
if res.0.len() != 1 {
return Err(Error::Message(format!("unexpected response: {:?}", res)));
}
let res = res.0.into_iter().next().unwrap();
if res.success {
println!("Success.");
Ok(())
} else {
Err(Error::Message(format!(
"Failure: {}",
res.error.unwrap_or_default()
)))
}
}
}