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)),
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!("data_center_id: {}", terminal_text(&node.data_center_id)),
79        format!(
80            "data_center_name: {}",
81            terminal_text(&node.data_center_name)
82        ),
83        format!("owner: {}", terminal_text(node.owner.trim())),
84        format!("region: {}", terminal_text(&node.region)),
85        format!(
86            "guestos_version: {}",
87            optional_text(node.guestos_version.as_ref())
88        ),
89        format!(
90            "guestos_tee_active: {}",
91            optional_bool(node.guestos_tee_active)
92        ),
93        format!("ip_address: {}", optional_text(node.ip_address.as_ref())),
94        format!(
95            "ipv4_connectivity_status: {}",
96            optional_bool(node.ipv4_connectivity_status)
97        ),
98        format!(
99            "node_hardware_generation: {}",
100            optional_text(node.node_hardware_generation.as_ref())
101        ),
102        format!("alert_name: {}", optional_text(node.alert_name.as_ref())),
103    ]);
104    lines.join("\n")
105}
106
107fn node_table(nodes: &[CloudEngineNodeRow]) -> String {
108    let rows = nodes
109        .iter()
110        .map(|node| {
111            [
112                node.node_id.clone(),
113                terminal_text(&node.status),
114                node.cloud_engine_subnet_id
115                    .as_ref()
116                    .map_or_else(|| "-".to_string(), Clone::clone),
117                node.node_provider_id.clone(),
118                node.node_operator_id.clone(),
119                terminal_text(&node.data_center_id),
120                node.guestos_version
121                    .as_ref()
122                    .map_or_else(|| "-".to_string(), Clone::clone),
123            ]
124        })
125        .collect::<Vec<_>>();
126    render(
127        &[
128            "Node",
129            "Status",
130            "CE Subnet",
131            "Provider",
132            "Operator",
133            "DC",
134            "GuestOS",
135        ],
136        &rows,
137        &[
138            Alignment::Left,
139            Alignment::Left,
140            Alignment::Left,
141            Alignment::Left,
142            Alignment::Left,
143            Alignment::Left,
144            Alignment::Left,
145        ],
146        REPORT_TABLE_OPTIONS,
147    )
148}
149
150fn optional_bool(value: Option<bool>) -> &'static str {
151    value.map_or("-", yes_no)
152}