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