use crate::{
nns_data_center::{
NnsDataCenterCacheRequest, NnsDataCenterHostError, NnsDataCenterListReport,
NnsDataCenterListRequest, build_nns_data_center_list_report,
},
nns_node::{
NNS_NODE_SUBNET_KIND_APPLICATION, NNS_NODE_SUBNET_KIND_SYSTEM,
NNS_NODE_SUBNET_KIND_UNKNOWN, NnsNodeCacheRequest, NnsNodeHostError, NnsNodeListFilters,
NnsNodeListReport, NnsNodeListRequest, build_nns_node_list_report,
},
nns_node_operator::{
NnsNodeOperatorCacheRequest, NnsNodeOperatorHostError, NnsNodeOperatorListReport,
NnsNodeOperatorListRequest, build_nns_node_operator_list_report,
},
nns_node_provider::{
NnsNodeProviderCacheRequest, NnsNodeProviderHostError, NnsNodeProviderListReport,
NnsNodeProviderListRequest, build_nns_node_provider_list_report,
},
nns_render::yes_no,
subnet_catalog::{
DEFAULT_STALE_AFTER_SECONDS, SubnetCatalogCacheRequest, SubnetCatalogFilters,
SubnetCatalogHostError, SubnetCatalogListReport, SubnetCatalogListRequest,
build_subnet_catalog_list_report,
},
table::{ColumnAlign, render_table},
};
use canic_subnet_catalog::{MAINNET_NETWORK, SubnetKind};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use thiserror::Error as ThisError;
pub const NNS_TOPOLOGY_SUMMARY_REPORT_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsTopologySummaryRequest {
pub icp_root: PathBuf,
pub network: String,
pub source_endpoint: String,
pub now_unix_secs: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NnsTopologySummaryReport {
pub schema_version: u32,
pub network: String,
pub source_endpoint: String,
pub subnet_count: usize,
pub application_subnet_count: usize,
pub system_subnet_count: usize,
pub unknown_subnet_count: usize,
pub routing_range_count: usize,
pub node_count: usize,
pub application_node_count: usize,
pub system_node_count: usize,
pub unknown_node_count: usize,
pub node_provider_count: usize,
pub node_operator_count: usize,
pub data_center_count: usize,
pub subnet_catalog_stale: bool,
pub subnet_catalog_stale_reason: String,
pub registry_versions: Vec<NnsTopologyRegistryVersionRow>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NnsTopologyRegistryVersionRow {
pub source: String,
pub registry_version: u64,
pub fetched_at: String,
pub source_endpoint: String,
pub stale: Option<bool>,
}
#[derive(Debug, ThisError)]
pub enum NnsTopologyHostError {
#[error(
"`canic nns topology` supports only the mainnet `ic` network\n\nThe NNS topology summary is derived from public Internet Computer mainnet registry records.\nLocal replica NNS registry discovery is not implemented yet.\n\nTry:\n canic --network ic nns topology summary"
)]
UnsupportedNetwork { network: String },
#[error(transparent)]
Subnet(#[from] SubnetCatalogHostError),
#[error(transparent)]
Node(#[from] NnsNodeHostError),
#[error(transparent)]
NodeProvider(#[from] NnsNodeProviderHostError),
#[error(transparent)]
NodeOperator(#[from] NnsNodeOperatorHostError),
#[error(transparent)]
DataCenter(#[from] NnsDataCenterHostError),
}
pub fn build_nns_topology_summary_report(
request: &NnsTopologySummaryRequest,
) -> Result<NnsTopologySummaryReport, NnsTopologyHostError> {
enforce_mainnet_network(&request.network)?;
let subnet_report = build_subnet_catalog_list_report(&SubnetCatalogListRequest {
cache: SubnetCatalogCacheRequest {
icp_root: request.icp_root.clone(),
network: request.network.clone(),
},
now_unix_secs: request.now_unix_secs,
stale_after_seconds: DEFAULT_STALE_AFTER_SECONDS,
filters: SubnetCatalogFilters::default(),
show_ranges: false,
range_limit: 1,
range_offset: 0,
})?;
let node_report = build_nns_node_list_report(&NnsNodeListRequest {
cache: NnsNodeCacheRequest {
icp_root: request.icp_root.clone(),
network: request.network.clone(),
},
source_endpoint: request.source_endpoint.clone(),
now_unix_secs: request.now_unix_secs,
filters: NnsNodeListFilters::default(),
})?;
let node_provider_report = build_nns_node_provider_list_report(&NnsNodeProviderListRequest {
cache: NnsNodeProviderCacheRequest {
icp_root: request.icp_root.clone(),
network: request.network.clone(),
},
source_endpoint: request.source_endpoint.clone(),
now_unix_secs: request.now_unix_secs,
})?;
let node_operator_report = build_nns_node_operator_list_report(&NnsNodeOperatorListRequest {
cache: NnsNodeOperatorCacheRequest {
icp_root: request.icp_root.clone(),
network: request.network.clone(),
},
source_endpoint: request.source_endpoint.clone(),
now_unix_secs: request.now_unix_secs,
})?;
let data_center_report = build_nns_data_center_list_report(&NnsDataCenterListRequest {
cache: NnsDataCenterCacheRequest {
icp_root: request.icp_root.clone(),
network: request.network.clone(),
},
source_endpoint: request.source_endpoint.clone(),
now_unix_secs: request.now_unix_secs,
})?;
Ok(topology_summary_report_from_reports(
request.network.clone(),
request.source_endpoint.clone(),
subnet_report,
node_report,
node_provider_report,
node_operator_report,
data_center_report,
))
}
#[must_use]
pub fn nns_topology_summary_report_text(report: &NnsTopologySummaryReport) -> String {
let mut lines = Vec::new();
lines.push(format!(
"topology: {} subnets {} nodes {} node_operators {} node_providers {} data_centers {}",
report.network,
report.subnet_count,
report.node_count,
report.node_operator_count,
report.node_provider_count,
report.data_center_count
));
lines.push("counts:".to_string());
lines.push(render_count_table(report));
lines.push("subnet_kinds:".to_string());
lines.push(render_kind_table(report));
lines.push("registry_versions:".to_string());
lines.push(render_registry_version_table(report));
lines.join("\n")
}
fn topology_summary_report_from_reports(
network: String,
source_endpoint: String,
subnet_report: SubnetCatalogListReport,
node_report: NnsNodeListReport,
node_provider_report: NnsNodeProviderListReport,
node_operator_report: NnsNodeOperatorListReport,
data_center_report: NnsDataCenterListReport,
) -> NnsTopologySummaryReport {
let application_subnet_count = subnet_count_by_kind(&subnet_report, SubnetKind::Application);
let system_subnet_count = subnet_count_by_kind(&subnet_report, SubnetKind::System);
let unknown_subnet_count = subnet_count_by_kind(&subnet_report, SubnetKind::Unknown);
let application_node_count =
node_count_by_subnet_kind(&node_report, NNS_NODE_SUBNET_KIND_APPLICATION);
let system_node_count = node_count_by_subnet_kind(&node_report, NNS_NODE_SUBNET_KIND_SYSTEM);
let unknown_node_count = node_count_by_subnet_kind(&node_report, NNS_NODE_SUBNET_KIND_UNKNOWN);
let registry_versions = vec![
registry_version_row(
"subnet_catalog",
subnet_report.registry_version,
subnet_report.fetched_at.clone(),
None,
Some(subnet_report.catalog_stale),
),
registry_version_row(
"nodes",
node_report.registry_version,
node_report.fetched_at.clone(),
Some(node_report.source_endpoint.clone()),
None,
),
registry_version_row(
"node_providers",
node_provider_report.registry_version,
node_provider_report.fetched_at.clone(),
Some(node_provider_report.source_endpoint.clone()),
None,
),
registry_version_row(
"node_operators",
node_operator_report.registry_version,
node_operator_report.fetched_at.clone(),
Some(node_operator_report.source_endpoint.clone()),
None,
),
registry_version_row(
"data_centers",
data_center_report.registry_version,
data_center_report.fetched_at.clone(),
Some(data_center_report.source_endpoint.clone()),
None,
),
];
NnsTopologySummaryReport {
schema_version: NNS_TOPOLOGY_SUMMARY_REPORT_SCHEMA_VERSION,
network,
source_endpoint,
subnet_count: subnet_report.subnets.len(),
application_subnet_count,
system_subnet_count,
unknown_subnet_count,
routing_range_count: subnet_report
.subnets
.iter()
.map(|subnet| subnet.range_count)
.sum(),
node_count: node_report.node_count,
application_node_count,
system_node_count,
unknown_node_count,
node_provider_count: node_provider_report.node_provider_count,
node_operator_count: node_operator_report.node_operator_count,
data_center_count: data_center_report.data_center_count,
subnet_catalog_stale: subnet_report.catalog_stale,
subnet_catalog_stale_reason: subnet_report.stale_reason,
registry_versions,
}
}
fn enforce_mainnet_network(network: &str) -> Result<(), NnsTopologyHostError> {
if network == MAINNET_NETWORK {
return Ok(());
}
Err(NnsTopologyHostError::UnsupportedNetwork {
network: network.to_string(),
})
}
fn subnet_count_by_kind(report: &SubnetCatalogListReport, kind: SubnetKind) -> usize {
report
.subnets
.iter()
.filter(|subnet| subnet.subnet_kind == kind)
.count()
}
fn node_count_by_subnet_kind(report: &NnsNodeListReport, kind: &str) -> usize {
report
.nodes
.iter()
.filter(|node| node.subnet_kind.eq_ignore_ascii_case(kind))
.count()
}
fn registry_version_row(
source: &str,
registry_version: u64,
fetched_at: String,
source_endpoint: Option<String>,
stale: Option<bool>,
) -> NnsTopologyRegistryVersionRow {
NnsTopologyRegistryVersionRow {
source: source.to_string(),
registry_version,
fetched_at,
source_endpoint: source_endpoint.unwrap_or_else(|| "-".to_string()),
stale,
}
}
fn render_count_table(report: &NnsTopologySummaryReport) -> String {
let headers = ["METRIC", "COUNT"];
let rows = [
["subnets".to_string(), report.subnet_count.to_string()],
[
"routing_ranges".to_string(),
report.routing_range_count.to_string(),
],
["nodes".to_string(), report.node_count.to_string()],
[
"node_operators".to_string(),
report.node_operator_count.to_string(),
],
[
"node_providers".to_string(),
report.node_provider_count.to_string(),
],
[
"data_centers".to_string(),
report.data_center_count.to_string(),
],
];
let alignments = [ColumnAlign::Left, ColumnAlign::Right];
render_table(&headers, &rows, &alignments)
}
fn render_kind_table(report: &NnsTopologySummaryReport) -> String {
let headers = ["KIND", "SUBNETS", "NODES"];
let rows = [
[
NNS_NODE_SUBNET_KIND_APPLICATION.to_string(),
report.application_subnet_count.to_string(),
report.application_node_count.to_string(),
],
[
NNS_NODE_SUBNET_KIND_SYSTEM.to_string(),
report.system_subnet_count.to_string(),
report.system_node_count.to_string(),
],
[
NNS_NODE_SUBNET_KIND_UNKNOWN.to_string(),
report.unknown_subnet_count.to_string(),
report.unknown_node_count.to_string(),
],
];
let alignments = [ColumnAlign::Left, ColumnAlign::Right, ColumnAlign::Right];
render_table(&headers, &rows, &alignments)
}
fn render_registry_version_table(report: &NnsTopologySummaryReport) -> String {
let headers = ["SOURCE", "VERSION", "FETCHED_AT", "STALE", "ENDPOINT"];
let rows = report
.registry_versions
.iter()
.map(|row| {
[
row.source.clone(),
row.registry_version.to_string(),
row.fetched_at.clone(),
row.stale
.map_or_else(|| "-".to_string(), |stale| yes_no(stale).to_string()),
row.source_endpoint.clone(),
]
})
.collect::<Vec<_>>();
let alignments = [
ColumnAlign::Left,
ColumnAlign::Right,
ColumnAlign::Left,
ColumnAlign::Left,
ColumnAlign::Left,
];
render_table(&headers, &rows, &alignments)
}
#[cfg(test)]
mod tests;