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::dashboard_provenance_lines,
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 = dashboard_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 = dashboard_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 node_table(nodes: &[CloudEngineNodeRow]) -> String {
103    let rows = nodes
104        .iter()
105        .map(|node| {
106            [
107                node.node_id.clone(),
108                sanitize_text(&node.status),
109                node.cloud_engine_subnet_id
110                    .as_ref()
111                    .map_or_else(|| "-".to_string(), Clone::clone),
112                node.node_provider_id.clone(),
113                node.node_operator_id.clone(),
114                sanitize_text(&node.data_center_id),
115                node.guestos_version
116                    .as_ref()
117                    .map_or_else(|| "-".to_string(), Clone::clone),
118            ]
119        })
120        .collect::<Vec<_>>();
121    render_table(
122        &[
123            "Node",
124            "Status",
125            "CE Subnet",
126            "Provider",
127            "Operator",
128            "DC",
129            "GuestOS",
130        ],
131        &rows,
132        &[
133            ColumnAlign::Left,
134            ColumnAlign::Left,
135            ColumnAlign::Left,
136            ColumnAlign::Left,
137            ColumnAlign::Left,
138            ColumnAlign::Left,
139            ColumnAlign::Left,
140        ],
141    )
142}
143
144fn optional_bool(value: Option<bool>) -> &'static str {
145    value.map_or("-", yes_no)
146}