Skip to content

Commit fcf34da

Browse files
committed
wip mmhealth node status
1 parent a5bed9e commit fcf34da

3 files changed

Lines changed: 366 additions & 0 deletions

File tree

src/health-node-example.in

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
mmhealth:Event:HEADER:version:reserved:reserved:node:component:entityname:entitytype:event:arguments:activesince:identifier:ishidden:message:eventtype:severity:fullidentifier:
2+
mmhealth:State:HEADER:version:reserved:reserved:node:component:entityname:entitytype:status:laststatuschange:lastcheck:
3+
mmhealth:State:0:1:::frontend2-ib0:NODE:frontend2-ib0:NODE:TIPS:2026-06-02 10%3A01%3A12.368897 CEST:-:
4+
mmhealth:State:0:1:::frontend2-ib0:GPFS:frontend2-ib0:NODE:TIPS:2026-06-02 10%3A01%3A12.356696 CEST:2026-06-10 12%3A57%3A06.506009 CEST:
5+
mmhealth:Event:0:1:::frontend2-ib0:GPFS:frontend2-ib0:NODE:unexpected_operating_system::2026-06-01 15%3A23%3A10.262787 CEST::no:An unexpected operating system was detected.:TIP:TIP:4261159282176233844/111/gpfs/oscheck/:
6+
mmhealth:State:0:1:::frontend2-ib0:NETWORK:frontend2-ib0:NODE:HEALTHY:2026-06-01 15%3A23%3A05.806465 CEST:2026-06-10 12%3A57%3A06.215959 CEST:
7+
mmhealth:State:0:1:::frontend2-ib0:NETWORK:ib0:NIC:HEALTHY:2026-06-01 15%3A23%3A05.822885 CEST:2026-06-10 12%3A57%3A06.215959 CEST:
8+
mmhealth:State:0:1:::frontend2-ib0:NETWORK:hfi1_0/1:IB_RDMA:HEALTHY:2026-06-02 10%3A03%3A38.393121 CEST:2026-06-10 12%3A57%3A06.215959 CEST:
9+
mmhealth:State:0:1:::frontend2-ib0:FILESYSTEM:frontend2-ib0:NODE:TIPS:2026-06-02 10%3A01%3A38.208388 CEST:2026-06-10 12%3A57%3A06.217093 CEST:
10+
mmhealth:State:0:1:::frontend2-ib0:FILESYSTEM:gpfs1:FILESYSTEM:TIPS:2026-06-02 10%3A01%3A38.211983 CEST:2026-06-10 12%3A57%3A06.217093 CEST:
11+
mmhealth:Event:0:1:::frontend2-ib0:FILESYSTEM:gpfs1:FILESYSTEM:ill_unbalanced_fs:gpfs1:2026-06-02 10%3A01%3A38.192126 CEST:gpfs1:no:The file system gpfs1 is not properly balanced.:TIP:TIP:4261159282176233844/filesystem/fs_balance/gpfs1:
12+
mmhealth:State:0:1:::frontend2-ib0:PERFMON:frontend2-ib0:NODE:HEALTHY:2026-06-02 08%3A27%3A52.101843 CEST:2026-06-10 12%3A56%3A54.204032 CEST:
13+
mmhealth:State:0:1:::frontend2-ib0:THRESHOLD:frontend2-ib0:NODE:HEALTHY:2026-06-01 15%3A23%3A02.022864 CEST:-:

src/health.rs

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
//! `mmhealth` parsing.
2+
3+
use std::io::{BufRead, Write};
4+
use std::process::Command;
5+
6+
use anyhow::{Context, Result, anyhow};
7+
8+
/// Returns the `mmhealth` status at node level.
9+
///
10+
/// # Errors
11+
///
12+
/// Returns an error if running `mmhealth` fails or if parsing its output fails.
13+
pub fn node() -> Result<NodeStates> {
14+
let mut cmd = Command::new("mmhealth");
15+
cmd.arg("node");
16+
cmd.arg("show");
17+
cmd.arg("-Y");
18+
19+
let output = cmd
20+
.output()
21+
.with_context(|| format!("error running: {cmd:?}"))?;
22+
23+
let data = NodeStates::from_reader(output.stdout.as_slice())?;
24+
25+
Ok(data)
26+
}
27+
28+
/// Status at node level.
29+
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
30+
pub struct NodeStates {
31+
states: Vec<NodeState>,
32+
}
33+
34+
impl NodeStates {
35+
fn from_reader<Input: BufRead>(input: Input) -> Result<Self> {
36+
let mut event_index = NodeEventIndex::default();
37+
let mut state_index = NodeStateIndex::default();
38+
let mut data = Self::default();
39+
40+
for line in input.lines() {
41+
let line = line?;
42+
43+
let tokens = line.split(':').collect::<Vec<_>>();
44+
45+
if tokens[1] == "State" {
46+
if tokens[2] == "HEADER" {
47+
state_index = NodeStateIndex::default();
48+
state_index.with_tokens(&tokens);
49+
} else {
50+
let new = NodeState::from_tokens(&tokens, &state_index)?;
51+
data.states.push(new);
52+
}
53+
}
54+
55+
if tokens[1] == "Event" {
56+
if tokens[2] == "HEADER" {
57+
event_index = NodeEventIndex::default();
58+
event_index.with_tokens(&tokens);
59+
} else {
60+
let event = NodeEvent::from_tokens(&tokens, &event_index)?;
61+
62+
let needle = data.states.iter_mut().find(|state| {
63+
state.component == event.component
64+
&& state.entityname == event.entityname
65+
&& state.entitytype == event.entitytype
66+
});
67+
68+
if let Some(state) = needle {
69+
state.events.push(event);
70+
}
71+
}
72+
}
73+
}
74+
75+
Ok(data)
76+
}
77+
}
78+
79+
/// Single node event.
80+
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
81+
pub struct NodeEvent {
82+
component: String,
83+
entityname: String,
84+
entitytype: String,
85+
event: String,
86+
arguments: Option<String>,
87+
identifier: Option<String>,
88+
message: String,
89+
}
90+
91+
/// Single status at node level.
92+
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
93+
pub struct NodeState {
94+
component: String,
95+
entityname: String,
96+
entitytype: String,
97+
status: String,
98+
events: Vec<NodeEvent>,
99+
}
100+
101+
// ----------------------------------------------------------------------------
102+
// boiler-platy parsing
103+
// ----------------------------------------------------------------------------
104+
105+
impl NodeState {
106+
fn from_tokens(tokens: &[&str], index: &NodeStateIndex) -> Result<Self> {
107+
let component = index
108+
.component
109+
.ok_or_else(|| anyhow!("no component index"))?;
110+
let component = tokens[component].into();
111+
112+
let entityname = index
113+
.entityname
114+
.ok_or_else(|| anyhow!("no entityname index"))?;
115+
let entityname = tokens[entityname].into();
116+
117+
let entitytype = index
118+
.entitytype
119+
.ok_or_else(|| anyhow!("no entitytype index"))?;
120+
let entitytype = tokens[entitytype].into();
121+
122+
let status = index.status.ok_or_else(|| anyhow!("no status index"))?;
123+
let status = tokens[status].into();
124+
125+
Ok(Self {
126+
component,
127+
entityname,
128+
entitytype,
129+
status,
130+
events: vec![],
131+
})
132+
}
133+
}
134+
135+
#[derive(Debug, Default)]
136+
struct NodeStateIndex {
137+
component: Option<usize>,
138+
entityname: Option<usize>,
139+
entitytype: Option<usize>,
140+
status: Option<usize>,
141+
}
142+
143+
impl NodeStateIndex {
144+
fn with_tokens(&mut self, tokens: &[&str]) {
145+
for (i, token) in tokens.iter().enumerate() {
146+
match *token {
147+
"component" => self.component = Some(i),
148+
"entityname" => self.entityname = Some(i),
149+
"entitytype" => self.entitytype = Some(i),
150+
"status" => self.status = Some(i),
151+
_ => {}
152+
}
153+
}
154+
}
155+
}
156+
157+
impl NodeEvent {
158+
fn from_tokens(tokens: &[&str], index: &NodeEventIndex) -> Result<Self> {
159+
let component = index
160+
.component
161+
.ok_or_else(|| anyhow!("no component index"))?;
162+
let component = tokens[component].into();
163+
164+
let entityname = index
165+
.entityname
166+
.ok_or_else(|| anyhow!("no entityname index"))?;
167+
let entityname = tokens[entityname].into();
168+
169+
let entitytype = index
170+
.entitytype
171+
.ok_or_else(|| anyhow!("no entitytype index"))?;
172+
let entitytype = tokens[entitytype].into();
173+
174+
let event = index.event.ok_or_else(|| anyhow!("no event index"))?;
175+
let event = tokens[event].into();
176+
177+
let arguments = index
178+
.arguments
179+
.ok_or_else(|| anyhow!("no arguments index"))?;
180+
let arguments = tokens[arguments];
181+
let arguments = (!arguments.is_empty()).then_some(arguments.into());
182+
183+
let identifier = index
184+
.identifier
185+
.ok_or_else(|| anyhow!("no identifier index"))?;
186+
let identifier = tokens[identifier];
187+
let identifier = (!identifier.is_empty()).then_some(identifier.into());
188+
189+
let message =
190+
index.message.ok_or_else(|| anyhow!("no message index"))?;
191+
let message = tokens[message].into();
192+
193+
Ok(Self {
194+
component,
195+
entityname,
196+
entitytype,
197+
event,
198+
arguments,
199+
identifier,
200+
message,
201+
})
202+
}
203+
}
204+
205+
#[derive(Debug, Default)]
206+
struct NodeEventIndex {
207+
component: Option<usize>,
208+
entityname: Option<usize>,
209+
entitytype: Option<usize>,
210+
event: Option<usize>,
211+
arguments: Option<usize>,
212+
identifier: Option<usize>,
213+
message: Option<usize>,
214+
}
215+
216+
impl NodeEventIndex {
217+
fn with_tokens(&mut self, tokens: &[&str]) {
218+
for (i, token) in tokens.iter().enumerate() {
219+
match *token {
220+
"component" => self.component = Some(i),
221+
"entityname" => self.entityname = Some(i),
222+
"entitytype" => self.entitytype = Some(i),
223+
"event" => self.event = Some(i),
224+
"arguments" => self.arguments = Some(i),
225+
"identifier" => self.identifier = Some(i),
226+
"message" => self.message = Some(i),
227+
_ => {}
228+
}
229+
}
230+
}
231+
}
232+
233+
// ----------------------------------------------------------------------------
234+
// prometheus
235+
// ----------------------------------------------------------------------------
236+
237+
// impl crate::prom::ToText for NodeState {
238+
// fn to_prom(&self, output: &mut impl Write) -> Result<()> {
239+
// writeln!(output, "# HELP gpfs_diag_deadlocks GPFS deadlock nodes.")?;
240+
// writeln!(output, "# TYPE gpfs_diag_deadlocks gauge")?;
241+
// writeln!(output, "gpfs_diag_deadlocks {}", self.node_list.len())?;
242+
243+
// Ok(())
244+
// }
245+
// }
246+
247+
// ----------------------------------------------------------------------------
248+
// tests
249+
// ----------------------------------------------------------------------------
250+
251+
#[cfg(test)]
252+
mod tests {
253+
use super::*;
254+
255+
#[test]
256+
fn parse_node() {
257+
let input = include_str!("health-node-example.in");
258+
259+
let data = NodeStates::from_reader(input.as_bytes()).unwrap();
260+
261+
assert_eq!(data.states.len(), 9);
262+
263+
assert_eq!(
264+
data.states,
265+
vec![
266+
NodeState {
267+
component: "NODE".into(),
268+
entityname: "frontend2-ib0".into(),
269+
entitytype: "NODE".into(),
270+
status: "TIPS".into(),
271+
events: vec![],
272+
},
273+
NodeState {
274+
component: "GPFS".into(),
275+
entityname: "frontend2-ib0".into(),
276+
entitytype: "NODE".into(),
277+
status: "TIPS".into(),
278+
events: vec![NodeEvent {
279+
component: "GPFS".into(),
280+
entityname: "frontend2-ib0".into(),
281+
entitytype: "NODE".into(),
282+
event: "unexpected_operating_system".into(),
283+
arguments: None,
284+
identifier: None,
285+
message:
286+
"An unexpected operating system was detected."
287+
.into()
288+
}]
289+
},
290+
NodeState {
291+
component: "NETWORK".into(),
292+
entityname: "frontend2-ib0".into(),
293+
entitytype: "NODE".into(),
294+
status: "HEALTHY".into(),
295+
events: vec![],
296+
},
297+
NodeState {
298+
component: "NETWORK".into(),
299+
entityname: "ib0".into(),
300+
entitytype: "NIC".into(),
301+
status: "HEALTHY".into(),
302+
events: vec![],
303+
},
304+
NodeState {
305+
component: "NETWORK".into(),
306+
entityname: "hfi1_0/1".into(),
307+
entitytype: "IB_RDMA".into(),
308+
status: "HEALTHY".into(),
309+
events: vec![],
310+
},
311+
NodeState {
312+
component: "FILESYSTEM".into(),
313+
entityname: "frontend2-ib0".into(),
314+
entitytype: "NODE".into(),
315+
status: "TIPS".into(),
316+
events: vec![],
317+
},
318+
NodeState {
319+
component: "FILESYSTEM".into(),
320+
entityname: "gpfs1".into(),
321+
entitytype: "FILESYSTEM".into(),
322+
status: "TIPS".into(),
323+
events: vec![NodeEvent {
324+
component: "FILESYSTEM".into(),
325+
entityname: "gpfs1".into(),
326+
entitytype: "FILESYSTEM".into(),
327+
event: "ill_unbalanced_fs".into(),
328+
arguments: Some("gpfs1".into()),
329+
identifier: Some("gpfs1".into()),
330+
message:
331+
"The file system gpfs1 is not properly balanced."
332+
.into()
333+
}]
334+
},
335+
NodeState {
336+
component: "PERFMON".into(),
337+
entityname: "frontend2-ib0".into(),
338+
entitytype: "NODE".into(),
339+
status: "HEALTHY".into(),
340+
events: vec![],
341+
},
342+
NodeState {
343+
component: "THRESHOLD".into(),
344+
entityname: "frontend2-ib0".into(),
345+
entitytype: "NODE".into(),
346+
status: "HEALTHY".into(),
347+
events: vec![],
348+
}
349+
]
350+
);
351+
}
352+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod diag;
99
pub mod disk;
1010
pub mod fileset;
1111
pub mod fs;
12+
pub mod health;
1213
pub mod mgr;
1314
pub mod nmon;
1415
pub mod nsd;

0 commit comments

Comments
 (0)