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