From 14d7edf0dca5e1ac4f040cf9197bc0cb413e526b Mon Sep 17 00:00:00 2001 From: Simeon Romanov Date: Wed, 15 Nov 2023 17:07:41 +0300 Subject: [PATCH] add detailed node api --- CHANGELOG.md | 1 + backend/src/lib.rs | 4 + backend/src/models/api.rs | 36 +++++++ backend/src/services/api.rs | 183 +++++++++++++++++++++++++++++++++++- backend/src/services/mod.rs | 5 +- 5 files changed, 227 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8de08296..aaaad31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,3 +14,4 @@ Bob Management GUI changelog - Home page, backend (#18) - Node list page, backend (#19) - VDisk list page, backend (#20) +- Detailed node information page, backend (#21) diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 4d7f8804..812fc781 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -39,6 +39,8 @@ impl Modify for SecurityAddon { services::api::get_nodes, services::api::raw_metrics_by_node, services::api::raw_configuration_by_node, + services::api::get_nodes, + services::api::get_detailed_node_info, services::api::get_vdisks, ), components( @@ -59,6 +61,8 @@ impl Modify for SecurityAddon { models::api::SpaceInfo, models::api::VDisk, models::api::VDiskStatus, + models::api::DetailedNode, + models::api::DetailedNodeMetrics, models::api::Operation, models::api::RPS, models::api::RawMetricEntry, diff --git a/backend/src/models/api.rs b/backend/src/models/api.rs index f433ae7c..74e3ca54 100644 --- a/backend/src/models/api.rs +++ b/backend/src/models/api.rs @@ -260,6 +260,42 @@ pub enum VDiskStatus { Offline, } +#[derive(ToSchema, Debug, Clone, Serialize)] +pub struct DetailedNode { + pub name: String, + + pub hostname: String, + + pub vdisks: Vec, + + #[serde(flatten)] + pub status: NodeStatus, + + pub metrics: DetailedNodeMetrics, + + pub disks: Vec, +} + +#[derive(ToSchema, Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DetailedNodeMetrics { + pub rps: RPS, + + pub alien_count: u64, + + pub corrupted_count: u64, + + pub space: SpaceInfo, + + pub cpu_load: u64, + + pub total_ram: u64, + + pub used_ram: u64, + + pub descr_amount: u64, +} + #[derive( ToSchema, Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, PartialOrd, Ord, EnumIter, )] diff --git a/backend/src/services/api.rs b/backend/src/services/api.rs index 95bab177..ed8fff85 100644 --- a/backend/src/services/api.rs +++ b/backend/src/services/api.rs @@ -7,7 +7,10 @@ use crate::{ use super::{ auth::HttpClient, - methods::{request_configuration, request_metrics, request_nodes, request_vdisks}, + methods::{ + request_configuration, request_disks, request_metrics, request_nodes, request_space, + request_status, request_vdisks, + }, prelude::*, }; @@ -558,6 +561,53 @@ fn disk_status_from_space(space: &dto::SpaceInfo, occupied_space: u64) -> DiskSt } } +// Bad function, 6 args :( +async fn process_vdisks_for_node( + client: HttpBobClient, + virt_disks: &[dto::VDisk], + node_name: NodeName, + all_disks: &HashMap, + nodes: &HashMap<&NodeName, &dto::Node>, + partitions_count_on_vdisk: &HashMap, +) -> Vec { + let mut res_replicas = vec![]; + let mut res_vdisks = vec![]; + for (vdisk, replicas) in virt_disks + .iter() + .filter_map(|vdisk| vdisk.replicas.as_ref().map(|repl| (vdisk, repl))) + .filter(|(_, replicas)| replicas.iter().any(|replica| replica.node == node_name)) + { + for replica in replicas { + res_replicas.push(( + vdisk.id, + process_replica(&client, replica.clone(), all_disks, nodes).await, + )); + } + res_vdisks.push(VDisk { + id: vdisk.id as u64, + status: if res_replicas + .iter() + .any(|(_, replica)| matches!(replica.status, ReplicaStatus::Offline(_))) + { + VDiskStatus::Bad + } else { + VDiskStatus::Good + }, + partition_count: partitions_count_on_vdisk + .get(&vdisk.id) + .copied() + .unwrap_or_default() as u64, + replicas: res_replicas + .iter() + .filter(|(id, _)| id == &vdisk.id) + .map(|(_, replica)| replica.clone()) + .collect(), + }); + } + + res_vdisks +} + /// Get Raw Metrics from Node /// /// # Errors @@ -608,6 +658,137 @@ pub async fn raw_configuration_by_node( Ok(Json(request_configuration(client.as_ref()).await?)) } +/// Get Detailed Information on Node +/// +/// # Errors +/// +/// This function will return an error if the server was unable to get node'a client +/// or one of the requests to get information from the node fails +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = ApiV1::to_path(), + path = "/nodes/{node_name}", + params ( + ("id", description = "Node's ID") + ), + responses( + (status = 200, body = DetailedNode, content_type = "application/json", description = "Detailed Node information"), + + (status = 401, description = "Unauthorized"), + (status = 404, description = "Node Not Found") + ), + security(("api_key" = [])) + ))] +pub async fn get_detailed_node_info( + Extension(client): Extension, + Path(node_name): Path, +) -> AxumResult> { + let mut all_disks: FuturesUnordered<_> = client + .cluster() + .map(move |node| { + let handle = node.clone(); + tokio::spawn(async move { handle.get_disks().await }) + }) + .collect(); + + let node_client = get_client_by_node(&client, node_name.clone()).await?; + + let virt_disks = request_vdisks(node_client.as_ref()).await?; + + let mut all_partitions: FuturesUnordered<_> = virt_disks + .iter() + .map(|vdisk| { + let id = vdisk.id; + let handle = client.api_main().clone(); + tokio::spawn(async move { (id, handle.get_partitions(id).await) }) + }) + .collect(); + + let metrics = request_metrics(node_client.as_ref()).await?; + let typed_metrics: TypedMetrics = metrics.clone().into(); + + let space = request_space(node_client.as_ref()).await?; + let node_status = request_status(node_client.as_ref()).await?; + let disks = request_disks(node_client.as_ref()).await?; + + let res_disks = proccess_disks(&disks, &space, &metrics); + let nodes = request_nodes(node_client.as_ref()).await?; + + let nodes = nodes + .iter() + .map(|node| (&node.name, node)) + .collect::>(); + + let mut proc_disks = HashMap::new(); + while let Some(disks) = all_disks.next().await { + let Ok(Ok(GetDisksResponse::AJSONArrayWithDisksAndTheirStates(disks))) = disks else { + tracing::error!("couldn't get disk inforamtion from node"); + continue; + }; + for disk in disks { + proc_disks.insert(disk.name, disk.is_active); + } + } + + let mut res_partitions = HashMap::new(); + while let Some(partitions) = all_partitions.next().await { + let Ok((id, Ok(GetPartitionsResponse::NodeInfoAndJSONArrayWithPartitionsInfo(partitions)))) = + partitions + else { + // tracing::error!("couldn't get Partition inforamtion from node"); // Too noisy + continue; + }; + if let Some(partitions) = partitions.partitions { + res_partitions.insert(id, partitions.len()); + } + } + + let vdisks = process_vdisks_for_node( + client, + &virt_disks, + node_name, + &proc_disks, + &nodes, + &res_partitions, + ) + .await; + + let mut rps = RPS::new(); + + let status = NodeStatus::from_problems(NodeProblem::default_from_metrics(&typed_metrics)); + + rps[Operation::Get] = typed_metrics[RawMetricEntry::PearlGetCountRate].value; + rps[Operation::Put] = typed_metrics[RawMetricEntry::PearlPutCountRate].value; + rps[Operation::Exist] = typed_metrics[RawMetricEntry::PearlExistCountRate].value; + rps[Operation::Delete] = typed_metrics[RawMetricEntry::PearlDeleteCountRate].value; + + let res = Json(DetailedNode { + name: node_status.name, + hostname: node_status.address, + vdisks, + status, + metrics: DetailedNodeMetrics { + rps, + alien_count: typed_metrics[RawMetricEntry::BackendAlienCount].value, + corrupted_count: typed_metrics[RawMetricEntry::BackendCorruptedBlobCount].value, + space: SpaceInfo { + total_disk: space.total_disk_space_bytes, + free_disk: space.total_disk_space_bytes - space.used_disk_space_bytes, + used_disk: space.used_disk_space_bytes, + occupied_disk: space.occupied_disk_space_bytes, + }, + cpu_load: typed_metrics[RawMetricEntry::HardwareBobCpuLoad].value, + total_ram: typed_metrics[RawMetricEntry::HardwareTotalRam].value, + used_ram: typed_metrics[RawMetricEntry::HardwareUsedRam].value, + descr_amount: typed_metrics[RawMetricEntry::HardwareDescrAmount].value, + }, + disks: res_disks, + }); + tracing::trace!("send response: {res:?}"); + + Ok(res) +} + async fn get_client_by_node( client: &HttpBobClient, node_name: NodeName, diff --git a/backend/src/services/mod.rs b/backend/src/services/mod.rs index 7dcc3cd3..bb82b6b3 100644 --- a/backend/src/services/mod.rs +++ b/backend/src/services/mod.rs @@ -27,7 +27,9 @@ use api::{get_disks_count, get_nodes_count, get_rps, get_space}; use auth::{login, logout, require_auth, AuthState, BobUser, HttpBobClient, InMemorySessionStore}; use prelude::*; -use self::api::{get_nodes, raw_configuration_by_node, raw_metrics_by_node, get_vdisks}; +use self::api::{ + get_detailed_node_info, get_nodes, get_vdisks, raw_configuration_by_node, raw_metrics_by_node, +}; type BobAuthState = AuthState< BobUser, @@ -52,6 +54,7 @@ pub fn api_router_v1(auth_state: BobAuthState) -> Result, R .api_route("/nodes/rps", &Method::GET, get_rps) .api_route("/nodes/space", &Method::GET, get_space) .api_route("/nodes", &Method::GET, get_nodes) + .api_route("/nodes/:node_name", &Method::GET, get_detailed_node_info) .api_route( "/nodes/:node_name/metrics", &Method::GET,