aboutsummaryrefslogtreecommitdiff
path: root/src/garage/cli_v2/cluster.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/garage/cli_v2/cluster.rs')
-rw-r--r--src/garage/cli_v2/cluster.rs188
1 files changed, 188 insertions, 0 deletions
diff --git a/src/garage/cli_v2/cluster.rs b/src/garage/cli_v2/cluster.rs
new file mode 100644
index 00000000..0b5b9559
--- /dev/null
+++ b/src/garage/cli_v2/cluster.rs
@@ -0,0 +1,188 @@
+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()
+ )))
+ }
+ }
+}