mmoxi/
pool.rs

1//! `mmlspool` parsing.
2
3#![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
12// ----------------------------------------------------------------------------
13// CLI interface
14// ----------------------------------------------------------------------------
15
16/// Runs `mmlspool` on the given filesystem, and returns the parsed output.
17///
18/// # Errors
19///
20/// Returns an error if running `mmlspool` fails or if parsing its output
21/// fails.
22pub 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
47/// Runs `mmlspool` on all given filesystems, and returns the parsed output.
48///
49/// # Errors
50///
51/// Returns an error if running `mmlspool` fails or if parsing its output
52/// fails.
53pub 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// ----------------------------------------------------------------------------
68// data structures and parsing
69// ----------------------------------------------------------------------------
70
71/// A file system.
72#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
73pub struct Filesystem {
74    name: String,
75    pools: Vec<Pool>,
76}
77
78impl Filesystem {
79    /// Returns the file system name.
80    #[must_use]
81    pub fn name(&self) -> &str {
82        &self.name
83    }
84
85    /// Returns the pools.
86    #[must_use]
87    pub fn pools(&self) -> &[Pool] {
88        &self.pools
89    }
90}
91
92/// Pool size.
93#[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    /// Returns total data in kilobytes.
103    #[must_use]
104    pub const fn total_kb(&self) -> u64 {
105        self.total_kb
106    }
107
108    /// Returns free data in kilobytes.
109    #[must_use]
110    pub const fn free_kb(&self) -> u64 {
111        self.free_kb
112    }
113
114    /// Returns the used percentage.
115    #[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/// A storage pool.
126#[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    /// Returns the pool name.
135    #[must_use]
136    pub fn name(&self) -> &str {
137        &self.name
138    }
139
140    /// Returns the object data size.
141    #[must_use]
142    pub const fn data(&self) -> Option<&Size> {
143        self.data.as_ref()
144    }
145
146    /// Returns the metadata size.
147    #[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}