Skip to main content

ic_query/cloud_engine/node/
text.rs

1//! Module: cloud_engine::node::text
2//!
3//! Responsibility: render CloudEngine Type4 node reports as human-facing text.
4//! Does not own: JSON serialization, source validation, live calls, or process output.
5//! Boundary: separates off-chain Dashboard provenance from node tables.
6
7use super::{CloudEngineNodeInfoReport, CloudEngineNodeListReport, CloudEngineNodeRow};
8use crate::{
9    ic::IcDashboardReportProvenance,
10    table::{ColumnAlign, render_table},
11    text_value::{optional_text, sanitize_text, yes_no},
12};
13
14/// Render one complete explicitly scoped Type4 node list.
15#[must_use]
16pub fn cloud_engine_node_list_report_text(report: &CloudEngineNodeListReport) -> String {
17    let mut lines = provenance_lines(&report.provenance);
18    lines.extend([
19        format!("node_reward_type: {}", report.node_reward_type),
20        format!("included_statuses: {}", report.included_statuses.join(",")),
21        format!(
22            "requested_node_provider_id: {}",
23            optional_text(report.requested_node_provider_id.as_ref())
24        ),
25        format!("node_count: {}", report.node_count),
26        format!("status_up: {}", report.status_counts.up),
27        format!("status_down: {}", report.status_counts.down),
28        format!("status_disabled: {}", report.status_counts.disabled),
29        format!("status_degraded: {}", report.status_counts.degraded),
30        format!("status_unknown: {}", report.status_counts.unknown),
31        format!("node_provider_count: {}", report.node_provider_count),
32        format!(
33            "cloud_engine_subnet_count: {}",
34            report.cloud_engine_subnet_count
35        ),
36        format!(
37            "unassigned_cloud_engine_node_count: {}",
38            report.unassigned_cloud_engine_node_count
39        ),
40    ]);
41    if !report.nodes.is_empty() {
42        lines.push(String::new());
43        lines.push("CloudEngine nodes".to_string());
44        lines.push(node_table(&report.nodes));
45    }
46    lines.join("\n")
47}
48
49/// Render one exact Type4 node observation.
50#[must_use]
51pub fn cloud_engine_node_info_report_text(report: &CloudEngineNodeInfoReport) -> String {
52    let node = &report.node;
53    let mut lines = provenance_lines(&report.provenance);
54    lines.extend([
55        format!("node_id: {}", node.node_id),
56        format!("status: {}", sanitize_text(&node.status)),
57        format!("node_type: {}", sanitize_text(&node.node_type)),
58        format!(
59            "node_reward_type: {}",
60            sanitize_text(&node.node_reward_type)
61        ),
62        format!("node_provider_id: {}", node.node_provider_id),
63        format!(
64            "node_provider_name: {}",
65            sanitize_text(&node.node_provider_name)
66        ),
67        format!("node_operator_id: {}", node.node_operator_id),
68        format!(
69            "cloud_engine_subnet_id: {}",
70            optional_text(node.cloud_engine_subnet_id.as_ref())
71        ),
72        format!("subnet_id: {}", optional_text(node.subnet_id.as_ref())),
73        format!("data_center_id: {}", sanitize_text(&node.data_center_id)),
74        format!(
75            "data_center_name: {}",
76            sanitize_text(&node.data_center_name)
77        ),
78        format!("owner: {}", sanitize_text(node.owner.trim())),
79        format!("region: {}", sanitize_text(&node.region)),
80        format!(
81            "guestos_version: {}",
82            optional_text(node.guestos_version.as_ref())
83        ),
84        format!(
85            "guestos_tee_active: {}",
86            optional_bool(node.guestos_tee_active)
87        ),
88        format!("ip_address: {}", optional_text(node.ip_address.as_ref())),
89        format!(
90            "ipv4_connectivity_status: {}",
91            optional_bool(node.ipv4_connectivity_status)
92        ),
93        format!(
94            "node_hardware_generation: {}",
95            optional_text(node.node_hardware_generation.as_ref())
96        ),
97        format!("alert_name: {}", optional_text(node.alert_name.as_ref())),
98    ]);
99    lines.join("\n")
100}
101
102fn provenance_lines(provenance: &IcDashboardReportProvenance) -> Vec<String> {
103    vec![
104        format!("schema_version: {}", provenance.schema_version),
105        format!("network: {}", sanitize_text(&provenance.network)),
106        format!("authority: {}", sanitize_text(&provenance.authority)),
107        format!(
108            "source_endpoint: {}",
109            sanitize_text(&provenance.source_endpoint)
110        ),
111        format!("fetched_at: {}", sanitize_text(&provenance.fetched_at)),
112        format!("fetched_by: {}", sanitize_text(&provenance.fetched_by)),
113        format!("certified: {}", yes_no(provenance.certified)),
114        format!(
115            "point_in_time_guaranteed: {}",
116            yes_no(provenance.point_in_time_guaranteed)
117        ),
118    ]
119}
120
121fn node_table(nodes: &[CloudEngineNodeRow]) -> String {
122    let rows = nodes
123        .iter()
124        .map(|node| {
125            [
126                node.node_id.clone(),
127                sanitize_text(&node.status),
128                node.cloud_engine_subnet_id
129                    .as_ref()
130                    .map_or_else(|| "-".to_string(), Clone::clone),
131                node.node_provider_id.clone(),
132                node.node_operator_id.clone(),
133                sanitize_text(&node.data_center_id),
134                node.guestos_version
135                    .as_ref()
136                    .map_or_else(|| "-".to_string(), Clone::clone),
137            ]
138        })
139        .collect::<Vec<_>>();
140    render_table(
141        &[
142            "Node",
143            "Status",
144            "CE Subnet",
145            "Provider",
146            "Operator",
147            "DC",
148            "GuestOS",
149        ],
150        &rows,
151        &[
152            ColumnAlign::Left,
153            ColumnAlign::Left,
154            ColumnAlign::Left,
155            ColumnAlign::Left,
156            ColumnAlign::Left,
157            ColumnAlign::Left,
158            ColumnAlign::Left,
159        ],
160    )
161}
162
163fn optional_bool(value: Option<bool>) -> &'static str {
164    value.map_or("-", yes_no)
165}