ignition_core/actions/redundancy.rs
1//! `ign redundancy status` action (09-04, EXT-02) — a thin wrapper
2//! over the redundancy read. The wrapper indirection keeps the render
3//! arms symmetrical with the license merge (`{status: …}` envelope
4//! shape for the single-read families).
5//!
6//! `redundancy_status_pair` (24-04, D-11) reads a second node through
7//! its own profile and names the serving node. **Activity level decides,
8//! never role** (block C-S3): on failover `role` stays fixed while
9//! `activityLevel` moves. A Cold backup with `uptime: -1` is a healthy
10//! standby, not a fault, and nothing here warns about it.
11
12use serde::Serialize;
13
14use crate::client::GatewayApi;
15use crate::client::redundancy::{ActiveNode, RedundancyStatusWire, active_node};
16use crate::error::CoreError;
17
18/// Which node of a pair is serving, from the second-node read.
19#[derive(Debug, Serialize)]
20pub struct ActiveNodeReport {
21 /// `this`, `peer`, `both` (a split-brain signal, reported never
22 /// resolved) or `neither`; decided by `activityLevel` alone.
23 pub active: &'static str,
24 /// The `role` the active profile's node reports (display only).
25 pub this_role: String,
26 /// The `role` the peer profile's node reports (display only).
27 pub peer_role: String,
28 /// The peer profile's name.
29 pub peer_profile: String,
30 /// The peer node's flat wire status, capture-shaped.
31 pub peer: RedundancyStatusWire,
32}
33
34/// `ign redundancy status` output model.
35#[derive(Debug, Serialize)]
36pub struct RedundancyStatusResult {
37 /// The flat wire status, capture-shaped (units documented at the
38 /// model: uptime ms-since-start; lastSyncTimestamp -1 sentinel).
39 pub status: RedundancyStatusWire,
40 /// Present only with `--peer`: absent (not null) otherwise so the
41 /// single-node output stays byte-identical.
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub active_node: Option<ActiveNodeReport>,
44}
45
46/// Read the redundancy status in one command.
47pub async fn redundancy_status(api: &dyn GatewayApi) -> Result<RedundancyStatusResult, CoreError> {
48 Ok(RedundancyStatusResult {
49 status: api.redundancy_status().await?,
50 active_node: None,
51 })
52}
53
54/// Read both nodes of a pair (each through its own client) and name the
55/// active one. `this` is the active profile's node, `peer` the second.
56pub async fn redundancy_status_pair(
57 this: &dyn GatewayApi,
58 peer: &dyn GatewayApi,
59 peer_profile: &str,
60) -> Result<RedundancyStatusResult, CoreError> {
61 let this_status = this.redundancy_status().await?;
62 let peer_status = peer.redundancy_status().await?;
63 let active = match active_node(&this_status, &peer_status) {
64 ActiveNode::OnlyA => "this",
65 ActiveNode::OnlyB => "peer",
66 ActiveNode::Both => "both",
67 ActiveNode::Neither => "neither",
68 };
69 let report = ActiveNodeReport {
70 active,
71 this_role: this_status.role.clone(),
72 peer_role: peer_status.role.clone(),
73 peer_profile: peer_profile.to_string(),
74 peer: peer_status,
75 };
76 Ok(RedundancyStatusResult {
77 status: this_status,
78 active_node: Some(report),
79 })
80}