1#![deny(clippy::all)]
4#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
5
6use std::io::Write;
7use std::process::Command;
8use std::str::FromStr;
9
10use anyhow::{anyhow, Context, Result};
11
12pub fn run(fs_name: &str) -> Result<Filesystem> {
23 let mut cmd = Command::new("mmlspool");
24 cmd.arg(fs_name);
25
26 let output = cmd
27 .output()
28 .with_context(|| format!("error running: {cmd:?}"))?;
29
30 if output.status.success() {
31 let output = String::from_utf8(output.stdout).with_context(|| {
32 format!("parsing {cmd:?} command output to UTF8")
33 })?;
34
35 let pools = parse_mmlspool_output(&output)
36 .context("parsing pools to internal data")?;
37
38 Ok(Filesystem {
39 name: fs_name.into(),
40 pools,
41 })
42 } else {
43 Err(anyhow!("error running: {:?}", cmd))
44 }
45}
46
47pub fn run_all<S>(fs_names: &[S]) -> Result<Vec<Filesystem>>
54where
55 S: AsRef<str>,
56{
57 let mut filesystems = Vec::with_capacity(fs_names.len());
58
59 for fs in fs_names {
60 let filesystem = run(fs.as_ref())?;
61 filesystems.push(filesystem);
62 }
63
64 Ok(filesystems)
65}
66
67#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
73pub struct Filesystem {
74 name: String,
75 pools: Vec<Pool>,
76}
77
78impl Filesystem {
79 #[must_use]
81 pub fn name(&self) -> &str {
82 &self.name
83 }
84
85 #[must_use]
87 pub fn pools(&self) -> &[Pool] {
88 &self.pools
89 }
90}
91
92#[derive(
94 Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default,
95)]
96pub struct Size {
97 total_kb: u64,
98 free_kb: u64,
99}
100
101impl Size {
102 #[must_use]
104 pub const fn total_kb(&self) -> u64 {
105 self.total_kb
106 }
107
108 #[must_use]
110 pub const fn free_kb(&self) -> u64 {
111 self.free_kb
112 }
113
114 #[must_use]
116 pub const fn used_percent(&self) -> u64 {
117 let used_kb = self.total_kb - self.free_kb;
118 let x = used_kb * 100;
119 let y = self.total_kb;
120
121 x / y
122 }
123}
124
125#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
127pub struct Pool {
128 name: String,
129 data: Option<Size>,
130 meta: Option<Size>,
131}
132
133impl Pool {
134 #[must_use]
136 pub fn name(&self) -> &str {
137 &self.name
138 }
139
140 #[must_use]
142 pub const fn data(&self) -> Option<&Size> {
143 self.data.as_ref()
144 }
145
146 #[must_use]
148 pub const fn meta(&self) -> Option<&Size> {
149 self.meta.as_ref()
150 }
151}
152
153impl FromStr for Pool {
154 type Err = anyhow::Error;
155
156 fn from_str(s: &str) -> Result<Self> {
157 let tokens = s
158 .split(' ')
159 .filter(|token| !token.is_empty())
160 .collect::<Vec<_>>();
161
162 let name = tokens[0].into();
163
164 let data = if tokens[4] == "yes" {
165 let total_kb = tokens[6].parse::<u64>().with_context(|| {
166 format!("parsing data totalkb token {} to u64", tokens[6])
167 })?;
168
169 let free_kb = tokens[7].parse::<u64>().with_context(|| {
170 format!("parsing data freekb token {} to u64", tokens[7])
171 })?;
172
173 Some(Size { total_kb, free_kb })
174 } else {
175 None
176 };
177
178 let meta = if tokens[5] == "yes" {
179 let (total_kb_token_id, free_kb_token_id) =
180 if tokens[8] == "(" { (10, 11) } else { (9, 10) };
181
182 let total_kb = tokens[total_kb_token_id]
183 .parse::<u64>()
184 .with_context(|| {
185 format!(
186 "parsing meta totalkb token {} to u64",
187 tokens[total_kb_token_id]
188 )
189 })?;
190
191 let free_kb = tokens[free_kb_token_id]
192 .parse::<u64>()
193 .with_context(|| {
194 format!(
195 "parsing meta freekb token {} to u64",
196 tokens[free_kb_token_id]
197 )
198 })?;
199
200 Some(Size { total_kb, free_kb })
201 } else {
202 None
203 };
204
205 if data.is_none() && meta.is_none() {
206 Err(anyhow!("pool {} contains neither data nor metadata", name))
207 } else {
208 Ok(Self { name, data, meta })
209 }
210 }
211}
212
213fn parse_mmlspool_output(s: &str) -> Result<Vec<Pool>> {
214 let mut pools = Vec::with_capacity(16);
215
216 for line in s.lines().skip(2) {
217 let pool = line
218 .parse()
219 .with_context(|| format!("parsing pool line: {line}"))?;
220
221 pools.push(pool);
222 }
223
224 Ok(pools)
225}
226
227impl crate::prom::ToText for Vec<Filesystem> {
228 fn to_prom(&self, output: &mut impl Write) -> Result<()> {
229 writeln!(
230 output,
231 "# HELP gpfs_fs_pool_total_kbytes GPFS pool size in kilobytes."
232 )?;
233 writeln!(output, "# TYPE gpfs_fs_pool_total_kbytes gauge")?;
234
235 for fs in self {
236 for pool in &fs.pools {
237 if let Some(size) = &pool.data {
238 writeln!(
239 output,
240 "gpfs_fs_pool_total_kbytes{{fs=\"{}\",pool=\"{}\",type=\"data\"}} {}",
241 fs.name,
242 pool.name,
243 size.total_kb
244 )?;
245 }
246
247 if let Some(size) = &pool.meta {
248 writeln!(
249 output,
250 "gpfs_fs_pool_total_kbytes{{fs=\"{}\",pool=\"{}\",type=\"meta\"}} {}",
251 fs.name,
252 pool.name,
253 size.total_kb
254 )?;
255 }
256 }
257 }
258
259 writeln!(
260 output,
261 "# HELP gpfs_fs_pool_free_kbytes GPFS pool free kilobytes."
262 )?;
263 writeln!(output, "# TYPE gpfs_fs_pool_free_kbytes gauge")?;
264
265 for fs in self {
266 for pool in &fs.pools {
267 if let Some(size) = &pool.data {
268 writeln!(
269 output,
270 "gpfs_fs_pool_free_kbytes{{fs=\"{}\",pool=\"{}\",type=\"data\"}} {}",
271 fs.name,
272 pool.name,
273 size.free_kb
274 )?;
275 }
276
277 if let Some(size) = &pool.meta {
278 writeln!(
279 output,
280 "gpfs_fs_pool_free_kbytes{{fs=\"{}\",pool=\"{}\",type=\"meta\"}} {}",
281 fs.name,
282 pool.name,
283 size.free_kb
284 )?;
285 }
286 }
287 }
288
289 Ok(())
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::prom::ToText;
297
298 #[test]
299 fn parse() {
300 let input = include_str!("pool-example.in");
301
302 let pools = parse_mmlspool_output(input).unwrap();
303 assert_eq!(pools.len(), 4);
304
305 assert_eq!(
306 pools[0],
307 Pool {
308 name: "system".into(),
309 data: None,
310 meta: Some(Size {
311 total_kb: 25_004_867_584,
312 free_kb: 9_798_959_104,
313 }),
314 }
315 );
316
317 assert_eq!(
318 pools[1],
319 Pool {
320 name: "nvme".into(),
321 data: Some(Size {
322 total_kb: 162_531_639_296,
323 free_kb: 114_505_474_048,
324 }),
325 meta: None,
326 }
327 );
328
329 assert_eq!(pools[1].data.unwrap().used_percent(), 29);
330
331 assert_eq!(
332 pools[2],
333 Pool {
334 name: "nlsas".into(),
335 data: Some(Size {
336 total_kb: 1_997_953_957_888,
337 free_kb: 1_981_410_271_232,
338 }),
339 meta: None,
340 }
341 );
342
343 assert_eq!(
344 pools[3],
345 Pool {
346 name: "dangerzone".into(),
347 data: Some(Size {
348 total_kb: 42,
349 free_kb: 42,
350 }),
351 meta: Some(Size {
352 total_kb: 42,
353 free_kb: 42,
354 }),
355 }
356 );
357 }
358
359 #[test]
360 fn prometheus() {
361 let fs = Filesystem {
362 name: "gpfs1".into(),
363 pools: vec![
364 Pool {
365 name: "system".into(),
366 data: None,
367 meta: Some(Size {
368 total_kb: 25_004_867_584,
369 free_kb: 9_798_959_104,
370 }),
371 },
372 Pool {
373 name: "nvme".into(),
374 data: Some(Size {
375 total_kb: 162_531_639_296,
376 free_kb: 114_505_474_048,
377 }),
378 meta: None,
379 },
380 Pool {
381 name: "nlsas".into(),
382 data: Some(Size {
383 total_kb: 1_997_953_957_888,
384 free_kb: 1_981_410_271_232,
385 }),
386 meta: None,
387 },
388 ],
389 };
390
391 let mut output = vec![];
392 vec![fs].to_prom(&mut output).unwrap();
393
394 let metrics = std::str::from_utf8(output.as_slice()).unwrap();
395
396 let expected = include_str!("pool-example.prom");
397 assert_eq!(metrics, expected);
398 }
399}