Skip to main content

canic_host/nns_topology/
mod.rs

1use crate::{
2    nns_data_center::{
3        NnsDataCenterCacheRequest, NnsDataCenterHostError, NnsDataCenterListReport,
4        NnsDataCenterListRequest, NnsDataCenterRefreshReport, NnsDataCenterRefreshRequest,
5        build_nns_data_center_list_report, refresh_nns_data_center_report,
6    },
7    nns_node::{
8        NNS_NODE_SUBNET_KIND_APPLICATION, NNS_NODE_SUBNET_KIND_SYSTEM,
9        NNS_NODE_SUBNET_KIND_UNKNOWN, NnsNodeCacheRequest, NnsNodeHostError, NnsNodeListFilters,
10        NnsNodeListReport, NnsNodeListRequest, NnsNodeRefreshReport, NnsNodeRefreshRequest,
11        build_nns_node_list_report, refresh_nns_node_report,
12    },
13    nns_node_operator::{
14        NnsNodeOperatorCacheRequest, NnsNodeOperatorHostError, NnsNodeOperatorListReport,
15        NnsNodeOperatorListRequest, NnsNodeOperatorRefreshReport, NnsNodeOperatorRefreshRequest,
16        build_nns_node_operator_list_report, refresh_nns_node_operator_report,
17    },
18    nns_node_provider::{
19        NnsNodeProviderCacheRequest, NnsNodeProviderHostError, NnsNodeProviderListReport,
20        NnsNodeProviderListRequest, NnsNodeProviderRefreshReport, NnsNodeProviderRefreshRequest,
21        build_nns_node_provider_list_report, refresh_nns_node_provider_report,
22    },
23    nns_render::yes_no,
24    subnet_catalog::{
25        DEFAULT_STALE_AFTER_SECONDS, SubnetCatalogCacheRequest, SubnetCatalogFilters,
26        SubnetCatalogHostError, SubnetCatalogListReport, SubnetCatalogListRequest,
27        SubnetCatalogRefreshReport, SubnetCatalogRefreshRequest, build_subnet_catalog_list_report,
28        refresh_subnet_catalog,
29    },
30    table::{ColumnAlign, render_table},
31};
32use canic_subnet_catalog::{MAINNET_NETWORK, SubnetKind};
33use serde::{Deserialize, Serialize};
34use std::{collections::BTreeSet, path::PathBuf};
35use thiserror::Error as ThisError;
36
37pub const NNS_TOPOLOGY_SUMMARY_REPORT_SCHEMA_VERSION: u32 = 2;
38pub const NNS_TOPOLOGY_REFRESH_REPORT_SCHEMA_VERSION: u32 = 1;
39
40///
41/// NnsTopologySummaryRequest
42///
43#[derive(Clone, Debug, Eq, PartialEq)]
44pub struct NnsTopologySummaryRequest {
45    pub icp_root: PathBuf,
46    pub network: String,
47    pub source_endpoint: String,
48    pub now_unix_secs: u64,
49}
50
51///
52/// NnsTopologyRefreshRequest
53///
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct NnsTopologyRefreshRequest {
56    pub icp_root: PathBuf,
57    pub network: String,
58    pub source_endpoint: String,
59    pub now_unix_secs: u64,
60    pub lock_stale_after_seconds: u64,
61    pub dry_run: bool,
62}
63
64///
65/// NnsTopologySummaryReport
66///
67#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
68pub struct NnsTopologySummaryReport {
69    pub schema_version: u32,
70    pub network: String,
71    pub source_endpoint: String,
72    pub subnet_count: usize,
73    pub application_subnet_count: usize,
74    pub system_subnet_count: usize,
75    pub unknown_subnet_count: usize,
76    pub routing_range_count: usize,
77    pub node_count: usize,
78    pub application_node_count: usize,
79    pub system_node_count: usize,
80    pub unknown_node_count: usize,
81    pub node_provider_count: usize,
82    pub node_operator_count: usize,
83    pub data_center_count: usize,
84    pub nodes_with_known_node_provider_count: usize,
85    pub nodes_with_unknown_node_provider_count: usize,
86    pub nodes_with_known_node_operator_count: usize,
87    pub nodes_with_unknown_node_operator_count: usize,
88    pub nodes_with_known_data_center_count: usize,
89    pub nodes_with_unknown_data_center_count: usize,
90    pub node_operators_with_known_node_provider_count: usize,
91    pub node_operators_with_unknown_node_provider_count: usize,
92    pub node_operators_with_known_data_center_count: usize,
93    pub node_operators_with_unknown_data_center_count: usize,
94    pub subnet_catalog_stale: bool,
95    pub subnet_catalog_stale_reason: String,
96    pub registry_versions: Vec<NnsTopologyRegistryVersionRow>,
97}
98
99///
100/// NnsTopologyRegistryVersionRow
101///
102#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
103pub struct NnsTopologyRegistryVersionRow {
104    pub source: String,
105    pub registry_version: u64,
106    pub fetched_at: String,
107    pub source_endpoint: String,
108    pub stale: Option<bool>,
109}
110
111///
112/// NnsTopologyRefreshReport
113///
114#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
115pub struct NnsTopologyRefreshReport {
116    pub schema_version: u32,
117    pub network: String,
118    pub source_endpoint: String,
119    pub dry_run: bool,
120    pub component_count: usize,
121    pub wrote_cache_count: usize,
122    pub replaced_existing_cache_count: usize,
123    pub components: Vec<NnsTopologyRefreshRow>,
124}
125
126///
127/// NnsTopologyRefreshRow
128///
129#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
130pub struct NnsTopologyRefreshRow {
131    pub source: String,
132    pub cache_path: String,
133    pub refresh_lock_path: String,
134    pub registry_version: u64,
135    pub fetched_at: String,
136    pub source_endpoint: String,
137    pub fetched_by: String,
138    pub dry_run: bool,
139    pub wrote_cache: bool,
140    pub replaced_existing_cache: bool,
141    pub item_count: usize,
142}
143
144///
145/// NnsTopologyHostError
146///
147#[derive(Debug, ThisError)]
148pub enum NnsTopologyHostError {
149    #[error(
150        "`canic nns topology` supports only the mainnet `ic` network\n\nThe NNS topology report 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\n  canic --network ic nns topology refresh"
151    )]
152    UnsupportedNetwork { network: String },
153
154    #[error(transparent)]
155    Subnet(#[from] SubnetCatalogHostError),
156
157    #[error(transparent)]
158    Node(#[from] NnsNodeHostError),
159
160    #[error(transparent)]
161    NodeProvider(#[from] NnsNodeProviderHostError),
162
163    #[error(transparent)]
164    NodeOperator(#[from] NnsNodeOperatorHostError),
165
166    #[error(transparent)]
167    DataCenter(#[from] NnsDataCenterHostError),
168}
169
170pub fn build_nns_topology_summary_report(
171    request: &NnsTopologySummaryRequest,
172) -> Result<NnsTopologySummaryReport, NnsTopologyHostError> {
173    enforce_mainnet_network(&request.network)?;
174
175    let subnet_report = build_subnet_catalog_list_report(&SubnetCatalogListRequest {
176        cache: SubnetCatalogCacheRequest {
177            icp_root: request.icp_root.clone(),
178            network: request.network.clone(),
179        },
180        now_unix_secs: request.now_unix_secs,
181        stale_after_seconds: DEFAULT_STALE_AFTER_SECONDS,
182        filters: SubnetCatalogFilters::default(),
183        show_ranges: false,
184        range_limit: 1,
185        range_offset: 0,
186    })?;
187    let node_report = build_nns_node_list_report(&NnsNodeListRequest {
188        cache: NnsNodeCacheRequest {
189            icp_root: request.icp_root.clone(),
190            network: request.network.clone(),
191        },
192        source_endpoint: request.source_endpoint.clone(),
193        now_unix_secs: request.now_unix_secs,
194        filters: NnsNodeListFilters::default(),
195    })?;
196    let node_provider_report = build_nns_node_provider_list_report(&NnsNodeProviderListRequest {
197        cache: NnsNodeProviderCacheRequest {
198            icp_root: request.icp_root.clone(),
199            network: request.network.clone(),
200        },
201        source_endpoint: request.source_endpoint.clone(),
202        now_unix_secs: request.now_unix_secs,
203    })?;
204    let node_operator_report = build_nns_node_operator_list_report(&NnsNodeOperatorListRequest {
205        cache: NnsNodeOperatorCacheRequest {
206            icp_root: request.icp_root.clone(),
207            network: request.network.clone(),
208        },
209        source_endpoint: request.source_endpoint.clone(),
210        now_unix_secs: request.now_unix_secs,
211    })?;
212    let data_center_report = build_nns_data_center_list_report(&NnsDataCenterListRequest {
213        cache: NnsDataCenterCacheRequest {
214            icp_root: request.icp_root.clone(),
215            network: request.network.clone(),
216        },
217        source_endpoint: request.source_endpoint.clone(),
218        now_unix_secs: request.now_unix_secs,
219    })?;
220
221    Ok(topology_summary_report_from_reports(
222        request.network.clone(),
223        request.source_endpoint.clone(),
224        subnet_report,
225        node_report,
226        node_provider_report,
227        node_operator_report,
228        data_center_report,
229    ))
230}
231
232pub fn refresh_nns_topology_report(
233    request: &NnsTopologyRefreshRequest,
234) -> Result<NnsTopologyRefreshReport, NnsTopologyHostError> {
235    enforce_mainnet_network(&request.network)?;
236
237    let subnet_report = refresh_subnet_catalog(&SubnetCatalogRefreshRequest {
238        cache: SubnetCatalogCacheRequest {
239            icp_root: request.icp_root.clone(),
240            network: request.network.clone(),
241        },
242        source_endpoint: request.source_endpoint.clone(),
243        now_unix_secs: request.now_unix_secs,
244        lock_stale_after_seconds: request.lock_stale_after_seconds,
245        dry_run: request.dry_run,
246        output_path: None,
247    })?;
248    let node_report = refresh_nns_node_report(&NnsNodeRefreshRequest {
249        cache: NnsNodeCacheRequest {
250            icp_root: request.icp_root.clone(),
251            network: request.network.clone(),
252        },
253        source_endpoint: request.source_endpoint.clone(),
254        now_unix_secs: request.now_unix_secs,
255        lock_stale_after_seconds: request.lock_stale_after_seconds,
256        dry_run: request.dry_run,
257        output_path: None,
258    })?;
259    let node_provider_report = refresh_nns_node_provider_report(&NnsNodeProviderRefreshRequest {
260        cache: NnsNodeProviderCacheRequest {
261            icp_root: request.icp_root.clone(),
262            network: request.network.clone(),
263        },
264        source_endpoint: request.source_endpoint.clone(),
265        now_unix_secs: request.now_unix_secs,
266        lock_stale_after_seconds: request.lock_stale_after_seconds,
267        dry_run: request.dry_run,
268        output_path: None,
269    })?;
270    let node_operator_report = refresh_nns_node_operator_report(&NnsNodeOperatorRefreshRequest {
271        cache: NnsNodeOperatorCacheRequest {
272            icp_root: request.icp_root.clone(),
273            network: request.network.clone(),
274        },
275        source_endpoint: request.source_endpoint.clone(),
276        now_unix_secs: request.now_unix_secs,
277        lock_stale_after_seconds: request.lock_stale_after_seconds,
278        dry_run: request.dry_run,
279        output_path: None,
280    })?;
281    let data_center_report = refresh_nns_data_center_report(&NnsDataCenterRefreshRequest {
282        cache: NnsDataCenterCacheRequest {
283            icp_root: request.icp_root.clone(),
284            network: request.network.clone(),
285        },
286        source_endpoint: request.source_endpoint.clone(),
287        now_unix_secs: request.now_unix_secs,
288        lock_stale_after_seconds: request.lock_stale_after_seconds,
289        dry_run: request.dry_run,
290        output_path: None,
291    })?;
292
293    Ok(topology_refresh_report_from_reports(
294        request.network.clone(),
295        request.source_endpoint.clone(),
296        request.dry_run,
297        NnsTopologyRefreshComponentReports {
298            subnet: subnet_report,
299            node: node_report,
300            node_provider: node_provider_report,
301            node_operator: node_operator_report,
302            data_center: data_center_report,
303        },
304    ))
305}
306
307#[must_use]
308pub fn nns_topology_summary_report_text(report: &NnsTopologySummaryReport) -> String {
309    let mut lines = Vec::new();
310    lines.push(format!(
311        "topology: {} subnets {} nodes {} node_operators {} node_providers {} data_centers {}",
312        report.network,
313        report.subnet_count,
314        report.node_count,
315        report.node_operator_count,
316        report.node_provider_count,
317        report.data_center_count
318    ));
319    lines.push("counts:".to_string());
320    lines.push(render_count_table(report));
321    lines.push("subnet_kinds:".to_string());
322    lines.push(render_kind_table(report));
323    lines.push("join_coverage:".to_string());
324    lines.push(render_join_coverage_table(report));
325    lines.push("registry_versions:".to_string());
326    lines.push(render_registry_version_table(report));
327    lines.join("\n")
328}
329
330#[must_use]
331pub fn nns_topology_refresh_report_text(report: &NnsTopologyRefreshReport) -> String {
332    let mut lines = Vec::new();
333    lines.push(format!(
334        "topology_refresh: {} components {} wrote {} replaced {} dry_run {}",
335        report.network,
336        report.component_count,
337        report.wrote_cache_count,
338        report.replaced_existing_cache_count,
339        yes_no(report.dry_run)
340    ));
341    lines.push(format!("source_endpoint: {}", report.source_endpoint));
342    lines.push(render_refresh_table(report));
343    lines.join("\n")
344}
345
346fn topology_summary_report_from_reports(
347    network: String,
348    source_endpoint: String,
349    subnet_report: SubnetCatalogListReport,
350    node_report: NnsNodeListReport,
351    node_provider_report: NnsNodeProviderListReport,
352    node_operator_report: NnsNodeOperatorListReport,
353    data_center_report: NnsDataCenterListReport,
354) -> NnsTopologySummaryReport {
355    let application_subnet_count = subnet_count_by_kind(&subnet_report, SubnetKind::Application);
356    let system_subnet_count = subnet_count_by_kind(&subnet_report, SubnetKind::System);
357    let unknown_subnet_count = subnet_count_by_kind(&subnet_report, SubnetKind::Unknown);
358    let application_node_count =
359        node_count_by_subnet_kind(&node_report, NNS_NODE_SUBNET_KIND_APPLICATION);
360    let system_node_count = node_count_by_subnet_kind(&node_report, NNS_NODE_SUBNET_KIND_SYSTEM);
361    let unknown_node_count = node_count_by_subnet_kind(&node_report, NNS_NODE_SUBNET_KIND_UNKNOWN);
362    let join_coverage = topology_summary_join_coverage_counts(
363        &node_report,
364        &node_provider_report,
365        &node_operator_report,
366        &data_center_report,
367    );
368    let registry_versions = topology_summary_registry_versions(
369        &subnet_report,
370        &node_report,
371        &node_provider_report,
372        &node_operator_report,
373        &data_center_report,
374    );
375
376    NnsTopologySummaryReport {
377        schema_version: NNS_TOPOLOGY_SUMMARY_REPORT_SCHEMA_VERSION,
378        network,
379        source_endpoint,
380        subnet_count: subnet_report.subnets.len(),
381        application_subnet_count,
382        system_subnet_count,
383        unknown_subnet_count,
384        routing_range_count: subnet_report
385            .subnets
386            .iter()
387            .map(|subnet| subnet.range_count)
388            .sum(),
389        node_count: node_report.node_count,
390        application_node_count,
391        system_node_count,
392        unknown_node_count,
393        node_provider_count: node_provider_report.node_provider_count,
394        node_operator_count: node_operator_report.node_operator_count,
395        data_center_count: data_center_report.data_center_count,
396        nodes_with_known_node_provider_count: join_coverage.nodes_with_known_node_provider_count,
397        nodes_with_unknown_node_provider_count: node_report
398            .node_count
399            .saturating_sub(join_coverage.nodes_with_known_node_provider_count),
400        nodes_with_known_node_operator_count: join_coverage.nodes_with_known_node_operator_count,
401        nodes_with_unknown_node_operator_count: node_report
402            .node_count
403            .saturating_sub(join_coverage.nodes_with_known_node_operator_count),
404        nodes_with_known_data_center_count: join_coverage.nodes_with_known_data_center_count,
405        nodes_with_unknown_data_center_count: node_report
406            .node_count
407            .saturating_sub(join_coverage.nodes_with_known_data_center_count),
408        node_operators_with_known_node_provider_count: join_coverage
409            .node_operators_with_known_node_provider_count,
410        node_operators_with_unknown_node_provider_count: node_operator_report
411            .node_operator_count
412            .saturating_sub(join_coverage.node_operators_with_known_node_provider_count),
413        node_operators_with_known_data_center_count: join_coverage
414            .node_operators_with_known_data_center_count,
415        node_operators_with_unknown_data_center_count: node_operator_report
416            .node_operator_count
417            .saturating_sub(join_coverage.node_operators_with_known_data_center_count),
418        subnet_catalog_stale: subnet_report.catalog_stale,
419        subnet_catalog_stale_reason: subnet_report.stale_reason,
420        registry_versions,
421    }
422}
423
424///
425/// NnsTopologyJoinCoverageCounts
426///
427struct NnsTopologyJoinCoverageCounts {
428    nodes_with_known_node_provider_count: usize,
429    nodes_with_known_node_operator_count: usize,
430    nodes_with_known_data_center_count: usize,
431    node_operators_with_known_node_provider_count: usize,
432    node_operators_with_known_data_center_count: usize,
433}
434
435fn topology_summary_join_coverage_counts(
436    node_report: &NnsNodeListReport,
437    node_provider_report: &NnsNodeProviderListReport,
438    node_operator_report: &NnsNodeOperatorListReport,
439    data_center_report: &NnsDataCenterListReport,
440) -> NnsTopologyJoinCoverageCounts {
441    let node_provider_principals = node_provider_report
442        .node_providers
443        .iter()
444        .map(|provider| provider.node_provider_principal.as_str())
445        .collect::<BTreeSet<_>>();
446    let node_operator_principals = node_operator_report
447        .node_operators
448        .iter()
449        .map(|operator| operator.node_operator_principal.as_str())
450        .collect::<BTreeSet<_>>();
451    let data_center_ids = data_center_report
452        .data_centers
453        .iter()
454        .map(|data_center| data_center.data_center_id.as_str())
455        .collect::<BTreeSet<_>>();
456
457    NnsTopologyJoinCoverageCounts {
458        nodes_with_known_node_provider_count: node_count_with_known_node_provider(
459            node_report,
460            &node_provider_principals,
461        ),
462        nodes_with_known_node_operator_count: node_count_with_known_node_operator(
463            node_report,
464            &node_operator_principals,
465        ),
466        nodes_with_known_data_center_count: node_count_with_known_data_center(
467            node_report,
468            &data_center_ids,
469        ),
470        node_operators_with_known_node_provider_count: operator_count_with_known_node_provider(
471            node_operator_report,
472            &node_provider_principals,
473        ),
474        node_operators_with_known_data_center_count: operator_count_with_known_data_center(
475            node_operator_report,
476            &data_center_ids,
477        ),
478    }
479}
480
481fn topology_summary_registry_versions(
482    subnet_report: &SubnetCatalogListReport,
483    node_report: &NnsNodeListReport,
484    node_provider_report: &NnsNodeProviderListReport,
485    node_operator_report: &NnsNodeOperatorListReport,
486    data_center_report: &NnsDataCenterListReport,
487) -> Vec<NnsTopologyRegistryVersionRow> {
488    vec![
489        registry_version_row(
490            "subnet_catalog",
491            subnet_report.registry_version,
492            subnet_report.fetched_at.clone(),
493            None,
494            Some(subnet_report.catalog_stale),
495        ),
496        registry_version_row(
497            "nodes",
498            node_report.registry_version,
499            node_report.fetched_at.clone(),
500            Some(node_report.source_endpoint.clone()),
501            None,
502        ),
503        registry_version_row(
504            "node_providers",
505            node_provider_report.registry_version,
506            node_provider_report.fetched_at.clone(),
507            Some(node_provider_report.source_endpoint.clone()),
508            None,
509        ),
510        registry_version_row(
511            "node_operators",
512            node_operator_report.registry_version,
513            node_operator_report.fetched_at.clone(),
514            Some(node_operator_report.source_endpoint.clone()),
515            None,
516        ),
517        registry_version_row(
518            "data_centers",
519            data_center_report.registry_version,
520            data_center_report.fetched_at.clone(),
521            Some(data_center_report.source_endpoint.clone()),
522            None,
523        ),
524    ]
525}
526
527fn topology_refresh_report_from_reports(
528    network: String,
529    source_endpoint: String,
530    dry_run: bool,
531    reports: NnsTopologyRefreshComponentReports,
532) -> NnsTopologyRefreshReport {
533    let components = vec![
534        refresh_row_from_subnet_report(reports.subnet),
535        refresh_row_from_node_report(reports.node),
536        refresh_row_from_node_provider_report(reports.node_provider),
537        refresh_row_from_node_operator_report(reports.node_operator),
538        refresh_row_from_data_center_report(reports.data_center),
539    ];
540    let wrote_cache_count = components
541        .iter()
542        .filter(|component| component.wrote_cache)
543        .count();
544    let replaced_existing_cache_count = components
545        .iter()
546        .filter(|component| component.replaced_existing_cache)
547        .count();
548
549    NnsTopologyRefreshReport {
550        schema_version: NNS_TOPOLOGY_REFRESH_REPORT_SCHEMA_VERSION,
551        network,
552        source_endpoint,
553        dry_run,
554        component_count: components.len(),
555        wrote_cache_count,
556        replaced_existing_cache_count,
557        components,
558    }
559}
560
561///
562/// NnsTopologyRefreshComponentReports
563///
564struct NnsTopologyRefreshComponentReports {
565    subnet: SubnetCatalogRefreshReport,
566    node: NnsNodeRefreshReport,
567    node_provider: NnsNodeProviderRefreshReport,
568    node_operator: NnsNodeOperatorRefreshReport,
569    data_center: NnsDataCenterRefreshReport,
570}
571
572fn refresh_row_from_subnet_report(report: SubnetCatalogRefreshReport) -> NnsTopologyRefreshRow {
573    NnsTopologyRefreshRow {
574        source: "subnet_catalog".to_string(),
575        cache_path: report.catalog_path,
576        refresh_lock_path: report.refresh_lock_path,
577        registry_version: report.registry_version,
578        fetched_at: report.fetched_at,
579        source_endpoint: report.source_endpoint,
580        fetched_by: report.fetched_by,
581        dry_run: report.dry_run,
582        wrote_cache: report.wrote_catalog,
583        replaced_existing_cache: report.replaced_existing_catalog,
584        item_count: report.subnet_count,
585    }
586}
587
588fn refresh_row_from_node_report(report: NnsNodeRefreshReport) -> NnsTopologyRefreshRow {
589    NnsTopologyRefreshRow {
590        source: "nodes".to_string(),
591        cache_path: report.cache_path,
592        refresh_lock_path: report.refresh_lock_path,
593        registry_version: report.registry_version,
594        fetched_at: report.fetched_at,
595        source_endpoint: report.source_endpoint,
596        fetched_by: report.fetched_by,
597        dry_run: report.dry_run,
598        wrote_cache: report.wrote_cache,
599        replaced_existing_cache: report.replaced_existing_cache,
600        item_count: report.node_count,
601    }
602}
603
604fn refresh_row_from_node_provider_report(
605    report: NnsNodeProviderRefreshReport,
606) -> NnsTopologyRefreshRow {
607    NnsTopologyRefreshRow {
608        source: "node_providers".to_string(),
609        cache_path: report.cache_path,
610        refresh_lock_path: report.refresh_lock_path,
611        registry_version: report.registry_version,
612        fetched_at: report.fetched_at,
613        source_endpoint: report.source_endpoint,
614        fetched_by: report.fetched_by,
615        dry_run: report.dry_run,
616        wrote_cache: report.wrote_cache,
617        replaced_existing_cache: report.replaced_existing_cache,
618        item_count: report.node_provider_count,
619    }
620}
621
622fn refresh_row_from_node_operator_report(
623    report: NnsNodeOperatorRefreshReport,
624) -> NnsTopologyRefreshRow {
625    NnsTopologyRefreshRow {
626        source: "node_operators".to_string(),
627        cache_path: report.cache_path,
628        refresh_lock_path: report.refresh_lock_path,
629        registry_version: report.registry_version,
630        fetched_at: report.fetched_at,
631        source_endpoint: report.source_endpoint,
632        fetched_by: report.fetched_by,
633        dry_run: report.dry_run,
634        wrote_cache: report.wrote_cache,
635        replaced_existing_cache: report.replaced_existing_cache,
636        item_count: report.node_operator_count,
637    }
638}
639
640fn refresh_row_from_data_center_report(
641    report: NnsDataCenterRefreshReport,
642) -> NnsTopologyRefreshRow {
643    NnsTopologyRefreshRow {
644        source: "data_centers".to_string(),
645        cache_path: report.cache_path,
646        refresh_lock_path: report.refresh_lock_path,
647        registry_version: report.registry_version,
648        fetched_at: report.fetched_at,
649        source_endpoint: report.source_endpoint,
650        fetched_by: report.fetched_by,
651        dry_run: report.dry_run,
652        wrote_cache: report.wrote_cache,
653        replaced_existing_cache: report.replaced_existing_cache,
654        item_count: report.data_center_count,
655    }
656}
657
658fn enforce_mainnet_network(network: &str) -> Result<(), NnsTopologyHostError> {
659    if network == MAINNET_NETWORK {
660        return Ok(());
661    }
662    Err(NnsTopologyHostError::UnsupportedNetwork {
663        network: network.to_string(),
664    })
665}
666
667fn subnet_count_by_kind(report: &SubnetCatalogListReport, kind: SubnetKind) -> usize {
668    report
669        .subnets
670        .iter()
671        .filter(|subnet| subnet.subnet_kind == kind)
672        .count()
673}
674
675fn node_count_by_subnet_kind(report: &NnsNodeListReport, kind: &str) -> usize {
676    report
677        .nodes
678        .iter()
679        .filter(|node| node.subnet_kind.eq_ignore_ascii_case(kind))
680        .count()
681}
682
683fn node_count_with_known_node_provider(
684    report: &NnsNodeListReport,
685    providers: &BTreeSet<&str>,
686) -> usize {
687    report
688        .nodes
689        .iter()
690        .filter(|node| providers.contains(node.node_provider_principal.as_str()))
691        .count()
692}
693
694fn node_count_with_known_node_operator(
695    report: &NnsNodeListReport,
696    operators: &BTreeSet<&str>,
697) -> usize {
698    report
699        .nodes
700        .iter()
701        .filter(|node| operators.contains(node.node_operator_principal.as_str()))
702        .count()
703}
704
705fn node_count_with_known_data_center(
706    report: &NnsNodeListReport,
707    data_centers: &BTreeSet<&str>,
708) -> usize {
709    report
710        .nodes
711        .iter()
712        .filter(|node| data_centers.contains(node.data_center_id.as_str()))
713        .count()
714}
715
716fn operator_count_with_known_node_provider(
717    report: &NnsNodeOperatorListReport,
718    providers: &BTreeSet<&str>,
719) -> usize {
720    report
721        .node_operators
722        .iter()
723        .filter(|operator| providers.contains(operator.node_provider_principal.as_str()))
724        .count()
725}
726
727fn operator_count_with_known_data_center(
728    report: &NnsNodeOperatorListReport,
729    data_centers: &BTreeSet<&str>,
730) -> usize {
731    report
732        .node_operators
733        .iter()
734        .filter(|operator| data_centers.contains(operator.data_center_id.as_str()))
735        .count()
736}
737
738fn registry_version_row(
739    source: &str,
740    registry_version: u64,
741    fetched_at: String,
742    source_endpoint: Option<String>,
743    stale: Option<bool>,
744) -> NnsTopologyRegistryVersionRow {
745    NnsTopologyRegistryVersionRow {
746        source: source.to_string(),
747        registry_version,
748        fetched_at,
749        source_endpoint: source_endpoint.unwrap_or_else(|| "-".to_string()),
750        stale,
751    }
752}
753
754fn render_count_table(report: &NnsTopologySummaryReport) -> String {
755    let headers = ["METRIC", "COUNT"];
756    let rows = [
757        ["subnets".to_string(), report.subnet_count.to_string()],
758        [
759            "routing_ranges".to_string(),
760            report.routing_range_count.to_string(),
761        ],
762        ["nodes".to_string(), report.node_count.to_string()],
763        [
764            "node_operators".to_string(),
765            report.node_operator_count.to_string(),
766        ],
767        [
768            "node_providers".to_string(),
769            report.node_provider_count.to_string(),
770        ],
771        [
772            "data_centers".to_string(),
773            report.data_center_count.to_string(),
774        ],
775    ];
776    let alignments = [ColumnAlign::Left, ColumnAlign::Right];
777    render_table(&headers, &rows, &alignments)
778}
779
780fn render_kind_table(report: &NnsTopologySummaryReport) -> String {
781    let headers = ["KIND", "SUBNETS", "NODES"];
782    let rows = [
783        [
784            NNS_NODE_SUBNET_KIND_APPLICATION.to_string(),
785            report.application_subnet_count.to_string(),
786            report.application_node_count.to_string(),
787        ],
788        [
789            NNS_NODE_SUBNET_KIND_SYSTEM.to_string(),
790            report.system_subnet_count.to_string(),
791            report.system_node_count.to_string(),
792        ],
793        [
794            NNS_NODE_SUBNET_KIND_UNKNOWN.to_string(),
795            report.unknown_subnet_count.to_string(),
796            report.unknown_node_count.to_string(),
797        ],
798    ];
799    let alignments = [ColumnAlign::Left, ColumnAlign::Right, ColumnAlign::Right];
800    render_table(&headers, &rows, &alignments)
801}
802
803fn render_join_coverage_table(report: &NnsTopologySummaryReport) -> String {
804    let headers = ["LINK", "KNOWN", "UNKNOWN"];
805    let rows = [
806        [
807            "nodes_to_node_providers".to_string(),
808            report.nodes_with_known_node_provider_count.to_string(),
809            report.nodes_with_unknown_node_provider_count.to_string(),
810        ],
811        [
812            "nodes_to_node_operators".to_string(),
813            report.nodes_with_known_node_operator_count.to_string(),
814            report.nodes_with_unknown_node_operator_count.to_string(),
815        ],
816        [
817            "nodes_to_data_centers".to_string(),
818            report.nodes_with_known_data_center_count.to_string(),
819            report.nodes_with_unknown_data_center_count.to_string(),
820        ],
821        [
822            "node_operators_to_node_providers".to_string(),
823            report
824                .node_operators_with_known_node_provider_count
825                .to_string(),
826            report
827                .node_operators_with_unknown_node_provider_count
828                .to_string(),
829        ],
830        [
831            "node_operators_to_data_centers".to_string(),
832            report
833                .node_operators_with_known_data_center_count
834                .to_string(),
835            report
836                .node_operators_with_unknown_data_center_count
837                .to_string(),
838        ],
839    ];
840    let alignments = [ColumnAlign::Left, ColumnAlign::Right, ColumnAlign::Right];
841    render_table(&headers, &rows, &alignments)
842}
843
844fn render_registry_version_table(report: &NnsTopologySummaryReport) -> String {
845    let headers = ["SOURCE", "VERSION", "FETCHED_AT", "STALE", "ENDPOINT"];
846    let rows = report
847        .registry_versions
848        .iter()
849        .map(|row| {
850            [
851                row.source.clone(),
852                row.registry_version.to_string(),
853                row.fetched_at.clone(),
854                row.stale
855                    .map_or_else(|| "-".to_string(), |stale| yes_no(stale).to_string()),
856                row.source_endpoint.clone(),
857            ]
858        })
859        .collect::<Vec<_>>();
860    let alignments = [
861        ColumnAlign::Left,
862        ColumnAlign::Right,
863        ColumnAlign::Left,
864        ColumnAlign::Left,
865        ColumnAlign::Left,
866    ];
867    render_table(&headers, &rows, &alignments)
868}
869
870fn render_refresh_table(report: &NnsTopologyRefreshReport) -> String {
871    let headers = [
872        "SOURCE",
873        "COUNT",
874        "VERSION",
875        "FETCHED_AT",
876        "WROTE",
877        "REPLACED",
878        "CACHE",
879    ];
880    let rows = report
881        .components
882        .iter()
883        .map(|row| {
884            [
885                row.source.clone(),
886                row.item_count.to_string(),
887                row.registry_version.to_string(),
888                row.fetched_at.clone(),
889                yes_no(row.wrote_cache).to_string(),
890                yes_no(row.replaced_existing_cache).to_string(),
891                row.cache_path.clone(),
892            ]
893        })
894        .collect::<Vec<_>>();
895    let alignments = [
896        ColumnAlign::Left,
897        ColumnAlign::Right,
898        ColumnAlign::Right,
899        ColumnAlign::Left,
900        ColumnAlign::Left,
901        ColumnAlign::Left,
902        ColumnAlign::Left,
903    ];
904    render_table(&headers, &rows, &alignments)
905}
906
907#[cfg(test)]
908mod tests;